From 260e1426d49eb97f382b7290b75d5723045a845f Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Sun, 23 Jul 2023 06:30:35 -0500 Subject: [PATCH 001/174] gah --- app/LilaComponents.scala | 1 + app/controllers/LocalPlay.scala | 19 ++++++ app/views/localPlay.scala | 31 +++++++++ conf/routes | 3 + pnpm-lock.yaml | 33 ++++++++++ ui/@types/lichess/index.d.ts | 4 +- ui/localPlay/css/_local-play.scss | 21 +++++++ ui/localPlay/css/build/_local-play.scss | 11 ++++ .../css/build/local-play.ltr.dark.scss | 3 + .../css/build/local-play.ltr.light.scss | 3 + .../css/build/local-play.ltr.transp.scss | 3 + .../css/build/local-play.rtl.dark.scss | 3 + .../css/build/local-play.rtl.light.scss | 3 + .../css/build/local-play.rtl.transp.scss | 3 + ui/localPlay/package.json | 32 ++++++++++ ui/localPlay/src/chessground.ts | 63 +++++++++++++++++++ ui/localPlay/src/ctrl.ts | 43 +++++++++++++ ui/localPlay/src/interfaces.ts | 2 + ui/localPlay/src/main.ts | 28 +++++++++ ui/localPlay/src/socket.ts | 0 ui/localPlay/src/view.ts | 44 +++++++++++++ ui/localPlay/tsconfig.json | 16 +++++ ui/site/src/component/powertip.ts | 12 ++-- ui/site/src/site.lichess.globals.ts | 2 +- 24 files changed, 374 insertions(+), 9 deletions(-) create mode 100644 app/controllers/LocalPlay.scala create mode 100644 app/views/localPlay.scala create mode 100644 ui/localPlay/css/_local-play.scss create mode 100644 ui/localPlay/css/build/_local-play.scss create mode 100644 ui/localPlay/css/build/local-play.ltr.dark.scss create mode 100644 ui/localPlay/css/build/local-play.ltr.light.scss create mode 100644 ui/localPlay/css/build/local-play.ltr.transp.scss create mode 100644 ui/localPlay/css/build/local-play.rtl.dark.scss create mode 100644 ui/localPlay/css/build/local-play.rtl.light.scss create mode 100644 ui/localPlay/css/build/local-play.rtl.transp.scss create mode 100644 ui/localPlay/package.json create mode 100644 ui/localPlay/src/chessground.ts create mode 100644 ui/localPlay/src/ctrl.ts create mode 100644 ui/localPlay/src/interfaces.ts create mode 100644 ui/localPlay/src/main.ts create mode 100644 ui/localPlay/src/socket.ts create mode 100644 ui/localPlay/src/view.ts create mode 100644 ui/localPlay/tsconfig.json diff --git a/app/LilaComponents.scala b/app/LilaComponents.scala index 13e0c4e02f7d9..9c3dcb9220347 100644 --- a/app/LilaComponents.scala +++ b/app/LilaComponents.scala @@ -121,6 +121,7 @@ final class LilaComponents( lazy val irwin: Irwin = wire[Irwin] lazy val learn: Learn = wire[Learn] lazy val lobby: Lobby = wire[Lobby] + lazy val localPlay: LocalPlay = wire[LocalPlay] lazy val main: Main = wire[Main] lazy val msg: Msg = wire[Msg] lazy val mod: Mod = wire[Mod] diff --git a/app/controllers/LocalPlay.scala b/app/controllers/LocalPlay.scala new file mode 100644 index 0000000000000..7c9be09e63523 --- /dev/null +++ b/app/controllers/LocalPlay.scala @@ -0,0 +1,19 @@ +package controllers + +import play.api.libs.json.* +import play.api.mvc.* +import views.* + +import lila.app.{ given, * } +import lila.common.Json.given +import lila.common.config.MaxPerSecond +import lila.user.User +import lila.common.LangPath +import play.api.i18n.Lang +import lila.rating.{ Perf, PerfType } + +final class LocalPlay(env: Env) extends LilaController(env): + + def index = Open: + NoBot: + Ok.page(views.html.localPlay.index) diff --git a/app/views/localPlay.scala b/app/views/localPlay.scala new file mode 100644 index 0000000000000..0715b32989380 --- /dev/null +++ b/app/views/localPlay.scala @@ -0,0 +1,31 @@ +package views.html + +import controllers.routes +import play.api.libs.json.{ JsObject, Json } + +import lila.app.templating.Environment.{ given, * } +import lila.app.ui.ScalatagsTemplate.* +import lila.common.Json.given +import lila.common.String.html.safeJsonValue + +object localPlay: + def index(using ctx: PageContext) = + views.html.base.layout( + title = "Play vs Bots", + moreJs = jsModuleInit("localPlay"), + moreCss = cssTag("local-play"), + csp = analysisCsp.some, + openGraph = lila.app.ui + .OpenGraph( + title = "Play vs Bots", + description = "Play vs Bots", + url = s"$netBaseUrl${controllers.routes.LocalPlay.index}" + ) + .some, + chessground = false, + withHrefLangs = lila.common.LangPath(controllers.routes.LocalPlay.index).some + ) { + main(id := "local-play")( + div + ) + } diff --git a/conf/routes b/conf/routes index 79a1a71d117b4..b67573e015001 100644 --- a/conf/routes +++ b/conf/routes @@ -310,6 +310,9 @@ POST /upload/image/streamer controllers.Streamer.pictureApply GET /streamer/:username controllers.Streamer.show(username) GET /streamer/:username/redirect controllers.Streamer.redirect(username) +# LocalPlay +GET /local-play controllers.LocalPlay.index + # Round GET /$gameId<\w{8}> controllers.Round.watcher(gameId, color = "white") GET /$gameId<\w{8}>/$color controllers.Round.watcher(gameId, color) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c110eeb9f2fb..f1b42fccbe126 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -458,6 +458,39 @@ importers: specifier: ^3.5.1 version: 3.5.1 + ui/localPlay: + dependencies: + '@types/cash': + specifier: workspace:* + version: link:../@types/cash + '@types/lichess': + specifier: workspace:* + version: link:../@types/lichess + chess: + specifier: workspace:* + version: link:../chess + chessops: + specifier: ^0.12.7 + version: 0.12.7 + common: + specifier: workspace:* + version: link:../common + game: + specifier: workspace:* + version: link:../game + nvui: + specifier: workspace:* + version: link:../nvui + puz: + specifier: workspace:* + version: link:../puz + snabbdom: + specifier: ^3.5.1 + version: 3.5.1 + tree: + specifier: workspace:* + version: link:../tree + ui/mod: dependencies: '@types/cash': diff --git a/ui/@types/lichess/index.d.ts b/ui/@types/lichess/index.d.ts index 088f1de6928da..23948cf000349 100644 --- a/ui/@types/lichess/index.d.ts +++ b/ui/@types/lichess/index.d.ts @@ -301,7 +301,7 @@ type Nvui = (redraw: () => void) => { interface Window { lichess: Lichess; - un$(cash: Cash): T; + $as(cash: Cash): T; readonly chrome?: unknown; readonly moment: any; Chessground: any; @@ -575,4 +575,4 @@ interface Dictionary { type SocketHandlers = Dictionary<(d: any) => void>; declare const lichess: Lichess; -declare const un$: (cash: Cash) => T; +declare const $as: (cash: Cash) => T; diff --git a/ui/localPlay/css/_local-play.scss b/ui/localPlay/css/_local-play.scss new file mode 100644 index 0000000000000..d58f8b37a7068 --- /dev/null +++ b/ui/localPlay/css/_local-play.scss @@ -0,0 +1,21 @@ +$mq-col2: $mq-col2-uniboard; + +#local-play { + display: grid; + + grid-row-gap: $block-gap; + grid-column-gap: $block-gap; + grid-template-areas: 'board' 'side'; + + @include breakpoint($mq-col2) { + grid-template-columns: var(--col2-uniboard-width) $col2-uniboard-table; + grid-template-rows: fit-content(0); + grid-template-areas: 'board side'; + } + + .about__link { + margin-top: 2vh; + text-align: center; + font-size: 0.8em; + } +} diff --git a/ui/localPlay/css/build/_local-play.scss b/ui/localPlay/css/build/_local-play.scss new file mode 100644 index 0000000000000..95ca68f206df0 --- /dev/null +++ b/ui/localPlay/css/build/_local-play.scss @@ -0,0 +1,11 @@ +@import '../../../common/css/plugin'; +@import '../../../common/css/layout/uniboard'; +@import '../../../common/css/component/board-resize'; +@import '../../../common/css/component/bar-glider'; +@import '../../../common/css/component/zen-toggle'; +@import '../../../common/css/component/fbt'; +@import '../../../common/css/vendor/chessground/coords'; +@import '../../../chess/css/promotion'; +@import '../../../puz/css/puz'; + +@import '../local-play'; diff --git a/ui/localPlay/css/build/local-play.ltr.dark.scss b/ui/localPlay/css/build/local-play.ltr.dark.scss new file mode 100644 index 0000000000000..0234b74a7a162 --- /dev/null +++ b/ui/localPlay/css/build/local-play.ltr.dark.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/ltr'; +@import '../../../common/css/theme/dark'; +@import 'local-play'; diff --git a/ui/localPlay/css/build/local-play.ltr.light.scss b/ui/localPlay/css/build/local-play.ltr.light.scss new file mode 100644 index 0000000000000..b128e239a7df0 --- /dev/null +++ b/ui/localPlay/css/build/local-play.ltr.light.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/ltr'; +@import '../../../common/css/theme/light'; +@import 'local-play'; diff --git a/ui/localPlay/css/build/local-play.ltr.transp.scss b/ui/localPlay/css/build/local-play.ltr.transp.scss new file mode 100644 index 0000000000000..5834c8012e52b --- /dev/null +++ b/ui/localPlay/css/build/local-play.ltr.transp.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/ltr'; +@import '../../../common/css/theme/transp'; +@import 'local-play'; diff --git a/ui/localPlay/css/build/local-play.rtl.dark.scss b/ui/localPlay/css/build/local-play.rtl.dark.scss new file mode 100644 index 0000000000000..52e615ea96824 --- /dev/null +++ b/ui/localPlay/css/build/local-play.rtl.dark.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/rtl'; +@import '../../../common/css/theme/dark'; +@import 'local-play'; diff --git a/ui/localPlay/css/build/local-play.rtl.light.scss b/ui/localPlay/css/build/local-play.rtl.light.scss new file mode 100644 index 0000000000000..92ea951ace40a --- /dev/null +++ b/ui/localPlay/css/build/local-play.rtl.light.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/rtl'; +@import '../../../common/css/theme/light'; +@import 'local-play'; diff --git a/ui/localPlay/css/build/local-play.rtl.transp.scss b/ui/localPlay/css/build/local-play.rtl.transp.scss new file mode 100644 index 0000000000000..84017ff85aca0 --- /dev/null +++ b/ui/localPlay/css/build/local-play.rtl.transp.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/rtl'; +@import '../../../common/css/theme/transp'; +@import 'local-play'; diff --git a/ui/localPlay/package.json b/ui/localPlay/package.json new file mode 100644 index 0000000000000..c1862396171ee --- /dev/null +++ b/ui/localPlay/package.json @@ -0,0 +1,32 @@ +{ + "name": "local-play", + "version": "2.0.0", + "private": true, + "description": "lichess.org local play", + "author": "Thibault Duplessis", + "license": "AGPL-3.0-or-later", + "dependencies": { + "@types/cash": "workspace:*", + "@types/lichess": "workspace:*", + "chess": "workspace:*", + "chessops": "^0.12.7", + "common": "workspace:*", + "game": "workspace:*", + "nvui": "workspace:*", + "puz": "workspace:*", + "snabbdom": "^3.5.1", + "tree": "workspace:*" + }, + "scripts": { + "compile": "tsc", + "dev": "tsc", + "prod": "tsc" + }, + "lichess": { + "modules": { + "esm": { + "src/main.ts": "localPlay" + } + } + } +} diff --git a/ui/localPlay/src/chessground.ts b/ui/localPlay/src/chessground.ts new file mode 100644 index 0000000000000..501bad1845b69 --- /dev/null +++ b/ui/localPlay/src/chessground.ts @@ -0,0 +1,63 @@ +import resizeHandle from 'common/resize'; +import { Config as CgConfig } from 'chessground/config'; +import * as Prefs from 'common/prefs'; +import { Ctrl } from './ctrl'; + +const pref = { + coords: Prefs.Coords.Hidden, + is3d: false, + destination: false, + rookCastle: false, + moveEvent: 0, + highlight: false, + animation: 0, +}; + +export function makeConfig(ctrl: Ctrl): CgConfig { + const opts = ctrl.getCgOpts(); + return { + fen: opts.fen, + orientation: opts.orientation, + turnColor: opts.turnColor, + check: opts.check, + lastMove: opts.lastMove, + coordinates: pref.coords !== Prefs.Coords.Hidden, + addPieceZIndex: pref.is3d, + addDimensionsCssVarsTo: document.body, + movable: { + free: false, + color: opts.movable!.color, + dests: opts.movable!.dests, + showDests: pref.destination, + rookCastle: pref.rookCastle, + }, + draggable: { + enabled: pref.moveEvent > 0, + showGhost: pref.highlight, + }, + selectable: { + enabled: pref.moveEvent !== 1, + }, + events: { + move: ctrl.userMove, + insert(elements) { + resizeHandle(elements, Prefs.ShowResizeHandle.OnlyAtStart, 0, p => p == 0); + }, + }, + premovable: { + enabled: false, + }, + drawable: { + enabled: true, + defaultSnapToValidMove: lichess.storage.boolean('arrow.snap').getOrDefault(true), + }, + highlight: { + lastMove: pref.highlight, + check: pref.highlight, + }, + animation: { + duration: pref.animation, + }, + disableContextMenu: true, + }; +} diff --git a/ui/localPlay/src/ctrl.ts b/ui/localPlay/src/ctrl.ts new file mode 100644 index 0000000000000..6e8c9c3c5104a --- /dev/null +++ b/ui/localPlay/src/ctrl.ts @@ -0,0 +1,43 @@ +import { LocalPlayOpts } from './interfaces'; +import { PromotionCtrl } from 'chess/promotion'; +import { prop, Prop } from 'common'; +//import { makeFen } from 'chessops/fen'; +import { Api as CgApi } from 'chessground/api'; +import { Config as CgConfig } from 'chessground/config'; + +export class Ctrl { + promotion: PromotionCtrl; + ground = prop(false) as Prop; + flipped = false; + redraw: () => void = () => {}; + + constructor(readonly opts: LocalPlayOpts) { + this.promotion = new PromotionCtrl(this.withGround, this.setGround, this.redraw); + } + + getCgOpts = (): CgConfig => { + return { + movable: { + color: 'both', + }, + }; + }; + + withGround = (f: (cg: CgApi) => A): A | false => { + const g = this.ground(); + return g && f(g); + }; + + userMove = (orig: Key, dest: Key) => { + orig; + dest; + }; + + flip = () => { + this.flipped = !this.flipped; + this.withGround(g => g.toggleOrientation()); + this.redraw(); + }; + + private setGround = () => this.withGround(g => g.set(this.getCgOpts())); +} diff --git a/ui/localPlay/src/interfaces.ts b/ui/localPlay/src/interfaces.ts new file mode 100644 index 0000000000000..6fb6c6b5844d2 --- /dev/null +++ b/ui/localPlay/src/interfaces.ts @@ -0,0 +1,2 @@ +export interface LocalPlayOpts {} +export interface Controller {} diff --git a/ui/localPlay/src/main.ts b/ui/localPlay/src/main.ts new file mode 100644 index 0000000000000..9999ec12016c0 --- /dev/null +++ b/ui/localPlay/src/main.ts @@ -0,0 +1,28 @@ +import { attributesModule, classModule, init } from 'snabbdom'; +import { Ctrl } from './ctrl'; +import view from './view'; +import { LocalPlayOpts, Controller } from './interfaces'; +import menuHover from 'common/menuHover'; +import { Chessground } from 'chessground'; + +const patch = init([classModule, attributesModule]); + +export async function initModule(opts: LocalPlayOpts) { + // make a StrongSocket + const ctrl = new Ctrl(opts); + + const blueprint = view(ctrl as Controller); + const element = document.querySelector('main#local-play') as HTMLElement; + element.innerHTML = ''; + let vnode = patch(element, blueprint); + + function redraw() { + vnode = patch(vnode, view(ctrl)); + } + redraw(); + menuHover(); +} + +// that's for the rest of lichess to access chessground +// without having to include it a second time +window.Chessground = Chessground; diff --git a/ui/localPlay/src/socket.ts b/ui/localPlay/src/socket.ts new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/ui/localPlay/src/view.ts b/ui/localPlay/src/view.ts new file mode 100644 index 0000000000000..7b48b99137b4a --- /dev/null +++ b/ui/localPlay/src/view.ts @@ -0,0 +1,44 @@ +//import { Controller } from './interfaces'; +import { Chessground } from 'chessground'; +import { h, VNode } from 'snabbdom'; +import { makeConfig as makeCgConfig } from './chessground'; +//import { getNow } from 'puz/util'; +//import * as licon from 'common/licon'; +//import { onInsert } from 'common/snabbdom'; + +export default function (ctrl: any): VNode { + return h('div.local-play', renderPlay(ctrl)); +} + +function chessground(ctrl: any): VNode { + return h('div.cg-wrap', { + hook: { + insert: vnode => ctrl.ground(Chessground(vnode.elm as HTMLElement, makeCgConfig(ctrl))), + }, + }); +} + +function renderPlay(ctrl: any): VNode[] { + return [ + h('div.puz-board.main-board', [chessground(ctrl), ctrl.promotion.view()]), + h('div.puz-side', [ + renderStart(ctrl), + h('div.puz-bots', [ + // ... + ]), + h('div.puz-side__table', [renderControls(ctrl)]), + ]), + ]; +} + +function renderControls(ctrl: any): VNode { + ctrl; + return h('div.puz-side__control', ['gah']); +} + +function renderStart(ctrl: any): VNode { + ctrl; + return h('div.puz-side__top.puz-side__start', [ + h('div.puz-side__start__text', [h('strong', 'Play vs Bots'), h('span', 'gah')]), + ]); +} diff --git a/ui/localPlay/tsconfig.json b/ui/localPlay/tsconfig.json new file mode 100644 index 0000000000000..b5ca6f5a06ec2 --- /dev/null +++ b/ui/localPlay/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "esModuleInterop": true, + "noEmit": true + }, + "references": [ + { "path": "../chess/tsconfig.json" }, + { "path": "../common/tsconfig.json" }, + { "path": "../game/tsconfig.json" }, + { "path": "../puz/tsconfig.json" }, + { "path": "../nvui/tsconfig.json" }, + { "path": "../tree/tsconfig.json" } + ], + "isolatedModules": true +} diff --git a/ui/site/src/component/powertip.ts b/ui/site/src/component/powertip.ts index 854667f00abc5..80a8c28c5c52c 100644 --- a/ui/site/src/component/powertip.ts +++ b/ui/site/src/component/powertip.ts @@ -249,7 +249,7 @@ class DisplayController { el: WithTooltip; constructor(readonly element: Cash, readonly options: Options, readonly tipController: TooltipController) { - this.el = un$(element); + this.el = $as(element); this.scoped = session.scoped[options.popupId!]; // expose the methods } @@ -454,7 +454,7 @@ class TooltipController { } showTip(element: Cash) { - un$(element).hasActiveHover = true; + $as(element).hasActiveHover = true; this.doShowTip(element); } @@ -464,7 +464,7 @@ class TooltipController { // in the code. if that happens then we need to not proceed or we may // have the fadeout callback for the last tooltip execute immediately // after this code runs, causing bugs. - if (!un$(element).hasActiveHover) return; + if (!$as(element).hasActiveHover) return; // if the tooltip is open and we got asked to open another one then the // old one is still in its fadeOut cycle, so wait and try again @@ -482,7 +482,7 @@ class TooltipController { // trigger powerTipPreRender event if (this.options.preRender) { - this.options.preRender(un$(element)); + this.options.preRender($as(element)); } this.scoped.activeHover = element; @@ -509,8 +509,8 @@ class TooltipController { this.scoped.desyncTimeout = clearInterval(this.scoped.desyncTimeout); // reset element state - un$(element).hasActiveHover = false; - un$(element).forcedOpen = false; + $as(element).hasActiveHover = false; + $as(element).forcedOpen = false; // fade out this.tipElement.hide(); diff --git a/ui/site/src/site.lichess.globals.ts b/ui/site/src/site.lichess.globals.ts index f7fc7690d6ac2..acb9c6bf9d60a 100644 --- a/ui/site/src/site.lichess.globals.ts +++ b/ui/site/src/site.lichess.globals.ts @@ -30,7 +30,7 @@ import { format as timeago, formatter as dateFormat } from './component/timeago' import watchers from './component/watchers'; export default () => { - window.un$ = (cash: Cash) => cash[0] as T; + window.$as = (cash: Cash) => cash[0] as T; const l = window.lichess; l.StrongSocket = StrongSocket; l.mousetrap = new Mousetrap(document); From 7ab1bffe95e9371072d88718806bad5c8cb24be5 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Sun, 23 Jul 2023 18:15:42 -0500 Subject: [PATCH 002/174] updatez --- app/controllers/LocalPlay.scala | 2 +- app/views/localPlay.scala | 8 +-- pnpm-lock.yaml | 120 ++++++++++++++------------------ ui/localPlay/package.json | 3 +- ui/localPlay/src/chessground.ts | 4 +- ui/localPlay/src/ctrl.ts | 51 ++++++++++++-- ui/localPlay/src/main.ts | 4 +- ui/localPlay/src/view.ts | 3 +- 8 files changed, 111 insertions(+), 84 deletions(-) diff --git a/app/controllers/LocalPlay.scala b/app/controllers/LocalPlay.scala index 7c9be09e63523..566d19a3ec276 100644 --- a/app/controllers/LocalPlay.scala +++ b/app/controllers/LocalPlay.scala @@ -16,4 +16,4 @@ final class LocalPlay(env: Env) extends LilaController(env): def index = Open: NoBot: - Ok.page(views.html.localPlay.index) + Ok.page(views.html.localPlay.index).map(_.enableSharedArrayBuffer) diff --git a/app/views/localPlay.scala b/app/views/localPlay.scala index 0715b32989380..ec389b5cb97b7 100644 --- a/app/views/localPlay.scala +++ b/app/views/localPlay.scala @@ -22,10 +22,8 @@ object localPlay: url = s"$netBaseUrl${controllers.routes.LocalPlay.index}" ) .some, - chessground = false, - withHrefLangs = lila.common.LangPath(controllers.routes.LocalPlay.index).some + zoomable = true, + chessground = false ) { - main(id := "local-play")( - div - ) + main } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1b42fccbe126..98acb0df3d0de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,7 +67,7 @@ importers: version: 28.1.3 ts-jest: specifier: ^28.0.7 - version: 28.0.8(@babel/core@7.20.5)(jest@28.1.3)(typescript@5.1.5) + version: 28.0.8(@babel/core@7.20.5)(jest@28.1.3)(typescript@5.1.6) ui/@build: dependencies: @@ -490,6 +490,9 @@ importers: tree: specifier: workspace:* version: link:../tree + zerofish: + specifier: 0.0.2 + version: 0.0.2 ui/mod: dependencies: @@ -2494,7 +2497,7 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: '@jest/types': 28.1.3 - '@types/node': 20.3.2 + '@types/node': 18.16.18 chalk: 4.1.2 jest-message-util: 28.1.3 jest-util: 28.1.3 @@ -2515,14 +2518,14 @@ packages: '@jest/test-result': 28.1.3 '@jest/transform': 28.1.3 '@jest/types': 28.1.3 - '@types/node': 20.3.2 + '@types/node': 18.16.18 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.7.0 exit: 0.1.2 graceful-fs: 4.2.10 jest-changed-files: 28.1.3 - jest-config: 28.1.3(@types/node@20.3.2) + jest-config: 28.1.3(@types/node@18.16.18) jest-haste-map: 28.1.3 jest-message-util: 28.1.3 jest-regex-util: 28.0.2 @@ -2550,7 +2553,7 @@ packages: dependencies: '@jest/fake-timers': 28.1.3 '@jest/types': 28.1.3 - '@types/node': 20.3.2 + '@types/node': 18.16.18 jest-mock: 28.1.3 dev: false @@ -2560,7 +2563,7 @@ packages: dependencies: '@jest/fake-timers': 29.3.1 '@jest/types': 29.3.1 - '@types/node': 20.3.2 + '@types/node': 18.16.18 jest-mock: 29.3.1 dev: false @@ -2604,7 +2607,7 @@ packages: dependencies: '@jest/types': 28.1.3 '@sinonjs/fake-timers': 9.1.2 - '@types/node': 20.3.2 + '@types/node': 18.16.18 jest-message-util: 28.1.3 jest-mock: 28.1.3 jest-util: 28.1.3 @@ -2616,7 +2619,7 @@ packages: dependencies: '@jest/types': 29.3.1 '@sinonjs/fake-timers': 9.1.2 - '@types/node': 20.3.2 + '@types/node': 18.16.18 jest-message-util: 29.3.1 jest-mock: 29.3.1 jest-util: 29.3.1 @@ -2660,7 +2663,7 @@ packages: '@jest/transform': 28.1.3 '@jest/types': 28.1.3 '@jridgewell/trace-mapping': 0.3.17 - '@types/node': 20.3.2 + '@types/node': 18.16.18 chalk: 4.1.2 collect-v8-coverage: 1.0.1 exit: 0.1.2 @@ -2779,7 +2782,7 @@ packages: '@jest/schemas': 28.1.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.3.2 + '@types/node': 18.16.18 '@types/yargs': 17.0.17 chalk: 4.1.2 dev: false @@ -2791,7 +2794,7 @@ packages: '@jest/schemas': 29.0.0 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.3.2 + '@types/node': 18.16.18 '@types/yargs': 17.0.17 chalk: 4.1.2 dev: false @@ -2938,6 +2941,10 @@ packages: resolution: {integrity: sha512-rWr/ryzOUi9r/zUA2GK2qLWGBIBmDeIojBQXuvR76pulHUoEGMJ2A7NWShUaA5AE90ha+l9tlsyGz2UioQE9cg==} dev: false + /@types/emscripten@1.39.6: + resolution: {integrity: sha512-H90aoynNhhkQP6DRweEjJp5vfUVdIj7tdPLsu7pq89vODD/lcugKfZOsfgwpvM6XUewEp2N5dCg1Uf3Qe55Dcg==} + dev: false + /@types/fnando__sparkline@0.3.4: resolution: {integrity: sha512-FWU1zw7CVJYVeDk77FGphTUabfPims4F/Yq+WFB0Gh647lLtiXHWn8vpfT95Fl65IsNBDOhEbxJdhmERMGubNQ==} dev: false @@ -2949,7 +2956,7 @@ packages: /@types/graceful-fs@4.1.5: resolution: {integrity: sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==} dependencies: - '@types/node': 20.3.2 + '@types/node': 18.16.18 dev: false /@types/highcharts@4.2.57: @@ -2984,7 +2991,7 @@ packages: /@types/jsdom@16.2.15: resolution: {integrity: sha512-nwF87yjBKuX/roqGYerZZM0Nv1pZDMAT5YhOHYeM/72Fic+VEqJh4nyoqoapzJnW3pUlfxPY5FhgsJtM+dRnQQ==} dependencies: - '@types/node': 20.3.2 + '@types/node': 18.16.18 '@types/parse5': 6.0.3 '@types/tough-cookie': 4.0.2 dev: false @@ -3001,6 +3008,10 @@ packages: resolution: {integrity: sha512-vOBLVQeCQfIcF/2Y7eKFTqrMnizK5lRNQ7ykML/5RuwVXVWxYkgwS7xbt4B6fKCUPgbSL5FSsjHQpaGQP/dQmw==} dev: false + /@types/node@20.4.4: + resolution: {integrity: sha512-CukZhumInROvLq3+b5gLev+vgpsIqC2D0deQr/yS1WnxvmYLlJXZpaQrQiseMY+6xusl79E04UjWoqyr+t1/Ew==} + dev: false + /@types/parse5@6.0.3: resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} dev: false @@ -4428,7 +4439,7 @@ packages: '@jest/expect': 28.1.3 '@jest/test-result': 28.1.3 '@jest/types': 28.1.3 - '@types/node': 20.3.2 + '@types/node': 18.16.18 chalk: 4.1.2 co: 4.6.0 dedent: 0.7.0 @@ -4514,45 +4525,6 @@ packages: - supports-color dev: false - /jest-config@28.1.3(@types/node@20.3.2): - resolution: {integrity: sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - peerDependencies: - '@types/node': '*' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - ts-node: - optional: true - dependencies: - '@babel/core': 7.20.5 - '@jest/test-sequencer': 28.1.3 - '@jest/types': 28.1.3 - '@types/node': 20.3.2 - babel-jest: 28.1.3(@babel/core@7.20.5) - chalk: 4.1.2 - ci-info: 3.7.0 - deepmerge: 4.2.2 - glob: 7.2.3 - graceful-fs: 4.2.10 - jest-circus: 28.1.3 - jest-environment-node: 28.1.3 - jest-get-type: 28.0.2 - jest-regex-util: 28.0.2 - jest-resolve: 28.1.3 - jest-runner: 28.1.3 - jest-util: 28.1.3 - jest-validate: 28.1.3 - micromatch: 4.0.5 - parse-json: 5.2.0 - pretty-format: 28.1.3 - slash: 3.0.0 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - dev: false - /jest-diff@28.1.3: resolution: {integrity: sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -4617,7 +4589,7 @@ packages: '@jest/environment': 28.1.3 '@jest/fake-timers': 28.1.3 '@jest/types': 28.1.3 - '@types/node': 20.3.2 + '@types/node': 18.16.18 jest-mock: 28.1.3 jest-util: 28.1.3 dev: false @@ -4638,7 +4610,7 @@ packages: dependencies: '@jest/types': 28.1.3 '@types/graceful-fs': 4.1.5 - '@types/node': 20.3.2 + '@types/node': 18.16.18 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.10 @@ -4657,7 +4629,7 @@ packages: dependencies: '@jest/types': 29.3.1 '@types/graceful-fs': 4.1.5 - '@types/node': 20.3.2 + '@types/node': 18.16.18 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.10 @@ -4733,7 +4705,7 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: '@jest/types': 28.1.3 - '@types/node': 20.3.2 + '@types/node': 18.16.18 dev: false /jest-mock@29.3.1: @@ -4741,7 +4713,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.3.1 - '@types/node': 20.3.2 + '@types/node': 18.16.18 jest-util: 29.3.1 dev: false @@ -4801,7 +4773,7 @@ packages: '@jest/test-result': 28.1.3 '@jest/transform': 28.1.3 '@jest/types': 28.1.3 - '@types/node': 20.3.2 + '@types/node': 18.16.18 chalk: 4.1.2 emittery: 0.10.2 graceful-fs: 4.2.10 @@ -4919,7 +4891,7 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: '@jest/types': 28.1.3 - '@types/node': 20.3.2 + '@types/node': 18.16.18 chalk: 4.1.2 ci-info: 3.7.0 graceful-fs: 4.2.10 @@ -4931,7 +4903,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.3.1 - '@types/node': 20.3.2 + '@types/node': 18.16.18 chalk: 4.1.2 ci-info: 3.7.0 graceful-fs: 4.2.10 @@ -4956,7 +4928,7 @@ packages: dependencies: '@jest/test-result': 28.1.3 '@jest/types': 28.1.3 - '@types/node': 20.3.2 + '@types/node': 18.16.18 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.10.2 @@ -4968,7 +4940,7 @@ packages: resolution: {integrity: sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: - '@types/node': 20.3.2 + '@types/node': 18.16.18 merge-stream: 2.0.0 supports-color: 8.1.1 dev: false @@ -4977,7 +4949,7 @@ packages: resolution: {integrity: sha512-lY4AnnmsEWeiXirAIA0c9SDPbuCBq8IYuDVL8PMm0MZ2PEs2yPvRA/J64QBXuZp7CYKrDM/rmNrc9/i3KJQncw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 20.3.2 + '@types/node': 18.16.18 jest-util: 29.3.1 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -5961,7 +5933,7 @@ packages: punycode: 2.3.0 dev: false - /ts-jest@28.0.8(@babel/core@7.20.5)(jest@28.1.3)(typescript@5.1.5): + /ts-jest@28.0.8(@babel/core@7.20.5)(jest@28.1.3)(typescript@5.1.6): resolution: {integrity: sha512-5FaG0lXmRPzApix8oFG8RKjAz4ehtm8yMKOTy5HX3fY6W8kmvOrmcY0hKDElW52FJov+clhUbrKAqofnj4mXTg==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} hasBin: true @@ -5991,7 +5963,7 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.3.8 - typescript: 5.1.5 + typescript: 5.1.6 yargs-parser: 21.1.1 dev: false @@ -6048,6 +6020,12 @@ packages: hasBin: true dev: false + /typescript@5.1.6: + resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} + engines: {node: '>=14.17'} + hasBin: true + dev: false + /unicode-canonical-property-names-ecmascript@2.0.0: resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} engines: {node: '>=4'} @@ -6278,6 +6256,16 @@ packages: engines: {node: '>=10'} dev: false + /zerofish@0.0.2: + resolution: {integrity: sha512-8TjuOI/pgWWp6iOAA9hS+qJvnNVOc+fPLFKgqfO7EahONnD78s1hd0ItqpfsGCoTf0JDjTexENPrSX2pPKPWTQ==} + dependencies: + '@types/emscripten': 1.39.6 + '@types/node': 20.4.4 + '@types/web': 0.0.84 + esbuild: 0.18.4 + typescript: 5.1.6 + dev: false + /zxcvbn@4.4.2: resolution: {integrity: sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ==} dev: false diff --git a/ui/localPlay/package.json b/ui/localPlay/package.json index c1862396171ee..2b5ba5391f466 100644 --- a/ui/localPlay/package.json +++ b/ui/localPlay/package.json @@ -15,7 +15,8 @@ "nvui": "workspace:*", "puz": "workspace:*", "snabbdom": "^3.5.1", - "tree": "workspace:*" + "tree": "workspace:*", + "zerofish": "0.0.2" }, "scripts": { "compile": "tsc", diff --git a/ui/localPlay/src/chessground.ts b/ui/localPlay/src/chessground.ts index 501bad1845b69..d96f894f74d51 100644 --- a/ui/localPlay/src/chessground.ts +++ b/ui/localPlay/src/chessground.ts @@ -21,14 +21,14 @@ export function makeConfig(ctrl: Ctrl): CgConfig { turnColor: opts.turnColor, check: opts.check, lastMove: opts.lastMove, - coordinates: pref.coords !== Prefs.Coords.Hidden, + coordinates: true, //pref.coords !== Prefs.Coords.Hidden, addPieceZIndex: pref.is3d, addDimensionsCssVarsTo: document.body, movable: { free: false, color: opts.movable!.color, dests: opts.movable!.dests, - showDests: pref.destination, + showDests: true, //pref.destination, rookCastle: pref.rookCastle, }, draggable: { diff --git a/ui/localPlay/src/ctrl.ts b/ui/localPlay/src/ctrl.ts index 6e8c9c3c5104a..53a78ca472a70 100644 --- a/ui/localPlay/src/ctrl.ts +++ b/ui/localPlay/src/ctrl.ts @@ -1,24 +1,40 @@ import { LocalPlayOpts } from './interfaces'; import { PromotionCtrl } from 'chess/promotion'; import { prop, Prop } from 'common'; -//import { makeFen } from 'chessops/fen'; +import { makeBoardFen } from 'chessops/fen'; +import { Chess, makeSquare, parseSquare } from 'chessops'; +import makeZerofish, { Zerofish } from 'zerofish'; import { Api as CgApi } from 'chessground/api'; import { Config as CgConfig } from 'chessground/config'; +import { Key } from 'chessground/types'; export class Ctrl { promotion: PromotionCtrl; ground = prop(false) as Prop; flipped = false; - redraw: () => void = () => {}; + chess = Chess.default(); + zf: Zerofish; - constructor(readonly opts: LocalPlayOpts) { + constructor(readonly opts: LocalPlayOpts, readonly redraw: () => void) { this.promotion = new PromotionCtrl(this.withGround, this.setGround, this.redraw); + makeZerofish().then(zf => (this.zf = zf)); } getCgOpts = (): CgConfig => { + const cgDests = new Map( + [...this.chess.allDests()].map( + ([s, ds]) => [makeSquare(s), [...ds].map(d => makeSquare(d))] as [Key, Key[]] + ) + ); + console.log(this.chess); return { + fen: makeBoardFen(this.chess.board), + orientation: this.flipped ? 'black' : 'white', + turnColor: this.chess.turn, + movable: { - color: 'both', + color: 'white', //this.chess.turn, + dests: cgDests, }, }; }; @@ -28,9 +44,32 @@ export class Ctrl { return g && f(g); }; + apiMove = (uci: Uci) => { + this.chess.play({ from: parseSquare(uci.slice(0, 2))!, to: parseSquare(uci.slice(2))! }); + this.withGround(g => { + g.move(uci.slice(0, 2) as Key, uci.slice(2) as Key); + g.set({ + turnColor: 'white', //this.chess.turn, + movable: { + dests: new Map( + [...this.chess.allDests()].map( + ([s, ds]) => [makeSquare(s), [...ds].map(d => makeSquare(d))] as [Key, Key[]] + ) + ), + }, + }); + }); + this.redraw(); + }; + userMove = (orig: Key, dest: Key) => { - orig; - dest; + this.chess.play({ from: parseSquare(orig)!, to: parseSquare(dest)! }); + console.log('userMove', this.chess); + //this.redraw(); + this.zf.goFish(makeBoardFen(this.chess.board), { depth: 10 }).then(m => { + console.log('zf', m); + this.apiMove(m[0].moves[0]); + }); }; flip = () => { diff --git a/ui/localPlay/src/main.ts b/ui/localPlay/src/main.ts index 9999ec12016c0..7e8f05286173c 100644 --- a/ui/localPlay/src/main.ts +++ b/ui/localPlay/src/main.ts @@ -9,10 +9,10 @@ const patch = init([classModule, attributesModule]); export async function initModule(opts: LocalPlayOpts) { // make a StrongSocket - const ctrl = new Ctrl(opts); + const ctrl = new Ctrl(opts, redraw); const blueprint = view(ctrl as Controller); - const element = document.querySelector('main#local-play') as HTMLElement; + const element = document.querySelector('main') as HTMLElement; element.innerHTML = ''; let vnode = patch(element, blueprint); diff --git a/ui/localPlay/src/view.ts b/ui/localPlay/src/view.ts index 7b48b99137b4a..254888a29577b 100644 --- a/ui/localPlay/src/view.ts +++ b/ui/localPlay/src/view.ts @@ -7,10 +7,11 @@ import { makeConfig as makeCgConfig } from './chessground'; //import { onInsert } from 'common/snabbdom'; export default function (ctrl: any): VNode { - return h('div.local-play', renderPlay(ctrl)); + return h('div#local-play', renderPlay(ctrl)); } function chessground(ctrl: any): VNode { + console.log('chessground'); return h('div.cg-wrap', { hook: { insert: vnode => ctrl.ground(Chessground(vnode.elm as HTMLElement, makeCgConfig(ctrl))), From c93e10c8b150e203e09f0d9c9c05278db99d5203 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Mon, 24 Jul 2023 11:41:54 -0500 Subject: [PATCH 003/174] gah --- pnpm-lock.yaml | 20 +-------- ui/localPlay/css/_local-play.scss | 29 ++++++++++++- ui/localPlay/package.json | 7 +++- ui/localPlay/src/chessground.ts | 4 +- ui/localPlay/src/ctrl.ts | 69 ++++++++++++++++++++++++------- ui/localPlay/src/view.ts | 43 +++++++++---------- 6 files changed, 115 insertions(+), 57 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98acb0df3d0de..160b16f225ad9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -492,7 +492,7 @@ importers: version: link:../tree zerofish: specifier: 0.0.2 - version: 0.0.2 + version: link:../../../zerofish ui/mod: dependencies: @@ -2941,10 +2941,6 @@ packages: resolution: {integrity: sha512-rWr/ryzOUi9r/zUA2GK2qLWGBIBmDeIojBQXuvR76pulHUoEGMJ2A7NWShUaA5AE90ha+l9tlsyGz2UioQE9cg==} dev: false - /@types/emscripten@1.39.6: - resolution: {integrity: sha512-H90aoynNhhkQP6DRweEjJp5vfUVdIj7tdPLsu7pq89vODD/lcugKfZOsfgwpvM6XUewEp2N5dCg1Uf3Qe55Dcg==} - dev: false - /@types/fnando__sparkline@0.3.4: resolution: {integrity: sha512-FWU1zw7CVJYVeDk77FGphTUabfPims4F/Yq+WFB0Gh647lLtiXHWn8vpfT95Fl65IsNBDOhEbxJdhmERMGubNQ==} dev: false @@ -3008,10 +3004,6 @@ packages: resolution: {integrity: sha512-vOBLVQeCQfIcF/2Y7eKFTqrMnizK5lRNQ7ykML/5RuwVXVWxYkgwS7xbt4B6fKCUPgbSL5FSsjHQpaGQP/dQmw==} dev: false - /@types/node@20.4.4: - resolution: {integrity: sha512-CukZhumInROvLq3+b5gLev+vgpsIqC2D0deQr/yS1WnxvmYLlJXZpaQrQiseMY+6xusl79E04UjWoqyr+t1/Ew==} - dev: false - /@types/parse5@6.0.3: resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} dev: false @@ -6256,16 +6248,6 @@ packages: engines: {node: '>=10'} dev: false - /zerofish@0.0.2: - resolution: {integrity: sha512-8TjuOI/pgWWp6iOAA9hS+qJvnNVOc+fPLFKgqfO7EahONnD78s1hd0ItqpfsGCoTf0JDjTexENPrSX2pPKPWTQ==} - dependencies: - '@types/emscripten': 1.39.6 - '@types/node': 20.4.4 - '@types/web': 0.0.84 - esbuild: 0.18.4 - typescript: 5.1.6 - dev: false - /zxcvbn@4.4.2: resolution: {integrity: sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ==} dev: false diff --git a/ui/localPlay/css/_local-play.scss b/ui/localPlay/css/_local-play.scss index d58f8b37a7068..3ed1cc35b44b1 100644 --- a/ui/localPlay/css/_local-play.scss +++ b/ui/localPlay/css/_local-play.scss @@ -8,7 +8,7 @@ $mq-col2: $mq-col2-uniboard; grid-template-areas: 'board' 'side'; @include breakpoint($mq-col2) { - grid-template-columns: var(--col2-uniboard-width) $col2-uniboard-table; + grid-template-columns: var(--col2-uniboard-width) auto; grid-template-rows: fit-content(0); grid-template-areas: 'board side'; } @@ -18,4 +18,31 @@ $mq-col2: $mq-col2-uniboard; text-align: center; font-size: 0.8em; } + + .puz-side { + .puz-bot { + width: 300px; + height: 100px; + border: 2px dashed #888; + } + .puz-bot.hilite { + background-color: #888; + } + #pgn { + height: 100%; + } + span { + @extend %flex-between; + * { + margin: 0 15px; + } + } + #num-games { + width: 60px; + } + #go { + flex: auto; + padding: 15px 30px; + } + } } diff --git a/ui/localPlay/package.json b/ui/localPlay/package.json index 2b5ba5391f466..5e121da12aa0a 100644 --- a/ui/localPlay/package.json +++ b/ui/localPlay/package.json @@ -28,6 +28,11 @@ "esm": { "src/main.ts": "localPlay" } - } + }, + "copy": [ + { "src": "node_modules/zerofish/dist/zerofishEngine.js", "dest": "../../public/compiled" }, + { "src": "node_modules/zerofish/dist/zerofishEngine.wasm", "dest": "../../public/compiled" }, + { "src": "node_modules/zerofish/dist/zerofishEngine.worker.js", "dest": "../../public/compiled" } + ] } } diff --git a/ui/localPlay/src/chessground.ts b/ui/localPlay/src/chessground.ts index d96f894f74d51..de1a5583fd283 100644 --- a/ui/localPlay/src/chessground.ts +++ b/ui/localPlay/src/chessground.ts @@ -30,6 +30,9 @@ export function makeConfig(ctrl: Ctrl): CgConfig { dests: opts.movable!.dests, showDests: true, //pref.destination, rookCastle: pref.rookCastle, + events: { + after: ctrl.userMove, + }, }, draggable: { enabled: pref.moveEvent > 0, @@ -39,7 +42,6 @@ export function makeConfig(ctrl: Ctrl): CgConfig { enabled: pref.moveEvent !== 1, }, events: { - move: ctrl.userMove, insert(elements) { resizeHandle(elements, Prefs.ShowResizeHandle.OnlyAtStart, 0, p => p == 0); }, diff --git a/ui/localPlay/src/ctrl.ts b/ui/localPlay/src/ctrl.ts index 53a78ca472a70..cec6d93f499a9 100644 --- a/ui/localPlay/src/ctrl.ts +++ b/ui/localPlay/src/ctrl.ts @@ -1,7 +1,7 @@ import { LocalPlayOpts } from './interfaces'; import { PromotionCtrl } from 'chess/promotion'; import { prop, Prop } from 'common'; -import { makeBoardFen } from 'chessops/fen'; +import { makeFen } from 'chessops/fen'; import { Chess, makeSquare, parseSquare } from 'chessops'; import makeZerofish, { Zerofish } from 'zerofish'; import { Api as CgApi } from 'chessground/api'; @@ -13,27 +13,57 @@ export class Ctrl { ground = prop(false) as Prop; flipped = false; chess = Chess.default(); - zf: Zerofish; + zf: { white: Zerofish; black: Zerofish }; + whiteEl: Cash; + blackEl: Cash; constructor(readonly opts: LocalPlayOpts, readonly redraw: () => void) { this.promotion = new PromotionCtrl(this.withGround, this.setGround, this.redraw); - makeZerofish().then(zf => (this.zf = zf)); + Promise.all([makeZerofish(), makeZerofish()]).then(([wz, bz]) => { + this.zf ??= { white: wz, black: bz }; + }); } + dropHandler(color: 'white' | 'black', el: HTMLElement) { + const $el = $(el); + $el.on('dragenter dragover dragleave drop', e => { + console.log('gibbins'); + e.preventDefault(); + e.stopPropagation(); + }); + $el.on('dragenter dragover', e => $(e.eventTarget).addClass('hilite')); + $el.on('dragleave drop', e => $(e.eventTarget).removeClass('hilite')); + $el.on('drop', e => { + const reader = new FileReader(); + const weights = e.dataTransfer.files.item(0) as File; + reader.onload = e => this.setZero(color, weights, new Uint8Array(e.target!.result as ArrayBuffer)); + reader.readAsArrayBuffer(weights); + }); + } + setZero(color: 'white' | 'black', f: File, data: Uint8Array) { + this.zf[color].setZeroWeights(data); + $(`#${color}`).text(f.name); + } + go() { + //const numTimes = parseInt($('#num-games').val() as string) || 1; + this.chess.reset(); + this.zf.white.goZero(makeFen(this.chess.toSetup())).then(m => { + this.apiMove(m); + }); + } getCgOpts = (): CgConfig => { const cgDests = new Map( [...this.chess.allDests()].map( ([s, ds]) => [makeSquare(s), [...ds].map(d => makeSquare(d))] as [Key, Key[]] ) ); - console.log(this.chess); return { - fen: makeBoardFen(this.chess.board), + fen: makeFen(this.chess.toSetup()), orientation: this.flipped ? 'black' : 'white', turnColor: this.chess.turn, movable: { - color: 'white', //this.chess.turn, + color: this.chess.turn, dests: cgDests, }, }; @@ -45,31 +75,42 @@ export class Ctrl { }; apiMove = (uci: Uci) => { + console.log('apiMove', uci); + console.log(`apiMove ${uci} by ${this.chess.turn}`); this.chess.play({ from: parseSquare(uci.slice(0, 2))!, to: parseSquare(uci.slice(2))! }); + console.log('apiMove after', this.chess.turn); this.withGround(g => { g.move(uci.slice(0, 2) as Key, uci.slice(2) as Key); g.set({ - turnColor: 'white', //this.chess.turn, - movable: { + turnColor: this.chess.turn, + /*movable: { dests: new Map( [...this.chess.allDests()].map( ([s, ds]) => [makeSquare(s), [...ds].map(d => makeSquare(d))] as [Key, Key[]] ) ), - }, + },*/ }); }); this.redraw(); + setTimeout(() => { + console.log('calling makeZero'); + this.zf[this.chess.turn] + .goZero(makeFen(this.chess.toSetup())) + .then(m => { + console.log('makeZero returned', m); + this.apiMove(m); + }) + .catch(e => console.log('makeZero error', e)); + }); }; userMove = (orig: Key, dest: Key) => { this.chess.play({ from: parseSquare(orig)!, to: parseSquare(dest)! }); - console.log('userMove', this.chess); - //this.redraw(); - this.zf.goFish(makeBoardFen(this.chess.board), { depth: 10 }).then(m => { - console.log('zf', m); + + /*this.zf.goFish(makeBoardFen(this.chess.board), { depth: 10 }).then(m => { this.apiMove(m[0].moves[0]); - }); + });*/ }; flip = () => { diff --git a/ui/localPlay/src/view.ts b/ui/localPlay/src/view.ts index 254888a29577b..ce9355ece3f79 100644 --- a/ui/localPlay/src/view.ts +++ b/ui/localPlay/src/view.ts @@ -1,17 +1,13 @@ -//import { Controller } from './interfaces'; import { Chessground } from 'chessground'; import { h, VNode } from 'snabbdom'; import { makeConfig as makeCgConfig } from './chessground'; -//import { getNow } from 'puz/util'; -//import * as licon from 'common/licon'; -//import { onInsert } from 'common/snabbdom'; +import { onInsert } from 'common/snabbdom'; export default function (ctrl: any): VNode { return h('div#local-play', renderPlay(ctrl)); } function chessground(ctrl: any): VNode { - console.log('chessground'); return h('div.cg-wrap', { hook: { insert: vnode => ctrl.ground(Chessground(vnode.elm as HTMLElement, makeCgConfig(ctrl))), @@ -23,23 +19,28 @@ function renderPlay(ctrl: any): VNode[] { return [ h('div.puz-board.main-board', [chessground(ctrl), ctrl.promotion.view()]), h('div.puz-side', [ - renderStart(ctrl), - h('div.puz-bots', [ - // ... + h( + 'div', + h('div#black.puz-bot', { hook: onInsert(el => ctrl.dropHandler('black', el)) }, [ + h('p', 'Drop black weights here'), + ]) + ), + h('div#pgn'), + h('div', [ + h('div#white.puz-bot', { hook: onInsert(el => ctrl.dropHandler('white', el)) }, [ + h('p', 'Drop white weights here'), + ]), + h('hr'), + h('span', [ + h('input#num-games', { attrs: { type: 'text', value: '1' } }), + 'games', + h( + 'button#go.button', + { hook: onInsert(el => el.addEventListener('click', ctrl.go.bind(ctrl))) }, + 'GO' + ), + ]), ]), - h('div.puz-side__table', [renderControls(ctrl)]), ]), ]; } - -function renderControls(ctrl: any): VNode { - ctrl; - return h('div.puz-side__control', ['gah']); -} - -function renderStart(ctrl: any): VNode { - ctrl; - return h('div.puz-side__top.puz-side__start', [ - h('div.puz-side__start__text', [h('strong', 'Play vs Bots'), h('span', 'gah')]), - ]); -} From 98e2f29b4392fa30d66a34f67f6667c2b1b6bd0a Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Mon, 24 Jul 2023 16:06:04 -0500 Subject: [PATCH 004/174] fixes --- pnpm-lock.yaml | 2 +- ui/localPlay/css/_local-play.scss | 4 -- ui/localPlay/package.json | 2 +- ui/localPlay/src/ctrl.ts | 98 ++++++++++++++++--------------- ui/localPlay/src/view.ts | 17 +++--- 5 files changed, 60 insertions(+), 63 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 160b16f225ad9..a946a70e63321 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -491,7 +491,7 @@ importers: specifier: workspace:* version: link:../tree zerofish: - specifier: 0.0.2 + specifier: ^0.0.3 version: link:../../../zerofish ui/mod: diff --git a/ui/localPlay/css/_local-play.scss b/ui/localPlay/css/_local-play.scss index 3ed1cc35b44b1..3db4c390dada4 100644 --- a/ui/localPlay/css/_local-play.scss +++ b/ui/localPlay/css/_local-play.scss @@ -37,12 +37,8 @@ $mq-col2: $mq-col2-uniboard; margin: 0 15px; } } - #num-games { - width: 60px; - } #go { flex: auto; - padding: 15px 30px; } } } diff --git a/ui/localPlay/package.json b/ui/localPlay/package.json index 5e121da12aa0a..4144302423169 100644 --- a/ui/localPlay/package.json +++ b/ui/localPlay/package.json @@ -16,7 +16,7 @@ "puz": "workspace:*", "snabbdom": "^3.5.1", "tree": "workspace:*", - "zerofish": "0.0.2" + "zerofish": "^0.0.4" }, "scripts": { "compile": "tsc", diff --git a/ui/localPlay/src/ctrl.ts b/ui/localPlay/src/ctrl.ts index cec6d93f499a9..87da58b5f5913 100644 --- a/ui/localPlay/src/ctrl.ts +++ b/ui/localPlay/src/ctrl.ts @@ -1,24 +1,23 @@ import { LocalPlayOpts } from './interfaces'; import { PromotionCtrl } from 'chess/promotion'; -import { prop, Prop } from 'common'; import { makeFen } from 'chessops/fen'; -import { Chess, makeSquare, parseSquare } from 'chessops'; +import { Chess, makeSquare, parseSquare, opposite } from 'chessops'; import makeZerofish, { Zerofish } from 'zerofish'; import { Api as CgApi } from 'chessground/api'; import { Config as CgConfig } from 'chessground/config'; import { Key } from 'chessground/types'; +type Player = 'human' | 'zero' | 'fish'; export class Ctrl { promotion: PromotionCtrl; - ground = prop(false) as Prop; + cg: CgApi; flipped = false; chess = Chess.default(); zf: { white: Zerofish; black: Zerofish }; - whiteEl: Cash; - blackEl: Cash; + players: { white: Player; black: Player } = { white: 'human', black: 'fish' }; constructor(readonly opts: LocalPlayOpts, readonly redraw: () => void) { - this.promotion = new PromotionCtrl(this.withGround, this.setGround, this.redraw); + this.promotion = new PromotionCtrl(f => f(this.cg), this.setGround, this.redraw); Promise.all([makeZerofish(), makeZerofish()]).then(([wz, bz]) => { this.zf ??= { white: wz, black: bz }; }); @@ -27,12 +26,11 @@ export class Ctrl { dropHandler(color: 'white' | 'black', el: HTMLElement) { const $el = $(el); $el.on('dragenter dragover dragleave drop', e => { - console.log('gibbins'); e.preventDefault(); e.stopPropagation(); }); - $el.on('dragenter dragover', e => $(e.eventTarget).addClass('hilite')); - $el.on('dragleave drop', e => $(e.eventTarget).removeClass('hilite')); + $el.on('dragenter dragover', _ => $el.addClass('hilite')); + $el.on('dragleave drop', _ => $el.removeClass('hilite')); $el.on('drop', e => { const reader = new FileReader(); const weights = e.dataTransfer.files.item(0) as File; @@ -43,13 +41,17 @@ export class Ctrl { setZero(color: 'white' | 'black', f: File, data: Uint8Array) { this.zf[color].setZeroWeights(data); $(`#${color}`).text(f.name); + this.players[color] = 'zero'; + if (this.players[opposite(color)] !== 'human') $('#go').removeClass('disabled'); } go() { //const numTimes = parseInt($('#num-games').val() as string) || 1; this.chess.reset(); - this.zf.white.goZero(makeFen(this.chess.toSetup())).then(m => { - this.apiMove(m); - }); + this.cg.set({ fen: makeFen(this.chess.toSetup()) }); + console.log(makeFen(this.chess.toSetup())); + this.zf.white.reset(); + this.zf.black.reset(); + this.botMove(); } getCgOpts = (): CgConfig => { const cgDests = new Map( @@ -69,55 +71,55 @@ export class Ctrl { }; }; - withGround = (f: (cg: CgApi) => A): A | false => { - const g = this.ground(); - return g && f(g); - }; - apiMove = (uci: Uci) => { - console.log('apiMove', uci); - console.log(`apiMove ${uci} by ${this.chess.turn}`); this.chess.play({ from: parseSquare(uci.slice(0, 2))!, to: parseSquare(uci.slice(2))! }); - console.log('apiMove after', this.chess.turn); - this.withGround(g => { - g.move(uci.slice(0, 2) as Key, uci.slice(2) as Key); - g.set({ - turnColor: this.chess.turn, - /*movable: { - dests: new Map( - [...this.chess.allDests()].map( - ([s, ds]) => [makeSquare(s), [...ds].map(d => makeSquare(d))] as [Key, Key[]] - ) - ), - },*/ - }); + this.cg.move(uci.slice(0, 2) as Key, uci.slice(2) as Key); + this.cg.set({ + turnColor: this.chess.turn, + movable: { + dests: new Map( + [...this.chess.allDests()].map( + ([s, ds]) => [makeSquare(s), [...ds].map(d => makeSquare(d))] as [Key, Key[]] + ) + ), + }, + check: this.chess.isCheck(), }); this.redraw(); - setTimeout(() => { - console.log('calling makeZero'); - this.zf[this.chess.turn] - .goZero(makeFen(this.chess.toSetup())) - .then(m => { - console.log('makeZero returned', m); - this.apiMove(m); - }) - .catch(e => console.log('makeZero error', e)); - }); + if (this.chess.isEnd()) { + console.log('game over'); + return; + } + if (this.players[this.chess.turn] === 'human') return; + setTimeout(() => this.botMove()); }; userMove = (orig: Key, dest: Key) => { this.chess.play({ from: parseSquare(orig)!, to: parseSquare(dest)! }); - - /*this.zf.goFish(makeBoardFen(this.chess.board), { depth: 10 }).then(m => { - this.apiMove(m[0].moves[0]); - });*/ + if (this.chess.isEnd()) { + console.log('game over'); + return; + } + this.botMove(); }; + botMove() { + if (this.players[this.chess.turn] === 'human') return; + if (this.players[this.chess.turn] === 'zero') { + this.zf[this.chess.turn].goZero(makeFen(this.chess.toSetup())).then(m => { + this.apiMove(m); + }); + } else { + this.zf[this.chess.turn].goFish(makeFen(this.chess.toSetup()), { depth: 10 }).then(pvs => { + this.apiMove(pvs[0].moves[0]); + }); + } + } flip = () => { this.flipped = !this.flipped; - this.withGround(g => g.toggleOrientation()); + this.cg.toggleOrientation(); this.redraw(); }; - private setGround = () => this.withGround(g => g.set(this.getCgOpts())); + private setGround = () => this.cg.set(this.getCgOpts()); } diff --git a/ui/localPlay/src/view.ts b/ui/localPlay/src/view.ts index ce9355ece3f79..57639c538bf96 100644 --- a/ui/localPlay/src/view.ts +++ b/ui/localPlay/src/view.ts @@ -10,7 +10,7 @@ export default function (ctrl: any): VNode { function chessground(ctrl: any): VNode { return h('div.cg-wrap', { hook: { - insert: vnode => ctrl.ground(Chessground(vnode.elm as HTMLElement, makeCgConfig(ctrl))), + insert: vnode => (ctrl.cg = Chessground(vnode.elm as HTMLElement, makeCgConfig(ctrl))), }, }); } @@ -22,24 +22,23 @@ function renderPlay(ctrl: any): VNode[] { h( 'div', h('div#black.puz-bot', { hook: onInsert(el => ctrl.dropHandler('black', el)) }, [ - h('p', 'Drop black weights here'), + h('p', 'Drop black weights here (otherwise stockfish)'), ]) ), h('div#pgn'), h('div', [ h('div#white.puz-bot', { hook: onInsert(el => ctrl.dropHandler('white', el)) }, [ - h('p', 'Drop white weights here'), + h('p', 'Drop white weights here (otherwise human)'), ]), h('hr'), - h('span', [ - h('input#num-games', { attrs: { type: 'text', value: '1' } }), - 'games', + h( + 'span', h( - 'button#go.button', + 'button#go.button.disabled', { hook: onInsert(el => el.addEventListener('click', ctrl.go.bind(ctrl))) }, 'GO' - ), - ]), + ) + ), ]), ]), ]; From 1a9952939909920ed01dedaa6f68040c9187f259 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Mon, 24 Jul 2023 16:18:31 -0500 Subject: [PATCH 005/174] version --- pnpm-lock.yaml | 2560 +++++++++++++++++++++---------------- ui/localPlay/package.json | 2 +- 2 files changed, 1488 insertions(+), 1074 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a946a70e63321..71f972fe363b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,13 +49,13 @@ importers: dependencies: '@babel/core': specifier: ^7.17.10 - version: 7.20.5 + version: 7.17.10 '@babel/preset-env': specifier: ^7.17.10 - version: 7.20.2(@babel/core@7.20.5) + version: 7.17.10(@babel/core@7.17.10) '@types/jest': specifier: ^28.1.6 - version: 28.1.8 + version: 28.1.6 breakpoint-sass: specifier: ^2.7.1 version: 2.7.1 @@ -67,7 +67,7 @@ importers: version: 28.1.3 ts-jest: specifier: ^28.0.7 - version: 28.0.8(@babel/core@7.20.5)(jest@28.1.3)(typescript@5.1.6) + version: 28.0.7(@babel/core@7.17.10)(jest@28.1.3)(typescript@5.1.6) ui/@build: dependencies: @@ -491,8 +491,8 @@ importers: specifier: workspace:* version: link:../tree zerofish: - specifier: ^0.0.3 - version: link:../../../zerofish + specifier: ^0.0.4 + version: 0.0.4 ui/mod: dependencies: @@ -1026,1186 +1026,1324 @@ packages: engines: {node: '>=0.10.0'} dev: false - /@ampproject/remapping@2.2.0: - resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==} + /@ampproject/remapping@2.2.1: + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} engines: {node: '>=6.0.0'} dependencies: - '@jridgewell/gen-mapping': 0.1.1 - '@jridgewell/trace-mapping': 0.3.17 + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.18 dev: false - /@babel/code-frame@7.18.6: - resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} + /@babel/code-frame@7.22.5: + resolution: {integrity: sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/highlight': 7.18.6 + '@babel/highlight': 7.22.5 dev: false - /@babel/compat-data@7.20.5: - resolution: {integrity: sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g==} + /@babel/compat-data@7.22.9: + resolution: {integrity: sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==} engines: {node: '>=6.9.0'} dev: false - /@babel/core@7.20.5: - resolution: {integrity: sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ==} + /@babel/core@7.17.10: + resolution: {integrity: sha512-liKoppandF3ZcBnIYFjfSDHZLKdLHGJRkoWtG8zQyGJBQfIYobpnVGI5+pLBNtS6psFLDzyq8+h5HiVljW9PNA==} engines: {node: '>=6.9.0'} dependencies: - '@ampproject/remapping': 2.2.0 - '@babel/code-frame': 7.18.6 - '@babel/generator': 7.20.5 - '@babel/helper-compilation-targets': 7.20.0(@babel/core@7.20.5) - '@babel/helper-module-transforms': 7.20.2 - '@babel/helpers': 7.20.6 - '@babel/parser': 7.20.5 - '@babel/template': 7.18.10 - '@babel/traverse': 7.20.5 - '@babel/types': 7.20.5 + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.22.5 + '@babel/generator': 7.22.9 + '@babel/helper-compilation-targets': 7.22.9(@babel/core@7.17.10) + '@babel/helper-module-transforms': 7.22.9(@babel/core@7.17.10) + '@babel/helpers': 7.22.6 + '@babel/parser': 7.22.7 + '@babel/template': 7.22.5 + '@babel/traverse': 7.22.8 + '@babel/types': 7.22.5 convert-source-map: 1.9.0 debug: 4.3.4 gensync: 1.0.0-beta.2 - json5: 2.2.2 - semver: 6.3.0 + json5: 2.2.3 + semver: 6.3.1 transitivePeerDependencies: - supports-color dev: false - /@babel/generator@7.20.5: - resolution: {integrity: sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==} + /@babel/core@7.22.9: + resolution: {integrity: sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 - '@jridgewell/gen-mapping': 0.3.2 + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.22.5 + '@babel/generator': 7.22.9 + '@babel/helper-compilation-targets': 7.22.9(@babel/core@7.22.9) + '@babel/helper-module-transforms': 7.22.9(@babel/core@7.22.9) + '@babel/helpers': 7.22.6 + '@babel/parser': 7.22.7 + '@babel/template': 7.22.5 + '@babel/traverse': 7.22.8 + '@babel/types': 7.22.5 + convert-source-map: 1.9.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@babel/generator@7.22.9: + resolution: {integrity: sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.5 + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.18 jsesc: 2.5.2 dev: false - /@babel/helper-annotate-as-pure@7.18.6: - resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} + /@babel/helper-annotate-as-pure@7.22.5: + resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 dev: false - /@babel/helper-builder-binary-assignment-operator-visitor@7.18.9: - resolution: {integrity: sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==} + /@babel/helper-builder-binary-assignment-operator-visitor@7.22.5: + resolution: {integrity: sha512-m1EP3lVOPptR+2DwD125gziZNcmoNSHGmJROKoy87loWUQyJaVXDgpmruWqDARZSmtYQ+Dl25okU8+qhVzuykw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-explode-assignable-expression': 7.18.6 - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 dev: false - /@babel/helper-compilation-targets@7.20.0(@babel/core@7.20.5): - resolution: {integrity: sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==} + /@babel/helper-compilation-targets@7.22.9(@babel/core@7.17.10): + resolution: {integrity: sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/compat-data': 7.20.5 - '@babel/core': 7.20.5 - '@babel/helper-validator-option': 7.18.6 - browserslist: 4.21.4 - semver: 6.3.0 + '@babel/compat-data': 7.22.9 + '@babel/core': 7.17.10 + '@babel/helper-validator-option': 7.22.5 + browserslist: 4.21.9 + lru-cache: 5.1.1 + semver: 6.3.1 dev: false - /@babel/helper-create-class-features-plugin@7.20.5(@babel/core@7.20.5): - resolution: {integrity: sha512-3RCdA/EmEaikrhayahwToF0fpweU/8o2p8vhc1c/1kftHOdTKuC65kik/TLc+qfbS8JKw4qqJbne4ovICDhmww==} + /@babel/helper-compilation-targets@7.22.9(@babel/core@7.22.9): + resolution: {integrity: sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.19.0 - '@babel/helper-member-expression-to-functions': 7.18.9 - '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/helper-replace-supers': 7.19.1 - '@babel/helper-split-export-declaration': 7.18.6 - transitivePeerDependencies: - - supports-color + '@babel/compat-data': 7.22.9 + '@babel/core': 7.22.9 + '@babel/helper-validator-option': 7.22.5 + browserslist: 4.21.9 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: false + + /@babel/helper-create-class-features-plugin@7.22.9(@babel/core@7.17.10): + resolution: {integrity: sha512-Pwyi89uO4YrGKxL/eNJ8lfEH55DnRloGPOseaA8NFNL6jAUnn+KccaISiFazCj5IolPPDjGSdzQzXVzODVRqUQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.17.10 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-function-name': 7.22.5 + '@babel/helper-member-expression-to-functions': 7.22.5 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-replace-supers': 7.22.9(@babel/core@7.17.10) + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + semver: 6.3.1 dev: false - /@babel/helper-create-regexp-features-plugin@7.20.5(@babel/core@7.20.5): - resolution: {integrity: sha512-m68B1lkg3XDGX5yCvGO0kPx3v9WIYLnzjKfPcQiwntEQa5ZeRkPmo2X/ISJc8qxWGfwUr+kvZAeEzAwLec2r2w==} + /@babel/helper-create-regexp-features-plugin@7.22.9(@babel/core@7.17.10): + resolution: {integrity: sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-annotate-as-pure': 7.18.6 - regexpu-core: 5.2.2 + '@babel/core': 7.17.10 + '@babel/helper-annotate-as-pure': 7.22.5 + regexpu-core: 5.3.2 + semver: 6.3.1 dev: false - /@babel/helper-define-polyfill-provider@0.3.3(@babel/core@7.20.5): + /@babel/helper-define-polyfill-provider@0.3.3(@babel/core@7.17.10): resolution: {integrity: sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==} peerDependencies: '@babel/core': ^7.4.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-compilation-targets': 7.20.0(@babel/core@7.20.5) - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-compilation-targets': 7.22.9(@babel/core@7.17.10) + '@babel/helper-plugin-utils': 7.22.5 debug: 4.3.4 lodash.debounce: 4.0.8 - resolve: 1.22.1 - semver: 6.3.0 + resolve: 1.22.2 + semver: 6.3.1 transitivePeerDependencies: - supports-color dev: false - /@babel/helper-environment-visitor@7.18.9: - resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==} + /@babel/helper-environment-visitor@7.22.5: + resolution: {integrity: sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==} engines: {node: '>=6.9.0'} dev: false - /@babel/helper-explode-assignable-expression@7.18.6: - resolution: {integrity: sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==} + /@babel/helper-function-name@7.22.5: + resolution: {integrity: sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/template': 7.22.5 + '@babel/types': 7.22.5 dev: false - /@babel/helper-function-name@7.19.0: - resolution: {integrity: sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==} + /@babel/helper-hoist-variables@7.22.5: + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/template': 7.18.10 - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 dev: false - /@babel/helper-hoist-variables@7.18.6: - resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} + /@babel/helper-member-expression-to-functions@7.22.5: + resolution: {integrity: sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 dev: false - /@babel/helper-member-expression-to-functions@7.18.9: - resolution: {integrity: sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==} + /@babel/helper-module-imports@7.22.5: + resolution: {integrity: sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 dev: false - /@babel/helper-module-imports@7.18.6: - resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} + /@babel/helper-module-transforms@7.22.9(@babel/core@7.17.10): + resolution: {integrity: sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 dependencies: - '@babel/types': 7.20.5 + '@babel/core': 7.17.10 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-module-imports': 7.22.5 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.5 dev: false - /@babel/helper-module-transforms@7.20.2: - resolution: {integrity: sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==} + /@babel/helper-module-transforms@7.22.9(@babel/core@7.22.9): + resolution: {integrity: sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 dependencies: - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-simple-access': 7.20.2 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/helper-validator-identifier': 7.19.1 - '@babel/template': 7.18.10 - '@babel/traverse': 7.20.5 - '@babel/types': 7.20.5 - transitivePeerDependencies: - - supports-color + '@babel/core': 7.22.9 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-module-imports': 7.22.5 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.5 dev: false - /@babel/helper-optimise-call-expression@7.18.6: - resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==} + /@babel/helper-optimise-call-expression@7.22.5: + resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 dev: false - /@babel/helper-plugin-utils@7.20.2: - resolution: {integrity: sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==} + /@babel/helper-plugin-utils@7.22.5: + resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} engines: {node: '>=6.9.0'} dev: false - /@babel/helper-remap-async-to-generator@7.18.9(@babel/core@7.20.5): - resolution: {integrity: sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==} + /@babel/helper-remap-async-to-generator@7.22.9(@babel/core@7.17.10): + resolution: {integrity: sha512-8WWC4oR4Px+tr+Fp0X3RHDVfINGpF3ad1HIbrc8A77epiR6eMMc6jsgozkzT2uDiOOdoS9cLIQ+XD2XvI2WSmQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-wrap-function': 7.20.5 - '@babel/types': 7.20.5 - transitivePeerDependencies: - - supports-color + '@babel/core': 7.17.10 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-wrap-function': 7.22.9 dev: false - /@babel/helper-replace-supers@7.19.1: - resolution: {integrity: sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw==} + /@babel/helper-replace-supers@7.22.9(@babel/core@7.17.10): + resolution: {integrity: sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 dependencies: - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-member-expression-to-functions': 7.18.9 - '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/traverse': 7.20.5 - '@babel/types': 7.20.5 - transitivePeerDependencies: - - supports-color + '@babel/core': 7.17.10 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-member-expression-to-functions': 7.22.5 + '@babel/helper-optimise-call-expression': 7.22.5 dev: false - /@babel/helper-simple-access@7.20.2: - resolution: {integrity: sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==} + /@babel/helper-simple-access@7.22.5: + resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 dev: false - /@babel/helper-skip-transparent-expression-wrappers@7.20.0: - resolution: {integrity: sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==} + /@babel/helper-skip-transparent-expression-wrappers@7.22.5: + resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 dev: false - /@babel/helper-split-export-declaration@7.18.6: - resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} + /@babel/helper-split-export-declaration@7.22.6: + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 dev: false - /@babel/helper-string-parser@7.19.4: - resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==} + /@babel/helper-string-parser@7.22.5: + resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} engines: {node: '>=6.9.0'} dev: false - /@babel/helper-validator-identifier@7.19.1: - resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} + /@babel/helper-validator-identifier@7.22.5: + resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} engines: {node: '>=6.9.0'} dev: false - /@babel/helper-validator-option@7.18.6: - resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==} + /@babel/helper-validator-option@7.22.5: + resolution: {integrity: sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==} engines: {node: '>=6.9.0'} dev: false - /@babel/helper-wrap-function@7.20.5: - resolution: {integrity: sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==} + /@babel/helper-wrap-function@7.22.9: + resolution: {integrity: sha512-sZ+QzfauuUEfxSEjKFmi3qDSHgLsTPK/pEpoD/qonZKOtTPTLbf59oabPQ4rKekt9lFcj/hTZaOhWwFYrgjk+Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-function-name': 7.19.0 - '@babel/template': 7.18.10 - '@babel/traverse': 7.20.5 - '@babel/types': 7.20.5 - transitivePeerDependencies: - - supports-color + '@babel/helper-function-name': 7.22.5 + '@babel/template': 7.22.5 + '@babel/types': 7.22.5 dev: false - /@babel/helpers@7.20.6: - resolution: {integrity: sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w==} + /@babel/helpers@7.22.6: + resolution: {integrity: sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/template': 7.18.10 - '@babel/traverse': 7.20.5 - '@babel/types': 7.20.5 + '@babel/template': 7.22.5 + '@babel/traverse': 7.22.8 + '@babel/types': 7.22.5 transitivePeerDependencies: - supports-color dev: false - /@babel/highlight@7.18.6: - resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} + /@babel/highlight@7.22.5: + resolution: {integrity: sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-validator-identifier': 7.19.1 + '@babel/helper-validator-identifier': 7.22.5 chalk: 2.4.2 js-tokens: 4.0.0 dev: false - /@babel/parser@7.20.5: - resolution: {integrity: sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==} + /@babel/parser@7.22.7: + resolution: {integrity: sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==} engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 dev: false - /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.18.9(@babel/core@7.20.5): - resolution: {integrity: sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==} + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.13.0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 - '@babel/plugin-proposal-optional-chaining': 7.18.9(@babel/core@7.20.5) + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/plugin-transform-optional-chaining': 7.22.6(@babel/core@7.17.10) dev: false - /@babel/plugin-proposal-async-generator-functions@7.20.1(@babel/core@7.20.5): - resolution: {integrity: sha512-Gh5rchzSwE4kC+o/6T8waD0WHEQIsDmjltY8WnWRXHUdH8axZhuH86Ov9M72YhJfDrZseQwuuWaaIT/TmePp3g==} + /@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.17.10): + resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-remap-async-to-generator': 7.18.9(@babel/core@7.20.5) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.20.5) - transitivePeerDependencies: - - supports-color + '@babel/core': 7.17.10 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-remap-async-to-generator': 7.22.9(@babel/core@7.17.10) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.17.10) dev: false - /@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.20.5): + /@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.17.10): resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-create-class-features-plugin': 7.20.5(@babel/core@7.20.5) - '@babel/helper-plugin-utils': 7.20.2 - transitivePeerDependencies: - - supports-color + '@babel/core': 7.17.10 + '@babel/helper-create-class-features-plugin': 7.22.9(@babel/core@7.17.10) + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-proposal-class-static-block@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==} + /@babel/plugin-proposal-class-static-block@7.21.0(@babel/core@7.17.10): + resolution: {integrity: sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.12.0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-create-class-features-plugin': 7.20.5(@babel/core@7.20.5) - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.20.5) - transitivePeerDependencies: - - supports-color + '@babel/core': 7.17.10 + '@babel/helper-create-class-features-plugin': 7.22.9(@babel/core@7.17.10) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.17.10) dev: false - /@babel/plugin-proposal-dynamic-import@7.18.6(@babel/core@7.20.5): + /@babel/plugin-proposal-dynamic-import@7.18.6(@babel/core@7.17.10): resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.20.5) + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.17.10) dev: false - /@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.20.5): + /@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.17.10): resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.20.5) + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.17.10) dev: false - /@babel/plugin-proposal-json-strings@7.18.6(@babel/core@7.20.5): + /@babel/plugin-proposal-json-strings@7.18.6(@babel/core@7.17.10): resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.20.5) + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.17.10) dev: false - /@babel/plugin-proposal-logical-assignment-operators@7.18.9(@babel/core@7.20.5): - resolution: {integrity: sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==} + /@babel/plugin-proposal-logical-assignment-operators@7.20.7(@babel/core@7.17.10): + resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.20.5) + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.17.10) dev: false - /@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.20.5): + /@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.17.10): resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.20.5) + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.17.10) dev: false - /@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.20.5): + /@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.17.10): resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.20.5) + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.17.10) dev: false - /@babel/plugin-proposal-object-rest-spread@7.20.2(@babel/core@7.20.5): - resolution: {integrity: sha512-Ks6uej9WFK+fvIMesSqbAto5dD8Dz4VuuFvGJFKgIGSkJuRGcrwGECPA1fDgQK3/DbExBJpEkTeYeB8geIFCSQ==} + /@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.17.10): + resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.20.5 - '@babel/core': 7.20.5 - '@babel/helper-compilation-targets': 7.20.0(@babel/core@7.20.5) - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-transform-parameters': 7.20.5(@babel/core@7.20.5) + '@babel/compat-data': 7.22.9 + '@babel/core': 7.17.10 + '@babel/helper-compilation-targets': 7.22.9(@babel/core@7.17.10) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.17.10) + '@babel/plugin-transform-parameters': 7.22.5(@babel/core@7.17.10) dev: false - /@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.20.5): + /@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.17.10): resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.20.5) + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.17.10) dev: false - /@babel/plugin-proposal-optional-chaining@7.18.9(@babel/core@7.20.5): - resolution: {integrity: sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==} + /@babel/plugin-proposal-optional-chaining@7.21.0(@babel/core@7.17.10): + resolution: {integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.20.5) + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.17.10) dev: false - /@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.20.5): + /@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.17.10): resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-create-class-features-plugin': 7.20.5(@babel/core@7.20.5) - '@babel/helper-plugin-utils': 7.20.2 - transitivePeerDependencies: - - supports-color + '@babel/core': 7.17.10 + '@babel/helper-create-class-features-plugin': 7.22.9(@babel/core@7.17.10) + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-proposal-private-property-in-object@7.20.5(@babel/core@7.20.5): - resolution: {integrity: sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ==} + /@babel/plugin-proposal-private-property-in-object@7.21.11(@babel/core@7.17.10): + resolution: {integrity: sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-create-class-features-plugin': 7.20.5(@babel/core@7.20.5) - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.20.5) - transitivePeerDependencies: - - supports-color + '@babel/core': 7.17.10 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-create-class-features-plugin': 7.22.9(@babel/core@7.17.10) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.17.10) dev: false - /@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.20.5): + /@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.17.10): resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} engines: {node: '>=4'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-create-regexp-features-plugin': 7.20.5(@babel/core@7.20.5) - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-create-regexp-features-plugin': 7.22.9(@babel/core@7.17.10) + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.20.5): + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.17.10): resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.20.5): + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.22.9): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.17.10): resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.22.9): + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.17.10): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.20.5): + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.22.9): resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.20.5): + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.17.10): resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.20.5): + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.17.10): resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.20.5): + /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.17.10): resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-syntax-import-assertions@7.20.0(@babel/core@7.20.5): - resolution: {integrity: sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==} - engines: {node: '>=6.9.0'} + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.17.10): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.20.5): + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.22.9): resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.17.10): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.20.5): + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.22.9): resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-syntax-jsx@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} + /@babel/plugin-syntax-jsx@7.22.5(@babel/core@7.22.9): + resolution: {integrity: sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.17.10): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.20.5): + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.22.9): resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.17.10): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.20.5): + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.22.9): resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.20.5): + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.17.10): resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.20.5): + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.22.9): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.17.10): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.22.9): resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.17.10): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.20.5): + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.22.9): resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.20.5): + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.17.10): resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.20.5): + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.22.9): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.17.10): resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.20.5): + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.17.10): resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-syntax-typescript@7.20.0(@babel/core@7.20.5): - resolution: {integrity: sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==} + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.22.9): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-arrow-functions@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==} + /@babel/plugin-syntax-typescript@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-async-to-generator@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==} + /@babel/plugin-syntax-typescript@7.22.5(@babel/core@7.22.9): + resolution: {integrity: sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-remap-async-to-generator': 7.18.9(@babel/core@7.20.5) - transitivePeerDependencies: - - supports-color + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-arrow-functions@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-async-to-generator@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.17.10 + '@babel/helper-module-imports': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-remap-async-to-generator': 7.22.9(@babel/core@7.17.10) dev: false - /@babel/plugin-transform-block-scoped-functions@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==} + /@babel/plugin-transform-block-scoped-functions@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-block-scoping@7.20.5(@babel/core@7.20.5): - resolution: {integrity: sha512-WvpEIW9Cbj9ApF3yJCjIEEf1EiNJLtXagOrL5LNWEZOo3jv8pmPoYTSNJQvqej8OavVlgOoOPw6/htGZro6IkA==} + /@babel/plugin-transform-block-scoping@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-EcACl1i5fSQ6bt+YGuU/XGCeZKStLmyVGytWkpyhCLeQVA0eu6Wtiw92V+I1T/hnezUv7j74dA/Ro69gWcU+hg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-classes@7.20.2(@babel/core@7.20.5): - resolution: {integrity: sha512-9rbPp0lCVVoagvtEyQKSo5L8oo0nQS/iif+lwlAz29MccX2642vWDlSZK+2T2buxbopotId2ld7zZAzRfz9j1g==} + /@babel/plugin-transform-classes@7.22.6(@babel/core@7.17.10): + resolution: {integrity: sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-compilation-targets': 7.20.0(@babel/core@7.20.5) - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.19.0 - '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-replace-supers': 7.19.1 - '@babel/helper-split-export-declaration': 7.18.6 + '@babel/core': 7.17.10 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-compilation-targets': 7.22.9(@babel/core@7.17.10) + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-function-name': 7.22.5 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-replace-supers': 7.22.9(@babel/core@7.17.10) + '@babel/helper-split-export-declaration': 7.22.6 globals: 11.12.0 - transitivePeerDependencies: - - supports-color dev: false - /@babel/plugin-transform-computed-properties@7.18.9(@babel/core@7.20.5): - resolution: {integrity: sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==} + /@babel/plugin-transform-computed-properties@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/template': 7.22.5 dev: false - /@babel/plugin-transform-destructuring@7.20.2(@babel/core@7.20.5): - resolution: {integrity: sha512-mENM+ZHrvEgxLTBXUiQ621rRXZes3KWUv6NdQlrnr1TkWVw+hUjQBZuP2X32qKlrlG2BzgR95gkuCRSkJl8vIw==} + /@babel/plugin-transform-destructuring@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-GfqcFuGW8vnEqTUBM7UtPd5A4q797LTvvwKxXTgRsFjoqaJiEg9deBG6kWeQYkVEL569NpnmpC0Pkr/8BLKGnQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-dotall-regex@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==} + /@babel/plugin-transform-dotall-regex@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-create-regexp-features-plugin': 7.20.5(@babel/core@7.20.5) - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-create-regexp-features-plugin': 7.22.9(@babel/core@7.17.10) + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-duplicate-keys@7.18.9(@babel/core@7.20.5): - resolution: {integrity: sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==} + /@babel/plugin-transform-duplicate-keys@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-exponentiation-operator@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==} + /@babel/plugin-transform-exponentiation-operator@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-for-of@7.18.8(@babel/core@7.20.5): - resolution: {integrity: sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==} + /@babel/plugin-transform-for-of@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-function-name@7.18.9(@babel/core@7.20.5): - resolution: {integrity: sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==} + /@babel/plugin-transform-function-name@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-compilation-targets': 7.20.0(@babel/core@7.20.5) - '@babel/helper-function-name': 7.19.0 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-compilation-targets': 7.22.9(@babel/core@7.17.10) + '@babel/helper-function-name': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-literals@7.18.9(@babel/core@7.20.5): - resolution: {integrity: sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==} + /@babel/plugin-transform-literals@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-member-expression-literals@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==} + /@babel/plugin-transform-member-expression-literals@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-modules-amd@7.19.6(@babel/core@7.20.5): - resolution: {integrity: sha512-uG3od2mXvAtIFQIh0xrpLH6r5fpSQN04gIVovl+ODLdUMANokxQLZnPBHcjmv3GxRjnqwLuHvppjjcelqUFZvg==} + /@babel/plugin-transform-modules-amd@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-module-transforms': 7.20.2 - '@babel/helper-plugin-utils': 7.20.2 - transitivePeerDependencies: - - supports-color + '@babel/core': 7.17.10 + '@babel/helper-module-transforms': 7.22.9(@babel/core@7.17.10) + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-modules-commonjs@7.19.6(@babel/core@7.20.5): - resolution: {integrity: sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ==} + /@babel/plugin-transform-modules-commonjs@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-module-transforms': 7.20.2 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-simple-access': 7.20.2 - transitivePeerDependencies: - - supports-color + '@babel/core': 7.17.10 + '@babel/helper-module-transforms': 7.22.9(@babel/core@7.17.10) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-simple-access': 7.22.5 dev: false - /@babel/plugin-transform-modules-systemjs@7.19.6(@babel/core@7.20.5): - resolution: {integrity: sha512-fqGLBepcc3kErfR9R3DnVpURmckXP7gj7bAlrTQyBxrigFqszZCkFkcoxzCp2v32XmwXLvbw+8Yq9/b+QqksjQ==} + /@babel/plugin-transform-modules-systemjs@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-module-transforms': 7.20.2 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-identifier': 7.19.1 - transitivePeerDependencies: - - supports-color + '@babel/core': 7.17.10 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-module-transforms': 7.22.9(@babel/core@7.17.10) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-identifier': 7.22.5 dev: false - /@babel/plugin-transform-modules-umd@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==} + /@babel/plugin-transform-modules-umd@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-module-transforms': 7.20.2 - '@babel/helper-plugin-utils': 7.20.2 - transitivePeerDependencies: - - supports-color + '@babel/core': 7.17.10 + '@babel/helper-module-transforms': 7.22.9(@babel/core@7.17.10) + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-named-capturing-groups-regex@7.20.5(@babel/core@7.20.5): - resolution: {integrity: sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==} + /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-create-regexp-features-plugin': 7.20.5(@babel/core@7.20.5) - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-create-regexp-features-plugin': 7.22.9(@babel/core@7.17.10) + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-new-target@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==} + /@babel/plugin-transform-new-target@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-object-super@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==} + /@babel/plugin-transform-object-super@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-replace-supers': 7.19.1 - transitivePeerDependencies: - - supports-color + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-replace-supers': 7.22.9(@babel/core@7.17.10) + dev: false + + /@babel/plugin-transform-optional-chaining@7.22.6(@babel/core@7.17.10): + resolution: {integrity: sha512-Vd5HiWml0mDVtcLHIoEU5sw6HOUW/Zk0acLs/SAeuLzkGNOPc9DB4nkUajemhCmTIz3eiaKREZn2hQQqF79YTg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.17.10) dev: false - /@babel/plugin-transform-parameters@7.20.5(@babel/core@7.20.5): - resolution: {integrity: sha512-h7plkOmcndIUWXZFLgpbrh2+fXAi47zcUX7IrOQuZdLD0I0KvjJ6cvo3BEcAOsDOcZhVKGJqv07mkSqK0y2isQ==} + /@babel/plugin-transform-parameters@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-property-literals@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==} + /@babel/plugin-transform-property-literals@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-regenerator@7.20.5(@babel/core@7.20.5): - resolution: {integrity: sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==} + /@babel/plugin-transform-regenerator@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-rR7KePOE7gfEtNTh9Qw+iO3Q/e4DEsoQ+hdvM6QUDH7JRJ5qxq5AA52ZzBWbI5i9lfNuvySgOGP8ZN7LAmaiPw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 regenerator-transform: 0.15.1 dev: false - /@babel/plugin-transform-reserved-words@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==} + /@babel/plugin-transform-reserved-words@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-shorthand-properties@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==} + /@babel/plugin-transform-shorthand-properties@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-spread@7.19.0(@babel/core@7.20.5): - resolution: {integrity: sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==} + /@babel/plugin-transform-spread@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 dev: false - /@babel/plugin-transform-sticky-regex@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==} + /@babel/plugin-transform-sticky-regex@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-template-literals@7.18.9(@babel/core@7.20.5): - resolution: {integrity: sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==} + /@babel/plugin-transform-template-literals@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-typeof-symbol@7.18.9(@babel/core@7.20.5): - resolution: {integrity: sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==} + /@babel/plugin-transform-typeof-symbol@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-unicode-escapes@7.18.10(@babel/core@7.20.5): - resolution: {integrity: sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==} + /@babel/plugin-transform-unicode-escapes@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-biEmVg1IYB/raUO5wT1tgfacCef15Fbzhkx493D3urBI++6hpJ+RFG4SrWMn0NEZLfvilqKf3QDrRVZHo08FYg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-unicode-regex@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==} + /@babel/plugin-transform-unicode-regex@7.22.5(@babel/core@7.17.10): + resolution: {integrity: sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-create-regexp-features-plugin': 7.20.5(@babel/core@7.20.5) - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.17.10 + '@babel/helper-create-regexp-features-plugin': 7.22.9(@babel/core@7.17.10) + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/preset-env@7.20.2(@babel/core@7.20.5): - resolution: {integrity: sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==} + /@babel/preset-env@7.17.10(@babel/core@7.17.10): + resolution: {integrity: sha512-YNgyBHZQpeoBSRBg0xixsZzfT58Ze1iZrajvv0lJc70qDDGuGfonEnMGfWeSY0mQ3JTuCWFbMkzFRVafOyJx4g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.20.5 - '@babel/core': 7.20.5 - '@babel/helper-compilation-targets': 7.20.0(@babel/core@7.20.5) - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-option': 7.18.6 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.18.9(@babel/core@7.20.5) - '@babel/plugin-proposal-async-generator-functions': 7.20.1(@babel/core@7.20.5) - '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-proposal-class-static-block': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-proposal-dynamic-import': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-proposal-export-namespace-from': 7.18.9(@babel/core@7.20.5) - '@babel/plugin-proposal-json-strings': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-proposal-logical-assignment-operators': 7.18.9(@babel/core@7.20.5) - '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-proposal-numeric-separator': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-proposal-object-rest-spread': 7.20.2(@babel/core@7.20.5) - '@babel/plugin-proposal-optional-catch-binding': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-proposal-optional-chaining': 7.18.9(@babel/core@7.20.5) - '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-proposal-private-property-in-object': 7.20.5(@babel/core@7.20.5) - '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.20.5) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.20.5) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.20.5) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-import-assertions': 7.20.0(@babel/core@7.20.5) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.20.5) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.20.5) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.20.5) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.20.5) - '@babel/plugin-transform-arrow-functions': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-async-to-generator': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-block-scoped-functions': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-block-scoping': 7.20.5(@babel/core@7.20.5) - '@babel/plugin-transform-classes': 7.20.2(@babel/core@7.20.5) - '@babel/plugin-transform-computed-properties': 7.18.9(@babel/core@7.20.5) - '@babel/plugin-transform-destructuring': 7.20.2(@babel/core@7.20.5) - '@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-duplicate-keys': 7.18.9(@babel/core@7.20.5) - '@babel/plugin-transform-exponentiation-operator': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-for-of': 7.18.8(@babel/core@7.20.5) - '@babel/plugin-transform-function-name': 7.18.9(@babel/core@7.20.5) - '@babel/plugin-transform-literals': 7.18.9(@babel/core@7.20.5) - '@babel/plugin-transform-member-expression-literals': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-modules-amd': 7.19.6(@babel/core@7.20.5) - '@babel/plugin-transform-modules-commonjs': 7.19.6(@babel/core@7.20.5) - '@babel/plugin-transform-modules-systemjs': 7.19.6(@babel/core@7.20.5) - '@babel/plugin-transform-modules-umd': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-named-capturing-groups-regex': 7.20.5(@babel/core@7.20.5) - '@babel/plugin-transform-new-target': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-object-super': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-parameters': 7.20.5(@babel/core@7.20.5) - '@babel/plugin-transform-property-literals': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-regenerator': 7.20.5(@babel/core@7.20.5) - '@babel/plugin-transform-reserved-words': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-shorthand-properties': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-spread': 7.19.0(@babel/core@7.20.5) - '@babel/plugin-transform-sticky-regex': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-template-literals': 7.18.9(@babel/core@7.20.5) - '@babel/plugin-transform-typeof-symbol': 7.18.9(@babel/core@7.20.5) - '@babel/plugin-transform-unicode-escapes': 7.18.10(@babel/core@7.20.5) - '@babel/plugin-transform-unicode-regex': 7.18.6(@babel/core@7.20.5) - '@babel/preset-modules': 0.1.5(@babel/core@7.20.5) - '@babel/types': 7.20.5 - babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.20.5) - babel-plugin-polyfill-corejs3: 0.6.0(@babel/core@7.20.5) - babel-plugin-polyfill-regenerator: 0.4.1(@babel/core@7.20.5) - core-js-compat: 3.26.1 - semver: 6.3.0 + '@babel/compat-data': 7.22.9 + '@babel/core': 7.17.10 + '@babel/helper-compilation-targets': 7.22.9(@babel/core@7.17.10) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.22.5 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-proposal-async-generator-functions': 7.20.7(@babel/core@7.17.10) + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.17.10) + '@babel/plugin-proposal-class-static-block': 7.21.0(@babel/core@7.17.10) + '@babel/plugin-proposal-dynamic-import': 7.18.6(@babel/core@7.17.10) + '@babel/plugin-proposal-export-namespace-from': 7.18.9(@babel/core@7.17.10) + '@babel/plugin-proposal-json-strings': 7.18.6(@babel/core@7.17.10) + '@babel/plugin-proposal-logical-assignment-operators': 7.20.7(@babel/core@7.17.10) + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.17.10) + '@babel/plugin-proposal-numeric-separator': 7.18.6(@babel/core@7.17.10) + '@babel/plugin-proposal-object-rest-spread': 7.20.7(@babel/core@7.17.10) + '@babel/plugin-proposal-optional-catch-binding': 7.18.6(@babel/core@7.17.10) + '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.17.10) + '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.17.10) + '@babel/plugin-proposal-private-property-in-object': 7.21.11(@babel/core@7.17.10) + '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.17.10) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.17.10) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.17.10) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.17.10) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.17.10) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.17.10) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.17.10) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.17.10) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.17.10) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.17.10) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.17.10) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.17.10) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.17.10) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.17.10) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.17.10) + '@babel/plugin-transform-arrow-functions': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-async-to-generator': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-block-scoped-functions': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-block-scoping': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-classes': 7.22.6(@babel/core@7.17.10) + '@babel/plugin-transform-computed-properties': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-destructuring': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-dotall-regex': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-duplicate-keys': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-exponentiation-operator': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-for-of': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-function-name': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-literals': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-member-expression-literals': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-modules-amd': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-modules-commonjs': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-modules-systemjs': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-modules-umd': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-new-target': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-object-super': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-parameters': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-property-literals': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-regenerator': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-reserved-words': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-shorthand-properties': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-spread': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-sticky-regex': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-template-literals': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-typeof-symbol': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-unicode-escapes': 7.22.5(@babel/core@7.17.10) + '@babel/plugin-transform-unicode-regex': 7.22.5(@babel/core@7.17.10) + '@babel/preset-modules': 0.1.6(@babel/core@7.17.10) + '@babel/types': 7.22.5 + babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.17.10) + babel-plugin-polyfill-corejs3: 0.5.3(@babel/core@7.17.10) + babel-plugin-polyfill-regenerator: 0.3.1(@babel/core@7.17.10) + core-js-compat: 3.31.1 + semver: 6.3.1 transitivePeerDependencies: - supports-color dev: false - /@babel/preset-modules@0.1.5(@babel/core@7.20.5): - resolution: {integrity: sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==} + /@babel/preset-modules@0.1.6(@babel/core@7.17.10): + resolution: {integrity: sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.20.5) - '@babel/types': 7.20.5 + '@babel/core': 7.17.10 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.17.10) + '@babel/plugin-transform-dotall-regex': 7.22.5(@babel/core@7.17.10) + '@babel/types': 7.22.5 esutils: 2.0.3 dev: false - /@babel/runtime@7.20.6: - resolution: {integrity: sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==} + /@babel/regjsgen@0.8.0: + resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} + dev: false + + /@babel/runtime@7.22.6: + resolution: {integrity: sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==} engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.13.11 dev: false - /@babel/template@7.18.10: - resolution: {integrity: sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==} + /@babel/template@7.22.5: + resolution: {integrity: sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.18.6 - '@babel/parser': 7.20.5 - '@babel/types': 7.20.5 + '@babel/code-frame': 7.22.5 + '@babel/parser': 7.22.7 + '@babel/types': 7.22.5 dev: false - /@babel/traverse@7.20.5: - resolution: {integrity: sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==} + /@babel/traverse@7.22.8: + resolution: {integrity: sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.18.6 - '@babel/generator': 7.20.5 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.19.0 - '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/parser': 7.20.5 - '@babel/types': 7.20.5 + '@babel/code-frame': 7.22.5 + '@babel/generator': 7.22.9 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-function-name': 7.22.5 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.22.7 + '@babel/types': 7.22.5 debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: - supports-color dev: false - /@babel/types@7.20.5: - resolution: {integrity: sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==} + /@babel/types@7.22.5: + resolution: {integrity: sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-string-parser': 7.19.4 - '@babel/helper-validator-identifier': 7.19.1 + '@babel/helper-string-parser': 7.22.5 + '@babel/helper-validator-identifier': 7.22.5 to-fast-properties: 2.0.0 dev: false @@ -2217,6 +2355,15 @@ packages: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: false + /@esbuild/android-arm64@0.18.16: + resolution: {integrity: sha512-wsCqSPqLz+6Ov+OM4EthU43DyYVVyfn15S4j1bJzylDpc1r1jZFFfJQNfDuT8SlgwuqpmpJXK4uPlHGw6ve7eA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: false + optional: true + /@esbuild/android-arm64@0.18.4: resolution: {integrity: sha512-yQVgO+V307hA2XhzELQ6F91CBGX7gSnlVGAj5YIqjQOxThDpM7fOcHT2YLJbE6gNdPtgRSafQrsK8rJ9xHCaZg==} engines: {node: '>=12'} @@ -2226,6 +2373,15 @@ packages: dev: false optional: true + /@esbuild/android-arm@0.18.16: + resolution: {integrity: sha512-gCHjjQmA8L0soklKbLKA6pgsLk1byULuHe94lkZDzcO3/Ta+bbeewJioEn1Fr7kgy9NWNFy/C+MrBwC6I/WCug==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: false + optional: true + /@esbuild/android-arm@0.18.4: resolution: {integrity: sha512-yKmQC9IiuvHdsNEbPHSprnMHg6OhL1cSeQZLzPpgzJBJ9ppEg9GAZN8MKj1TcmB4tZZUrq5xjK7KCmhwZP8iDA==} engines: {node: '>=12'} @@ -2235,6 +2391,15 @@ packages: dev: false optional: true + /@esbuild/android-x64@0.18.16: + resolution: {integrity: sha512-ldsTXolyA3eTQ1//4DS+E15xl0H/3DTRJaRL0/0PgkqDsI0fV/FlOtD+h0u/AUJr+eOTlZv4aC9gvfppo3C4sw==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: false + optional: true + /@esbuild/android-x64@0.18.4: resolution: {integrity: sha512-yLKXMxQg6sk1ntftxQ5uwyVgG4/S2E7UoOCc5N4YZW7fdkfRiYEXqm7CMuIfY2Vs3FTrNyKmSfNevIuIvJnMww==} engines: {node: '>=12'} @@ -2244,6 +2409,15 @@ packages: dev: false optional: true + /@esbuild/darwin-arm64@0.18.16: + resolution: {integrity: sha512-aBxruWCII+OtluORR/KvisEw0ALuw/qDQWvkoosA+c/ngC/Kwk0lLaZ+B++LLS481/VdydB2u6tYpWxUfnLAIw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + /@esbuild/darwin-arm64@0.18.4: resolution: {integrity: sha512-MVPEoZjZpk2xQ1zckZrb8eQuQib+QCzdmMs3YZAYEQPg+Rztk5pUxGyk8htZOC8Z38NMM29W+MqY9Sqo/sDGKw==} engines: {node: '>=12'} @@ -2253,6 +2427,15 @@ packages: dev: false optional: true + /@esbuild/darwin-x64@0.18.16: + resolution: {integrity: sha512-6w4Dbue280+rp3LnkgmriS1icOUZDyPuZo/9VsuMUTns7SYEiOaJ7Ca1cbhu9KVObAWfmdjUl4gwy9TIgiO5eA==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + /@esbuild/darwin-x64@0.18.4: resolution: {integrity: sha512-uEsRtYRUDsz7i2tXg/t/SyF+5gU1cvi9B6B8i5ebJgtUUHJYWyIPIesmIOL4/+bywjxsDMA/XrNFMgMffLnh5A==} engines: {node: '>=12'} @@ -2262,6 +2445,15 @@ packages: dev: false optional: true + /@esbuild/freebsd-arm64@0.18.16: + resolution: {integrity: sha512-x35fCebhe9s979DGKbVAwXUOcTmCIE32AIqB9CB1GralMIvxdnMLAw5CnID17ipEw9/3MvDsusj/cspYt2ZLNQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + /@esbuild/freebsd-arm64@0.18.4: resolution: {integrity: sha512-I8EOigqWnOHRin6Zp5Y1cfH3oT54bd7Sdz/VnpUNksbOtfp8IWRTH4pgkgO5jWaRQPjCpJcOpdRjYAMjPt8wXg==} engines: {node: '>=12'} @@ -2271,6 +2463,15 @@ packages: dev: false optional: true + /@esbuild/freebsd-x64@0.18.16: + resolution: {integrity: sha512-YM98f+PeNXF3GbxIJlUsj+McUWG1irguBHkszCIwfr3BXtXZsXo0vqybjUDFfu9a8Wr7uUD/YSmHib+EeGAFlg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + /@esbuild/freebsd-x64@0.18.4: resolution: {integrity: sha512-1bHfgMz/cNMjbpsYxjVgMJ1iwKq+NdDPlACBrWULD7ZdFmBQrhMicMaKb5CdmdVyvIwXmasOuF4r6Iq574kUTA==} engines: {node: '>=12'} @@ -2280,6 +2481,15 @@ packages: dev: false optional: true + /@esbuild/linux-arm64@0.18.16: + resolution: {integrity: sha512-XIqhNUxJiuy+zsR77+H5Z2f7s4YRlriSJKtvx99nJuG5ATuJPjmZ9n0ANgnGlPCpXGSReFpgcJ7O3SMtzIFeiQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-arm64@0.18.4: resolution: {integrity: sha512-J42vLHaYREyiBwH0eQE4/7H1DTfZx8FuxyWSictx4d7ezzuKE3XOkIvOg+SQzRz7T9HLVKzq2tvbAov4UfufBw==} engines: {node: '>=12'} @@ -2289,6 +2499,15 @@ packages: dev: false optional: true + /@esbuild/linux-arm@0.18.16: + resolution: {integrity: sha512-b5ABb+5Ha2C9JkeZXV+b+OruR1tJ33ePmv9ZwMeETSEKlmu/WJ45XTTG+l6a2KDsQtJJ66qo/hbSGBtk0XVLHw==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-arm@0.18.4: resolution: {integrity: sha512-4XCGqM/Ay1LCXUBH59bL4JbSbbTK1K22dWHymWMGaEh2sQCDOUw+OQxozYV/YdBb91leK2NbuSrE2BRamwgaYw==} engines: {node: '>=12'} @@ -2298,6 +2517,15 @@ packages: dev: false optional: true + /@esbuild/linux-ia32@0.18.16: + resolution: {integrity: sha512-no+pfEpwnRvIyH+txbBAWtjxPU9grslmTBfsmDndj7bnBmr55rOo/PfQmRfz7Qg9isswt1FP5hBbWb23fRWnow==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-ia32@0.18.4: resolution: {integrity: sha512-4ksIqFwhq7OExty7Sl1n0vqQSCqTG4sU6i99G2yuMr28CEOUZ/60N+IO9hwI8sIxBqmKmDgncE1n5CMu/3m0IA==} engines: {node: '>=12'} @@ -2307,6 +2535,15 @@ packages: dev: false optional: true + /@esbuild/linux-loong64@0.18.16: + resolution: {integrity: sha512-Zbnczs9ZXjmo0oZSS0zbNlJbcwKXa/fcNhYQjahDs4Xg18UumpXG/lwM2lcSvHS3mTrRyCYZvJbmzYc4laRI1g==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-loong64@0.18.4: resolution: {integrity: sha512-bsWtoVHkGQgAsFXioDueXRiUIfSGrVkJjBBz4gcBJxXcD461cWFQFyu8Fxdj9TP+zEeqJ8C/O4LFFMBNi6Fscw==} engines: {node: '>=12'} @@ -2316,6 +2553,15 @@ packages: dev: false optional: true + /@esbuild/linux-mips64el@0.18.16: + resolution: {integrity: sha512-YMF7hih1HVR/hQVa/ot4UVffc5ZlrzEb3k2ip0nZr1w6fnYypll9td2qcoMLvd3o8j3y6EbJM3MyIcXIVzXvQQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-mips64el@0.18.4: resolution: {integrity: sha512-LRD9Fu8wJQgIOOV1o3nRyzrheFYjxA0C1IVWZ93eNRRWBKgarYFejd5WBtrp43cE4y4D4t3qWWyklm73Mrsd/g==} engines: {node: '>=12'} @@ -2325,6 +2571,15 @@ packages: dev: false optional: true + /@esbuild/linux-ppc64@0.18.16: + resolution: {integrity: sha512-Wkz++LZ29lDwUyTSEnzDaaP5OveOgTU69q9IyIw9WqLRxM4BjTBjz9un4G6TOvehWpf/J3gYVFN96TjGHrbcNQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-ppc64@0.18.4: resolution: {integrity: sha512-jtQgoZjM92gauVRxNaaG/TpL3Pr4WcL3Pwqi9QgdrBGrEXzB+twohQiWNSTycs6lUygakos4mm2h0B9/SHveng==} engines: {node: '>=12'} @@ -2334,6 +2589,15 @@ packages: dev: false optional: true + /@esbuild/linux-riscv64@0.18.16: + resolution: {integrity: sha512-LFMKZ30tk78/mUv1ygvIP+568bwf4oN6reG/uczXnz6SvFn4e2QUFpUpZY9iSJT6Qpgstrhef/nMykIXZtZWGQ==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-riscv64@0.18.4: resolution: {integrity: sha512-7WaU/kRZG0VCV09Xdlkg6LNAsfU9SAxo6XEdaZ8ffO4lh+DZoAhGTx7+vTMOXKxa+r2w1LYDGxfJa2rcgagMRA==} engines: {node: '>=12'} @@ -2343,6 +2607,15 @@ packages: dev: false optional: true + /@esbuild/linux-s390x@0.18.16: + resolution: {integrity: sha512-3ZC0BgyYHYKfZo3AV2/66TD/I9tlSBaW7eWTEIkrQQKfJIifKMMttXl9FrAg+UT0SGYsCRLI35Gwdmm96vlOjg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-s390x@0.18.4: resolution: {integrity: sha512-D19ed0xreKQvC5t+ArE2njSnm18WPpE+1fhwaiJHf+Xwqsq+/SUaV8Mx0M27nszdU+Atq1HahrgCOZCNNEASUg==} engines: {node: '>=12'} @@ -2352,6 +2625,15 @@ packages: dev: false optional: true + /@esbuild/linux-x64@0.18.16: + resolution: {integrity: sha512-xu86B3647DihHJHv/wx3NCz2Dg1gjQ8bbf9cVYZzWKY+gsvxYmn/lnVlqDRazObc3UMwoHpUhNYaZset4X8IPA==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-x64@0.18.4: resolution: {integrity: sha512-Rx3AY1sxyiO/gvCGP00nL69L60dfmWyjKWY06ugpB8Ydpdsfi3BHW58HWC24K3CAjAPSwxcajozC2PzA9JBS1g==} engines: {node: '>=12'} @@ -2361,6 +2643,15 @@ packages: dev: false optional: true + /@esbuild/netbsd-x64@0.18.16: + resolution: {integrity: sha512-uVAgpimx9Ffw3xowtg/7qQPwHFx94yCje+DoBx+LNm2ePDpQXHrzE+Sb0Si2VBObYz+LcRps15cq+95YM7gkUw==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: false + optional: true + /@esbuild/netbsd-x64@0.18.4: resolution: {integrity: sha512-AaShPmN9c6w1mKRpliKFlaWcSkpBT4KOlk93UfFgeI3F3cbjzdDKGsbKnOZozmYbE1izZKLmNJiW0sFM+A5JPA==} engines: {node: '>=12'} @@ -2370,6 +2661,15 @@ packages: dev: false optional: true + /@esbuild/openbsd-x64@0.18.16: + resolution: {integrity: sha512-6OjCQM9wf7z8/MBi6BOWaTL2AS/SZudsZtBziXMtNI8r/U41AxS9x7jn0ATOwVy08OotwkPqGRMkpPR2wcTJXA==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: false + optional: true + /@esbuild/openbsd-x64@0.18.4: resolution: {integrity: sha512-tRGvGwou3BrvHVvF8HxTqEiC5VtPzySudS9fh2jBIKpLX7HCW8jIkW+LunkFDNwhslx4xMAgh0jAHsx/iCymaQ==} engines: {node: '>=12'} @@ -2379,6 +2679,15 @@ packages: dev: false optional: true + /@esbuild/sunos-x64@0.18.16: + resolution: {integrity: sha512-ZoNkruFYJp9d1LbUYCh8awgQDvB9uOMZqlQ+gGEZR7v6C+N6u7vPr86c+Chih8niBR81Q/bHOSKGBK3brJyvkQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: false + optional: true + /@esbuild/sunos-x64@0.18.4: resolution: {integrity: sha512-acORFDI95GKhmAnlH8EarBeuqoy/j3yxIU+FDB91H3+ZON+8HhTadtT450YkaMzX6lEWbhi+mjVUCj00M5yyOQ==} engines: {node: '>=12'} @@ -2388,6 +2697,15 @@ packages: dev: false optional: true + /@esbuild/win32-arm64@0.18.16: + resolution: {integrity: sha512-+j4anzQ9hrs+iqO+/wa8UE6TVkKua1pXUb0XWFOx0FiAj6R9INJ+WE//1/Xo6FG1vB5EpH3ko+XcgwiDXTxcdw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@esbuild/win32-arm64@0.18.4: resolution: {integrity: sha512-1NxP+iOk8KSvS1L9SSxEvBAJk39U0GiGZkiiJGbuDF9G4fG7DSDw6XLxZMecAgmvQrwwx7yVKdNN3GgNh0UfKg==} engines: {node: '>=12'} @@ -2397,6 +2715,15 @@ packages: dev: false optional: true + /@esbuild/win32-ia32@0.18.16: + resolution: {integrity: sha512-5PFPmq3sSKTp9cT9dzvI67WNfRZGvEVctcZa1KGjDDu4n3H8k59Inbk0du1fz0KrAbKKNpJbdFXQMDUz7BG4rQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@esbuild/win32-ia32@0.18.4: resolution: {integrity: sha512-OKr8jze93vbgqZ/r23woWciTixUwLa976C9W7yNBujtnVHyvsL/ocYG61tsktUfJOpyIz5TsohkBZ6Lo2+PCcQ==} engines: {node: '>=12'} @@ -2406,6 +2733,15 @@ packages: dev: false optional: true + /@esbuild/win32-x64@0.18.16: + resolution: {integrity: sha512-sCIVrrtcWN5Ua7jYXNG1xD199IalrbfV2+0k/2Zf2OyV2FtnQnMgdzgpRAbi4AWlKJj1jkX+M+fEGPQj6BQB4w==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@esbuild/win32-x64@0.18.4: resolution: {integrity: sha512-qJr3wVvcLjPFcV4AMDS3iquhBfTef2zo/jlm8RMxmiRp3Vy2HY8WMxrykJlcbCnqLXZPA0YZxZGND6eug85ogg==} engines: {node: '>=12'} @@ -2425,18 +2761,18 @@ packages: eslint-visitor-keys: 3.4.1 dev: false - /@eslint-community/regexpp@4.5.1: - resolution: {integrity: sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==} + /@eslint-community/regexpp@4.6.1: + resolution: {integrity: sha512-O7x6dMstWLn2ktjcoiNLDkAGG2EjveHL+Vvc+n0fXumkJYAcSqcVYKtwDU+hDZ0uDUsnUagSYaZrOLAYE8un1A==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dev: false - /@eslint/eslintrc@2.0.3: - resolution: {integrity: sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==} + /@eslint/eslintrc@2.1.0: + resolution: {integrity: sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 debug: 4.3.4 - espree: 9.5.2 + espree: 9.6.1 globals: 13.20.0 ignore: 5.2.4 import-fresh: 3.3.0 @@ -2497,7 +2833,7 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: '@jest/types': 28.1.3 - '@types/node': 18.16.18 + '@types/node': 18.17.0 chalk: 4.1.2 jest-message-util: 28.1.3 jest-util: 28.1.3 @@ -2518,14 +2854,14 @@ packages: '@jest/test-result': 28.1.3 '@jest/transform': 28.1.3 '@jest/types': 28.1.3 - '@types/node': 18.16.18 + '@types/node': 18.17.0 ansi-escapes: 4.3.2 chalk: 4.1.2 - ci-info: 3.7.0 + ci-info: 3.8.0 exit: 0.1.2 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-changed-files: 28.1.3 - jest-config: 28.1.3(@types/node@18.16.18) + jest-config: 28.1.3(@types/node@18.17.0) jest-haste-map: 28.1.3 jest-message-util: 28.1.3 jest-regex-util: 28.0.2 @@ -2553,18 +2889,18 @@ packages: dependencies: '@jest/fake-timers': 28.1.3 '@jest/types': 28.1.3 - '@types/node': 18.16.18 + '@types/node': 18.17.0 jest-mock: 28.1.3 dev: false - /@jest/environment@29.3.1: - resolution: {integrity: sha512-pMmvfOPmoa1c1QpfFW0nXYtNLpofqo4BrCIk6f2kW4JFeNlHV2t3vd+3iDLf31e2ot2Mec0uqZfmI+U0K2CFag==} + /@jest/environment@29.6.1: + resolution: {integrity: sha512-RMMXx4ws+Gbvw3DfLSuo2cfQlK7IwGbpuEWXCqyYDcqYTI+9Ju3a5hDnXaxjNsa6uKh9PQF2v+qg+RLe63tz5A==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/fake-timers': 29.3.1 - '@jest/types': 29.3.1 - '@types/node': 18.16.18 - jest-mock: 29.3.1 + '@jest/fake-timers': 29.6.1 + '@jest/types': 29.6.1 + '@types/node': 18.17.0 + jest-mock: 29.6.1 dev: false /@jest/expect-utils@28.1.3: @@ -2574,11 +2910,11 @@ packages: jest-get-type: 28.0.2 dev: false - /@jest/expect-utils@29.3.1: - resolution: {integrity: sha512-wlrznINZI5sMjwvUoLVk617ll/UYfGIZNxmbU+Pa7wmkL4vYzhV9R2pwVqUh4NWWuLQWkI8+8mOkxs//prKQ3g==} + /@jest/expect-utils@29.6.1: + resolution: {integrity: sha512-o319vIf5pEMx0LmzSxxkYYxo4wrRLKHq9dP1yJU7FoPTB0LfAKSz8SWD6D/6U3v/O52t9cF5t+MeJiRsfk7zMw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - jest-get-type: 29.2.0 + jest-get-type: 29.4.3 dev: false /@jest/expect@28.1.3: @@ -2591,12 +2927,12 @@ packages: - supports-color dev: false - /@jest/expect@29.3.1: - resolution: {integrity: sha512-QivM7GlSHSsIAWzgfyP8dgeExPRZ9BIe2LsdPyEhCGkZkoyA+kGsoIzbKAfZCvvRzfZioKwPtCZIt5SaoxYCvg==} + /@jest/expect@29.6.1: + resolution: {integrity: sha512-N5xlPrAYaRNyFgVf2s9Uyyvr795jnB6rObuPx4QFvNJz8aAjpZUDfO4bh5G/xuplMID8PrnuF1+SfSyDxhsgYg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - expect: 29.3.1 - jest-snapshot: 29.3.1 + expect: 29.6.1 + jest-snapshot: 29.6.1 transitivePeerDependencies: - supports-color dev: false @@ -2607,22 +2943,22 @@ packages: dependencies: '@jest/types': 28.1.3 '@sinonjs/fake-timers': 9.1.2 - '@types/node': 18.16.18 + '@types/node': 18.17.0 jest-message-util: 28.1.3 jest-mock: 28.1.3 jest-util: 28.1.3 dev: false - /@jest/fake-timers@29.3.1: - resolution: {integrity: sha512-iHTL/XpnDlFki9Tq0Q1GGuVeQ8BHZGIYsvCO5eN/O/oJaRzofG9Xndd9HuSDBI/0ZS79pg0iwn07OMTQ7ngF2A==} + /@jest/fake-timers@29.6.1: + resolution: {integrity: sha512-RdgHgbXyosCDMVYmj7lLpUwXA4c69vcNzhrt69dJJdf8azUrpRh3ckFCaTPNjsEeRi27Cig0oKDGxy5j7hOgHg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.3.1 - '@sinonjs/fake-timers': 9.1.2 - '@types/node': 18.16.18 - jest-message-util: 29.3.1 - jest-mock: 29.3.1 - jest-util: 29.3.1 + '@jest/types': 29.6.1 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 18.17.0 + jest-message-util: 29.6.1 + jest-mock: 29.6.1 + jest-util: 29.6.1 dev: false /@jest/globals@28.1.3: @@ -2640,10 +2976,10 @@ packages: resolution: {integrity: sha512-cTicd134vOcwO59OPaB6AmdHQMCtWOe+/DitpTZVxWgMJ+YvXL1HNAmPyiGbSHmF/mXVBkvlm8YYtQhyHPnV6Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.3.1 - '@jest/expect': 29.3.1 - '@jest/types': 29.3.1 - jest-mock: 29.3.1 + '@jest/environment': 29.6.1 + '@jest/expect': 29.6.1 + '@jest/types': 29.6.1 + jest-mock: 29.6.1 transitivePeerDependencies: - supports-color dev: false @@ -2662,13 +2998,13 @@ packages: '@jest/test-result': 28.1.3 '@jest/transform': 28.1.3 '@jest/types': 28.1.3 - '@jridgewell/trace-mapping': 0.3.17 - '@types/node': 18.16.18 + '@jridgewell/trace-mapping': 0.3.18 + '@types/node': 18.17.0 chalk: 4.1.2 - collect-v8-coverage: 1.0.1 + collect-v8-coverage: 1.0.2 exit: 0.1.2 glob: 7.2.3 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 istanbul-lib-coverage: 3.2.0 istanbul-lib-instrument: 5.2.1 istanbul-lib-report: 3.0.0 @@ -2681,7 +3017,7 @@ packages: string-length: 4.0.2 strip-ansi: 6.0.1 terminal-link: 2.1.1 - v8-to-istanbul: 9.0.1 + v8-to-istanbul: 9.1.0 transitivePeerDependencies: - supports-color dev: false @@ -2693,20 +3029,20 @@ packages: '@sinclair/typebox': 0.24.51 dev: false - /@jest/schemas@29.0.0: - resolution: {integrity: sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==} + /@jest/schemas@29.6.0: + resolution: {integrity: sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@sinclair/typebox': 0.24.51 + '@sinclair/typebox': 0.27.8 dev: false /@jest/source-map@28.1.2: resolution: {integrity: sha512-cV8Lx3BeStJb8ipPHnqVw/IM2VCMWO3crWZzYodSIkxXnRcXJipCdx1JCK0K5MsJJouZQTH73mzf4vgxRaH9ww==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: - '@jridgewell/trace-mapping': 0.3.17 + '@jridgewell/trace-mapping': 0.3.18 callsites: 3.1.0 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 dev: false /@jest/test-result@28.1.3: @@ -2716,7 +3052,7 @@ packages: '@jest/console': 28.1.3 '@jest/types': 28.1.3 '@types/istanbul-lib-coverage': 2.0.4 - collect-v8-coverage: 1.0.1 + collect-v8-coverage: 1.0.2 dev: false /@jest/test-sequencer@28.1.3: @@ -2724,7 +3060,7 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: '@jest/test-result': 28.1.3 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-haste-map: 28.1.3 slash: 3.0.0 dev: false @@ -2733,42 +3069,42 @@ packages: resolution: {integrity: sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: - '@babel/core': 7.20.5 + '@babel/core': 7.17.10 '@jest/types': 28.1.3 - '@jridgewell/trace-mapping': 0.3.17 + '@jridgewell/trace-mapping': 0.3.18 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 convert-source-map: 1.9.0 fast-json-stable-stringify: 2.1.0 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-haste-map: 28.1.3 jest-regex-util: 28.0.2 jest-util: 28.1.3 micromatch: 4.0.5 - pirates: 4.0.5 + pirates: 4.0.6 slash: 3.0.0 write-file-atomic: 4.0.2 transitivePeerDependencies: - supports-color dev: false - /@jest/transform@29.3.1: - resolution: {integrity: sha512-8wmCFBTVGYqFNLWfcOWoVuMuKYPUBTnTMDkdvFtAYELwDOl9RGwOsvQWGPFxDJ8AWY9xM/8xCXdqmPK3+Q5Lug==} + /@jest/transform@29.6.1: + resolution: {integrity: sha512-URnTneIU3ZjRSaf906cvf6Hpox3hIeJXRnz3VDSw5/X93gR8ycdfSIEy19FlVx8NFmpN7fe3Gb1xF+NjXaQLWg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/core': 7.20.5 - '@jest/types': 29.3.1 - '@jridgewell/trace-mapping': 0.3.17 + '@babel/core': 7.22.9 + '@jest/types': 29.6.1 + '@jridgewell/trace-mapping': 0.3.18 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 convert-source-map: 2.0.0 fast-json-stable-stringify: 2.1.0 - graceful-fs: 4.2.10 - jest-haste-map: 29.3.1 - jest-regex-util: 29.2.0 - jest-util: 29.3.1 + graceful-fs: 4.2.11 + jest-haste-map: 29.6.1 + jest-regex-util: 29.4.3 + jest-util: 29.6.1 micromatch: 4.0.5 - pirates: 4.0.5 + pirates: 4.0.6 slash: 3.0.0 write-file-atomic: 4.0.2 transitivePeerDependencies: @@ -2782,38 +3118,30 @@ packages: '@jest/schemas': 28.1.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 18.16.18 - '@types/yargs': 17.0.17 + '@types/node': 18.17.0 + '@types/yargs': 17.0.24 chalk: 4.1.2 dev: false - /@jest/types@29.3.1: - resolution: {integrity: sha512-d0S0jmmTpjnhCmNpApgX3jrUZgZ22ivKJRvL2lli5hpCRoNnp1f85r2/wpKfXuYu8E7Jjh1hGfhPyup1NM5AmA==} + /@jest/types@29.6.1: + resolution: {integrity: sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/schemas': 29.0.0 + '@jest/schemas': 29.6.0 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 18.16.18 - '@types/yargs': 17.0.17 + '@types/node': 18.17.0 + '@types/yargs': 17.0.24 chalk: 4.1.2 dev: false - /@jridgewell/gen-mapping@0.1.1: - resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} - engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.14 - dev: false - - /@jridgewell/gen-mapping@0.3.2: - resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} + /@jridgewell/gen-mapping@0.3.3: + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} engines: {node: '>=6.0.0'} dependencies: '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.14 - '@jridgewell/trace-mapping': 0.3.17 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.18 dev: false /@jridgewell/resolve-uri@3.1.0: @@ -2830,8 +3158,12 @@ packages: resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} dev: false - /@jridgewell/trace-mapping@0.3.17: - resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==} + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: false + + /@jridgewell/trace-mapping@0.3.18: + resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} dependencies: '@jridgewell/resolve-uri': 3.1.0 '@jridgewell/sourcemap-codec': 1.4.14 @@ -2862,12 +3194,28 @@ packages: resolution: {integrity: sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==} dev: false + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: false + /@sinonjs/commons@1.8.6: resolution: {integrity: sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==} dependencies: type-detect: 4.0.8 dev: false + /@sinonjs/commons@3.0.0: + resolution: {integrity: sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==} + dependencies: + type-detect: 4.0.8 + dev: false + + /@sinonjs/fake-timers@10.3.0: + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + dependencies: + '@sinonjs/commons': 3.0.0 + dev: false + /@sinonjs/fake-timers@9.1.2: resolution: {integrity: sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==} dependencies: @@ -2877,14 +3225,14 @@ packages: /@toast-ui/editor@3.1.7: resolution: {integrity: sha512-SEfahMrrphveuGhOQyYX3UwjJWjCRnnL6pVPc67uVDBS/JsOESJDG2kcfW9MHJhHnOv6m6WV1jRQW9kPatgumw==} dependencies: - dompurify: 2.4.1 - prosemirror-commands: 1.1.12 - prosemirror-history: 1.1.3 - prosemirror-inputrules: 1.1.3 - prosemirror-keymap: 1.1.5 - prosemirror-model: 1.18.3 - prosemirror-state: 1.4.2 - prosemirror-view: 1.29.1 + dompurify: 2.4.7 + prosemirror-commands: 1.5.2 + prosemirror-history: 1.3.2 + prosemirror-inputrules: 1.2.1 + prosemirror-keymap: 1.2.2 + prosemirror-model: 1.19.3 + prosemirror-state: 1.4.3 + prosemirror-view: 1.31.6 dev: false /@tootallnate/once@2.0.0: @@ -2896,33 +3244,33 @@ packages: resolution: {integrity: sha512-aEE9vSGLA6fF4g9zp6Y2q2UE6GWMYliksQ3VSkR+5n87GqbbbeFiFRwoIBvN5ojc5i6lkIeHfzzNhMdQVNiWMg==} dev: false - /@types/babel__core@7.1.20: - resolution: {integrity: sha512-PVb6Bg2QuscZ30FvOU7z4guG6c926D9YRvOxEaelzndpMsvP+YM74Q/dAFASpg2l6+XLalxSGxcq/lrgYWZtyQ==} + /@types/babel__core@7.20.1: + resolution: {integrity: sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==} dependencies: - '@babel/parser': 7.20.5 - '@babel/types': 7.20.5 + '@babel/parser': 7.22.7 + '@babel/types': 7.22.5 '@types/babel__generator': 7.6.4 '@types/babel__template': 7.4.1 - '@types/babel__traverse': 7.18.3 + '@types/babel__traverse': 7.20.1 dev: false /@types/babel__generator@7.6.4: resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 dev: false /@types/babel__template@7.4.1: resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} dependencies: - '@babel/parser': 7.20.5 - '@babel/types': 7.20.5 + '@babel/parser': 7.22.7 + '@babel/types': 7.22.5 dev: false - /@types/babel__traverse@7.18.3: - resolution: {integrity: sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==} + /@types/babel__traverse@7.20.1: + resolution: {integrity: sha512-MitHFXnhtgwsGZWtT68URpOvLN4EREih1u3QtQiN4VdAxWKRVvGCSvw/Qth0M0Qq3pJpnGOu5JaM/ydK7OGbqg==} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 dev: false /@types/chess.js@0.10.1: @@ -2941,6 +3289,10 @@ packages: resolution: {integrity: sha512-rWr/ryzOUi9r/zUA2GK2qLWGBIBmDeIojBQXuvR76pulHUoEGMJ2A7NWShUaA5AE90ha+l9tlsyGz2UioQE9cg==} dev: false + /@types/emscripten@1.39.6: + resolution: {integrity: sha512-H90aoynNhhkQP6DRweEjJp5vfUVdIj7tdPLsu7pq89vODD/lcugKfZOsfgwpvM6XUewEp2N5dCg1Uf3Qe55Dcg==} + dev: false + /@types/fnando__sparkline@0.3.4: resolution: {integrity: sha512-FWU1zw7CVJYVeDk77FGphTUabfPims4F/Yq+WFB0Gh647lLtiXHWn8vpfT95Fl65IsNBDOhEbxJdhmERMGubNQ==} dev: false @@ -2949,10 +3301,10 @@ packages: resolution: {integrity: sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==} dev: false - /@types/graceful-fs@4.1.5: - resolution: {integrity: sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==} + /@types/graceful-fs@4.1.6: + resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} dependencies: - '@types/node': 18.16.18 + '@types/node': 18.17.0 dev: false /@types/highcharts@4.2.57: @@ -2977,17 +3329,17 @@ packages: '@types/istanbul-lib-report': 3.0.0 dev: false - /@types/jest@28.1.8: - resolution: {integrity: sha512-8TJkV++s7B6XqnDrzR1m/TT0A0h948Pnl/097veySPN67VRAgQ4gZ7n2KfJo2rVq6njQjdxU3GCCyDvAeuHoiw==} + /@types/jest@28.1.6: + resolution: {integrity: sha512-0RbGAFMfcBJKOmqRazM8L98uokwuwD5F8rHrv/ZMbrZBwVOWZUyPG6VFNscjYr/vjM3Vu4fRrCPbOs42AfemaQ==} dependencies: - expect: 28.1.3 + jest-matcher-utils: 28.1.3 pretty-format: 28.1.3 dev: false /@types/jsdom@16.2.15: resolution: {integrity: sha512-nwF87yjBKuX/roqGYerZZM0Nv1pZDMAT5YhOHYeM/72Fic+VEqJh4nyoqoapzJnW3pUlfxPY5FhgsJtM+dRnQQ==} dependencies: - '@types/node': 18.16.18 + '@types/node': 18.17.0 '@types/parse5': 6.0.3 '@types/tough-cookie': 4.0.2 dev: false @@ -3000,32 +3352,36 @@ packages: resolution: {integrity: sha512-/aNaQZD0+iSBAGnvvN2Cx92HqE5sZCPZtx2TsK+4nvV23fFe09jVDvpArXr2j9DnYlzuU9WuoykDDc6wqvpNcw==} dev: false - /@types/node@20.3.2: - resolution: {integrity: sha512-vOBLVQeCQfIcF/2Y7eKFTqrMnizK5lRNQ7ykML/5RuwVXVWxYkgwS7xbt4B6fKCUPgbSL5FSsjHQpaGQP/dQmw==} + /@types/node@18.17.0: + resolution: {integrity: sha512-GXZxEtOxYGFchyUzxvKI14iff9KZ2DI+A6a37o6EQevtg6uO9t+aUZKcaC1Te5Ng1OnLM7K9NVVj+FbecD9cJg==} + dev: false + + /@types/node@20.4.4: + resolution: {integrity: sha512-CukZhumInROvLq3+b5gLev+vgpsIqC2D0deQr/yS1WnxvmYLlJXZpaQrQiseMY+6xusl79E04UjWoqyr+t1/Ew==} dev: false /@types/parse5@6.0.3: resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} dev: false - /@types/prettier@2.7.1: - resolution: {integrity: sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==} + /@types/prettier@2.7.3: + resolution: {integrity: sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==} dev: false /@types/prop-types@15.7.5: resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} dev: false - /@types/react@18.0.26: - resolution: {integrity: sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==} + /@types/react@18.2.15: + resolution: {integrity: sha512-oEjE7TQt1fFTFSbf8kkNuc798ahTUzn3Le67/PWjE8MAfYAD/qB7O8hSTcromLFqHCt9bcdOg5GXMokzTjJ5SA==} dependencies: '@types/prop-types': 15.7.5 - '@types/scheduler': 0.16.2 - csstype: 3.1.1 + '@types/scheduler': 0.16.3 + csstype: 3.1.2 dev: false - /@types/scheduler@0.16.2: - resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} + /@types/scheduler@0.16.3: + resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} dev: false /@types/semver@7.5.0: @@ -3051,15 +3407,15 @@ packages: /@types/yaireo__tagify@4.16.0: resolution: {integrity: sha512-seljsH9d92TqQl+dW+KxrfQR5O46SQD7IeUsArWqeq3Ji+uxcRsQQq0BL1GfCx6XprYdRQZxgWHqqrBRScP9XQ==} dependencies: - '@types/react': 18.0.26 + '@types/react': 18.2.15 dev: false /@types/yargs-parser@21.0.0: resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} dev: false - /@types/yargs@17.0.17: - resolution: {integrity: sha512-72bWxFKTK6uwWJAVT+3rF6Jo6RTojiJ27FQo8Rf60AL+VZbzoVPnMFhKsUnbjR8A3BTCYQ7Mv3hnl8T0A+CX9g==} + /@types/yargs@17.0.24: + resolution: {integrity: sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==} dependencies: '@types/yargs-parser': 21.0.0 dev: false @@ -3079,7 +3435,7 @@ packages: typescript: optional: true dependencies: - '@eslint-community/regexpp': 4.5.1 + '@eslint-community/regexpp': 4.6.1 '@typescript-eslint/parser': 5.60.1(eslint@8.43.0)(typescript@5.1.5) '@typescript-eslint/scope-manager': 5.60.1 '@typescript-eslint/type-utils': 5.60.1(eslint@8.43.0)(typescript@5.1.5) @@ -3089,7 +3445,7 @@ packages: grapheme-splitter: 1.0.4 ignore: 5.2.4 natural-compare-lite: 1.4.0 - semver: 7.5.3 + semver: 7.5.4 tsutils: 3.21.0(typescript@5.1.5) typescript: 5.1.5 transitivePeerDependencies: @@ -3163,7 +3519,7 @@ packages: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.3 + semver: 7.5.4 tsutils: 3.21.0(typescript@5.1.5) typescript: 5.1.5 transitivePeerDependencies: @@ -3184,7 +3540,7 @@ packages: '@typescript-eslint/typescript-estree': 5.60.1(typescript@5.1.5) eslint: 8.43.0 eslint-scope: 5.1.1 - semver: 7.5.3 + semver: 7.5.4 transitivePeerDependencies: - supports-color - typescript @@ -3217,12 +3573,12 @@ packages: acorn-walk: 7.2.0 dev: false - /acorn-jsx@5.3.2(acorn@8.9.0): + /acorn-jsx@5.3.2(acorn@8.10.0): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - acorn: 8.9.0 + acorn: 8.10.0 dev: false /acorn-walk@7.2.0: @@ -3236,14 +3592,8 @@ packages: hasBin: true dev: false - /acorn@8.8.1: - resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: false - - /acorn@8.9.0: - resolution: {integrity: sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==} + /acorn@8.10.0: + resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} engines: {node: '>=0.4.0'} hasBin: true dev: false @@ -3335,19 +3685,19 @@ packages: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: false - /babel-jest@28.1.3(@babel/core@7.20.5): + /babel-jest@28.1.3(@babel/core@7.17.10): resolution: {integrity: sha512-epUaPOEWMk3cWX0M/sPvCHHCe9fMFAa/9hXEgKP8nFfNl/jlGkE9ucq9NqkZGXLDduCJYS0UvSlPUwC0S+rH6Q==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} peerDependencies: '@babel/core': ^7.8.0 dependencies: - '@babel/core': 7.20.5 + '@babel/core': 7.17.10 '@jest/transform': 28.1.3 - '@types/babel__core': 7.1.20 + '@types/babel__core': 7.20.1 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 28.1.3(@babel/core@7.20.5) + babel-preset-jest: 28.1.3(@babel/core@7.17.10) chalk: 4.1.2 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 slash: 3.0.0 transitivePeerDependencies: - supports-color @@ -3357,7 +3707,7 @@ packages: resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} engines: {node: '>=8'} dependencies: - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.22.5 '@istanbuljs/load-nyc-config': 1.1.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-instrument: 5.2.1 @@ -3370,77 +3720,97 @@ packages: resolution: {integrity: sha512-Ys3tUKAmfnkRUpPdpa98eYrAR0nV+sSFUZZEGuQ2EbFd1y4SOLtD5QDNHAq+bb9a+bbXvYQC4b+ID/THIMcU6Q==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: - '@babel/template': 7.18.10 - '@babel/types': 7.20.5 - '@types/babel__core': 7.1.20 - '@types/babel__traverse': 7.18.3 + '@babel/template': 7.22.5 + '@babel/types': 7.22.5 + '@types/babel__core': 7.20.1 + '@types/babel__traverse': 7.20.1 dev: false - /babel-plugin-polyfill-corejs2@0.3.3(@babel/core@7.20.5): + /babel-plugin-polyfill-corejs2@0.3.3(@babel/core@7.17.10): resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.20.5 - '@babel/core': 7.20.5 - '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.20.5) - semver: 6.3.0 + '@babel/compat-data': 7.22.9 + '@babel/core': 7.17.10 + '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.17.10) + semver: 6.3.1 transitivePeerDependencies: - supports-color dev: false - /babel-plugin-polyfill-corejs3@0.6.0(@babel/core@7.20.5): - resolution: {integrity: sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==} + /babel-plugin-polyfill-corejs3@0.5.3(@babel/core@7.17.10): + resolution: {integrity: sha512-zKsXDh0XjnrUEW0mxIHLfjBfnXSMr5Q/goMe/fxpQnLm07mcOZiIZHBNWCMx60HmdvjxfXcalac0tfFg0wqxyw==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.20.5) - core-js-compat: 3.26.1 + '@babel/core': 7.17.10 + '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.17.10) + core-js-compat: 3.31.1 transitivePeerDependencies: - supports-color dev: false - /babel-plugin-polyfill-regenerator@0.4.1(@babel/core@7.20.5): - resolution: {integrity: sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==} + /babel-plugin-polyfill-regenerator@0.3.1(@babel/core@7.17.10): + resolution: {integrity: sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.20.5) + '@babel/core': 7.17.10 + '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.17.10) transitivePeerDependencies: - supports-color dev: false - /babel-preset-current-node-syntax@1.0.1(@babel/core@7.20.5): + /babel-preset-current-node-syntax@1.0.1(@babel/core@7.17.10): + resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.17.10 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.17.10) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.17.10) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.17.10) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.17.10) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.17.10) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.17.10) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.17.10) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.17.10) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.17.10) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.17.10) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.17.10) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.17.10) + dev: false + + /babel-preset-current-node-syntax@1.0.1(@babel/core@7.22.9): resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.20.5 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.20.5) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.20.5) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.20.5) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.20.5) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.20.5) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.20.5) - dev: false - - /babel-preset-jest@28.1.3(@babel/core@7.20.5): + '@babel/core': 7.22.9 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.22.9) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.22.9) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.22.9) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.22.9) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.22.9) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.22.9) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.22.9) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.22.9) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.22.9) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.22.9) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.22.9) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.22.9) + dev: false + + /babel-preset-jest@28.1.3(@babel/core@7.17.10): resolution: {integrity: sha512-L+fupJvlWAHbQfn74coNX3zf60LXMJsezNvvx8eIh7iOR1luJ1poxYgQk1F8PYtNq/6QODDHCqsSnTFSWC491A==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.20.5 + '@babel/core': 7.17.10 babel-plugin-jest-hoist: 28.1.3 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.20.5) + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.17.10) dev: false /balanced-match@1.0.2: @@ -3469,15 +3839,15 @@ packages: resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==} dev: false - /browserslist@4.21.4: - resolution: {integrity: sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==} + /browserslist@4.21.9: + resolution: {integrity: sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001439 - electron-to-chromium: 1.4.284 - node-releases: 2.0.8 - update-browserslist-db: 1.0.10(browserslist@4.21.4) + caniuse-lite: 1.0.30001517 + electron-to-chromium: 1.4.469 + node-releases: 2.0.13 + update-browserslist-db: 1.0.11(browserslist@4.21.9) dev: false /bs-logger@0.2.6: @@ -3512,8 +3882,8 @@ packages: engines: {node: '>=10'} dev: false - /caniuse-lite@1.0.30001439: - resolution: {integrity: sha512-1MgUzEkoMO6gKfXflStpYgZDlFM7M/ck/bgfVCACO5vnAf0fXoNVHdWtqGU+MYca+4bL9Z5bpOVmR33cWW9G2A==} + /caniuse-lite@1.0.30001517: + resolution: {integrity: sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==} dev: false /chalk@2.4.2: @@ -3571,13 +3941,13 @@ packages: '@badrap/result': 0.2.13 dev: false - /ci-info@3.7.0: - resolution: {integrity: sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog==} + /ci-info@3.8.0: + resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} engines: {node: '>=8'} dev: false - /cjs-module-lexer@1.2.2: - resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==} + /cjs-module-lexer@1.2.3: + resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} dev: false /cliui@8.0.1: @@ -3594,8 +3964,8 @@ packages: engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} dev: false - /collect-v8-coverage@1.0.1: - resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==} + /collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} dev: false /color-convert@1.9.3: @@ -3638,10 +4008,10 @@ packages: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} dev: false - /core-js-compat@3.26.1: - resolution: {integrity: sha512-622/KzTudvXCDLRw70iHW4KKs1aGpcRcowGWyYJr2DEBfRrd6hNJybxSWJFuZYD4ma86xhrwDDHxmDaIq4EA8A==} + /core-js-compat@3.31.1: + resolution: {integrity: sha512-wIDWd2s5/5aJSdpOJHfSibxNODxoGoWOBHt8JSPB41NOE94M7kuTPZCYLOlTtuoXTsBPKobpJ6T+y0SSy5L9SA==} dependencies: - browserslist: 4.21.4 + browserslist: 4.21.9 dev: false /cross-spawn@7.0.3: @@ -3668,8 +4038,8 @@ packages: cssom: 0.3.8 dev: false - /csstype@3.1.1: - resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} + /csstype@3.1.2: + resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} dev: false /data-urls@3.0.2: @@ -3714,8 +4084,8 @@ packages: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: false - /deepmerge@4.2.2: - resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} + /deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} dev: false @@ -3734,8 +4104,8 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dev: false - /diff-sequences@29.3.1: - resolution: {integrity: sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ==} + /diff-sequences@29.4.3: + resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: false @@ -3760,16 +4130,16 @@ packages: webidl-conversions: 7.0.0 dev: false - /dompurify@2.4.1: - resolution: {integrity: sha512-ewwFzHzrrneRjxzmK6oVz/rZn9VWspGFRDb4/rRtIsM1n36t9AKma/ye8syCpcw+XJ25kOK/hOG7t1j2I2yBqA==} + /dompurify@2.4.7: + resolution: {integrity: sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ==} dev: false /dragscroll@0.0.8: resolution: {integrity: sha512-nMrx/KErHpEkiKZlrghcT/nLWCj8vEJgv6s6TF84gmgn6uROPx2wRvClkcnjSEyvppYY9okOI1DIv573Toql1w==} dev: false - /electron-to-chromium@1.4.284: - resolution: {integrity: sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==} + /electron-to-chromium@1.4.469: + resolution: {integrity: sha512-HRN9XQjElxJBrdDky5iiUUr3eDwXGTg6Cp4IV8MuNc8VqMkYSneSnIe6poFKx9PsNzkudCgaWCBVxwDqirwQWQ==} dev: false /emittery@0.10.2: @@ -3787,6 +4157,36 @@ packages: is-arrayish: 0.2.1 dev: false + /esbuild@0.18.16: + resolution: {integrity: sha512-1xLsOXrDqwdHxyXb/x/SOyg59jpf/SH7YMvU5RNSU7z3TInaASNJWNFJ6iRvLvLETZMasF3d1DdZLg7sgRimRQ==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.18.16 + '@esbuild/android-arm64': 0.18.16 + '@esbuild/android-x64': 0.18.16 + '@esbuild/darwin-arm64': 0.18.16 + '@esbuild/darwin-x64': 0.18.16 + '@esbuild/freebsd-arm64': 0.18.16 + '@esbuild/freebsd-x64': 0.18.16 + '@esbuild/linux-arm': 0.18.16 + '@esbuild/linux-arm64': 0.18.16 + '@esbuild/linux-ia32': 0.18.16 + '@esbuild/linux-loong64': 0.18.16 + '@esbuild/linux-mips64el': 0.18.16 + '@esbuild/linux-ppc64': 0.18.16 + '@esbuild/linux-riscv64': 0.18.16 + '@esbuild/linux-s390x': 0.18.16 + '@esbuild/linux-x64': 0.18.16 + '@esbuild/netbsd-x64': 0.18.16 + '@esbuild/openbsd-x64': 0.18.16 + '@esbuild/sunos-x64': 0.18.16 + '@esbuild/win32-arm64': 0.18.16 + '@esbuild/win32-ia32': 0.18.16 + '@esbuild/win32-x64': 0.18.16 + dev: false + /esbuild@0.18.4: resolution: {integrity: sha512-9rxWV/Cb2DMUXfe9aUsYtqg0KTlw146ElFH22kYeK9KVV1qT082X4lpmiKsa12ePiCcIcB686TQJxaGAa9TFvA==} engines: {node: '>=12'} @@ -3837,15 +4237,14 @@ packages: engines: {node: '>=10'} dev: false - /escodegen@2.0.0: - resolution: {integrity: sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==} + /escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} engines: {node: '>=6.0'} hasBin: true dependencies: esprima: 4.0.1 estraverse: 5.3.0 esutils: 2.0.3 - optionator: 0.8.3 optionalDependencies: source-map: 0.6.1 dev: false @@ -3867,8 +4266,8 @@ packages: estraverse: 4.3.0 dev: false - /eslint-scope@7.2.0: - resolution: {integrity: sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==} + /eslint-scope@7.2.1: + resolution: {integrity: sha512-CvefSOsDdaYYvxChovdrPo/ZGt8d5lrJWleAc1diXRKhHGiTYEI26cvo8Kle/wGnsizoCJjK73FMg1/IkIwiNA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: esrecurse: 4.3.0 @@ -3886,8 +4285,8 @@ packages: hasBin: true dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.43.0) - '@eslint-community/regexpp': 4.5.1 - '@eslint/eslintrc': 2.0.3 + '@eslint-community/regexpp': 4.6.1 + '@eslint/eslintrc': 2.1.0 '@eslint/js': 8.43.0 '@humanwhocodes/config-array': 0.11.10 '@humanwhocodes/module-importer': 1.0.1 @@ -3898,9 +4297,9 @@ packages: debug: 4.3.4 doctrine: 3.0.0 escape-string-regexp: 4.0.0 - eslint-scope: 7.2.0 + eslint-scope: 7.2.1 eslint-visitor-keys: 3.4.1 - espree: 9.5.2 + espree: 9.6.1 esquery: 1.5.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -3928,12 +4327,12 @@ packages: - supports-color dev: false - /espree@9.5.2: - resolution: {integrity: sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==} + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - acorn: 8.9.0 - acorn-jsx: 5.3.2(acorn@8.9.0) + acorn: 8.10.0 + acorn-jsx: 5.3.2(acorn@8.10.0) eslint-visitor-keys: 3.4.1 dev: false @@ -4003,15 +4402,16 @@ packages: jest-util: 28.1.3 dev: false - /expect@29.3.1: - resolution: {integrity: sha512-gGb1yTgU30Q0O/tQq+z30KBWv24ApkMgFUpvKBkyLUBL68Wv8dHdJxTBZFl/iT8K/bqDHvUYRH6IIN3rToopPA==} + /expect@29.6.1: + resolution: {integrity: sha512-XEdDLonERCU1n9uR56/Stx9OqojaLAQtZf9PrCHH9Hl8YXiEIka3H4NXJ3NOIBmQJTg7+j7buh34PMHfJujc8g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/expect-utils': 29.3.1 - jest-get-type: 29.2.0 - jest-matcher-utils: 29.3.1 - jest-message-util: 29.3.1 - jest-util: 29.3.1 + '@jest/expect-utils': 29.6.1 + '@types/node': 18.17.0 + jest-get-type: 29.4.3 + jest-matcher-utils: 29.6.1 + jest-message-util: 29.6.1 + jest-util: 29.6.1 dev: false /fast-deep-equal@3.1.3: @@ -4029,6 +4429,17 @@ packages: micromatch: 4.0.5 dev: false + /fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: false + /fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} dev: false @@ -4183,14 +4594,14 @@ packages: dependencies: array-union: 2.1.0 dir-glob: 3.0.1 - fast-glob: 3.2.12 + fast-glob: 3.3.1 ignore: 5.2.4 merge2: 1.4.1 slash: 3.0.0 dev: false - /graceful-fs@4.2.10: - resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} dev: false /grapheme-splitter@1.0.4: @@ -4318,8 +4729,8 @@ packages: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} dev: false - /is-core-module@2.11.0: - resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} + /is-core-module@2.12.1: + resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==} dependencies: has: 1.0.3 dev: false @@ -4378,11 +4789,11 @@ packages: resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} engines: {node: '>=8'} dependencies: - '@babel/core': 7.20.5 - '@babel/parser': 7.20.5 + '@babel/core': 7.17.10 + '@babel/parser': 7.22.7 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.0 - semver: 6.3.0 + semver: 6.3.1 transitivePeerDependencies: - supports-color dev: false @@ -4431,7 +4842,7 @@ packages: '@jest/expect': 28.1.3 '@jest/test-result': 28.1.3 '@jest/types': 28.1.3 - '@types/node': 18.16.18 + '@types/node': 18.17.0 chalk: 4.1.2 co: 4.6.0 dedent: 0.7.0 @@ -4465,13 +4876,13 @@ packages: '@jest/types': 28.1.3 chalk: 4.1.2 exit: 0.1.2 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 import-local: 3.1.0 jest-config: 28.1.3(@types/node@18.16.18) jest-util: 28.1.3 jest-validate: 28.1.3 prompts: 2.4.2 - yargs: 17.6.2 + yargs: 17.7.2 transitivePeerDependencies: - '@types/node' - supports-color @@ -4490,16 +4901,55 @@ packages: ts-node: optional: true dependencies: - '@babel/core': 7.20.5 + '@babel/core': 7.17.10 '@jest/test-sequencer': 28.1.3 '@jest/types': 28.1.3 '@types/node': 18.16.18 - babel-jest: 28.1.3(@babel/core@7.20.5) + babel-jest: 28.1.3(@babel/core@7.17.10) + chalk: 4.1.2 + ci-info: 3.8.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 28.1.3 + jest-environment-node: 28.1.3 + jest-get-type: 28.0.2 + jest-regex-util: 28.0.2 + jest-resolve: 28.1.3 + jest-runner: 28.1.3 + jest-util: 28.1.3 + jest-validate: 28.1.3 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 28.1.3 + slash: 3.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: false + + /jest-config@28.1.3(@types/node@18.17.0): + resolution: {integrity: sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.17.10 + '@jest/test-sequencer': 28.1.3 + '@jest/types': 28.1.3 + '@types/node': 18.17.0 + babel-jest: 28.1.3(@babel/core@7.17.10) chalk: 4.1.2 - ci-info: 3.7.0 - deepmerge: 4.2.2 + ci-info: 3.8.0 + deepmerge: 4.3.1 glob: 7.2.3 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-circus: 28.1.3 jest-environment-node: 28.1.3 jest-get-type: 28.0.2 @@ -4527,14 +4977,14 @@ packages: pretty-format: 28.1.3 dev: false - /jest-diff@29.3.1: - resolution: {integrity: sha512-vU8vyiO7568tmin2lA3r2DP8oRvzhvRcD4DjpXc6uGveQodyk7CKLhQlCSiwgx3g0pFaE88/KLZ0yaTWMc4Uiw==} + /jest-diff@29.6.1: + resolution: {integrity: sha512-FsNCvinvl8oVxpNLttNQX7FAq7vR+gMDGj90tiP7siWw1UdakWUGqrylpsYrpvj908IYckm5Y0Q7azNAozU1Kg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: chalk: 4.1.2 - diff-sequences: 29.3.1 - jest-get-type: 29.2.0 - pretty-format: 29.3.1 + diff-sequences: 29.4.3 + jest-get-type: 29.4.3 + pretty-format: 29.6.1 dev: false /jest-docblock@28.1.1: @@ -4563,7 +5013,7 @@ packages: '@jest/fake-timers': 28.1.3 '@jest/types': 28.1.3 '@types/jsdom': 16.2.15 - '@types/node': 20.3.2 + '@types/node': 18.17.0 jest-mock: 28.1.3 jest-util: 28.1.3 jsdom: 19.0.0 @@ -4581,7 +5031,7 @@ packages: '@jest/environment': 28.1.3 '@jest/fake-timers': 28.1.3 '@jest/types': 28.1.3 - '@types/node': 18.16.18 + '@types/node': 18.17.0 jest-mock: 28.1.3 jest-util: 28.1.3 dev: false @@ -4591,8 +5041,8 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dev: false - /jest-get-type@29.2.0: - resolution: {integrity: sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA==} + /jest-get-type@29.4.3: + resolution: {integrity: sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: false @@ -4601,11 +5051,11 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: '@jest/types': 28.1.3 - '@types/graceful-fs': 4.1.5 - '@types/node': 18.16.18 + '@types/graceful-fs': 4.1.6 + '@types/node': 18.17.0 anymatch: 3.1.3 fb-watchman: 2.0.2 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-regex-util: 28.0.2 jest-util: 28.1.3 jest-worker: 28.1.3 @@ -4615,19 +5065,19 @@ packages: fsevents: 2.3.2 dev: false - /jest-haste-map@29.3.1: - resolution: {integrity: sha512-/FFtvoG1xjbbPXQLFef+WSU4yrc0fc0Dds6aRPBojUid7qlPqZvxdUBA03HW0fnVHXVCnCdkuoghYItKNzc/0A==} + /jest-haste-map@29.6.1: + resolution: {integrity: sha512-0m7f9PZXxOCk1gRACiVgX85knUKPKLPg4oRCjLoqIm9brTHXaorMA0JpmtmVkQiT8nmXyIVoZd/nnH1cfC33ig==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.3.1 - '@types/graceful-fs': 4.1.5 - '@types/node': 18.16.18 + '@jest/types': 29.6.1 + '@types/graceful-fs': 4.1.6 + '@types/node': 18.17.0 anymatch: 3.1.3 fb-watchman: 2.0.2 - graceful-fs: 4.2.10 - jest-regex-util: 29.2.0 - jest-util: 29.3.1 - jest-worker: 29.3.1 + graceful-fs: 4.2.11 + jest-regex-util: 29.4.3 + jest-util: 29.6.1 + jest-worker: 29.6.1 micromatch: 4.0.5 walker: 1.0.8 optionalDependencies: @@ -4652,42 +5102,42 @@ packages: pretty-format: 28.1.3 dev: false - /jest-matcher-utils@29.3.1: - resolution: {integrity: sha512-fkRMZUAScup3txIKfMe3AIZZmPEjWEdsPJFK3AIy5qRohWqQFg1qrmKfYXR9qEkNc7OdAu2N4KPHibEmy4HPeQ==} + /jest-matcher-utils@29.6.1: + resolution: {integrity: sha512-SLaztw9d2mfQQKHmJXKM0HCbl2PPVld/t9Xa6P9sgiExijviSp7TnZZpw2Fpt+OI3nwUO/slJbOfzfUMKKC5QA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: chalk: 4.1.2 - jest-diff: 29.3.1 - jest-get-type: 29.2.0 - pretty-format: 29.3.1 + jest-diff: 29.6.1 + jest-get-type: 29.4.3 + pretty-format: 29.6.1 dev: false /jest-message-util@28.1.3: resolution: {integrity: sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.22.5 '@jest/types': 28.1.3 '@types/stack-utils': 2.0.1 chalk: 4.1.2 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 micromatch: 4.0.5 pretty-format: 28.1.3 slash: 3.0.0 stack-utils: 2.0.6 dev: false - /jest-message-util@29.3.1: - resolution: {integrity: sha512-lMJTbgNcDm5z+6KDxWtqOFWlGQxD6XaYwBqHR8kmpkP+WWWG90I35kdtQHY67Ay5CSuydkTBbJG+tH9JShFCyA==} + /jest-message-util@29.6.1: + resolution: {integrity: sha512-KoAW2zAmNSd3Gk88uJ56qXUWbFk787QKmjjJVOjtGFmmGSZgDBrlIL4AfQw1xyMYPNVD7dNInfIbur9B2rd/wQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/code-frame': 7.18.6 - '@jest/types': 29.3.1 + '@babel/code-frame': 7.22.5 + '@jest/types': 29.6.1 '@types/stack-utils': 2.0.1 chalk: 4.1.2 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 micromatch: 4.0.5 - pretty-format: 29.3.1 + pretty-format: 29.6.1 slash: 3.0.0 stack-utils: 2.0.6 dev: false @@ -4697,16 +5147,16 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: '@jest/types': 28.1.3 - '@types/node': 18.16.18 + '@types/node': 18.17.0 dev: false - /jest-mock@29.3.1: - resolution: {integrity: sha512-H8/qFDtDVMFvFP4X8NuOT3XRDzOUTz+FeACjufHzsOIBAxivLqkB1PoLCaJx9iPPQ8dZThHPp/G3WRWyMgA3JA==} + /jest-mock@29.6.1: + resolution: {integrity: sha512-brovyV9HBkjXAEdRooaTQK42n8usKoSRR3gihzUpYeV/vwqgSoNfrksO7UfSACnPmxasO/8TmHM3w9Hp3G1dgw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.3.1 - '@types/node': 18.16.18 - jest-util: 29.3.1 + '@jest/types': 29.6.1 + '@types/node': 18.17.0 + jest-util: 29.6.1 dev: false /jest-pnp-resolver@1.2.3(jest-resolve@28.1.3): @@ -4726,8 +5176,8 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dev: false - /jest-regex-util@29.2.0: - resolution: {integrity: sha512-6yXn0kg2JXzH30cr2NlThF+70iuO/3irbaB4mh5WyqNIvLLP+B6sFdluO1/1RJmslyh/f9osnefECflHvTbwVA==} + /jest-regex-util@29.4.3: + resolution: {integrity: sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: false @@ -4746,13 +5196,13 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: chalk: 4.1.2 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-haste-map: 28.1.3 jest-pnp-resolver: 1.2.3(jest-resolve@28.1.3) jest-util: 28.1.3 jest-validate: 28.1.3 - resolve: 1.22.1 - resolve.exports: 1.1.0 + resolve: 1.22.2 + resolve.exports: 1.1.1 slash: 3.0.0 dev: false @@ -4765,10 +5215,10 @@ packages: '@jest/test-result': 28.1.3 '@jest/transform': 28.1.3 '@jest/types': 28.1.3 - '@types/node': 18.16.18 + '@types/node': 18.17.0 chalk: 4.1.2 emittery: 0.10.2 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-docblock: 28.1.1 jest-environment-node: 28.1.3 jest-haste-map: 28.1.3 @@ -4797,11 +5247,11 @@ packages: '@jest/transform': 28.1.3 '@jest/types': 28.1.3 chalk: 4.1.2 - cjs-module-lexer: 1.2.2 - collect-v8-coverage: 1.0.1 + cjs-module-lexer: 1.2.3 + collect-v8-coverage: 1.0.2 execa: 5.1.1 glob: 7.2.3 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-haste-map: 28.1.3 jest-message-util: 28.1.3 jest-mock: 28.1.3 @@ -4819,20 +5269,20 @@ packages: resolution: {integrity: sha512-4lzMgtiNlc3DU/8lZfmqxN3AYD6GGLbl+72rdBpXvcV+whX7mDrREzkPdp2RnmfIiWBg1YbuFSkXduF2JcafJg==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: - '@babel/core': 7.20.5 - '@babel/generator': 7.20.5 - '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.20.5) - '@babel/traverse': 7.20.5 - '@babel/types': 7.20.5 + '@babel/core': 7.17.10 + '@babel/generator': 7.22.9 + '@babel/plugin-syntax-typescript': 7.22.5(@babel/core@7.17.10) + '@babel/traverse': 7.22.8 + '@babel/types': 7.22.5 '@jest/expect-utils': 28.1.3 '@jest/transform': 28.1.3 '@jest/types': 28.1.3 - '@types/babel__traverse': 7.18.3 - '@types/prettier': 2.7.1 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.20.5) + '@types/babel__traverse': 7.20.1 + '@types/prettier': 2.7.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.17.10) chalk: 4.1.2 expect: 28.1.3 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-diff: 28.1.3 jest-get-type: 28.0.2 jest-haste-map: 28.1.3 @@ -4841,39 +5291,36 @@ packages: jest-util: 28.1.3 natural-compare: 1.4.0 pretty-format: 28.1.3 - semver: 7.5.3 + semver: 7.5.4 transitivePeerDependencies: - supports-color dev: false - /jest-snapshot@29.3.1: - resolution: {integrity: sha512-+3JOc+s28upYLI2OJM4PWRGK9AgpsMs/ekNryUV0yMBClT9B1DF2u2qay8YxcQd338PPYSFNb0lsar1B49sLDA==} + /jest-snapshot@29.6.1: + resolution: {integrity: sha512-G4UQE1QQ6OaCgfY+A0uR1W2AY0tGXUPQpoUClhWHq1Xdnx1H6JOrC2nH5lqnOEqaDgbHFgIwZ7bNq24HpB180A==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/core': 7.20.5 - '@babel/generator': 7.20.5 - '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.20.5) - '@babel/traverse': 7.20.5 - '@babel/types': 7.20.5 - '@jest/expect-utils': 29.3.1 - '@jest/transform': 29.3.1 - '@jest/types': 29.3.1 - '@types/babel__traverse': 7.18.3 - '@types/prettier': 2.7.1 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.20.5) + '@babel/core': 7.22.9 + '@babel/generator': 7.22.9 + '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.22.9) + '@babel/plugin-syntax-typescript': 7.22.5(@babel/core@7.22.9) + '@babel/types': 7.22.5 + '@jest/expect-utils': 29.6.1 + '@jest/transform': 29.6.1 + '@jest/types': 29.6.1 + '@types/prettier': 2.7.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.9) chalk: 4.1.2 - expect: 29.3.1 - graceful-fs: 4.2.10 - jest-diff: 29.3.1 - jest-get-type: 29.2.0 - jest-haste-map: 29.3.1 - jest-matcher-utils: 29.3.1 - jest-message-util: 29.3.1 - jest-util: 29.3.1 + expect: 29.6.1 + graceful-fs: 4.2.11 + jest-diff: 29.6.1 + jest-get-type: 29.4.3 + jest-matcher-utils: 29.6.1 + jest-message-util: 29.6.1 + jest-util: 29.6.1 natural-compare: 1.4.0 - pretty-format: 29.3.1 - semver: 7.5.3 + pretty-format: 29.6.1 + semver: 7.5.4 transitivePeerDependencies: - supports-color dev: false @@ -4883,22 +5330,22 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: '@jest/types': 28.1.3 - '@types/node': 18.16.18 + '@types/node': 18.17.0 chalk: 4.1.2 - ci-info: 3.7.0 - graceful-fs: 4.2.10 + ci-info: 3.8.0 + graceful-fs: 4.2.11 picomatch: 2.3.1 dev: false - /jest-util@29.3.1: - resolution: {integrity: sha512-7YOVZaiX7RJLv76ZfHt4nbNEzzTRiMW/IiOG7ZOKmTXmoGBxUDefgMAxQubu6WPVqP5zSzAdZG0FfLcC7HOIFQ==} + /jest-util@29.6.1: + resolution: {integrity: sha512-NRFCcjc+/uO3ijUVyNOQJluf8PtGCe/W6cix36+M3cTFgiYqFOOW5MgN4JOOcvbUhcKTYVd1CvHz/LWi8d16Mg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.3.1 - '@types/node': 18.16.18 + '@jest/types': 29.6.1 + '@types/node': 18.17.0 chalk: 4.1.2 - ci-info: 3.7.0 - graceful-fs: 4.2.10 + ci-info: 3.8.0 + graceful-fs: 4.2.11 picomatch: 2.3.1 dev: false @@ -4920,7 +5367,7 @@ packages: dependencies: '@jest/test-result': 28.1.3 '@jest/types': 28.1.3 - '@types/node': 18.16.18 + '@types/node': 18.17.0 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.10.2 @@ -4932,17 +5379,17 @@ packages: resolution: {integrity: sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: - '@types/node': 18.16.18 + '@types/node': 18.17.0 merge-stream: 2.0.0 supports-color: 8.1.1 dev: false - /jest-worker@29.3.1: - resolution: {integrity: sha512-lY4AnnmsEWeiXirAIA0c9SDPbuCBq8IYuDVL8PMm0MZ2PEs2yPvRA/J64QBXuZp7CYKrDM/rmNrc9/i3KJQncw==} + /jest-worker@29.6.1: + resolution: {integrity: sha512-U+Wrbca7S8ZAxAe9L6nb6g8kPdia5hj32Puu5iOqBCMTMWFHXuK6dOV2IFrpedbTV8fjMFLdWNttQTBL6u2MRA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 18.16.18 - jest-util: 29.3.1 + '@types/node': 18.17.0 + jest-util: 29.6.1 merge-stream: 2.0.0 supports-color: 8.1.1 dev: false @@ -4996,31 +5443,31 @@ packages: optional: true dependencies: abab: 2.0.6 - acorn: 8.8.1 + acorn: 8.10.0 acorn-globals: 6.0.0 cssom: 0.5.0 cssstyle: 2.3.0 data-urls: 3.0.2 decimal.js: 10.4.3 domexception: 4.0.0 - escodegen: 2.0.0 + escodegen: 2.1.0 form-data: 4.0.0 html-encoding-sniffer: 3.0.0 http-proxy-agent: 5.0.0 https-proxy-agent: 5.0.1 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.2 + nwsapi: 2.2.7 parse5: 6.0.1 saxes: 5.0.1 symbol-tree: 3.2.4 - tough-cookie: 4.1.2 + tough-cookie: 4.1.3 w3c-hr-time: 1.0.2 w3c-xmlserializer: 3.0.0 webidl-conversions: 7.0.0 whatwg-encoding: 2.0.0 whatwg-mimetype: 3.0.0 whatwg-url: 10.0.0 - ws: 8.11.0 + ws: 8.13.0 xml-name-validator: 4.0.0 transitivePeerDependencies: - bufferutil @@ -5051,8 +5498,8 @@ packages: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: false - /json5@2.2.2: - resolution: {integrity: sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==} + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} hasBin: true dev: false @@ -5067,14 +5514,6 @@ packages: engines: {node: '>=6'} dev: false - /levn@0.3.0: - resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} - engines: {node: '>= 0.8.0'} - dependencies: - prelude-ls: 1.1.2 - type-check: 0.3.2 - dev: false - /levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -5086,7 +5525,7 @@ packages: /lichess-pgn-viewer@1.6.1: resolution: {integrity: sha512-rKGRbpqy7xgcQ8S5Wneu86UFuJnsTPyVw0TJYCUHxO/+x5eIN8GF8TCxf2Q/aZ6YR4kec2SLjIBTik4KejHeZw==} dependencies: - '@types/node': 18.16.18 + '@types/node': 18.17.0 chessground: 8.3.13 chessops: 0.12.7 snabbdom: 3.5.1 @@ -5129,6 +5568,12 @@ packages: js-tokens: 4.0.0 dev: false + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: false + /lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -5140,7 +5585,7 @@ packages: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} dependencies: - semver: 6.3.0 + semver: 6.3.1 dev: false /make-error@1.3.6: @@ -5213,8 +5658,8 @@ packages: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} dev: false - /node-releases@2.0.8: - resolution: {integrity: sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==} + /node-releases@2.0.13: + resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} dev: false /normalize-path@3.0.0: @@ -5233,8 +5678,8 @@ packages: resolution: {integrity: sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==} dev: false - /nwsapi@2.2.2: - resolution: {integrity: sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==} + /nwsapi@2.2.7: + resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} dev: false /object-assign@4.1.1: @@ -5255,18 +5700,6 @@ packages: mimic-fn: 2.1.0 dev: false - /optionator@0.8.3: - resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} - engines: {node: '>= 0.8.0'} - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.3.0 - prelude-ls: 1.1.2 - type-check: 0.3.2 - word-wrap: 1.2.3 - dev: false - /optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} @@ -5279,8 +5712,8 @@ packages: type-check: 0.4.0 dev: false - /orderedmap@2.1.0: - resolution: {integrity: sha512-/pIFexOm6S70EPdznemIz3BQZoJ4VTFrhqzu0ACBqBgeLsLxq8e6Jim63ImIfwW/zAD1AlXpRMlOv3aghmo4dA==} + /orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} dev: false /p-limit@2.3.0: @@ -5327,7 +5760,7 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.22.5 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -5370,8 +5803,8 @@ packages: engines: {node: '>=8.6'} dev: false - /pirates@4.0.5: - resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} + /pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} dev: false @@ -5382,11 +5815,6 @@ packages: find-up: 4.1.0 dev: false - /prelude-ls@1.1.2: - resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} - engines: {node: '>= 0.8.0'} - dev: false - /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -5408,11 +5836,11 @@ packages: react-is: 18.2.0 dev: false - /pretty-format@29.3.1: - resolution: {integrity: sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg==} + /pretty-format@29.6.1: + resolution: {integrity: sha512-7jRj+yXO0W7e4/tSJKoR7HRIHLPPjtNaUGG2xxKQnGvPNRkgWcQ0AZX6P4KBRJN4FcTBWb3sa7DVUJmocYuoog==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/schemas': 29.0.0 + '@jest/schemas': 29.6.0 ansi-styles: 5.2.0 react-is: 18.2.0 dev: false @@ -5433,73 +5861,69 @@ packages: react-is: 16.13.1 dev: false - /prosemirror-commands@1.1.12: - resolution: {integrity: sha512-+CrMs3w/ZVPSkR+REg8KL/clyFLv/1+SgY/OMN+CB22Z24j9TZDje72vL36lOZ/E4NeRXuiCcmENcW/vAcG67A==} + /prosemirror-commands@1.5.2: + resolution: {integrity: sha512-hgLcPaakxH8tu6YvVAaILV2tXYsW3rAdDR8WNkeKGcgeMVQg3/TMhPdVoh7iAmfgVjZGtcOSjKiQaoeKjzd2mQ==} dependencies: - prosemirror-model: 1.18.3 - prosemirror-state: 1.4.2 - prosemirror-transform: 1.7.0 + prosemirror-model: 1.19.3 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.7.3 dev: false - /prosemirror-history@1.1.3: - resolution: {integrity: sha512-zGDotijea+vnfnyyUGyiy1wfOQhf0B/b6zYcCouBV8yo6JmrE9X23M5q7Nf/nATywEZbgRLG70R4DmfSTC+gfg==} + /prosemirror-history@1.3.2: + resolution: {integrity: sha512-/zm0XoU/N/+u7i5zepjmZAEnpvjDtzoPWW6VmKptcAnPadN/SStsBjMImdCEbb3seiNTpveziPTIrXQbHLtU1g==} dependencies: - prosemirror-state: 1.4.2 - prosemirror-transform: 1.7.0 - rope-sequence: 1.3.3 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.7.3 + prosemirror-view: 1.31.6 + rope-sequence: 1.3.4 dev: false - /prosemirror-inputrules@1.1.3: - resolution: {integrity: sha512-ZaHCLyBtvbyIHv0f5p6boQTIJjlD6o2NPZiEaZWT2DA+j591zS29QQEMT4lBqwcLW3qRSf7ZvoKNbf05YrsStw==} + /prosemirror-inputrules@1.2.1: + resolution: {integrity: sha512-3LrWJX1+ULRh5SZvbIQlwZafOXqp1XuV21MGBu/i5xsztd+9VD15x6OtN6mdqSFI7/8Y77gYUbQ6vwwJ4mr6QQ==} dependencies: - prosemirror-state: 1.4.2 - prosemirror-transform: 1.7.0 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.7.3 dev: false - /prosemirror-keymap@1.1.5: - resolution: {integrity: sha512-8SZgPH3K+GLsHL2wKuwBD9rxhsbnVBTwpHCO4VUO5GmqUQlxd/2GtBVWTsyLq4Dp3N9nGgPd3+lZFKUDuVp+Vw==} + /prosemirror-keymap@1.2.2: + resolution: {integrity: sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==} dependencies: - prosemirror-state: 1.4.2 - w3c-keyname: 2.2.6 + prosemirror-state: 1.4.3 + w3c-keyname: 2.2.8 dev: false - /prosemirror-model@1.18.3: - resolution: {integrity: sha512-yUVejauEY3F1r7PDy4UJKEGeIU+KFc71JQl5sNvG66CLVdKXRjhWpBW6KMeduGsmGOsw85f6EGrs6QxIKOVILA==} + /prosemirror-model@1.19.3: + resolution: {integrity: sha512-tgSnwN7BS7/UM0sSARcW+IQryx2vODKX4MI7xpqY2X+iaepJdKBPc7I4aACIsDV/LTaTjt12Z56MhDr9LsyuZQ==} dependencies: - orderedmap: 2.1.0 + orderedmap: 2.1.1 dev: false - /prosemirror-state@1.4.2: - resolution: {integrity: sha512-puuzLD2mz/oTdfgd8msFbe0A42j5eNudKAAPDB0+QJRw8cO1ygjLmhLrg9RvDpf87Dkd6D4t93qdef00KKNacQ==} + /prosemirror-state@1.4.3: + resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==} dependencies: - prosemirror-model: 1.18.3 - prosemirror-transform: 1.7.0 - prosemirror-view: 1.29.1 + prosemirror-model: 1.19.3 + prosemirror-transform: 1.7.3 + prosemirror-view: 1.31.6 dev: false - /prosemirror-transform@1.7.0: - resolution: {integrity: sha512-O4T697Cqilw06Zvc3Wm+e237R6eZtJL/xGMliCi+Uo8VL6qHk6afz1qq0zNjT3eZMuYwnP8ZS0+YxX/tfcE9TQ==} + /prosemirror-transform@1.7.3: + resolution: {integrity: sha512-qDapyx5lqYfxVeUWEw0xTGgeP2S8346QtE7DxkalsXlX89lpzkY6GZfulgfHyk1n4tf74sZ7CcXgcaCcGjsUtA==} dependencies: - prosemirror-model: 1.18.3 + prosemirror-model: 1.19.3 dev: false - /prosemirror-view@1.29.1: - resolution: {integrity: sha512-OhujVZSDsh0l0PyHNdfaBj6DBkbhYaCfbaxmTeFrMKd/eWS+G6IC+OAbmR9IsLC8Se1HSbphMaXnsXjupHL3UQ==} + /prosemirror-view@1.31.6: + resolution: {integrity: sha512-wwgErp+EWnuW4kGAYKrt90hhOetaoWpYNdOpnuQMXo1m4x/+uhauFeQoCCm8J30ZqAa4LgIER4yzKSO545gRfA==} dependencies: - prosemirror-model: 1.18.3 - prosemirror-state: 1.4.2 - prosemirror-transform: 1.7.0 + prosemirror-model: 1.19.3 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.7.3 dev: false /psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} dev: false - /punycode@2.1.1: - resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} - engines: {node: '>=6'} - dev: false - /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} @@ -5539,25 +5963,21 @@ packages: /regenerator-transform@0.15.1: resolution: {integrity: sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==} dependencies: - '@babel/runtime': 7.20.6 + '@babel/runtime': 7.22.6 dev: false - /regexpu-core@5.2.2: - resolution: {integrity: sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw==} + /regexpu-core@5.3.2: + resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} engines: {node: '>=4'} dependencies: + '@babel/regjsgen': 0.8.0 regenerate: 1.4.2 regenerate-unicode-properties: 10.1.0 - regjsgen: 0.7.1 regjsparser: 0.9.1 unicode-match-property-ecmascript: 2.0.0 unicode-match-property-value-ecmascript: 2.1.0 dev: false - /regjsgen@0.7.1: - resolution: {integrity: sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==} - dev: false - /regjsparser@0.9.1: resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} hasBin: true @@ -5591,16 +6011,16 @@ packages: engines: {node: '>=8'} dev: false - /resolve.exports@1.1.0: - resolution: {integrity: sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==} + /resolve.exports@1.1.1: + resolution: {integrity: sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==} engines: {node: '>=10'} dev: false - /resolve@1.22.1: - resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} + /resolve@1.22.2: + resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} hasBin: true dependencies: - is-core-module: 2.11.0 + is-core-module: 2.12.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 dev: false @@ -5617,8 +6037,8 @@ packages: glob: 7.2.3 dev: false - /rope-sequence@1.3.3: - resolution: {integrity: sha512-85aZYCxweiD5J8yTEbw+E6A27zSnLPNDL0WfPdw3YYodq7WjnTKo0q4dtyQ2gz23iPT8Q9CUyJtAaUNcTxRf5Q==} + /rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} dev: false /run-parallel@1.2.0: @@ -5646,21 +6066,13 @@ packages: xmlchars: 2.2.0 dev: false - /semver@6.3.0: - resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true dev: false - /semver@7.3.8: - resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} - engines: {node: '>=10'} - hasBin: true - dependencies: - lru-cache: 6.0.0 - dev: false - - /semver@7.5.3: - resolution: {integrity: sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==} + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} engines: {node: '>=10'} hasBin: true dependencies: @@ -5908,12 +6320,12 @@ packages: is-number: 7.0.0 dev: false - /tough-cookie@4.1.2: - resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==} + /tough-cookie@4.1.3: + resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} engines: {node: '>=6'} dependencies: psl: 1.9.0 - punycode: 2.1.1 + punycode: 2.3.0 universalify: 0.2.0 url-parse: 1.5.10 dev: false @@ -5925,8 +6337,8 @@ packages: punycode: 2.3.0 dev: false - /ts-jest@28.0.8(@babel/core@7.20.5)(jest@28.1.3)(typescript@5.1.6): - resolution: {integrity: sha512-5FaG0lXmRPzApix8oFG8RKjAz4ehtm8yMKOTy5HX3fY6W8kmvOrmcY0hKDElW52FJov+clhUbrKAqofnj4mXTg==} + /ts-jest@28.0.7(@babel/core@7.17.10)(jest@28.1.3)(typescript@5.1.6): + resolution: {integrity: sha512-wWXCSmTwBVmdvWrOpYhal79bDpioDy4rTT+0vyUnE3ZzM7LOAAGG9NXwzkEL/a516rQEgnMmS/WKP9jBPCVJyA==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} hasBin: true peerDependencies: @@ -5946,15 +6358,15 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.20.5 + '@babel/core': 7.17.10 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 jest: 28.1.3(@types/node@18.16.18) jest-util: 28.1.3 - json5: 2.2.2 + json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.3.8 + semver: 7.5.4 typescript: 5.1.6 yargs-parser: 21.1.1 dev: false @@ -5973,13 +6385,6 @@ packages: typescript: 5.1.5 dev: false - /type-check@0.3.2: - resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} - engines: {node: '>= 0.8.0'} - dependencies: - prelude-ls: 1.1.2 - dev: false - /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -6046,13 +6451,13 @@ packages: engines: {node: '>= 4.0.0'} dev: false - /update-browserslist-db@1.0.10(browserslist@4.21.4): - resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==} + /update-browserslist-db@1.0.11(browserslist@4.21.9): + resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' dependencies: - browserslist: 4.21.4 + browserslist: 4.21.9 escalade: 3.1.1 picocolors: 1.0.0 dev: false @@ -6075,11 +6480,11 @@ packages: hasBin: true dev: false - /v8-to-istanbul@9.0.1: - resolution: {integrity: sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==} + /v8-to-istanbul@9.1.0: + resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==} engines: {node: '>=10.12.0'} dependencies: - '@jridgewell/trace-mapping': 0.3.17 + '@jridgewell/trace-mapping': 0.3.18 '@types/istanbul-lib-coverage': 2.0.4 convert-source-map: 1.9.0 dev: false @@ -6097,8 +6502,8 @@ packages: browser-process-hrtime: 1.0.0 dev: false - /w3c-keyname@2.2.6: - resolution: {integrity: sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==} + /w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} dev: false /w3c-xmlserializer@3.0.0: @@ -6155,11 +6560,6 @@ packages: isexe: 2.0.0 dev: false - /word-wrap@1.2.3: - resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} - engines: {node: '>=0.10.0'} - dev: false - /wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -6181,12 +6581,12 @@ packages: signal-exit: 3.0.7 dev: false - /ws@8.11.0: - resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} + /ws@8.13.0: + resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 + utf-8-validate: '>=5.0.2' peerDependenciesMeta: bufferutil: optional: true @@ -6221,6 +6621,10 @@ packages: engines: {node: '>=10'} dev: false + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: false + /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} dev: false @@ -6230,8 +6634,8 @@ packages: engines: {node: '>=12'} dev: false - /yargs@17.6.2: - resolution: {integrity: sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==} + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} dependencies: cliui: 8.0.1 @@ -6248,6 +6652,16 @@ packages: engines: {node: '>=10'} dev: false + /zerofish@0.0.4: + resolution: {integrity: sha512-LC8M8DTv5tkyju0+s2bRonrqRRiaqRqy7rAW4A3mneOZynEKyzLujw1LMHunfMvcAIRc3PePU4jbh/cb1GIFxg==} + dependencies: + '@types/emscripten': 1.39.6 + '@types/node': 20.4.4 + '@types/web': 0.0.84 + esbuild: 0.18.16 + typescript: 5.1.6 + dev: false + /zxcvbn@4.4.2: resolution: {integrity: sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ==} dev: false diff --git a/ui/localPlay/package.json b/ui/localPlay/package.json index 4144302423169..eddc5b8dfb6ea 100644 --- a/ui/localPlay/package.json +++ b/ui/localPlay/package.json @@ -16,7 +16,7 @@ "puz": "workspace:*", "snabbdom": "^3.5.1", "tree": "workspace:*", - "zerofish": "^0.0.4" + "zerofish": "^0.0.5" }, "scripts": { "compile": "tsc", From 608f34ef69652d471a63841069e9d4d29487af2c Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Tue, 25 Jul 2023 08:09:41 -0500 Subject: [PATCH 006/174] gah --- pnpm-lock.yaml | 250 +----------------------------- ui/localPlay/css/_local-play.scss | 11 ++ ui/localPlay/src/ctrl.ts | 108 ++++++++++--- ui/localPlay/src/main.ts | 4 +- ui/localPlay/src/view.ts | 61 ++++---- 5 files changed, 130 insertions(+), 304 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71f972fe363b2..4f872203c05e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -491,8 +491,8 @@ importers: specifier: workspace:* version: link:../tree zerofish: - specifier: ^0.0.4 - version: 0.0.4 + specifier: ^0.0.5 + version: link:../../../zerofish ui/mod: dependencies: @@ -2355,15 +2355,6 @@ packages: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: false - /@esbuild/android-arm64@0.18.16: - resolution: {integrity: sha512-wsCqSPqLz+6Ov+OM4EthU43DyYVVyfn15S4j1bJzylDpc1r1jZFFfJQNfDuT8SlgwuqpmpJXK4uPlHGw6ve7eA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: false - optional: true - /@esbuild/android-arm64@0.18.4: resolution: {integrity: sha512-yQVgO+V307hA2XhzELQ6F91CBGX7gSnlVGAj5YIqjQOxThDpM7fOcHT2YLJbE6gNdPtgRSafQrsK8rJ9xHCaZg==} engines: {node: '>=12'} @@ -2373,15 +2364,6 @@ packages: dev: false optional: true - /@esbuild/android-arm@0.18.16: - resolution: {integrity: sha512-gCHjjQmA8L0soklKbLKA6pgsLk1byULuHe94lkZDzcO3/Ta+bbeewJioEn1Fr7kgy9NWNFy/C+MrBwC6I/WCug==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: false - optional: true - /@esbuild/android-arm@0.18.4: resolution: {integrity: sha512-yKmQC9IiuvHdsNEbPHSprnMHg6OhL1cSeQZLzPpgzJBJ9ppEg9GAZN8MKj1TcmB4tZZUrq5xjK7KCmhwZP8iDA==} engines: {node: '>=12'} @@ -2391,15 +2373,6 @@ packages: dev: false optional: true - /@esbuild/android-x64@0.18.16: - resolution: {integrity: sha512-ldsTXolyA3eTQ1//4DS+E15xl0H/3DTRJaRL0/0PgkqDsI0fV/FlOtD+h0u/AUJr+eOTlZv4aC9gvfppo3C4sw==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: false - optional: true - /@esbuild/android-x64@0.18.4: resolution: {integrity: sha512-yLKXMxQg6sk1ntftxQ5uwyVgG4/S2E7UoOCc5N4YZW7fdkfRiYEXqm7CMuIfY2Vs3FTrNyKmSfNevIuIvJnMww==} engines: {node: '>=12'} @@ -2409,15 +2382,6 @@ packages: dev: false optional: true - /@esbuild/darwin-arm64@0.18.16: - resolution: {integrity: sha512-aBxruWCII+OtluORR/KvisEw0ALuw/qDQWvkoosA+c/ngC/Kwk0lLaZ+B++LLS481/VdydB2u6tYpWxUfnLAIw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - /@esbuild/darwin-arm64@0.18.4: resolution: {integrity: sha512-MVPEoZjZpk2xQ1zckZrb8eQuQib+QCzdmMs3YZAYEQPg+Rztk5pUxGyk8htZOC8Z38NMM29W+MqY9Sqo/sDGKw==} engines: {node: '>=12'} @@ -2427,15 +2391,6 @@ packages: dev: false optional: true - /@esbuild/darwin-x64@0.18.16: - resolution: {integrity: sha512-6w4Dbue280+rp3LnkgmriS1icOUZDyPuZo/9VsuMUTns7SYEiOaJ7Ca1cbhu9KVObAWfmdjUl4gwy9TIgiO5eA==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - /@esbuild/darwin-x64@0.18.4: resolution: {integrity: sha512-uEsRtYRUDsz7i2tXg/t/SyF+5gU1cvi9B6B8i5ebJgtUUHJYWyIPIesmIOL4/+bywjxsDMA/XrNFMgMffLnh5A==} engines: {node: '>=12'} @@ -2445,15 +2400,6 @@ packages: dev: false optional: true - /@esbuild/freebsd-arm64@0.18.16: - resolution: {integrity: sha512-x35fCebhe9s979DGKbVAwXUOcTmCIE32AIqB9CB1GralMIvxdnMLAw5CnID17ipEw9/3MvDsusj/cspYt2ZLNQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: false - optional: true - /@esbuild/freebsd-arm64@0.18.4: resolution: {integrity: sha512-I8EOigqWnOHRin6Zp5Y1cfH3oT54bd7Sdz/VnpUNksbOtfp8IWRTH4pgkgO5jWaRQPjCpJcOpdRjYAMjPt8wXg==} engines: {node: '>=12'} @@ -2463,15 +2409,6 @@ packages: dev: false optional: true - /@esbuild/freebsd-x64@0.18.16: - resolution: {integrity: sha512-YM98f+PeNXF3GbxIJlUsj+McUWG1irguBHkszCIwfr3BXtXZsXo0vqybjUDFfu9a8Wr7uUD/YSmHib+EeGAFlg==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: false - optional: true - /@esbuild/freebsd-x64@0.18.4: resolution: {integrity: sha512-1bHfgMz/cNMjbpsYxjVgMJ1iwKq+NdDPlACBrWULD7ZdFmBQrhMicMaKb5CdmdVyvIwXmasOuF4r6Iq574kUTA==} engines: {node: '>=12'} @@ -2481,15 +2418,6 @@ packages: dev: false optional: true - /@esbuild/linux-arm64@0.18.16: - resolution: {integrity: sha512-XIqhNUxJiuy+zsR77+H5Z2f7s4YRlriSJKtvx99nJuG5ATuJPjmZ9n0ANgnGlPCpXGSReFpgcJ7O3SMtzIFeiQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@esbuild/linux-arm64@0.18.4: resolution: {integrity: sha512-J42vLHaYREyiBwH0eQE4/7H1DTfZx8FuxyWSictx4d7ezzuKE3XOkIvOg+SQzRz7T9HLVKzq2tvbAov4UfufBw==} engines: {node: '>=12'} @@ -2499,15 +2427,6 @@ packages: dev: false optional: true - /@esbuild/linux-arm@0.18.16: - resolution: {integrity: sha512-b5ABb+5Ha2C9JkeZXV+b+OruR1tJ33ePmv9ZwMeETSEKlmu/WJ45XTTG+l6a2KDsQtJJ66qo/hbSGBtk0XVLHw==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@esbuild/linux-arm@0.18.4: resolution: {integrity: sha512-4XCGqM/Ay1LCXUBH59bL4JbSbbTK1K22dWHymWMGaEh2sQCDOUw+OQxozYV/YdBb91leK2NbuSrE2BRamwgaYw==} engines: {node: '>=12'} @@ -2517,15 +2436,6 @@ packages: dev: false optional: true - /@esbuild/linux-ia32@0.18.16: - resolution: {integrity: sha512-no+pfEpwnRvIyH+txbBAWtjxPU9grslmTBfsmDndj7bnBmr55rOo/PfQmRfz7Qg9isswt1FP5hBbWb23fRWnow==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@esbuild/linux-ia32@0.18.4: resolution: {integrity: sha512-4ksIqFwhq7OExty7Sl1n0vqQSCqTG4sU6i99G2yuMr28CEOUZ/60N+IO9hwI8sIxBqmKmDgncE1n5CMu/3m0IA==} engines: {node: '>=12'} @@ -2535,15 +2445,6 @@ packages: dev: false optional: true - /@esbuild/linux-loong64@0.18.16: - resolution: {integrity: sha512-Zbnczs9ZXjmo0oZSS0zbNlJbcwKXa/fcNhYQjahDs4Xg18UumpXG/lwM2lcSvHS3mTrRyCYZvJbmzYc4laRI1g==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@esbuild/linux-loong64@0.18.4: resolution: {integrity: sha512-bsWtoVHkGQgAsFXioDueXRiUIfSGrVkJjBBz4gcBJxXcD461cWFQFyu8Fxdj9TP+zEeqJ8C/O4LFFMBNi6Fscw==} engines: {node: '>=12'} @@ -2553,15 +2454,6 @@ packages: dev: false optional: true - /@esbuild/linux-mips64el@0.18.16: - resolution: {integrity: sha512-YMF7hih1HVR/hQVa/ot4UVffc5ZlrzEb3k2ip0nZr1w6fnYypll9td2qcoMLvd3o8j3y6EbJM3MyIcXIVzXvQQ==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@esbuild/linux-mips64el@0.18.4: resolution: {integrity: sha512-LRD9Fu8wJQgIOOV1o3nRyzrheFYjxA0C1IVWZ93eNRRWBKgarYFejd5WBtrp43cE4y4D4t3qWWyklm73Mrsd/g==} engines: {node: '>=12'} @@ -2571,15 +2463,6 @@ packages: dev: false optional: true - /@esbuild/linux-ppc64@0.18.16: - resolution: {integrity: sha512-Wkz++LZ29lDwUyTSEnzDaaP5OveOgTU69q9IyIw9WqLRxM4BjTBjz9un4G6TOvehWpf/J3gYVFN96TjGHrbcNQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@esbuild/linux-ppc64@0.18.4: resolution: {integrity: sha512-jtQgoZjM92gauVRxNaaG/TpL3Pr4WcL3Pwqi9QgdrBGrEXzB+twohQiWNSTycs6lUygakos4mm2h0B9/SHveng==} engines: {node: '>=12'} @@ -2589,15 +2472,6 @@ packages: dev: false optional: true - /@esbuild/linux-riscv64@0.18.16: - resolution: {integrity: sha512-LFMKZ30tk78/mUv1ygvIP+568bwf4oN6reG/uczXnz6SvFn4e2QUFpUpZY9iSJT6Qpgstrhef/nMykIXZtZWGQ==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@esbuild/linux-riscv64@0.18.4: resolution: {integrity: sha512-7WaU/kRZG0VCV09Xdlkg6LNAsfU9SAxo6XEdaZ8ffO4lh+DZoAhGTx7+vTMOXKxa+r2w1LYDGxfJa2rcgagMRA==} engines: {node: '>=12'} @@ -2607,15 +2481,6 @@ packages: dev: false optional: true - /@esbuild/linux-s390x@0.18.16: - resolution: {integrity: sha512-3ZC0BgyYHYKfZo3AV2/66TD/I9tlSBaW7eWTEIkrQQKfJIifKMMttXl9FrAg+UT0SGYsCRLI35Gwdmm96vlOjg==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@esbuild/linux-s390x@0.18.4: resolution: {integrity: sha512-D19ed0xreKQvC5t+ArE2njSnm18WPpE+1fhwaiJHf+Xwqsq+/SUaV8Mx0M27nszdU+Atq1HahrgCOZCNNEASUg==} engines: {node: '>=12'} @@ -2625,15 +2490,6 @@ packages: dev: false optional: true - /@esbuild/linux-x64@0.18.16: - resolution: {integrity: sha512-xu86B3647DihHJHv/wx3NCz2Dg1gjQ8bbf9cVYZzWKY+gsvxYmn/lnVlqDRazObc3UMwoHpUhNYaZset4X8IPA==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@esbuild/linux-x64@0.18.4: resolution: {integrity: sha512-Rx3AY1sxyiO/gvCGP00nL69L60dfmWyjKWY06ugpB8Ydpdsfi3BHW58HWC24K3CAjAPSwxcajozC2PzA9JBS1g==} engines: {node: '>=12'} @@ -2643,15 +2499,6 @@ packages: dev: false optional: true - /@esbuild/netbsd-x64@0.18.16: - resolution: {integrity: sha512-uVAgpimx9Ffw3xowtg/7qQPwHFx94yCje+DoBx+LNm2ePDpQXHrzE+Sb0Si2VBObYz+LcRps15cq+95YM7gkUw==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: false - optional: true - /@esbuild/netbsd-x64@0.18.4: resolution: {integrity: sha512-AaShPmN9c6w1mKRpliKFlaWcSkpBT4KOlk93UfFgeI3F3cbjzdDKGsbKnOZozmYbE1izZKLmNJiW0sFM+A5JPA==} engines: {node: '>=12'} @@ -2661,15 +2508,6 @@ packages: dev: false optional: true - /@esbuild/openbsd-x64@0.18.16: - resolution: {integrity: sha512-6OjCQM9wf7z8/MBi6BOWaTL2AS/SZudsZtBziXMtNI8r/U41AxS9x7jn0ATOwVy08OotwkPqGRMkpPR2wcTJXA==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: false - optional: true - /@esbuild/openbsd-x64@0.18.4: resolution: {integrity: sha512-tRGvGwou3BrvHVvF8HxTqEiC5VtPzySudS9fh2jBIKpLX7HCW8jIkW+LunkFDNwhslx4xMAgh0jAHsx/iCymaQ==} engines: {node: '>=12'} @@ -2679,15 +2517,6 @@ packages: dev: false optional: true - /@esbuild/sunos-x64@0.18.16: - resolution: {integrity: sha512-ZoNkruFYJp9d1LbUYCh8awgQDvB9uOMZqlQ+gGEZR7v6C+N6u7vPr86c+Chih8niBR81Q/bHOSKGBK3brJyvkQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: false - optional: true - /@esbuild/sunos-x64@0.18.4: resolution: {integrity: sha512-acORFDI95GKhmAnlH8EarBeuqoy/j3yxIU+FDB91H3+ZON+8HhTadtT450YkaMzX6lEWbhi+mjVUCj00M5yyOQ==} engines: {node: '>=12'} @@ -2697,15 +2526,6 @@ packages: dev: false optional: true - /@esbuild/win32-arm64@0.18.16: - resolution: {integrity: sha512-+j4anzQ9hrs+iqO+/wa8UE6TVkKua1pXUb0XWFOx0FiAj6R9INJ+WE//1/Xo6FG1vB5EpH3ko+XcgwiDXTxcdw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: false - optional: true - /@esbuild/win32-arm64@0.18.4: resolution: {integrity: sha512-1NxP+iOk8KSvS1L9SSxEvBAJk39U0GiGZkiiJGbuDF9G4fG7DSDw6XLxZMecAgmvQrwwx7yVKdNN3GgNh0UfKg==} engines: {node: '>=12'} @@ -2715,15 +2535,6 @@ packages: dev: false optional: true - /@esbuild/win32-ia32@0.18.16: - resolution: {integrity: sha512-5PFPmq3sSKTp9cT9dzvI67WNfRZGvEVctcZa1KGjDDu4n3H8k59Inbk0du1fz0KrAbKKNpJbdFXQMDUz7BG4rQ==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: false - optional: true - /@esbuild/win32-ia32@0.18.4: resolution: {integrity: sha512-OKr8jze93vbgqZ/r23woWciTixUwLa976C9W7yNBujtnVHyvsL/ocYG61tsktUfJOpyIz5TsohkBZ6Lo2+PCcQ==} engines: {node: '>=12'} @@ -2733,15 +2544,6 @@ packages: dev: false optional: true - /@esbuild/win32-x64@0.18.16: - resolution: {integrity: sha512-sCIVrrtcWN5Ua7jYXNG1xD199IalrbfV2+0k/2Zf2OyV2FtnQnMgdzgpRAbi4AWlKJj1jkX+M+fEGPQj6BQB4w==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - /@esbuild/win32-x64@0.18.4: resolution: {integrity: sha512-qJr3wVvcLjPFcV4AMDS3iquhBfTef2zo/jlm8RMxmiRp3Vy2HY8WMxrykJlcbCnqLXZPA0YZxZGND6eug85ogg==} engines: {node: '>=12'} @@ -3289,10 +3091,6 @@ packages: resolution: {integrity: sha512-rWr/ryzOUi9r/zUA2GK2qLWGBIBmDeIojBQXuvR76pulHUoEGMJ2A7NWShUaA5AE90ha+l9tlsyGz2UioQE9cg==} dev: false - /@types/emscripten@1.39.6: - resolution: {integrity: sha512-H90aoynNhhkQP6DRweEjJp5vfUVdIj7tdPLsu7pq89vODD/lcugKfZOsfgwpvM6XUewEp2N5dCg1Uf3Qe55Dcg==} - dev: false - /@types/fnando__sparkline@0.3.4: resolution: {integrity: sha512-FWU1zw7CVJYVeDk77FGphTUabfPims4F/Yq+WFB0Gh647lLtiXHWn8vpfT95Fl65IsNBDOhEbxJdhmERMGubNQ==} dev: false @@ -3356,10 +3154,6 @@ packages: resolution: {integrity: sha512-GXZxEtOxYGFchyUzxvKI14iff9KZ2DI+A6a37o6EQevtg6uO9t+aUZKcaC1Te5Ng1OnLM7K9NVVj+FbecD9cJg==} dev: false - /@types/node@20.4.4: - resolution: {integrity: sha512-CukZhumInROvLq3+b5gLev+vgpsIqC2D0deQr/yS1WnxvmYLlJXZpaQrQiseMY+6xusl79E04UjWoqyr+t1/Ew==} - dev: false - /@types/parse5@6.0.3: resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} dev: false @@ -4157,36 +3951,6 @@ packages: is-arrayish: 0.2.1 dev: false - /esbuild@0.18.16: - resolution: {integrity: sha512-1xLsOXrDqwdHxyXb/x/SOyg59jpf/SH7YMvU5RNSU7z3TInaASNJWNFJ6iRvLvLETZMasF3d1DdZLg7sgRimRQ==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/android-arm': 0.18.16 - '@esbuild/android-arm64': 0.18.16 - '@esbuild/android-x64': 0.18.16 - '@esbuild/darwin-arm64': 0.18.16 - '@esbuild/darwin-x64': 0.18.16 - '@esbuild/freebsd-arm64': 0.18.16 - '@esbuild/freebsd-x64': 0.18.16 - '@esbuild/linux-arm': 0.18.16 - '@esbuild/linux-arm64': 0.18.16 - '@esbuild/linux-ia32': 0.18.16 - '@esbuild/linux-loong64': 0.18.16 - '@esbuild/linux-mips64el': 0.18.16 - '@esbuild/linux-ppc64': 0.18.16 - '@esbuild/linux-riscv64': 0.18.16 - '@esbuild/linux-s390x': 0.18.16 - '@esbuild/linux-x64': 0.18.16 - '@esbuild/netbsd-x64': 0.18.16 - '@esbuild/openbsd-x64': 0.18.16 - '@esbuild/sunos-x64': 0.18.16 - '@esbuild/win32-arm64': 0.18.16 - '@esbuild/win32-ia32': 0.18.16 - '@esbuild/win32-x64': 0.18.16 - dev: false - /esbuild@0.18.4: resolution: {integrity: sha512-9rxWV/Cb2DMUXfe9aUsYtqg0KTlw146ElFH22kYeK9KVV1qT082X4lpmiKsa12ePiCcIcB686TQJxaGAa9TFvA==} engines: {node: '>=12'} @@ -6652,16 +6416,6 @@ packages: engines: {node: '>=10'} dev: false - /zerofish@0.0.4: - resolution: {integrity: sha512-LC8M8DTv5tkyju0+s2bRonrqRRiaqRqy7rAW4A3mneOZynEKyzLujw1LMHunfMvcAIRc3PePU4jbh/cb1GIFxg==} - dependencies: - '@types/emscripten': 1.39.6 - '@types/node': 20.4.4 - '@types/web': 0.0.84 - esbuild: 0.18.16 - typescript: 5.1.6 - dev: false - /zxcvbn@4.4.2: resolution: {integrity: sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ==} dev: false diff --git a/ui/localPlay/css/_local-play.scss b/ui/localPlay/css/_local-play.scss index 3db4c390dada4..3f3061f0eee94 100644 --- a/ui/localPlay/css/_local-play.scss +++ b/ui/localPlay/css/_local-play.scss @@ -21,18 +21,29 @@ $mq-col2: $mq-col2-uniboard; .puz-side { .puz-bot { + @extend %flex-column; + align-items: center; width: 300px; height: 100px; border: 2px dashed #888; } + .totals { + font-size: xx-large; + text-align: center; + } + .puz-bot.hilite { background-color: #888; } #pgn { height: 100%; } + #num-games { + width: 120px; + } span { @extend %flex-between; + flex-wrap: nowrap; * { margin: 0 15px; } diff --git a/ui/localPlay/src/ctrl.ts b/ui/localPlay/src/ctrl.ts index 87da58b5f5913..f6e0d519a50a7 100644 --- a/ui/localPlay/src/ctrl.ts +++ b/ui/localPlay/src/ctrl.ts @@ -1,8 +1,8 @@ import { LocalPlayOpts } from './interfaces'; import { PromotionCtrl } from 'chess/promotion'; -import { makeFen } from 'chessops/fen'; -import { Chess, makeSquare, parseSquare, opposite } from 'chessops'; -import makeZerofish, { Zerofish } from 'zerofish'; +import { makeFen /*, parseFen*/ } from 'chessops/fen'; +import { Chess, makeSquare, parseSquare, opposite, charToRole /*, Role*/ } from 'chessops'; +import makeZerofish, { Zerofish, PV } from 'zerofish'; import { Api as CgApi } from 'chessground/api'; import { Config as CgConfig } from 'chessground/config'; import { Key } from 'chessground/types'; @@ -13,6 +13,9 @@ export class Ctrl { cg: CgApi; flipped = false; chess = Chess.default(); + fifty = 0; + threefold: Map = new Map(); + botFight?: { gamesLeft: number; white: number; black: number; draw: number }; zf: { white: Zerofish; black: Zerofish }; players: { white: Player; black: Player } = { white: 'human', black: 'fish' }; @@ -38,21 +41,27 @@ export class Ctrl { reader.readAsArrayBuffer(weights); }); } + setZero(color: 'white' | 'black', f: File, data: Uint8Array) { this.zf[color].setZeroWeights(data); - $(`#${color}`).text(f.name); + $(`#${color} p`).first().text(f.name); this.players[color] = 'zero'; if (this.players[opposite(color)] !== 'human') $('#go').removeClass('disabled'); } - go() { - //const numTimes = parseInt($('#num-games').val() as string) || 1; + + go(numGames?: number) { + if (numGames) { + this.botFight = { gamesLeft: numGames, white: 0, black: 0, draw: 0 }; + $('#go').addClass('disabled'); + } this.chess.reset(); this.cg.set({ fen: makeFen(this.chess.toSetup()) }); console.log(makeFen(this.chess.toSetup())); this.zf.white.reset(); this.zf.black.reset(); - this.botMove(); + this.doBots(); } + getCgOpts = (): CgConfig => { const cgDests = new Map( [...this.chess.allDests()].map( @@ -72,8 +81,25 @@ export class Ctrl { }; apiMove = (uci: Uci) => { - this.chess.play({ from: parseSquare(uci.slice(0, 2))!, to: parseSquare(uci.slice(2))! }); - this.cg.move(uci.slice(0, 2) as Key, uci.slice(2) as Key); + const promotion = charToRole(uci.slice(4)); + const move = { from: parseSquare(uci.slice(0, 2))!, to: parseSquare(uci.slice(2, 4))!, promotion }; + if (!this.chess.isLegal(move)) throw new Error(`illegal move ${uci}, ${makeFen(this.chess.toSetup())}}`); + this.cg.move(uci.slice(0, 2) as Key, uci.slice(2, 4) as Key); + if (promotion) { + this.cg.setPieces( + new Map([ + [ + uci.slice(2, 4) as Key, + { + color: this.chess.turn, + role: promotion, + promoted: true, + }, + ], + ]) + ); + } + this.chess.play(move); this.cg.set({ turnColor: this.chess.turn, movable: { @@ -86,12 +112,7 @@ export class Ctrl { check: this.chess.isCheck(), }); this.redraw(); - if (this.chess.isEnd()) { - console.log('game over'); - return; - } - if (this.players[this.chess.turn] === 'human') return; - setTimeout(() => this.botMove()); + this.doBots(); }; userMove = (orig: Key, dest: Key) => { @@ -100,21 +121,42 @@ export class Ctrl { console.log('game over'); return; } - this.botMove(); + this.doBots(); }; - botMove() { - if (this.players[this.chess.turn] === 'human') return; - if (this.players[this.chess.turn] === 'zero') { - this.zf[this.chess.turn].goZero(makeFen(this.chess.toSetup())).then(m => { - this.apiMove(m); - }); + async doBots() { + const moveType = this.players[this.chess.turn]; + if (moveType === 'human') return; + if (this.botFight && this.chess.isEnd()) { + const bf = this.botFight; + const result = this.chess.outcome(); + if (!result || !result.winner) bf.draw++; + else if (result.winner === 'white') bf.white++; + else bf.black++; + $('#white-totals').text(`${bf.white} / ${bf.draw} / ${bf.black}`); + $('#black-totals').text(`${bf.black} / ${bf.draw} / ${bf.white}`); + if (--bf.gamesLeft < 1) { + this.botFight = undefined; + $('#go').removeClass('disabled'); + return; + } + setTimeout(() => this.go()); + return; + } + const zf = this.zf[this.chess.turn], + fen = makeFen(this.chess.toSetup()); + let move; + if (moveType === 'zero') { + const [zeroMove, lines] = await Promise.all([zf.goZero(fen), zf.goFish(fen, { pvs: 8, depth: 8 })]); + move = this.chess.turn === 'black' ? testAdjust(zeroMove, lines) : zeroMove; + console.log(`${this.chess.turn} ${zeroMove === move ? 'zero' : 'ZEROFISH'} ${move}`); } else { - this.zf[this.chess.turn].goFish(makeFen(this.chess.toSetup()), { depth: 10 }).then(pvs => { - this.apiMove(pvs[0].moves[0]); - }); + move = (await zf.goFish(fen, { depth: 1 }))[0].moves[0]; + console.log(`${this.chess.turn} fish ${move}`); } + this.apiMove(move); } + flip = () => { this.flipped = !this.flipped; this.cg.toggleOrientation(); @@ -123,3 +165,19 @@ export class Ctrl { private setGround = () => this.cg.set(this.getCgOpts()); } + +function linesWithin(move: string, lines: PV[], bias = 0, threshold = 50) { + const zeroScore = lines.find(line => line.moves[0] === move)?.score ?? Number.NaN; + return lines.filter(fish => Math.abs(fish.score - bias - zeroScore) < threshold && fish.moves.length); +} + +function testAdjust(move: string, lines: PV[]) { + //if (!occurs(0.5)) return move; + lines = linesWithin(move, lines, 40, 40); + if (!lines.length) return move; + return lines[Math.floor(Math.random() * lines.length)].moves[0] ?? move; +} + +/*function occurs(chance: number) { + return Math.random() < chance; +}*/ diff --git a/ui/localPlay/src/main.ts b/ui/localPlay/src/main.ts index 7e8f05286173c..7966295a3d86a 100644 --- a/ui/localPlay/src/main.ts +++ b/ui/localPlay/src/main.ts @@ -1,7 +1,7 @@ import { attributesModule, classModule, init } from 'snabbdom'; import { Ctrl } from './ctrl'; import view from './view'; -import { LocalPlayOpts, Controller } from './interfaces'; +import { LocalPlayOpts } from './interfaces'; import menuHover from 'common/menuHover'; import { Chessground } from 'chessground'; @@ -11,7 +11,7 @@ export async function initModule(opts: LocalPlayOpts) { // make a StrongSocket const ctrl = new Ctrl(opts, redraw); - const blueprint = view(ctrl as Controller); + const blueprint = view(ctrl); const element = document.querySelector('main') as HTMLElement; element.innerHTML = ''; let vnode = patch(element, blueprint); diff --git a/ui/localPlay/src/view.ts b/ui/localPlay/src/view.ts index 57639c538bf96..ee2ce7801f2db 100644 --- a/ui/localPlay/src/view.ts +++ b/ui/localPlay/src/view.ts @@ -2,12 +2,20 @@ import { Chessground } from 'chessground'; import { h, VNode } from 'snabbdom'; import { makeConfig as makeCgConfig } from './chessground'; import { onInsert } from 'common/snabbdom'; +import { Ctrl } from './ctrl'; -export default function (ctrl: any): VNode { - return h('div#local-play', renderPlay(ctrl)); +export default function render(ctrl: Ctrl): VNode { + return h('div#local-play', [ + h('div.puz-board.main-board', [chessground(ctrl), ctrl.promotion.view()]), + h('div.puz-side', [ + h('div', bot(ctrl, 'black')), + h('div#pgn'), + h('div', [bot(ctrl, 'white'), h('hr'), controls(ctrl)]), + ]), + ]); } -function chessground(ctrl: any): VNode { +function chessground(ctrl: Ctrl): VNode { return h('div.cg-wrap', { hook: { insert: vnode => (ctrl.cg = Chessground(vnode.elm as HTMLElement, makeCgConfig(ctrl))), @@ -15,31 +23,26 @@ function chessground(ctrl: any): VNode { }); } -function renderPlay(ctrl: any): VNode[] { - return [ - h('div.puz-board.main-board', [chessground(ctrl), ctrl.promotion.view()]), - h('div.puz-side', [ - h( - 'div', - h('div#black.puz-bot', { hook: onInsert(el => ctrl.dropHandler('black', el)) }, [ - h('p', 'Drop black weights here (otherwise stockfish)'), - ]) - ), - h('div#pgn'), - h('div', [ - h('div#white.puz-bot', { hook: onInsert(el => ctrl.dropHandler('white', el)) }, [ - h('p', 'Drop white weights here (otherwise human)'), - ]), - h('hr'), - h( - 'span', - h( - 'button#go.button.disabled', - { hook: onInsert(el => el.addEventListener('click', ctrl.go.bind(ctrl))) }, - 'GO' - ) +function bot(ctrl: Ctrl, color: Color): VNode { + return h(`div#${color}.puz-bot`, { hook: onInsert(el => ctrl.dropHandler(color, el)) }, [ + h('p', 'Drop weights here (otherwise stockfish)'), + h(`p#${color}-totals.totals`), + ]); +} + +function controls(ctrl: Ctrl) { + return h('span', [ + h( + 'button#go.button.disabled', + { + hook: onInsert(el => + el.addEventListener('click', () => ctrl.go(parseInt($('#num-games').val() as string) || 1)) ), - ]), - ]), - ]; + }, + 'GO' + ), + h('input#num-games', { + attrs: { type: 'number', min: '1', max: '1000', value: '1' }, + }), + ]); } From d59b41efba40a2353b77daeb3c63a8b88b0589b1 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Tue, 25 Jul 2023 11:53:32 -0500 Subject: [PATCH 007/174] fix lockfile --- pnpm-lock.yaml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f872203c05e0..3f7315a304861 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -492,7 +492,7 @@ importers: version: link:../tree zerofish: specifier: ^0.0.5 - version: link:../../../zerofish + version: 0.0.5 ui/mod: dependencies: @@ -3091,6 +3091,10 @@ packages: resolution: {integrity: sha512-rWr/ryzOUi9r/zUA2GK2qLWGBIBmDeIojBQXuvR76pulHUoEGMJ2A7NWShUaA5AE90ha+l9tlsyGz2UioQE9cg==} dev: false + /@types/emscripten@1.39.6: + resolution: {integrity: sha512-H90aoynNhhkQP6DRweEjJp5vfUVdIj7tdPLsu7pq89vODD/lcugKfZOsfgwpvM6XUewEp2N5dCg1Uf3Qe55Dcg==} + dev: false + /@types/fnando__sparkline@0.3.4: resolution: {integrity: sha512-FWU1zw7CVJYVeDk77FGphTUabfPims4F/Yq+WFB0Gh647lLtiXHWn8vpfT95Fl65IsNBDOhEbxJdhmERMGubNQ==} dev: false @@ -3154,6 +3158,10 @@ packages: resolution: {integrity: sha512-GXZxEtOxYGFchyUzxvKI14iff9KZ2DI+A6a37o6EQevtg6uO9t+aUZKcaC1Te5Ng1OnLM7K9NVVj+FbecD9cJg==} dev: false + /@types/node@20.4.4: + resolution: {integrity: sha512-CukZhumInROvLq3+b5gLev+vgpsIqC2D0deQr/yS1WnxvmYLlJXZpaQrQiseMY+6xusl79E04UjWoqyr+t1/Ew==} + dev: false + /@types/parse5@6.0.3: resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} dev: false @@ -6416,6 +6424,16 @@ packages: engines: {node: '>=10'} dev: false + /zerofish@0.0.5: + resolution: {integrity: sha512-ekdLX2fFxbsfx2pDfHDtf8mPXBNj5XKp5fScomZj3yQXk14kd0iQY49bmeD63lZCz9A8jl8q+PGpDwSVdY+Niw==} + dependencies: + '@types/emscripten': 1.39.6 + '@types/node': 20.4.4 + '@types/web': 0.0.84 + esbuild: 0.18.4 + typescript: 5.1.6 + dev: false + /zxcvbn@4.4.2: resolution: {integrity: sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ==} dev: false From 615c14c98ef95a005f9d8f9dc5f81bc3bac90a95 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Wed, 26 Jul 2023 21:31:12 -0500 Subject: [PATCH 008/174] fix zerofish wasm crashes --- pnpm-lock.yaml | 20 ++-- ui/localPlay/package.json | 2 +- ui/localPlay/src/chessground.ts | 2 +- ui/localPlay/src/ctrl.ts | 205 ++++++++++++++++++-------------- ui/localPlay/tsconfig.json | 5 +- 5 files changed, 131 insertions(+), 103 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f7315a304861..810d6b8577d13 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -491,8 +491,8 @@ importers: specifier: workspace:* version: link:../tree zerofish: - specifier: ^0.0.5 - version: 0.0.5 + specifier: ^0.0.6 + version: 0.0.6 ui/mod: dependencies: @@ -3091,8 +3091,8 @@ packages: resolution: {integrity: sha512-rWr/ryzOUi9r/zUA2GK2qLWGBIBmDeIojBQXuvR76pulHUoEGMJ2A7NWShUaA5AE90ha+l9tlsyGz2UioQE9cg==} dev: false - /@types/emscripten@1.39.6: - resolution: {integrity: sha512-H90aoynNhhkQP6DRweEjJp5vfUVdIj7tdPLsu7pq89vODD/lcugKfZOsfgwpvM6XUewEp2N5dCg1Uf3Qe55Dcg==} + /@types/emscripten@1.39.7: + resolution: {integrity: sha512-tLqYV94vuqDrXh515F/FOGtBcRMTPGvVV1LzLbtYDcQmmhtpf/gLYf+hikBbQk8MzOHNz37wpFfJbYAuSn8HqA==} dev: false /@types/fnando__sparkline@0.3.4: @@ -3158,8 +3158,8 @@ packages: resolution: {integrity: sha512-GXZxEtOxYGFchyUzxvKI14iff9KZ2DI+A6a37o6EQevtg6uO9t+aUZKcaC1Te5Ng1OnLM7K9NVVj+FbecD9cJg==} dev: false - /@types/node@20.4.4: - resolution: {integrity: sha512-CukZhumInROvLq3+b5gLev+vgpsIqC2D0deQr/yS1WnxvmYLlJXZpaQrQiseMY+6xusl79E04UjWoqyr+t1/Ew==} + /@types/node@20.4.5: + resolution: {integrity: sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg==} dev: false /@types/parse5@6.0.3: @@ -6424,11 +6424,11 @@ packages: engines: {node: '>=10'} dev: false - /zerofish@0.0.5: - resolution: {integrity: sha512-ekdLX2fFxbsfx2pDfHDtf8mPXBNj5XKp5fScomZj3yQXk14kd0iQY49bmeD63lZCz9A8jl8q+PGpDwSVdY+Niw==} + /zerofish@0.0.6: + resolution: {integrity: sha512-nFSJlc1zb7Edghzuql7vhPmBrxNhGkOAc4JtCCqx2vC4ZAoBR0GhfoKncwgjycOr5P32M4BeDYM/OymGf+lldQ==} dependencies: - '@types/emscripten': 1.39.6 - '@types/node': 20.4.4 + '@types/emscripten': 1.39.7 + '@types/node': 20.4.5 '@types/web': 0.0.84 esbuild: 0.18.4 typescript: 5.1.6 diff --git a/ui/localPlay/package.json b/ui/localPlay/package.json index eddc5b8dfb6ea..605676818e72b 100644 --- a/ui/localPlay/package.json +++ b/ui/localPlay/package.json @@ -16,7 +16,7 @@ "puz": "workspace:*", "snabbdom": "^3.5.1", "tree": "workspace:*", - "zerofish": "^0.0.5" + "zerofish": "^0.0.6" }, "scripts": { "compile": "tsc", diff --git a/ui/localPlay/src/chessground.ts b/ui/localPlay/src/chessground.ts index de1a5583fd283..bfc03cac8fff8 100644 --- a/ui/localPlay/src/chessground.ts +++ b/ui/localPlay/src/chessground.ts @@ -14,7 +14,7 @@ const pref = { }; export function makeConfig(ctrl: Ctrl): CgConfig { - const opts = ctrl.getCgOpts(); + const opts = ctrl.makeCgOpts(); return { fen: opts.fen, orientation: opts.orientation, diff --git a/ui/localPlay/src/ctrl.ts b/ui/localPlay/src/ctrl.ts index f6e0d519a50a7..cefdd2884837c 100644 --- a/ui/localPlay/src/ctrl.ts +++ b/ui/localPlay/src/ctrl.ts @@ -1,26 +1,32 @@ import { LocalPlayOpts } from './interfaces'; import { PromotionCtrl } from 'chess/promotion'; import { makeFen /*, parseFen*/ } from 'chessops/fen'; -import { Chess, makeSquare, parseSquare, opposite, charToRole /*, Role*/ } from 'chessops'; +import { Chess, makeSquare, parseSquare, opposite, charToRole, squareRank, Role } from 'chessops'; import makeZerofish, { Zerofish, PV } from 'zerofish'; import { Api as CgApi } from 'chessground/api'; import { Config as CgConfig } from 'chessground/config'; import { Key } from 'chessground/types'; type Player = 'human' | 'zero' | 'fish'; + export class Ctrl { promotion: PromotionCtrl; cg: CgApi; flipped = false; chess = Chess.default(); - fifty = 0; + fiftyMovePly = 0; + fen = ''; threefold: Map = new Map(); botFight?: { gamesLeft: number; white: number; black: number; draw: number }; zf: { white: Zerofish; black: Zerofish }; players: { white: Player; black: Player } = { white: 'human', black: 'fish' }; constructor(readonly opts: LocalPlayOpts, readonly redraw: () => void) { - this.promotion = new PromotionCtrl(f => f(this.cg), this.setGround, this.redraw); + this.promotion = new PromotionCtrl( + f => f(this.cg), + () => this.cg.set(this.makeCgOpts()), + this.redraw + ); Promise.all([makeZerofish(), makeZerofish()]).then(([wz, bz]) => { this.zf ??= { white: wz, black: bz }; }); @@ -37,133 +43,152 @@ export class Ctrl { $el.on('drop', e => { const reader = new FileReader(); const weights = e.dataTransfer.files.item(0) as File; - reader.onload = e => this.setZero(color, weights, new Uint8Array(e.target!.result as ArrayBuffer)); + reader.onload = e => { + this.zf[color].setZeroWeights(new Uint8Array(e.target!.result as ArrayBuffer)); + $(`#${color} p`).first().text(weights.name); + this.players[color] = 'zero'; + if (this.players[opposite(color)] !== 'human') $('#go').removeClass('disabled'); + }; reader.readAsArrayBuffer(weights); }); } - setZero(color: 'white' | 'black', f: File, data: Uint8Array) { - this.zf[color].setZeroWeights(data); - $(`#${color} p`).first().text(f.name); - this.players[color] = 'zero'; - if (this.players[opposite(color)] !== 'human') $('#go').removeClass('disabled'); - } - go(numGames?: number) { if (numGames) { this.botFight = { gamesLeft: numGames, white: 0, black: 0, draw: 0 }; $('#go').addClass('disabled'); } + this.fiftyMovePly = 0; + this.threefold.clear(); this.chess.reset(); - this.cg.set({ fen: makeFen(this.chess.toSetup()) }); + this.fen = makeFen(this.chess.toSetup()); + this.cg.set({ fen: this.fen }); console.log(makeFen(this.chess.toSetup())); this.zf.white.reset(); this.zf.black.reset(); - this.doBots(); + this.getBotMove(); } - getCgOpts = (): CgConfig => { - const cgDests = new Map( - [...this.chess.allDests()].map( - ([s, ds]) => [makeSquare(s), [...ds].map(d => makeSquare(d))] as [Key, Key[]] - ) - ); - return { - fen: makeFen(this.chess.toSetup()), - orientation: this.flipped ? 'black' : 'white', - turnColor: this.chess.turn, + gameOver(reason: 'threefold' | 'fifty' | 'white' | 'black' | 'draw') { + console.log(`game over ${reason}`); + // blah blah do outcome stuff + if (!this.botFight) return; + const bf = this.botFight; + const result = this.chess.outcome(); + if (!result || !result.winner) bf.draw++; + else if (result.winner === 'white') bf.white++; + else bf.black++; + $('#white-totals').text(`${bf.white} / ${bf.draw} / ${bf.black}`); + $('#black-totals').text(`${bf.black} / ${bf.draw} / ${bf.white}`); + if (--bf.gamesLeft < 1) { + this.botFight = undefined; + $('#go').removeClass('disabled'); + return; + } + setTimeout(() => this.go()); + } - movable: { - color: this.chess.turn, - dests: cgDests, - }, - }; - }; + move(uci: Uci, user = false) { + const promotion = charToRole(uci.slice(4)), + from = parseSquare(uci.slice(0, 2))!, + to = parseSquare(uci.slice(2, 4))!, + piece = this.chess.board.getRole(from), + move = { from, to, promotion }; + let finished = false; - apiMove = (uci: Uci) => { - const promotion = charToRole(uci.slice(4)); - const move = { from: parseSquare(uci.slice(0, 2))!, to: parseSquare(uci.slice(2, 4))!, promotion }; if (!this.chess.isLegal(move)) throw new Error(`illegal move ${uci}, ${makeFen(this.chess.toSetup())}}`); - this.cg.move(uci.slice(0, 2) as Key, uci.slice(2, 4) as Key); - if (promotion) { - this.cg.setPieces( - new Map([ - [ - uci.slice(2, 4) as Key, - { - color: this.chess.turn, - role: promotion, - promoted: true, - }, - ], - ]) + + if (piece === 'pawn' || this.chess.board.get(to)) this.fiftyMovePly = 0; + else this.fiftyMovePly++; + + this.chess.play(move); + this.fen = makeFen(this.chess.toSetup()); + const fenCount = (this.threefold.get(this.fen) ?? 0) + 1; + this.threefold.set(this.fen, fenCount); + + if (this.chess.isEnd() || fenCount >= 3 || this.fiftyMovePly >= 100) { + this.gameOver( + this.chess.outcome()?.winner ?? + (this.fiftyMovePly >= 100 ? 'fifty' : fenCount >= 3 ? 'threefold' : 'draw') ); + finished = true; } - this.chess.play(move); - this.cg.set({ - turnColor: this.chess.turn, - movable: { - dests: new Map( - [...this.chess.allDests()].map( - ([s, ds]) => [makeSquare(s), [...ds].map(d => makeSquare(d))] as [Key, Key[]] - ) - ), - }, - check: this.chess.isCheck(), - }); - this.redraw(); - this.doBots(); - }; + if (user && (squareRank(to) === 0 || squareRank(to) === 7) && piece === 'pawn') { + return; + // oh noes PromotionCtrl! put it back! + } else this.cgMove(uci); + if (!finished) this.getBotMove(); + } userMove = (orig: Key, dest: Key) => { - this.chess.play({ from: parseSquare(orig)!, to: parseSquare(dest)! }); - if (this.chess.isEnd()) { - console.log('game over'); - return; - } - this.doBots(); + this.move(orig + dest, true); }; - async doBots() { + async getBotMove() { const moveType = this.players[this.chess.turn]; if (moveType === 'human') return; - if (this.botFight && this.chess.isEnd()) { - const bf = this.botFight; - const result = this.chess.outcome(); - if (!result || !result.winner) bf.draw++; - else if (result.winner === 'white') bf.white++; - else bf.black++; - $('#white-totals').text(`${bf.white} / ${bf.draw} / ${bf.black}`); - $('#black-totals').text(`${bf.black} / ${bf.draw} / ${bf.white}`); - if (--bf.gamesLeft < 1) { - this.botFight = undefined; - $('#go').removeClass('disabled'); - return; - } - setTimeout(() => this.go()); - return; - } - const zf = this.zf[this.chess.turn], - fen = makeFen(this.chess.toSetup()); + const zf = this.zf[this.chess.turn]; let move; if (moveType === 'zero') { - const [zeroMove, lines] = await Promise.all([zf.goZero(fen), zf.goFish(fen, { pvs: 8, depth: 8 })]); + console.log(this.chess.turn, this.fen); + const [zeroMove, lines] = await Promise.all([ + zf.goZero(this.fen), + zf.goFish(this.fen, { pvs: 8, depth: 6 }), + ]); move = this.chess.turn === 'black' ? testAdjust(zeroMove, lines) : zeroMove; console.log(`${this.chess.turn} ${zeroMove === move ? 'zero' : 'ZEROFISH'} ${move}`); } else { - move = (await zf.goFish(fen, { depth: 1 }))[0].moves[0]; + move = (await zf.goFish(this.fen, { depth: 4 }))[0].moves[0]; console.log(`${this.chess.turn} fish ${move}`); } - this.apiMove(move); + this.move(move); + } + + cgMove(uci: Uci) { + const from = uci.slice(0, 2) as Key, + to = uci.slice(2, 4) as Key, + role = charToRole(uci.slice(4)) as Role, + turn = this.chess.turn; + this.cg.move(from, to); + if (role) this.cg.setPieces(new Map([[to, { color: this.chess.turn, role, promoted: true }]])); + const dests = + this.players[turn] !== 'human' + ? new Map() + : new Map( + [...this.chess.allDests()].map( + ([s, ds]) => [makeSquare(s), [...ds].map(d => makeSquare(d))] as [Key, Key[]] + ) + ); + this.cg.set({ + turnColor: turn, + movable: { dests }, + check: this.chess.isCheck() ? turn : false, + }); } + makeCgOpts = (): CgConfig => { + const cgDests = new Map( + [...this.chess.allDests()].map( + ([s, ds]) => [makeSquare(s), [...ds].map(d => makeSquare(d))] as [Key, Key[]] + ) + ); + return { + fen: makeFen(this.chess.toSetup()), + orientation: this.flipped ? 'black' : 'white', + turnColor: this.chess.turn, + + movable: { + color: this.chess.turn, + dests: cgDests, + }, + }; + }; + flip = () => { this.flipped = !this.flipped; this.cg.toggleOrientation(); this.redraw(); }; - - private setGround = () => this.cg.set(this.getCgOpts()); } function linesWithin(move: string, lines: PV[], bias = 0, threshold = 50) { diff --git a/ui/localPlay/tsconfig.json b/ui/localPlay/tsconfig.json index b5ca6f5a06ec2..97bdb45cec233 100644 --- a/ui/localPlay/tsconfig.json +++ b/ui/localPlay/tsconfig.json @@ -2,7 +2,10 @@ "extends": "../tsconfig.base.json", "compilerOptions": { "esModuleInterop": true, - "noEmit": true + "noEmit": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "allowUnreachableCode": true, }, "references": [ { "path": "../chess/tsconfig.json" }, From a7b55612774fc9c96860045c76f9e971411c3b19 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Thu, 27 Jul 2023 22:19:31 -0500 Subject: [PATCH 009/174] 0.0.7 --- pnpm-lock.yaml | 8 +- ui/localPlay/package.json | 4 +- ui/localPlay/src/chessground.ts | 2 +- ui/localPlay/src/ctrl.ts | 186 +++++++++++++++++--------------- 4 files changed, 108 insertions(+), 92 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 810d6b8577d13..8d6225df9fb41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -491,8 +491,8 @@ importers: specifier: workspace:* version: link:../tree zerofish: - specifier: ^0.0.6 - version: 0.0.6 + specifier: ^0.0.7 + version: 0.0.7 ui/mod: dependencies: @@ -6424,8 +6424,8 @@ packages: engines: {node: '>=10'} dev: false - /zerofish@0.0.6: - resolution: {integrity: sha512-nFSJlc1zb7Edghzuql7vhPmBrxNhGkOAc4JtCCqx2vC4ZAoBR0GhfoKncwgjycOr5P32M4BeDYM/OymGf+lldQ==} + /zerofish@0.0.7: + resolution: {integrity: sha512-fxSIWjQTSF0h5SSiIifEKqt/CPEpxNQOfsdU8l8ZBK2cKYam6Ak7WN+Wpinz6RgZKsJ+qAPp6WLwxt8Kw97p+A==} dependencies: '@types/emscripten': 1.39.7 '@types/node': 20.4.5 diff --git a/ui/localPlay/package.json b/ui/localPlay/package.json index 605676818e72b..ce9c93cb294a1 100644 --- a/ui/localPlay/package.json +++ b/ui/localPlay/package.json @@ -3,7 +3,7 @@ "version": "2.0.0", "private": true, "description": "lichess.org local play", - "author": "Thibault Duplessis", + "author": "T-Bone Duplexus", "license": "AGPL-3.0-or-later", "dependencies": { "@types/cash": "workspace:*", @@ -16,7 +16,7 @@ "puz": "workspace:*", "snabbdom": "^3.5.1", "tree": "workspace:*", - "zerofish": "^0.0.6" + "zerofish": "^0.0.7" }, "scripts": { "compile": "tsc", diff --git a/ui/localPlay/src/chessground.ts b/ui/localPlay/src/chessground.ts index bfc03cac8fff8..7f32f218b3b0e 100644 --- a/ui/localPlay/src/chessground.ts +++ b/ui/localPlay/src/chessground.ts @@ -14,7 +14,7 @@ const pref = { }; export function makeConfig(ctrl: Ctrl): CgConfig { - const opts = ctrl.makeCgOpts(); + const opts = ctrl.cgOpts(); return { fen: opts.fen, orientation: opts.orientation, diff --git a/ui/localPlay/src/ctrl.ts b/ui/localPlay/src/ctrl.ts index cefdd2884837c..167e740ac6043 100644 --- a/ui/localPlay/src/ctrl.ts +++ b/ui/localPlay/src/ctrl.ts @@ -1,7 +1,8 @@ import { LocalPlayOpts } from './interfaces'; import { PromotionCtrl } from 'chess/promotion'; import { makeFen /*, parseFen*/ } from 'chessops/fen'; -import { Chess, makeSquare, parseSquare, opposite, charToRole, squareRank, Role } from 'chessops'; +import { Chess, Role } from 'chessops'; +import * as Chops from 'chessops'; import makeZerofish, { Zerofish, PV } from 'zerofish'; import { Api as CgApi } from 'chessground/api'; import { Config as CgConfig } from 'chessground/config'; @@ -16,15 +17,15 @@ export class Ctrl { chess = Chess.default(); fiftyMovePly = 0; fen = ''; - threefold: Map = new Map(); - botFight?: { gamesLeft: number; white: number; black: number; draw: number }; + threefoldFens: Map = new Map(); + totals: { gamesLeft: number; white: number; black: number; draw: number }; zf: { white: Zerofish; black: Zerofish }; players: { white: Player; black: Player } = { white: 'human', black: 'fish' }; constructor(readonly opts: LocalPlayOpts, readonly redraw: () => void) { this.promotion = new PromotionCtrl( f => f(this.cg), - () => this.cg.set(this.makeCgOpts()), + () => this.cg.set(this.cgOpts()), this.redraw ); Promise.all([makeZerofish(), makeZerofish()]).then(([wz, bz]) => { @@ -47,77 +48,74 @@ export class Ctrl { this.zf[color].setZeroWeights(new Uint8Array(e.target!.result as ArrayBuffer)); $(`#${color} p`).first().text(weights.name); this.players[color] = 'zero'; - if (this.players[opposite(color)] !== 'human') $('#go').removeClass('disabled'); + if (this.players[Chops.opposite(color)] !== 'human') $('#go').removeClass('disabled'); }; reader.readAsArrayBuffer(weights); }); } go(numGames?: number) { - if (numGames) { - this.botFight = { gamesLeft: numGames, white: 0, black: 0, draw: 0 }; - $('#go').addClass('disabled'); - } + this.totals ??= { gamesLeft: 1, white: 0, black: 0, draw: 0 }; + if (numGames) this.totals.gamesLeft = numGames; + $('#go').addClass('disabled'); this.fiftyMovePly = 0; - this.threefold.clear(); + this.threefoldFens.clear(); this.chess.reset(); this.fen = makeFen(this.chess.toSetup()); this.cg.set({ fen: this.fen }); - console.log(makeFen(this.chess.toSetup())); this.zf.white.reset(); this.zf.black.reset(); this.getBotMove(); } - gameOver(reason: 'threefold' | 'fifty' | 'white' | 'black' | 'draw') { - console.log(`game over ${reason}`); - // blah blah do outcome stuff - if (!this.botFight) return; - const bf = this.botFight; - const result = this.chess.outcome(); - if (!result || !result.winner) bf.draw++; - else if (result.winner === 'white') bf.white++; - else bf.black++; - $('#white-totals').text(`${bf.white} / ${bf.draw} / ${bf.black}`); - $('#black-totals').text(`${bf.black} / ${bf.draw} / ${bf.white}`); - if (--bf.gamesLeft < 1) { - this.botFight = undefined; - $('#go').removeClass('disabled'); - return; - } - setTimeout(() => this.go()); + checkGameOver(userEnd?: 'whiteResign' | 'blackResign' | 'mutualDraw'): { + end: boolean; + result?: string; + reason?: string; + } { + let result = 'draw', + reason = userEnd ?? 'checkmate'; + if (this.chess.isCheckmate()) result = Chops.opposite(this.chess.turn); + else if (this.chess.isInsufficientMaterial()) reason = 'insufficient'; + else if (this.chess.isStalemate()) reason = 'stalemate'; + else if (this.fifty()) reason = 'fifty'; + else if (this.threefold()) reason = 'threefold'; + else if (userEnd) { + if (userEnd !== 'mutualDraw') reason = 'resign'; + if (userEnd === 'whiteResign') result = 'black'; + else if (userEnd === 'blackResign') result = 'white'; + } else return { end: false }; + return { end: true, result, reason }; } - move(uci: Uci, user = false) { - const promotion = charToRole(uci.slice(4)), - from = parseSquare(uci.slice(0, 2))!, - to = parseSquare(uci.slice(2, 4))!, - piece = this.chess.board.getRole(from), - move = { from, to, promotion }; - let finished = false; + doGameOver(result: string, reason: string) { + console.log(`game over ${result} ${reason}`); - if (!this.chess.isLegal(move)) throw new Error(`illegal move ${uci}, ${makeFen(this.chess.toSetup())}}`); + // blah blah do outcome stuff + if (result === 'white') this.totals.white++; + else if (result === 'black') this.totals.black++; + else this.totals.draw++; + $('#white-totals').text(`${this.totals.white} / ${this.totals.draw} / ${this.totals.black}`); + $('#black-totals').text(`${this.totals.black} / ${this.totals.draw} / ${this.totals.white}`); + if (--this.totals.gamesLeft < 1) $('#go').removeClass('disabled'); + else setTimeout(() => this.go()); + } - if (piece === 'pawn' || this.chess.board.get(to)) this.fiftyMovePly = 0; - else this.fiftyMovePly++; + move(uci: Uci, user = false) { + const move = Chops.parseUci(uci); + if (!move || !this.chess.isLegal(move)) + throw new Error(`illegal move ${uci}, ${makeFen(this.chess.toSetup())}}`); this.chess.play(move); this.fen = makeFen(this.chess.toSetup()); - const fenCount = (this.threefold.get(this.fen) ?? 0) + 1; - this.threefold.set(this.fen, fenCount); - - if (this.chess.isEnd() || fenCount >= 3 || this.fiftyMovePly >= 100) { - this.gameOver( - this.chess.outcome()?.winner ?? - (this.fiftyMovePly >= 100 ? 'fifty' : fenCount >= 3 ? 'threefold' : 'draw') - ); - finished = true; - } - if (user && (squareRank(to) === 0 || squareRank(to) === 7) && piece === 'pawn') { - return; - // oh noes PromotionCtrl! put it back! + this.fifty(move); + this.threefold('update'); + if (user && this.isPromotion(move)) { + return; // oh noes PromotionCtrl! put it back! put it back! } else this.cgMove(uci); - if (!finished) this.getBotMove(); + const { end, result, reason } = this.checkGameOver(); + if (end) this.doGameOver(result!, reason!); + else this.getBotMove(); } userMove = (orig: Key, dest: Key) => { @@ -130,12 +128,12 @@ export class Ctrl { const zf = this.zf[this.chess.turn]; let move; if (moveType === 'zero') { - console.log(this.chess.turn, this.fen); const [zeroMove, lines] = await Promise.all([ zf.goZero(this.fen), zf.goFish(this.fen, { pvs: 8, depth: 6 }), ]); - move = this.chess.turn === 'black' ? testAdjust(zeroMove, lines) : zeroMove; + // without randomSprinkle, lc0 will always play the same game + move = this.chess.turn === 'white' ? randomSprinkle(zeroMove, lines) : zeroMove; console.log(`${this.chess.turn} ${zeroMove === move ? 'zero' : 'ZEROFISH'} ${move}`); } else { move = (await zf.goFish(this.fen, { depth: 4 }))[0].moves[0]; @@ -145,44 +143,54 @@ export class Ctrl { } cgMove(uci: Uci) { - const from = uci.slice(0, 2) as Key, - to = uci.slice(2, 4) as Key, - role = charToRole(uci.slice(4)) as Role, - turn = this.chess.turn; + const { from, to, role } = splitUci(uci); this.cg.move(from, to); if (role) this.cg.setPieces(new Map([[to, { color: this.chess.turn, role, promoted: true }]])); - const dests = - this.players[turn] !== 'human' - ? new Map() - : new Map( - [...this.chess.allDests()].map( - ([s, ds]) => [makeSquare(s), [...ds].map(d => makeSquare(d))] as [Key, Key[]] - ) - ); - this.cg.set({ - turnColor: turn, - movable: { dests }, - check: this.chess.isCheck() ? turn : false, - }); + this.cg.set(this.cgOpts(true)); } - makeCgOpts = (): CgConfig => { - const cgDests = new Map( - [...this.chess.allDests()].map( - ([s, ds]) => [makeSquare(s), [...ds].map(d => makeSquare(d))] as [Key, Key[]] - ) - ); + cgOpts(withFen = true): CgConfig { return { - fen: makeFen(this.chess.toSetup()), + fen: withFen ? this.fen : undefined, orientation: this.flipped ? 'black' : 'white', turnColor: this.chess.turn, - + check: this.chess.isCheck() ? this.chess.turn : false, movable: { color: this.chess.turn, - dests: cgDests, + dests: + this.players[this.chess.turn] !== 'human' + ? new Map() + : new Map([...this.chess.allDests()].map(([s, ds]) => [sq2key(s), [...ds].map(sq2key)])), }, }; - }; + } + + fifty(move?: Chops.Move) { + if (move) + if ( + !('from' in move) || + this.chess.board.getRole(move.from) === 'pawn' || + this.chess.board.get(move.to) + ) + this.fiftyMovePly = 0; + else this.fiftyMovePly++; + return this.fiftyMovePly >= 100; + } + + threefold(update: 'update' | false = false) { + const boardFen = this.fen.split('-')[0]; + const fenCount = (this.threefoldFens.get(boardFen) ?? 0) + 1; + if (update) this.threefoldFens.set(boardFen, fenCount); + return fenCount >= 3; + } + + isPromotion(move: Chops.Move) { + return ( + 'from' in move && + Chops.squareRank(move.to) === (this.chess.turn === 'white' ? 7 : 0) && + this.chess.board.getRole(move.from) === 'pawn' + ); + } flip = () => { this.flipped = !this.flipped; @@ -191,18 +199,26 @@ export class Ctrl { }; } +function sq2key(sq: number): Key { + return Chops.makeSquare(sq); +} + +function splitUci(uci: Uci): { from: Key; to: Key; role?: Role } { + return { from: uci.slice(0, 2) as Key, to: uci.slice(2, 4) as Key, role: Chops.charToRole(uci.slice(4)) }; +} + function linesWithin(move: string, lines: PV[], bias = 0, threshold = 50) { const zeroScore = lines.find(line => line.moves[0] === move)?.score ?? Number.NaN; return lines.filter(fish => Math.abs(fish.score - bias - zeroScore) < threshold && fish.moves.length); } -function testAdjust(move: string, lines: PV[]) { - //if (!occurs(0.5)) return move; - lines = linesWithin(move, lines, 40, 40); +function randomSprinkle(move: string, lines: PV[]) { + lines = linesWithin(move, lines, 0, 20); if (!lines.length) return move; return lines[Math.floor(Math.random() * lines.length)].moves[0] ?? move; } -/*function occurs(chance: number) { +/* +function occurs(chance: number) { return Math.random() < chance; }*/ From b2bf9998892bbc3e0472abce0234e432de8bb00c Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Fri, 28 Jul 2023 15:41:55 -0500 Subject: [PATCH 010/174] ui/build improvements --- ui/@build/src/build.ts | 91 +++++++++++++++++++++++++++------ ui/@build/src/main.ts | 4 +- ui/@build/src/parse.ts | 16 +++++- ui/@build/src/sass.ts | 2 +- ui/@build/src/tsc.ts | 64 +++++++++++------------ ui/localPlay/package.json | 6 +-- ui/localPlay/src/chessground.ts | 2 +- ui/localPlay/src/ctrl.ts | 78 ++++++++++++++-------------- ui/site/package.json | 62 ++-------------------- ui/voice/package.json | 7 +-- 10 files changed, 173 insertions(+), 159 deletions(-) diff --git a/ui/@build/src/build.ts b/ui/@build/src/build.ts index 10ee7cddce22b..98262bbaa6eef 100644 --- a/ui/@build/src/build.ts +++ b/ui/@build/src/build.ts @@ -2,11 +2,11 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import * as cps from 'node:child_process'; import * as ps from 'node:process'; -import { parseModules } from './parse'; +import { parseModules, globArray } from './parse'; import { tsc } from './tsc'; import { sass } from './sass'; import { esbuild } from './esbuild'; -import { LichessModule, env, errorMark, colors as c } from './main'; +import { LichessModule, Copy, env, errorMark, colors as c } from './main'; export let moduleDeps: Map; export let modules: Map; @@ -32,7 +32,9 @@ export async function build(mods: string[]) { await fs.promises.mkdir(env.cssDir, { recursive: true }); sass(); - tsc(() => esbuild()); + await tsc(); + await copies(); + await esbuild(); } export function postBuild() { @@ -51,23 +53,78 @@ export function preModule(mod: LichessModule | undefined) { const stdout = cps.execSync(`${args.join(' ')}`, { cwd: mod.root }); if (stdout) env.log(stdout, { ctx: mod.name }); }); - if (mod?.copy) +} + +export async function copies() { + const copyDirs = new Map(); + const watchDirs = new Set(); + let watchTimeout: NodeJS.Timeout | undefined; + const fire = () => { + watchDirs.forEach(d => copyDirs.get(d)?.forEach(globCopy)); + watchDirs.clear(); + watchTimeout = undefined; + }; + for (const mod of buildModules) { + if (!mod?.copy) continue; for (const cp of mod.copy) { - const sources: string[] = []; - const dest = path.join(mod.root, cp.dest) + path.sep; - if (typeof cp.src === 'string') { - sources.push(path.join(mod.root, cp.src)); - env.log(`[${c.grey(mod.name)}] copy '${c.cyan(cp.src)}'`); - } else if (Array.isArray(cp.src)) { - for (const s of cp.src) { - sources.push(path.join(mod.root, s)); - env.log(`[${c.grey(mod.name)}] copy '${c.cyan(s)}'`); - } + for (const src of await globCopy(cp)) { + copyDirs.set(src, [...(copyDirs.get(src) ?? []), cp]); } - fs.mkdirSync(dest, { recursive: true }); + } + if (!env.watch) continue; + for (const dir of copyDirs.keys()) { + const watcher = fs.watch(dir); + watcher.on('change', () => { + clearTimeout(watchTimeout); + watchDirs.add(dir); + watchTimeout = setTimeout(fire, 600); + }); + watcher.on('error', (err: Error) => env.error(err)); + } + } +} + +const globRe = /[*?!{}[\]()]|\*\*|\[[^[\]]*\]/; - cps.execFileSync('cp', ['-rf', ...sources, dest]); +async function globCopy(cp: Copy): Promise> { + const watchDirs = new Set(); + const dest = path.join(cp.mod.root, cp.dest) + path.sep; + + const globIndex = cp.src.search(globRe); + const globRoot = + globIndex > 0 && cp.src[globIndex - 1] === path.sep + ? cp.src.slice(0, globIndex - 1) + : path.dirname(cp.src.slice(0, globIndex)); + + const srcs = await globArray(cp.src, { cwd: cp.mod.root, abs: false }); + + watchDirs.add(path.join(cp.mod.root, globRoot)); + env.log(`[${c.grey(cp.mod.name)}] - Copy '${c.cyan(cp.src)}' to '${c.cyan(cp.dest)}'`); + for (const src of srcs) { + const srcPath = path.join(cp.mod.root, src); + watchDirs.add(path.dirname(srcPath)); + const destPath = path.join(dest, src.slice(globRoot.length)); + await copyOne(srcPath, destPath, cp.mod.name); + } + return watchDirs; +} + +async function copyOne(absSrc: string, absDest: string, modName: string) { + try { + const [src, dest] = ( + await Promise.allSettled([ + fs.promises.stat(absSrc), + fs.promises.stat(absDest), + fs.promises.mkdir(path.dirname(absDest), { recursive: true }), + ]) + ).map(x => (x.status === 'fulfilled' ? (x.value as fs.Stats) : undefined)); + if (src && (!dest || quantize(src.mtimeMs) != quantize(dest.mtimeMs))) { + await fs.promises.copyFile(absSrc, absDest); + fs.utimes(absDest, src.atime, src.mtime, () => {}); } + } catch (_) { + env.log(`[${c.grey(modName)}] - ${errorMark} - failed copy '${c.cyan(absSrc)}' to '${c.cyan(absDest)}'`); + } } function depsOne(modName: string): LichessModule[] { @@ -75,6 +132,8 @@ function depsOne(modName: string): LichessModule[] { return unique(collect(modName).map(name => modules.get(name))); } +const quantize = (n?: number, factor = 10000) => Math.floor((n ?? 0) / factor) * factor; + const depsMany = (modNames: string[]): LichessModule[] => unique(modNames.flatMap(depsOne)); const unique = (mods: (T | undefined)[]): T[] => [...new Set(mods.filter(x => x))] as T[]; diff --git a/ui/@build/src/main.ts b/ui/@build/src/main.ts index dfe00d98ccad9..38af01cae7050 100644 --- a/ui/@build/src/main.ts +++ b/ui/@build/src/main.ts @@ -61,8 +61,10 @@ export interface LichessModule { export interface Copy { // same as copy -rf lila/node_modules/${src} lila/public/${dest} - src: string | string[]; + src: string; dest: string; + mod: LichessModule; + isGlob?: boolean; // avoid reglobbing single files } export interface LichessBundle { diff --git a/ui/@build/src/parse.ts b/ui/@build/src/parse.ts index 0e1b5f25463b9..8cc49f5c72e8f 100644 --- a/ui/@build/src/parse.ts +++ b/ui/@build/src/parse.ts @@ -39,7 +39,6 @@ async function parseModule(moduleDir: string): Promise { pre: [], post: [], hasTsconfig: fs.existsSync(path.join(moduleDir, 'tsconfig.json')), - copy: pkg.lichess?.copy, }; parseScripts(mod, 'scripts' in pkg ? pkg.scripts : {}); @@ -59,6 +58,21 @@ async function parseModule(moduleDir: string): Promise { })); } } + if ('lichess' in pkg && 'copy' in pkg.lichess) { + const copy: any[] = Array.isArray(pkg.lichess.copy) ? pkg.lichess.copy : [pkg.lichess.copy]; + const flattener = new Map>(); + for (const s of copy) { + if (!Array.isArray(s.src)) s.src = [s.src]; + for (const src of s.src) { + const srcDest = flattener.get(src) ?? new Set(); + srcDest.add(s.dest); + flattener.set(src, srcDest); + } + } + mod.copy = []; + for (const [src, dests] of flattener.entries()) + for (const dest of dests) mod.copy.push({ src, dest, mod, isGlob: true }); + } return mod; } diff --git a/ui/@build/src/sass.ts b/ui/@build/src/sass.ts index 4f477a9cd5bbf..eb63c94513537 100644 --- a/ui/@build/src/sass.ts +++ b/ui/@build/src/sass.ts @@ -65,7 +65,7 @@ const builder = new (class { if (this.timeout) { clearTimeout(this.timeout); } - this.timeout = setTimeout(this.fire.bind(this), 200); + this.timeout = setTimeout(() => this.fire(), 200); return this.fileSet.size > oldCount; } diff --git a/ui/@build/src/tsc.ts b/ui/@build/src/tsc.ts index a3e36a5627d54..be9e133e94a19 100644 --- a/ui/@build/src/tsc.ts +++ b/ui/@build/src/tsc.ts @@ -4,45 +4,43 @@ import * as path from 'node:path'; import { buildModules } from './build'; import { env, colors as c, errorMark, lines } from './main'; -export async function tsc(onSuccess: () => void) { - if (!env.tsc) return onSuccess(); - let successCallbackTriggered = false; +export async function tsc(): Promise { + return new Promise(resolve => { + if (!env.tsc) return resolve(); - const cfgPath = path.join(env.buildDir, 'dist', 'build.tsconfig.json'); - const cfg: any = { files: [] }; - cfg.references = buildModules - .filter(x => x.hasTsconfig) - .map(x => ({ path: path.join(x.root, 'tsconfig.json') })); - await fs.promises.writeFile(cfgPath, JSON.stringify(cfg)); + const cfgPath = path.join(env.buildDir, 'dist', 'build.tsconfig.json'); + const cfg: any = { files: [] }; + cfg.references = buildModules + .filter(x => x.hasTsconfig) + .map(x => ({ path: path.join(x.root, 'tsconfig.json') })); + fs.writeFileSync(cfgPath, JSON.stringify(cfg)); - const tsc = cps.spawn( - 'tsc', - ['-b', cfgPath].concat(env.watch ? ['-w', '--preserveWatchOutput'] : ['--force']) - ); + const tsc = cps.spawn( + 'tsc', + ['-b', cfgPath].concat(env.watch ? ['-w', '--preserveWatchOutput'] : ['--force']) + ); - env.log(`Checking typescript`, { ctx: 'tsc' }); + env.log(`Checking typescript`, { ctx: 'tsc' }); - tsc.stdout?.on('data', (buf: Buffer) => { - // no way to magically get build events... - const txts = lines(buf.toString('utf8')); - for (const txt of txts) { - if (txt.includes('Found 0 errors')) { - if (!successCallbackTriggered) { - onSuccess(); - successCallbackTriggered = true; + tsc.stdout?.on('data', (buf: Buffer) => { + // no way to magically get build events... + const txts = lines(buf.toString('utf8')); + for (const txt of txts) { + if (txt.includes('Found 0 errors')) { + resolve(); + env.done(0, 'tsc'); + } else { + tscLog(txt); } - env.done(0, 'tsc'); - } else { - tscLog(txt); } - } - }); - tsc.stderr?.on('data', txt => env.log(txt, { ctx: 'tsc', error: true })); - tsc.addListener('close', code => { - env.done(code || 0, 'tsc'); - if (code) { - env.done(code, 'esbuild'); // fail both - } else onSuccess(); + }); + tsc.stderr?.on('data', txt => env.log(txt, { ctx: 'tsc', error: true })); + tsc.addListener('close', code => { + env.done(code || 0, 'tsc'); + if (code) { + env.done(code, 'esbuild'); // fail both + } else resolve(); + }); }); } diff --git a/ui/localPlay/package.json b/ui/localPlay/package.json index ce9c93cb294a1..eb52ee5f2150f 100644 --- a/ui/localPlay/package.json +++ b/ui/localPlay/package.json @@ -29,10 +29,6 @@ "src/main.ts": "localPlay" } }, - "copy": [ - { "src": "node_modules/zerofish/dist/zerofishEngine.js", "dest": "../../public/compiled" }, - { "src": "node_modules/zerofish/dist/zerofishEngine.wasm", "dest": "../../public/compiled" }, - { "src": "node_modules/zerofish/dist/zerofishEngine.worker.js", "dest": "../../public/compiled" } - ] + "copy": { "src": "node_modules/zerofish/dist/zerofishEngine.*", "dest": "../../public/compiled" } } } diff --git a/ui/localPlay/src/chessground.ts b/ui/localPlay/src/chessground.ts index 7f32f218b3b0e..5732cf5398eb9 100644 --- a/ui/localPlay/src/chessground.ts +++ b/ui/localPlay/src/chessground.ts @@ -31,7 +31,7 @@ export function makeConfig(ctrl: Ctrl): CgConfig { showDests: true, //pref.destination, rookCastle: pref.rookCastle, events: { - after: ctrl.userMove, + after: ctrl.cgUserMove, }, }, draggable: { diff --git a/ui/localPlay/src/ctrl.ts b/ui/localPlay/src/ctrl.ts index 167e740ac6043..6bb15a63d20b8 100644 --- a/ui/localPlay/src/ctrl.ts +++ b/ui/localPlay/src/ctrl.ts @@ -11,61 +11,41 @@ import { Key } from 'chessground/types'; type Player = 'human' | 'zero' | 'fish'; export class Ctrl { - promotion: PromotionCtrl; cg: CgApi; - flipped = false; chess = Chess.default(); - fiftyMovePly = 0; - fen = ''; - threefoldFens: Map = new Map(); + promotion: PromotionCtrl; + zf: { white?: Zerofish; black?: Zerofish }; totals: { gamesLeft: number; white: number; black: number; draw: number }; - zf: { white: Zerofish; black: Zerofish }; players: { white: Player; black: Player } = { white: 'human', black: 'fish' }; + fen = ''; + flipped = false; + fiftyMovePly = 0; + threefoldFens: Map = new Map(); + constructor(readonly opts: LocalPlayOpts, readonly redraw: () => void) { this.promotion = new PromotionCtrl( f => f(this.cg), () => this.cg.set(this.cgOpts()), this.redraw ); - Promise.all([makeZerofish(), makeZerofish()]).then(([wz, bz]) => { - this.zf ??= { white: wz, black: bz }; - }); - } - - dropHandler(color: 'white' | 'black', el: HTMLElement) { - const $el = $(el); - $el.on('dragenter dragover dragleave drop', e => { - e.preventDefault(); - e.stopPropagation(); - }); - $el.on('dragenter dragover', _ => $el.addClass('hilite')); - $el.on('dragleave drop', _ => $el.removeClass('hilite')); - $el.on('drop', e => { - const reader = new FileReader(); - const weights = e.dataTransfer.files.item(0) as File; - reader.onload = e => { - this.zf[color].setZeroWeights(new Uint8Array(e.target!.result as ArrayBuffer)); - $(`#${color} p`).first().text(weights.name); - this.players[color] = 'zero'; - if (this.players[Chops.opposite(color)] !== 'human') $('#go').removeClass('disabled'); - }; - reader.readAsArrayBuffer(weights); + Promise.all([/*makeZerofish(),*/ makeZerofish()]).then(([/*wz,*/ bz]) => { + this.zf ??= { white: /*wz*/ undefined, black: bz }; }); } go(numGames?: number) { this.totals ??= { gamesLeft: 1, white: 0, black: 0, draw: 0 }; if (numGames) this.totals.gamesLeft = numGames; - $('#go').addClass('disabled'); this.fiftyMovePly = 0; this.threefoldFens.clear(); this.chess.reset(); this.fen = makeFen(this.chess.toSetup()); this.cg.set({ fen: this.fen }); - this.zf.white.reset(); - this.zf.black.reset(); + this.zf.white?.reset(); + this.zf.black?.reset(); this.getBotMove(); + $('#go').addClass('disabled'); } checkGameOver(userEnd?: 'whiteResign' | 'blackResign' | 'mutualDraw'): { @@ -112,13 +92,13 @@ export class Ctrl { this.threefold('update'); if (user && this.isPromotion(move)) { return; // oh noes PromotionCtrl! put it back! put it back! - } else this.cgMove(uci); + } else this.updateCgBoard(uci); const { end, result, reason } = this.checkGameOver(); if (end) this.doGameOver(result!, reason!); else this.getBotMove(); } - userMove = (orig: Key, dest: Key) => { + cgUserMove = (orig: Key, dest: Key) => { this.move(orig + dest, true); }; @@ -129,20 +109,20 @@ export class Ctrl { let move; if (moveType === 'zero') { const [zeroMove, lines] = await Promise.all([ - zf.goZero(this.fen), - zf.goFish(this.fen, { pvs: 8, depth: 6 }), + zf!.goZero(this.fen), + zf!.goFish(this.fen, { pvs: 8, depth: 6 }), ]); // without randomSprinkle, lc0 will always play the same game move = this.chess.turn === 'white' ? randomSprinkle(zeroMove, lines) : zeroMove; console.log(`${this.chess.turn} ${zeroMove === move ? 'zero' : 'ZEROFISH'} ${move}`); } else { - move = (await zf.goFish(this.fen, { depth: 4 }))[0].moves[0]; + move = (await zf!.goFish(this.fen, { depth: 3 }))[0].moves[0]; console.log(`${this.chess.turn} fish ${move}`); } this.move(move); } - cgMove(uci: Uci) { + updateCgBoard(uci: Uci) { const { from, to, role } = splitUci(uci); this.cg.move(from, to); if (role) this.cg.setPieces(new Map([[to, { color: this.chess.turn, role, promoted: true }]])); @@ -197,6 +177,28 @@ export class Ctrl { this.cg.toggleOrientation(); this.redraw(); }; + + dropHandler(color: 'white' | 'black', el: HTMLElement) { + const $el = $(el); + $el.on('dragenter dragover dragleave drop', e => { + e.preventDefault(); + e.stopPropagation(); + }); + $el.on('dragenter dragover', _ => this.zf[color]) && $el.addClass('hilite'); + $el.on('dragleave drop', _ => this.zf[color] && $el.removeClass('hilite')); + $el.on('drop', e => { + if (!this.zf[color]) return; + const reader = new FileReader(); + const weights = e.dataTransfer.files.item(0) as File; + reader.onload = e => { + this.zf[color]!.setZeroWeights(new Uint8Array(e.target!.result as ArrayBuffer)); + $(`#${color} p`).first().text(weights.name); + this.players[color] = 'zero'; + if (this.players[Chops.opposite(color)] !== 'human') $('#go').removeClass('disabled'); + }; + reader.readAsArrayBuffer(weights); + }); + } } function sq2key(sq: number): Key { diff --git a/ui/site/package.json b/ui/site/package.json index bb85bf9ee8935..def4e76720752 100644 --- a/ui/site/package.json +++ b/ui/site/package.json @@ -39,7 +39,7 @@ }, "lichess": { "modules": { - "iife": {}, + "iife": { }, "esm": { "src/site.ts": "lichess", "src/tvEmbed.ts": "tvEmbed", @@ -77,62 +77,10 @@ } }, "copy": [ - { - "src": "node_modules/hopscotch/dist/js/hopscotch.min.js", - "dest": "../../public/vendor/hopscotch/dist/js" - }, - { - "src": "node_modules/hopscotch/dist/css/hopscotch.min.css", - "dest": "../../public/vendor/hopscotch/dist/css" - }, - { - "src": "node_modules/hopscotch/dist/img", - "dest": "../../public/vendor/hopscotch/dist" - }, - { - "src": [ - "node_modules/highcharts/highcharts.js", - "node_modules/highcharts/highcharts-more.js", - "node_modules/highcharts/highstock.js" - ], - "dest": "../../public/vendor/highcharts-4.2.5" - }, - { - "src": "node_modules/@yaireo/tagify/dist/tagify.min.js", - "dest": "../../public/vendor/tagify" - }, - { - "src": [ - "node_modules/stockfish.js/stockfish.js", - "node_modules/stockfish.js/stockfish.wasm", - "node_modules/stockfish.js/stockfish.wasm.js" - ], - "dest": "../../public/vendor/stockfish.js" - }, - { - "src": [ - "node_modules/stockfish.wasm/stockfish.js", - "node_modules/stockfish.wasm/stockfish.wasm", - "node_modules/stockfish.wasm/stockfish.worker.js" - ], - "dest": "../../public/vendor/stockfish.wasm" - }, - { - "src": [ - "node_modules/stockfish-mv.wasm/stockfish.js", - "node_modules/stockfish-mv.wasm/stockfish.wasm", - "node_modules/stockfish-mv.wasm/stockfish.worker.js" - ], - "dest": "../../public/vendor/stockfish-mv.wasm" - }, - { - "src": [ - "node_modules/stockfish-nnue.wasm/stockfish.js", - "node_modules/stockfish-nnue.wasm/stockfish.wasm", - "node_modules/stockfish-nnue.wasm/stockfish.worker.js" - ], - "dest": "../../public/vendor/stockfish-nnue.wasm" - } + { "src": "node_modules/hopscotch/dist/**", "dest": "../../public/vendor/hopscotch/dist" }, + { "src": "node_modules/highcharts/*.js", "dest": "../../public/vendor/highcharts-4.2.5" }, + { "src": "node_modules/@yaireo/tagify/dist/tagify.min.js", "dest": "../../public/vendor/tagify" }, + { "src": "node_modules/stockfish*/*.{js,wasm}", "dest": "../../public/vendor" } ] } } diff --git a/ui/voice/package.json b/ui/voice/package.json index da3f534a5106d..9445fb6c028ae 100644 --- a/ui/voice/package.json +++ b/ui/voice/package.json @@ -33,11 +33,6 @@ "src/move/moveCtrl.ts": "voice.move" } }, - "copy": [ - { - "src": "grammar", - "dest": "../../public/compiled" - } - ] + "copy": { "src": "grammar/**", "dest": "../../public/compiled/grammar" } } } From 59b5ce402dd63a4cbb05b0757c65cdc98f7c1600 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Fri, 28 Jul 2023 19:52:11 -0500 Subject: [PATCH 011/174] 0.0.8 --- pnpm-lock.yaml | 8 ++++---- ui/localPlay/package.json | 2 +- ui/localPlay/src/ctrl.ts | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8d6225df9fb41..573bae3039ad9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -491,8 +491,8 @@ importers: specifier: workspace:* version: link:../tree zerofish: - specifier: ^0.0.7 - version: 0.0.7 + specifier: ^0.0.8 + version: 0.0.8 ui/mod: dependencies: @@ -6424,8 +6424,8 @@ packages: engines: {node: '>=10'} dev: false - /zerofish@0.0.7: - resolution: {integrity: sha512-fxSIWjQTSF0h5SSiIifEKqt/CPEpxNQOfsdU8l8ZBK2cKYam6Ak7WN+Wpinz6RgZKsJ+qAPp6WLwxt8Kw97p+A==} + /zerofish@0.0.8: + resolution: {integrity: sha512-PwOmIMRcSx+K87WFVAdwrDgy4tEUXX1q+LSUdslsnVkslaBtkwQHoWLrLzWHRzXXHNruQAnKgKTVEvemiYZOtQ==} dependencies: '@types/emscripten': 1.39.7 '@types/node': 20.4.5 diff --git a/ui/localPlay/package.json b/ui/localPlay/package.json index eb52ee5f2150f..2e3bb19541eaf 100644 --- a/ui/localPlay/package.json +++ b/ui/localPlay/package.json @@ -16,7 +16,7 @@ "puz": "workspace:*", "snabbdom": "^3.5.1", "tree": "workspace:*", - "zerofish": "^0.0.7" + "zerofish": "^0.0.8" }, "scripts": { "compile": "tsc", diff --git a/ui/localPlay/src/ctrl.ts b/ui/localPlay/src/ctrl.ts index 6bb15a63d20b8..de5172c7d14ac 100644 --- a/ui/localPlay/src/ctrl.ts +++ b/ui/localPlay/src/ctrl.ts @@ -29,8 +29,8 @@ export class Ctrl { () => this.cg.set(this.cgOpts()), this.redraw ); - Promise.all([/*makeZerofish(),*/ makeZerofish()]).then(([/*wz,*/ bz]) => { - this.zf ??= { white: /*wz*/ undefined, black: bz }; + Promise.all([makeZerofish(), makeZerofish()]).then(([wz, bz]) => { + this.zf ??= { white: wz, black: bz }; }); } @@ -113,7 +113,7 @@ export class Ctrl { zf!.goFish(this.fen, { pvs: 8, depth: 6 }), ]); // without randomSprinkle, lc0 will always play the same game - move = this.chess.turn === 'white' ? randomSprinkle(zeroMove, lines) : zeroMove; + move = Math.random() < 0.5 ? randomSprinkle(zeroMove, lines) : zeroMove; console.log(`${this.chess.turn} ${zeroMove === move ? 'zero' : 'ZEROFISH'} ${move}`); } else { move = (await zf!.goFish(this.fen, { depth: 3 }))[0].moves[0]; @@ -184,8 +184,8 @@ export class Ctrl { e.preventDefault(); e.stopPropagation(); }); - $el.on('dragenter dragover', _ => this.zf[color]) && $el.addClass('hilite'); - $el.on('dragleave drop', _ => this.zf[color] && $el.removeClass('hilite')); + $el.on('dragenter dragover', () => this.zf[color] && $el.addClass('hilite')); + $el.on('dragleave drop', () => this.zf[color] && $el.removeClass('hilite')); $el.on('drop', e => { if (!this.zf[color]) return; const reader = new FileReader(); From 5caa6c868e89b4b753df573751ab33cad2122c1e Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Fri, 4 Aug 2023 04:02:19 -0500 Subject: [PATCH 012/174] . --- ui/@build/src/build.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ui/@build/src/build.ts b/ui/@build/src/build.ts index d364a55eeceb5..8114baa3792a2 100644 --- a/ui/@build/src/build.ts +++ b/ui/@build/src/build.ts @@ -118,11 +118,7 @@ async function copyOne(absSrc: string, absDest: string, modName: string) { fs.promises.mkdir(path.dirname(absDest), { recursive: true }), ]) ).map(x => (x.status === 'fulfilled' ? (x.value as fs.Stats) : undefined)); -<<<<<<< HEAD - if (src && (!dest || quantize(src.mtimeMs) != quantize(dest.mtimeMs))) { -======= if (src && (!dest || quantize(src.mtimeMs) !== quantize(dest.mtimeMs))) { ->>>>>>> upstream/master await fs.promises.copyFile(absSrc, absDest); fs.utimes(absDest, src.atime, src.mtime, () => {}); } From b704ab968d29670ff2b6dc645fe1f915dd365c99 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Sat, 5 Aug 2023 14:02:30 -0500 Subject: [PATCH 013/174] . --- ui/localPlay/css/_layout.scss | 61 +++++++++++++++++++++++++++ ui/localPlay/css/_local-play.scss | 61 +++++++++++++++++++++++++++ ui/localPlay/i18n.json | 68 +++++++++++++++++++++++++++++++ ui/localPlay/src/ctrl.ts | 13 ++++++ ui/round/src/ctrl.ts | 1 + ui/round/src/socket.ts | 8 +++- 6 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 ui/localPlay/css/_layout.scss create mode 100644 ui/localPlay/i18n.json diff --git a/ui/localPlay/css/_layout.scss b/ui/localPlay/css/_layout.scss new file mode 100644 index 0000000000000..d9ea8135f06b8 --- /dev/null +++ b/ui/localPlay/css/_layout.scss @@ -0,0 +1,61 @@ +#main-wrap { + --main-max-width: auto; + + @include breakpoint($mq-col1) { + --main-max-width: calc( + 100vh - #{$site-header-outer-height} - #{$col1-player-clock-height * 2} - #{$col1-moves-height} + ); + } +} + +.round { + grid-area: main; + display: grid; + grid-gap: $block-gap; + + &__app { + grid-area: app; + } + + &__side { + grid-area: side; + } + + &__underboard { + @extend %zen; + + grid-area: under; + } + + &__underchat { + @extend %zen; + + grid-area: uchat; + } + + grid-template-areas: 'app' 'under' 'side' 'uchat'; + + @include breakpoint($mq-col2) { + grid-template-columns: 1fr 1fr; + grid-template-rows: auto fit-content(0) fit-content(0); + grid-template-areas: 'app app' 'under side' 'under uchat'; + + &__app { + justify-self: center; + } + } + + @include breakpoint($mq-col3) { + grid-template-columns: $col3-uniboard-side var(--col3-uniboard-width) $col3-uniboard-table; + grid-template-rows: fit-content(0); + grid-template-areas: 'side app app' 'uchat under .'; + + @include crosstable-large; + } + + @include breakpoint($mq-col2-uniboard-squeeze) { + .crosstable { + display: none; + } + } +} diff --git a/ui/localPlay/css/_local-play.scss b/ui/localPlay/css/_local-play.scss index 3f3061f0eee94..9cdc1e38f7926 100644 --- a/ui/localPlay/css/_local-play.scss +++ b/ui/localPlay/css/_local-play.scss @@ -18,6 +18,67 @@ $mq-col2: $mq-col2-uniboard; text-align: center; font-size: 0.8em; } + #moves { + flex: 2 1 0; + display: flex; + flex-direction: column; + justify-content: space-between; + + // 0 size forces vertical scrollbar + overflow-y: auto; + overflow-x: hidden; + + // else a scrollbar appears sometimes + border-top: $border; + position: relative; + + /* required so line::before scrolls along the moves! */ + .result, + .status { + background: $c-bg-zebra; + text-align: center; + } + + .result { + border-top: $border; + font-weight: bold; + font-size: 1.2em; + padding: 5px 0 3px 0; + } + + .status { + font-size: 1em; + font-style: italic; + padding-bottom: 7px; + } + + button.next { + border: 0; + background: $c-bg-box; + color: $c-link; + padding: 0.5em; + width: 100%; + + @include transition; + + &:hover { + color: $c-link-hover; + } + + &::before { + margin-#{$end-direction}: 0.3em; + } + + &.highlighted { + background: mix($c-primary, $c-bg-box, 80%); + color: $c-primary-over; + + &:hover { + background: $c-primary; + } + } + } + } .puz-side { .puz-bot { diff --git a/ui/localPlay/i18n.json b/ui/localPlay/i18n.json new file mode 100644 index 0000000000000..4dcf70a2be4dc --- /dev/null +++ b/ui/localPlay/i18n.json @@ -0,0 +1,68 @@ +{ + "anonymous": "Anonymous", + "flipBoard": "Flip board", + "aiNameLevelAiLevel": "%1$s level %2$s", + "yourTurn": "Your turn", + "abortGame": "Abort game", + "proposeATakeback": "Propose a takeback", + "offerDraw": "Offer draw", + "resign": "Resign", + "opponentLeftCounter:one": "Your opponent left the game. You can claim victory in %s second.", + "opponentLeftCounter": "Your opponent left the game. You can claim victory in %s seconds.", + "opponentLeftChoices": "Your opponent left the game. You can claim victory, call the game a draw, or wait.", + "forceResignation": "Claim victory", + "forceDraw": "Call draw", + "threefoldRepetition": "Threefold repetition", + "claimADraw": "Claim a draw", + "drawOfferSent": "Draw offer sent", + "cancel": "Cancel", + "yourOpponentOffersADraw": "Your opponent offers a draw", + "accept": "Accept", + "decline": "Decline", + "takebackPropositionSent": "Takeback sent", + "yourOpponentProposesATakeback": "Your opponent proposes a takeback", + "thisAccountViolatedTos": "This account violated the Lichess Terms of Service", + "gameAborted": "Game aborted", + "checkmate": "Checkmate", + "cheatDetected": "Cheat Detected", + "whiteResigned": "White resigned", + "blackResigned": "Black resigned", + "whiteDidntMove": "White didn't move", + "blackDidntMove": "Black didn't move", + "stalemate": "Stalemate", + "whiteLeftTheGame": "White left the game", + "blackLeftTheGame": "Black left the game", + "draw": "Draw", + "whiteTimeOut": "White time out", + "blackTimeOut": "Black time out", + "whiteIsVictorious": "White is victorious", + "blackIsVictorious": "Black is victorious", + "drawByMutualAgreement": "Draw by mutual agreement", + "fiftyMovesWithoutProgress": "Fifty moves without progress", + "insufficientMaterial": "Insufficient material", + "withdraw": "Withdraw", + "rematch": "Rematch", + "rematchOfferSent": "Rematch offer sent", + "rematchOfferAccepted": "Rematch offer accepted", + "waitingForOpponent": "Waiting for opponent", + "cancelRematchOffer": "Cancel rematch offer", + "newOpponent": "New opponent", + "confirmMove": "Confirm move", + "viewRematch": "View rematch", + "whitePlays": "White to play", + "blackPlays": "Black to play", + "giveNbSeconds:one": "Give %s second", + "giveNbSeconds": "Give %s seconds", + "giveMoreTime": "Give more time", + "gameOver": "Game Over", + "analysis": "Analysis board", + "yourOpponentWantsToPlayANewGameWithYou": "Your opponent wants to play a new game with you", + "youPlayTheWhitePieces": "You play the white pieces", + "youPlayTheBlackPieces": "You play the black pieces", + "itsYourTurn": "It's your turn!", + "oneDay": "One day", + "nbDays:one": "%s day", + "nbDays": "%s days", + "nbHours:one": "%s hour", + "nbHours": "%s hours" +} \ No newline at end of file diff --git a/ui/localPlay/src/ctrl.ts b/ui/localPlay/src/ctrl.ts index de5172c7d14ac..4ff85944dbf9a 100644 --- a/ui/localPlay/src/ctrl.ts +++ b/ui/localPlay/src/ctrl.ts @@ -12,6 +12,7 @@ type Player = 'human' | 'zero' | 'fish'; export class Ctrl { cg: CgApi; + path = ''; chess = Chess.default(); promotion: PromotionCtrl; zf: { white?: Zerofish; black?: Zerofish }; @@ -81,6 +82,18 @@ export class Ctrl { else setTimeout(() => this.go()); } + jump(path: string) { + path; + /*this.path = path; + this.chess = Chess.fromSetup(Chops.parseFen(path)); + this.fen = makeFen(this.chess.toSetup()); + this.cg.set(this.cgOpts()); + this.fiftyMovePly = 0; + this.threefoldFens.clear(); + this.zf.white?.reset(); + this.zf.black?.reset();*/ + } + move(uci: Uci, user = false) { const move = Chops.parseUci(uci); if (!move || !this.chess.isLegal(move)) diff --git a/ui/round/src/ctrl.ts b/ui/round/src/ctrl.ts index 37f9ccc9f5478..c9c58e6b13396 100644 --- a/ui/round/src/ctrl.ts +++ b/ui/round/src/ctrl.ts @@ -93,6 +93,7 @@ export default class RoundController { keyboardHelp: boolean = location.hash === '#keyboard'; constructor(readonly opts: RoundOpts, readonly redraw: Redraw, readonly nvui?: NvuiPlugin) { + console.log('rounds', JSON.stringify(opts.i18n, undefined, 2)); round.massage(opts.data); const d = (this.data = opts.data); diff --git a/ui/round/src/socket.ts b/ui/round/src/socket.ts index 6e6c6590b6f38..19cb05e47ed56 100644 --- a/ui/round/src/socket.ts +++ b/ui/round/src/socket.ts @@ -158,7 +158,12 @@ export function make(send: SocketSend, ctrl: RoundController): RoundSocket { lichess.pubsub.on('ab.rep', n => send('rep', { n })); return { - send, + send: (typ: string, data?: any, opts?: any, noRetry?: boolean) => { + console.log('socket send', typ, JSON.stringify(data, undefined, 2)); + if (opts) console.log('send opts', JSON.stringify(opts, undefined, 2)); + if (noRetry !== undefined) console.log('send noRetry', noRetry); + send(typ, data, opts, noRetry); + }, handlers, moreTime: throttle(300, () => send('moretime')), outoftime: backoff(500, 1.1, () => send('flag', ctrl.data.game.player)), @@ -168,6 +173,7 @@ export function make(send: SocketSend, ctrl: RoundController): RoundSocket { send(typ, data); }, receive(typ: string, data: any): boolean { + console.log('socket receive', typ, JSON.stringify(data, undefined, 2)); const handler = handlers[typ]; if (handler) { handler(data); From 4366f4393809c94827ae246e0c54d26ad28f030e Mon Sep 17 00:00:00 2001 From: Jonathan Gamble <101470903+schlawg@users.noreply.github.com> Date: Sat, 5 Aug 2023 19:23:44 -0500 Subject: [PATCH 014/174] . --- pnpm-lock.yaml | 5 ++++- ui/localPlay/package.json | 6 +++++- ui/localPlay/tsconfig.json | 1 + ui/round/package.json | 1 + ui/round/src/interfaces.ts | 3 +++ ui/round/src/main.ts | 3 ++- ui/round/tsconfig.json | 8 +++++++- 7 files changed, 23 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 573bae3039ad9..351b85e11315e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' settings: autoInstallPeers: true @@ -484,6 +484,9 @@ importers: puz: specifier: workspace:* version: link:../puz + round: + specifier: workspace:* + version: link:../round snabbdom: specifier: ^3.5.1 version: 3.5.1 diff --git a/ui/localPlay/package.json b/ui/localPlay/package.json index 2e3bb19541eaf..5f97ec745f9bc 100644 --- a/ui/localPlay/package.json +++ b/ui/localPlay/package.json @@ -14,6 +14,7 @@ "game": "workspace:*", "nvui": "workspace:*", "puz": "workspace:*", + "round": "workspace:*", "snabbdom": "^3.5.1", "tree": "workspace:*", "zerofish": "^0.0.8" @@ -29,6 +30,9 @@ "src/main.ts": "localPlay" } }, - "copy": { "src": "node_modules/zerofish/dist/zerofishEngine.*", "dest": "../../public/compiled" } + "copy": { + "src": "node_modules/zerofish/dist/zerofishEngine.*", + "dest": "../../public/compiled" + } } } diff --git a/ui/localPlay/tsconfig.json b/ui/localPlay/tsconfig.json index 97bdb45cec233..04dbe488c3337 100644 --- a/ui/localPlay/tsconfig.json +++ b/ui/localPlay/tsconfig.json @@ -13,6 +13,7 @@ { "path": "../game/tsconfig.json" }, { "path": "../puz/tsconfig.json" }, { "path": "../nvui/tsconfig.json" }, + { "path": "../round/tsconfig.json" }, { "path": "../tree/tsconfig.json" } ], "isolatedModules": true diff --git a/ui/round/package.json b/ui/round/package.json index 0e735b6e66d3c..f9be4cd97ba75 100644 --- a/ui/round/package.json +++ b/ui/round/package.json @@ -11,6 +11,7 @@ ], "author": "Thibault Duplessis", "license": "AGPL-3.0-or-later", + "types": "dist/interfaces.d.ts", "dependencies": { "@types/cash": "workspace:*", "@types/lichess": "workspace:*", diff --git a/ui/round/src/interfaces.ts b/ui/round/src/interfaces.ts index f72ca8a6690b8..72b2e627d7f47 100644 --- a/ui/round/src/interfaces.ts +++ b/ui/round/src/interfaces.ts @@ -7,6 +7,8 @@ import { ChatCtrl, ChatPlugin } from 'chat'; import * as cg from 'chessground/types'; import * as Prefs from 'common/prefs'; +export { type CorresClockData } from './corresClock/corresClockCtrl'; + export interface Untyped { [key: string]: any; } @@ -87,6 +89,7 @@ export interface RoundOpts { crosstableEl: HTMLElement; i18n: I18nDict; chat?: ChatOpts; + local?: boolean; } export interface ChatOpts { diff --git a/ui/round/src/main.ts b/ui/round/src/main.ts index 727b2aac4d3a7..c7765625e0587 100644 --- a/ui/round/src/main.ts +++ b/ui/round/src/main.ts @@ -9,7 +9,8 @@ import { RoundOpts, NvuiPlugin } from './interfaces'; const patch = init([classModule, attributesModule]); export function initModule(opts: RoundOpts) { - boot(opts, app); + if (opts.local) app(opts); + else boot(opts, app); } function app(opts: RoundOpts, nvui?: NvuiPlugin) { diff --git a/ui/round/tsconfig.json b/ui/round/tsconfig.json index 6680e5fdb94bf..c3c2f07e2b15b 100644 --- a/ui/round/tsconfig.json +++ b/ui/round/tsconfig.json @@ -1,6 +1,12 @@ { "extends": "../tsconfig.base.json", - "compilerOptions": { "noEmit": true }, + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "composite": true, + "declaration": true, + "emitDeclarationOnly": true, + }, "isolatedModules": true, "references": [ { "path": "../chat/tsconfig.json" }, From cff13398b2d81887a55d61f08a477503fbb3bce1 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble <101470903+schlawg@users.noreply.github.com> Date: Sat, 5 Aug 2023 19:24:50 -0500 Subject: [PATCH 015/174] . --- ui/localPlay/src/data.ts | 117 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 ui/localPlay/src/data.ts diff --git a/ui/localPlay/src/data.ts b/ui/localPlay/src/data.ts new file mode 100644 index 0000000000000..3e986b865c9f9 --- /dev/null +++ b/ui/localPlay/src/data.ts @@ -0,0 +1,117 @@ +import { /*RoundOpts,*/ RoundData } from 'round'; +//import { Player, GameData } from 'game'; + +/*interface RoundApi { + socketReceive(typ: string, data: any): boolean; + moveOn: MoveOn; +}*/ + +const data: RoundData = { + game: { + id: 'x7hgwoir', + variant: { key: 'standard', name: 'Standard', short: 'Std' }, + speed: 'correspondence', + perf: 'correspondence', + rated: false, + fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1', + turns: 0, + source: 'friend', + status: { id: 20, name: 'started' }, + player: 'white', + }, + player: { + color: 'white', + user: { + id: 'anonymous', + username: 'Anonymous', + online: true, + perfs: {}, + }, + rating: 1628, + id: '7J3E', + isGone: false, + name: 'Anonymous', + onGame: true, + version: 0, + }, + opponent: { + color: 'black', + user: { + id: 'anonymous', + username: 'Anonymous', + online: true, + perfs: {}, + }, + id: '', + isGone: false, + name: 'Anonymous', + onGame: true, + rating: 1500, + version: 0, + }, + pref: { + animationDuration: 300, + coords: 1, + resizeHandle: 1, + replay: 2, + autoQueen: 2, + clockTenths: 1, + moveEvent: 2, + clockBar: true, + clockSound: true, + confirmResign: true, + rookCastle: true, + highlight: true, + destination: true, + enablePremove: true, + showCaptured: true, + blindfold: false, + is3d: false, + keyboardMove: true, + voiceMove: true, + ratings: true, + submitMove: false, + }, + steps: [{ ply: 0, san: '', uci: '', fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1' }], + correspondence: { + daysPerTurn: 2, + increment: 0, + white: 0, + black: 0, + showBar: true, + }, + takebackable: true, + moretimeable: true, +}; +gah(); +function gah() { + const socketUrl = /*opts.data.player.spectator + ? `/watch/${data.game.id}/${data.player.color}/v6` + :*/ `/play/${data.game.id}${data.player.id}/v6`; + lichess.socket = new lichess.StrongSocket(socketUrl, data.player.version, { + params: { userTv: false }, + receive(t: string, d: any) { + t, d; + //round.socketReceive(t, d); + }, + events: {}, + }); + + if (location.pathname.lastIndexOf('/round-next/', 0) === 0) history.replaceState(null, '', '/' + data.game.id); + $('#zentog').on('click', () => lichess.pubsub.emit('zen')); + lichess.storage.make('reload-round-tabs').listen(lichess.reload); + + if (!data.player.spectator && location.hostname != (document as any)['Location'.toLowerCase()].hostname) { + alert(`Games cannot be played through a web proxy. Please use ${location.hostname} instead.`); + lichess.socket.destroy(); + } +} + +export function makeRounds() { + const opts /*: RoundOpts*/ = { + element: document.querySelector('.round__app') as HTMLElement, + data, + socketSend: lichess.socket.send, + }; + lichess.loadEsm('round', { init: opts }); +} From 494a5d87fe1bbd49cdf7e94342e318fe4712e248 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Sun, 6 Aug 2023 19:47:07 -0500 Subject: [PATCH 016/174] . --- app/controllers/LocalPlay.scala | 8 +- .../botVsBot.scala} | 6 +- app/views/localPlay/vsBot.scala | 139 ++++++++++ conf/routes | 4 +- public/images/bots/baby-howard.webp | Bin 0 -> 23494 bytes public/images/bots/baby-robot.webp | Bin 0 -> 23224 bytes public/images/bots/beatrice.webp | Bin 0 -> 21930 bytes public/images/bots/coral.webp | Bin 0 -> 17228 bytes public/images/bots/floyd.webp | Bin 0 -> 19460 bytes ui/game/src/interfaces.ts | 1 + ui/localPlay/css/_local-play.scss | 153 +++++------ .../src/{ => botVsBot}/chessground.ts | 0 ui/localPlay/src/botVsBot/ctrl.ts | 239 ++++++++++++++++++ ui/localPlay/src/botVsBot/interfaces.ts | 2 + ui/localPlay/src/botVsBot/main.ts | 28 ++ ui/localPlay/src/botVsBot/view.ts | 48 ++++ ui/localPlay/src/bots.ts | 22 ++ ui/localPlay/src/ctrl.ts | 176 +++---------- ui/localPlay/src/data.ts | 66 +++-- ui/localPlay/src/main.ts | 13 +- ui/localPlay/src/socket.ts | 0 ui/localPlay/src/view.ts | 58 ++--- ui/round/src/ctrl.ts | 12 +- ui/round/src/interfaces.ts | 2 +- ui/round/src/main.ts | 7 +- ui/round/src/socket.ts | 6 +- ui/round/src/view/table.ts | 2 + ui/round/src/view/user.ts | 17 ++ 28 files changed, 675 insertions(+), 334 deletions(-) rename app/views/{localPlay.scala => localPlay/botVsBot.scala} (85%) create mode 100644 app/views/localPlay/vsBot.scala create mode 100644 public/images/bots/baby-howard.webp create mode 100644 public/images/bots/baby-robot.webp create mode 100644 public/images/bots/beatrice.webp create mode 100644 public/images/bots/coral.webp create mode 100644 public/images/bots/floyd.webp rename ui/localPlay/src/{ => botVsBot}/chessground.ts (100%) create mode 100644 ui/localPlay/src/botVsBot/ctrl.ts create mode 100644 ui/localPlay/src/botVsBot/interfaces.ts create mode 100644 ui/localPlay/src/botVsBot/main.ts create mode 100644 ui/localPlay/src/botVsBot/view.ts create mode 100644 ui/localPlay/src/bots.ts delete mode 100644 ui/localPlay/src/socket.ts diff --git a/app/controllers/LocalPlay.scala b/app/controllers/LocalPlay.scala index 566d19a3ec276..4a0e60a12d69b 100644 --- a/app/controllers/LocalPlay.scala +++ b/app/controllers/LocalPlay.scala @@ -14,6 +14,10 @@ import lila.rating.{ Perf, PerfType } final class LocalPlay(env: Env) extends LilaController(env): - def index = Open: + def vsBot = Open: NoBot: - Ok.page(views.html.localPlay.index).map(_.enableSharedArrayBuffer) + Ok.page(views.html.localPlay.vsBot.index).map(_.enableSharedArrayBuffer) + + def botVsBot = Open: + NoBot: + Ok.page(views.html.localPlay.botVsBot.index).map(_.enableSharedArrayBuffer) diff --git a/app/views/localPlay.scala b/app/views/localPlay/botVsBot.scala similarity index 85% rename from app/views/localPlay.scala rename to app/views/localPlay/botVsBot.scala index ec389b5cb97b7..a5a533b59cdfb 100644 --- a/app/views/localPlay.scala +++ b/app/views/localPlay/botVsBot.scala @@ -1,4 +1,4 @@ -package views.html +package views.html.localPlay import controllers.routes import play.api.libs.json.{ JsObject, Json } @@ -8,7 +8,7 @@ import lila.app.ui.ScalatagsTemplate.* import lila.common.Json.given import lila.common.String.html.safeJsonValue -object localPlay: +object botVsBot: def index(using ctx: PageContext) = views.html.base.layout( title = "Play vs Bots", @@ -19,7 +19,7 @@ object localPlay: .OpenGraph( title = "Play vs Bots", description = "Play vs Bots", - url = s"$netBaseUrl${controllers.routes.LocalPlay.index}" + url = s"$netBaseUrl${controllers.routes.LocalPlay.botVsBot}" ) .some, zoomable = true, diff --git a/app/views/localPlay/vsBot.scala b/app/views/localPlay/vsBot.scala new file mode 100644 index 0000000000000..02e904f646e1e --- /dev/null +++ b/app/views/localPlay/vsBot.scala @@ -0,0 +1,139 @@ +package views.html.localPlay + +import controllers.routes +import play.api.libs.json.{ JsObject, Json } +import play.api.i18n.Lang + +import lila.app.templating.Environment.{ given, * } +import lila.i18n.{ I18nKeys as trans } +import lila.app.ui.ScalatagsTemplate.* +import lila.common.Json.given +import lila.common.String.html.safeJsonValue +import lila.game.Pov +import views.html.round.bits + +object vsBot: + def index(using ctx: PageContext) = + views.html.base.layout( + title = "Play vs Bots", + moreJs = frag( + jsModuleInit( + "localPlay", + Json.obj( + "pref" -> lila.pref.JsonView.write(ctx.pref, false), + "i18n" -> i18n, + "userId" -> ctx.userId + ) + ) + ), + moreCss = frag( + cssTag("local-play"), + cssTag("round"), + ctx.pref.hasKeyboardMove option cssTag("keyboardMove"), + ctx.pref.hasVoice option cssTag("voice") + // ctx.blind option cssTag("round.nvui"), + ), + csp = analysisCsp.some, + openGraph = lila.app.ui + .OpenGraph( + title = "Play vs Bots", + description = "Play vs Bots", + url = s"$netBaseUrl${controllers.routes.LocalPlay.vsBot}" + ) + .some, + zoomable = true, + chessground = false + ) { + main(cls := "round")( + st.aside(cls := "round__side")( + st.section(id := "bot-view")( + div(id := "bot-tabs")( + div(cls := "bot-tab")(nbsp) + ), + div(id := "bot-content") + ) + ), + // bits.roundAppPreload(pov), + div(cls := "round__app")( + div(cls := "round__app__board main-board")(), + div(cls := "col1-rmoves-preload") + ), + div(cls := "round__underboard")( + // bits.crosstable(cross, pov.game), + // (playing.nonEmpty || simul.exists(_ isHost ctx.me)) option + div( + cls := "round__now-playing" + ) + ), + div(cls := "round__underchat")() + ) + } + def i18n(using Lang) = i18nJsObject(translations) + val translations = Vector( + trans.oneDay, + trans.nbDays, + trans.nbHours, + trans.nbSecondsToPlayTheFirstMove, + trans.kingInTheCenter, + trans.threeChecks, + trans.variantEnding, + trans.anonymous, + trans.flipBoard, + trans.aiNameLevelAiLevel, + trans.yourTurn, + trans.abortGame, + trans.proposeATakeback, + trans.offerDraw, + trans.resign, + trans.opponentLeftCounter, + trans.opponentLeftChoices, + trans.forceResignation, + trans.forceDraw, + trans.threefoldRepetition, + trans.claimADraw, + trans.drawOfferSent, + trans.cancel, + trans.yourOpponentOffersADraw, + trans.accept, + trans.decline, + trans.takebackPropositionSent, + trans.yourOpponentProposesATakeback, + trans.thisAccountViolatedTos, + trans.gameAborted, + trans.checkmate, + trans.cheatDetected, + trans.whiteResigned, + trans.blackResigned, + trans.whiteDidntMove, + trans.blackDidntMove, + trans.stalemate, + trans.whiteLeftTheGame, + trans.blackLeftTheGame, + trans.draw, + trans.whiteTimeOut, + trans.blackTimeOut, + trans.whiteIsVictorious, + trans.blackIsVictorious, + trans.drawByMutualAgreement, + trans.fiftyMovesWithoutProgress, + trans.insufficientMaterial, + trans.withdraw, + trans.rematch, + trans.rematchOfferSent, + trans.rematchOfferAccepted, + trans.waitingForOpponent, + trans.cancelRematchOffer, + trans.newOpponent, + trans.confirmMove, + trans.viewRematch, + trans.whitePlays, + trans.blackPlays, + trans.giveNbSeconds, + trans.preferences.giveMoreTime, + trans.gameOver, + trans.analysis, + trans.yourOpponentWantsToPlayANewGameWithYou, + trans.youPlayTheWhitePieces, + trans.youPlayTheBlackPieces, + trans.itsYourTurn + ) diff --git a/conf/routes b/conf/routes index 0bc9783fd1f53..bea07b0ba8b5c 100644 --- a/conf/routes +++ b/conf/routes @@ -311,7 +311,9 @@ GET /streamer/:username controllers.Streamer.show(username) GET /streamer/:username/redirect controllers.Streamer.redirect(username) # LocalPlay -GET /local-play controllers.LocalPlay.index +GET /local/vsbot controllers.LocalPlay.vsBot +GET /local/setup controllers.LocalPlay.setup +GET /local/botvsbot controllers.LocalPlay.botVsBot # Round GET /$gameId<\w{8}> controllers.Round.watcher(gameId, color = "white") diff --git a/public/images/bots/baby-howard.webp b/public/images/bots/baby-howard.webp new file mode 100644 index 0000000000000000000000000000000000000000..53e0e4751d0eb4dc9b333b71317cca57f37bbf97 GIT binary patch literal 23494 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 literal 0 HcmV?d00001 diff --git a/public/images/bots/baby-robot.webp b/public/images/bots/baby-robot.webp new file mode 100644 index 0000000000000000000000000000000000000000..9c56d595d66ff3c34d65f96cbcb879736bc34b60 GIT binary patch literal 23224 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 literal 0 HcmV?d00001 diff --git a/public/images/bots/beatrice.webp b/public/images/bots/beatrice.webp new file mode 100644 index 0000000000000000000000000000000000000000..807e259f459204a73ef64d01b26ee4158d5ccef0 GIT binary patch literal 21930 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 J1^=-0zW}k>)`0*3 literal 0 HcmV?d00001 diff --git a/public/images/bots/floyd.webp b/public/images/bots/floyd.webp new file mode 100644 index 0000000000000000000000000000000000000000..30c7df4c648c4d18ad8d9bdb6ff8830aa1c1edab GIT binary patch literal 19460 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 z zFL7!)UTYOKWV*>j$;=5u2Iq@!*^jJCrW?Y*els4>ZBS9@cYar=(yKwCHXU%)hsQA| zWrWC6VEbT{asgWi1{)^@4tq6;f;7NR*<;Hjs*d}Q0JfI8(x|<~3UPJ8szWHk&M&#BKdcT#zRH|a%?{uwaSlUPzh7Gq+HpTv5mCadwt(Mrrp zl9l4IdrJ~nHhKy)7+U^Dj#B$LMxay9D>*ks#a8jcjtr5Z^QoaqMY;^!&Tct_eaKy2Sh{(K<)Lq&xy(`C zI1f>*!vq13#{fO*SF?y6CboDDy>rgaKJ4AUIUoTk`{p^hTEJS_w(Ij!7$ulaYys@h*-i{Z9%1R_S~=cn9><3V5C$**zJ9QgG2gLUG< zB#Ou#7an-kNR-{o5H?}CX0^ymlDg)BmTuXZZ{~(c%QG+LkhG0hff#_Dn|dB6z3w-? z;1_@Z2DurSE@E8|r`Ber*obQ6uDO5O)o#xR5~Z&oFY?<{`ggf$*9?R%5&`?f!e= z-0c^llUIKSd?H_3%#}~poyCFeT ztQpGgQ5B9mUph2#GN_D>E*#b}$?k=*g_tf-C#-TiQQ^$sEA^N+X(!}$=@;KsxP?p5FOh_hf(YL3O1oO=KZJ$*6jF#dhn(qxuC03j_B9C{xeY-k*c z0e*{nyZ{%Uw!|8-A1}7KeJ<&*ol)26S--944_+NmH>iSZ-6S1RBlLj(B z87ZO8(_Ozn83Sd({+uM~t>W2?@J=txr30N78E|=)~)1yd8rgARL$@XVV-W#U&MG5(PKb#bdJ-8{`{?T7_wGa zNv}5fZ@V!kmY#P!keVCvz~7O?39%0V17TnSykF{~ESaOCR)oJEnU7f2VzpYM335W3 zbMZ0{Ygw*^T9FJfV^FN!1*R#PvTA3SC1_nw#Qg(>BO@jX)0eEubBw~d^iMqUqQS>6 ziiP<6gI~)oi9)v8=T4GtqeXD$x-BIWX<{_!M5QA#6xKmk{&UGHGgbJ8Z~y=cQUQ}0 zx~ODseOTTVZJ1s$q}KnaaCO~gyH*l!^=VBGP))-H854CWn7Z!^J;$N3;HJ0J^nPHJlv}-!z3`hArM(@vlVn=d^UPm@b8(qj!~AqdHn?_i zSdQz`%e3?6R!8$(Z~Mebtr>}zC_bcnGA;=teC`5fXbdGb8d zl9Jak;%yITN0iB?{nSpmj@0l>tJ(oqw^tZ(5ZTNgZmq|j=m;Zcw0&w>e-Qie7$j~x=>Id{9jY%g8Nm)0^L%u{XidlznJ0ux;TRYf zl8+$=)MKgLPQPk0g^A&Fh+}q4i*@A{4pLI%U8iWL(?Dk4ctVj6x{ReR`GNroZ{wh~ zT$pX1SH!GV@8G($UXG-@qkuZ&@;sm92w%-wOYCcxq~rt#DHfFeYWQK&82A27@jf%% zG!T%u{YLI#jiFmxfBQyYd(ixB(-)GTJDhI3xn!D4<*{U~1s;b=s-30Y8fKt=^6k0k zAUe+@%uo-=3T1HCAZ<^_TEDN$NN*@vk?NMjHf#p*=HD&21S-7un}SM&2GPeEabMUb*`KRQv_MtGI>j#DvVAsUwGz z6~Z9d>lpDH84vNM0XI3}8~aVgUTUAPq%y50w6rcp{*BW}9E>@IzP9N1;DiNLR{19E z718WAt_93{x(-E^ZDvXBk!K^r4t}ubwNt)nQ?2YlykIY1a7)HLt=g&Oo!vn5Gk`+n zqY!`qZohf5ap=q@X@HxC8IN@{NwJ6CtEc?^rh#;MHFW2CPWY2?YIpd#@rZ_Ya3(Qa zndX< = new Map(); + + constructor(readonly opts: LocalPlayOpts, readonly redraw: () => void) { + this.promotion = new PromotionCtrl( + f => f(this.cg), + () => this.cg.set(this.cgOpts()), + this.redraw + ); + Promise.all([makeZerofish(), makeZerofish()]).then(([wz, bz]) => { + this.zf ??= { white: wz, black: bz }; + }); + } + + go(numGames?: number) { + this.totals ??= { gamesLeft: 1, white: 0, black: 0, draw: 0 }; + if (numGames) this.totals.gamesLeft = numGames; + this.fiftyMovePly = 0; + this.threefoldFens.clear(); + this.chess.reset(); + this.fen = makeFen(this.chess.toSetup()); + this.cg.set({ fen: this.fen }); + this.zf.white?.reset(); + this.zf.black?.reset(); + this.getBotMove(); + $('#go').addClass('disabled'); + } + + checkGameOver(userEnd?: 'whiteResign' | 'blackResign' | 'mutualDraw'): { + end: boolean; + result?: string; + reason?: string; + } { + let result = 'draw', + reason = userEnd ?? 'checkmate'; + if (this.chess.isCheckmate()) result = Chops.opposite(this.chess.turn); + else if (this.chess.isInsufficientMaterial()) reason = 'insufficient'; + else if (this.chess.isStalemate()) reason = 'stalemate'; + else if (this.fifty()) reason = 'fifty'; + else if (this.threefold()) reason = 'threefold'; + else if (userEnd) { + if (userEnd !== 'mutualDraw') reason = 'resign'; + if (userEnd === 'whiteResign') result = 'black'; + else if (userEnd === 'blackResign') result = 'white'; + } else return { end: false }; + return { end: true, result, reason }; + } + + doGameOver(result: string, reason: string) { + console.log(`game over ${result} ${reason}`); + + // blah blah do outcome stuff + if (result === 'white') this.totals.white++; + else if (result === 'black') this.totals.black++; + else this.totals.draw++; + $('#white-totals').text(`${this.totals.white} / ${this.totals.draw} / ${this.totals.black}`); + $('#black-totals').text(`${this.totals.black} / ${this.totals.draw} / ${this.totals.white}`); + if (--this.totals.gamesLeft < 1) $('#go').removeClass('disabled'); + else setTimeout(() => this.go()); + } + + jump(path: string) { + path; + /*this.path = path; + this.chess = Chess.fromSetup(Chops.parseFen(path)); + this.fen = makeFen(this.chess.toSetup()); + this.cg.set(this.cgOpts()); + this.fiftyMovePly = 0; + this.threefoldFens.clear(); + this.zf.white?.reset(); + this.zf.black?.reset();*/ + } + + move(uci: Uci, user = false) { + const move = Chops.parseUci(uci); + if (!move || !this.chess.isLegal(move)) + throw new Error(`illegal move ${uci}, ${makeFen(this.chess.toSetup())}}`); + + this.chess.play(move); + this.fen = makeFen(this.chess.toSetup()); + this.fifty(move); + this.threefold('update'); + if (user && this.isPromotion(move)) { + return; // oh noes PromotionCtrl! put it back! put it back! + } else this.updateCgBoard(uci); + const { end, result, reason } = this.checkGameOver(); + if (end) this.doGameOver(result!, reason!); + else this.getBotMove(); + } + + cgUserMove = (orig: Key, dest: Key) => { + this.move(orig + dest, true); + }; + + async getBotMove() { + const moveType = this.players[this.chess.turn]; + if (moveType === 'human') return; + const zf = this.zf[this.chess.turn]; + let move; + if (moveType === 'zero') { + const [zeroMove, lines] = await Promise.all([ + zf!.goZero(this.fen), + zf!.goFish(this.fen, { pvs: 8, depth: 6 }), + ]); + // without randomSprinkle, lc0 will always play the same game + move = Math.random() < 0.5 ? randomSprinkle(zeroMove, lines) : zeroMove; + console.log(`${this.chess.turn} ${zeroMove === move ? 'zero' : 'ZEROFISH'} ${move}`); + } else { + move = (await zf!.goFish(this.fen, { depth: 3 }))[0].moves[0]; + console.log(`${this.chess.turn} fish ${move}`); + } + this.move(move); + } + + updateCgBoard(uci: Uci) { + const { from, to, role } = splitUci(uci); + this.cg.move(from, to); + if (role) this.cg.setPieces(new Map([[to, { color: this.chess.turn, role, promoted: true }]])); + this.cg.set(this.cgOpts(true)); + } + + cgOpts(withFen = true): CgConfig { + return { + fen: withFen ? this.fen : undefined, + orientation: this.flipped ? 'black' : 'white', + turnColor: this.chess.turn, + check: this.chess.isCheck() ? this.chess.turn : false, + movable: { + color: this.chess.turn, + dests: + this.players[this.chess.turn] !== 'human' + ? new Map() + : new Map([...this.chess.allDests()].map(([s, ds]) => [sq2key(s), [...ds].map(sq2key)])), + }, + }; + } + + fifty(move?: Chops.Move) { + if (move) + if ( + !('from' in move) || + this.chess.board.getRole(move.from) === 'pawn' || + this.chess.board.get(move.to) + ) + this.fiftyMovePly = 0; + else this.fiftyMovePly++; + return this.fiftyMovePly >= 100; + } + + threefold(update: 'update' | false = false) { + const boardFen = this.fen.split('-')[0]; + const fenCount = (this.threefoldFens.get(boardFen) ?? 0) + 1; + if (update) this.threefoldFens.set(boardFen, fenCount); + return fenCount >= 3; + } + + isPromotion(move: Chops.Move) { + return ( + 'from' in move && + Chops.squareRank(move.to) === (this.chess.turn === 'white' ? 7 : 0) && + this.chess.board.getRole(move.from) === 'pawn' + ); + } + + flip = () => { + this.flipped = !this.flipped; + this.cg.toggleOrientation(); + this.redraw(); + }; + + dropHandler(color: 'white' | 'black', el: HTMLElement) { + const $el = $(el); + $el.on('dragenter dragover dragleave drop', e => { + e.preventDefault(); + e.stopPropagation(); + }); + $el.on('dragenter dragover', () => this.zf[color] && $el.addClass('hilite')); + $el.on('dragleave drop', () => this.zf[color] && $el.removeClass('hilite')); + $el.on('drop', e => { + if (!this.zf[color]) return; + const reader = new FileReader(); + const weights = e.dataTransfer.files.item(0) as File; + reader.onload = e => { + this.zf[color]!.setZeroWeights(new Uint8Array(e.target!.result as ArrayBuffer)); + $(`#${color} p`).first().text(weights.name); + this.players[color] = 'zero'; + if (this.players[Chops.opposite(color)] !== 'human') $('#go').removeClass('disabled'); + }; + reader.readAsArrayBuffer(weights); + }); + } +} + +function sq2key(sq: number): Key { + return Chops.makeSquare(sq); +} + +function splitUci(uci: Uci): { from: Key; to: Key; role?: Role } { + return { from: uci.slice(0, 2) as Key, to: uci.slice(2, 4) as Key, role: Chops.charToRole(uci.slice(4)) }; +} + +function linesWithin(move: string, lines: PV[], bias = 0, threshold = 50) { + const zeroScore = lines.find(line => line.moves[0] === move)?.score ?? Number.NaN; + return lines.filter(fish => Math.abs(fish.score - bias - zeroScore) < threshold && fish.moves.length); +} + +function randomSprinkle(move: string, lines: PV[]) { + lines = linesWithin(move, lines, 0, 20); + if (!lines.length) return move; + return lines[Math.floor(Math.random() * lines.length)].moves[0] ?? move; +} + +/* +function occurs(chance: number) { + return Math.random() < chance; +}*/ diff --git a/ui/localPlay/src/botVsBot/interfaces.ts b/ui/localPlay/src/botVsBot/interfaces.ts new file mode 100644 index 0000000000000..6fb6c6b5844d2 --- /dev/null +++ b/ui/localPlay/src/botVsBot/interfaces.ts @@ -0,0 +1,2 @@ +export interface LocalPlayOpts {} +export interface Controller {} diff --git a/ui/localPlay/src/botVsBot/main.ts b/ui/localPlay/src/botVsBot/main.ts new file mode 100644 index 0000000000000..7966295a3d86a --- /dev/null +++ b/ui/localPlay/src/botVsBot/main.ts @@ -0,0 +1,28 @@ +import { attributesModule, classModule, init } from 'snabbdom'; +import { Ctrl } from './ctrl'; +import view from './view'; +import { LocalPlayOpts } from './interfaces'; +import menuHover from 'common/menuHover'; +import { Chessground } from 'chessground'; + +const patch = init([classModule, attributesModule]); + +export async function initModule(opts: LocalPlayOpts) { + // make a StrongSocket + const ctrl = new Ctrl(opts, redraw); + + const blueprint = view(ctrl); + const element = document.querySelector('main') as HTMLElement; + element.innerHTML = ''; + let vnode = patch(element, blueprint); + + function redraw() { + vnode = patch(vnode, view(ctrl)); + } + redraw(); + menuHover(); +} + +// that's for the rest of lichess to access chessground +// without having to include it a second time +window.Chessground = Chessground; diff --git a/ui/localPlay/src/botVsBot/view.ts b/ui/localPlay/src/botVsBot/view.ts new file mode 100644 index 0000000000000..ee2ce7801f2db --- /dev/null +++ b/ui/localPlay/src/botVsBot/view.ts @@ -0,0 +1,48 @@ +import { Chessground } from 'chessground'; +import { h, VNode } from 'snabbdom'; +import { makeConfig as makeCgConfig } from './chessground'; +import { onInsert } from 'common/snabbdom'; +import { Ctrl } from './ctrl'; + +export default function render(ctrl: Ctrl): VNode { + return h('div#local-play', [ + h('div.puz-board.main-board', [chessground(ctrl), ctrl.promotion.view()]), + h('div.puz-side', [ + h('div', bot(ctrl, 'black')), + h('div#pgn'), + h('div', [bot(ctrl, 'white'), h('hr'), controls(ctrl)]), + ]), + ]); +} + +function chessground(ctrl: Ctrl): VNode { + return h('div.cg-wrap', { + hook: { + insert: vnode => (ctrl.cg = Chessground(vnode.elm as HTMLElement, makeCgConfig(ctrl))), + }, + }); +} + +function bot(ctrl: Ctrl, color: Color): VNode { + return h(`div#${color}.puz-bot`, { hook: onInsert(el => ctrl.dropHandler(color, el)) }, [ + h('p', 'Drop weights here (otherwise stockfish)'), + h(`p#${color}-totals.totals`), + ]); +} + +function controls(ctrl: Ctrl) { + return h('span', [ + h( + 'button#go.button.disabled', + { + hook: onInsert(el => + el.addEventListener('click', () => ctrl.go(parseInt($('#num-games').val() as string) || 1)) + ), + }, + 'GO' + ), + h('input#num-games', { + attrs: { type: 'number', min: '1', max: '1000', value: '1' }, + }), + ]); +} diff --git a/ui/localPlay/src/bots.ts b/ui/localPlay/src/bots.ts new file mode 100644 index 0000000000000..550e95ab8331a --- /dev/null +++ b/ui/localPlay/src/bots.ts @@ -0,0 +1,22 @@ +export const bots = [ + { + name: 'Coral', + description: 'Coral is a simple bot that plays random moves.', + image: '/assets/images/bots/coral.webp', + }, + { + name: 'Baby Howard', + description: 'Baby Howard is a bot that plays random moves.', + image: '/assets/images/bots/babyHoward.webp', + }, + { + name: 'Baby Bot', + description: 'Baby Bot is a bot that plays random moves.', + image: '/assets/images/bots/babyBot.webp', + }, + { + name: 'Beatrice', + description: 'Beatrice is a bot that plays random moves.', + image: '/assets/images/bots/beatrice.webp', + }, +]; diff --git a/ui/localPlay/src/ctrl.ts b/ui/localPlay/src/ctrl.ts index 4ff85944dbf9a..7bfb98d8948e6 100644 --- a/ui/localPlay/src/ctrl.ts +++ b/ui/localPlay/src/ctrl.ts @@ -1,52 +1,28 @@ import { LocalPlayOpts } from './interfaces'; -import { PromotionCtrl } from 'chess/promotion'; +import { makeRounds } from './data'; import { makeFen /*, parseFen*/ } from 'chessops/fen'; -import { Chess, Role } from 'chessops'; +import { makeSanAndPlay } from 'chessops/san'; +import { Chess } from 'chessops'; import * as Chops from 'chessops'; import makeZerofish, { Zerofish, PV } from 'zerofish'; -import { Api as CgApi } from 'chessground/api'; -import { Config as CgConfig } from 'chessground/config'; -import { Key } from 'chessground/types'; - -type Player = 'human' | 'zero' | 'fish'; +type Tab = string; export class Ctrl { - cg: CgApi; path = ''; chess = Chess.default(); - promotion: PromotionCtrl; - zf: { white?: Zerofish; black?: Zerofish }; - totals: { gamesLeft: number; white: number; black: number; draw: number }; - players: { white: Player; black: Player } = { white: 'human', black: 'fish' }; - + zf: Zerofish | undefined; + round: SocketSend; fen = ''; - flipped = false; fiftyMovePly = 0; threefoldFens: Map = new Map(); constructor(readonly opts: LocalPlayOpts, readonly redraw: () => void) { - this.promotion = new PromotionCtrl( - f => f(this.cg), - () => this.cg.set(this.cgOpts()), - this.redraw - ); - Promise.all([makeZerofish(), makeZerofish()]).then(([wz, bz]) => { - this.zf ??= { white: wz, black: bz }; + makeZerofish().then(zf => { + this.zf = zf; + // fetch model as arrraybuffer and + //this.zf!.setZeroWeights(new Uint8Array()); }); - } - - go(numGames?: number) { - this.totals ??= { gamesLeft: 1, white: 0, black: 0, draw: 0 }; - if (numGames) this.totals.gamesLeft = numGames; - this.fiftyMovePly = 0; - this.threefoldFens.clear(); - this.chess.reset(); - this.fen = makeFen(this.chess.toSetup()); - this.cg.set({ fen: this.fen }); - this.zf.white?.reset(); - this.zf.black?.reset(); - this.getBotMove(); - $('#go').addClass('disabled'); + makeRounds(this).then(round => (this.round = round)); } checkGameOver(userEnd?: 'whiteResign' | 'blackResign' | 'mutualDraw'): { @@ -73,89 +49,43 @@ export class Ctrl { console.log(`game over ${result} ${reason}`); // blah blah do outcome stuff - if (result === 'white') this.totals.white++; - else if (result === 'black') this.totals.black++; - else this.totals.draw++; - $('#white-totals').text(`${this.totals.white} / ${this.totals.draw} / ${this.totals.black}`); - $('#black-totals').text(`${this.totals.black} / ${this.totals.draw} / ${this.totals.white}`); - if (--this.totals.gamesLeft < 1) $('#go').removeClass('disabled'); - else setTimeout(() => this.go()); } - jump(path: string) { - path; - /*this.path = path; - this.chess = Chess.fromSetup(Chops.parseFen(path)); - this.fen = makeFen(this.chess.toSetup()); - this.cg.set(this.cgOpts()); - this.fiftyMovePly = 0; - this.threefoldFens.clear(); - this.zf.white?.reset(); - this.zf.black?.reset();*/ - } - - move(uci: Uci, user = false) { + move(uci: Uci) { const move = Chops.parseUci(uci); if (!move || !this.chess.isLegal(move)) throw new Error(`illegal move ${uci}, ${makeFen(this.chess.toSetup())}}`); - - this.chess.play(move); + console.log( + `before - turn ${this.chess.turn}, half ${this.chess.halfmoves}, full ${this.chess.fullmoves}, fen '${this.fen}'` + ); + const san = makeSanAndPlay(this.chess, move); + console.log(this.chess.fullmoves); this.fen = makeFen(this.chess.toSetup()); + console.log( + `after - turn ${this.chess.turn}, half ${this.chess.halfmoves}, full ${this.chess.fullmoves}, fen '${this.fen}'` + ); this.fifty(move); this.threefold('update'); - if (user && this.isPromotion(move)) { - return; // oh noes PromotionCtrl! put it back! put it back! - } else this.updateCgBoard(uci); const { end, result, reason } = this.checkGameOver(); if (end) this.doGameOver(result!, reason!); - else this.getBotMove(); - } - - cgUserMove = (orig: Key, dest: Key) => { - this.move(orig + dest, true); - }; - async getBotMove() { - const moveType = this.players[this.chess.turn]; - if (moveType === 'human') return; - const zf = this.zf[this.chess.turn]; - let move; - if (moveType === 'zero') { - const [zeroMove, lines] = await Promise.all([ - zf!.goZero(this.fen), - zf!.goFish(this.fen, { pvs: 8, depth: 6 }), - ]); - // without randomSprinkle, lc0 will always play the same game - move = Math.random() < 0.5 ? randomSprinkle(zeroMove, lines) : zeroMove; - console.log(`${this.chess.turn} ${zeroMove === move ? 'zero' : 'ZEROFISH'} ${move}`); - } else { - move = (await zf!.goFish(this.fen, { depth: 3 }))[0].moves[0]; - console.log(`${this.chess.turn} fish ${move}`); - } - this.move(move); + this.round('move', { + uci, + fen: this.fen, + ply: 2 * (this.chess.fullmoves - 1) + (this.chess.turn === 'black' ? 1 : 0), + dests: this.dests, + san, + }); } - updateCgBoard(uci: Uci) { - const { from, to, role } = splitUci(uci); - this.cg.move(from, to); - if (role) this.cg.setPieces(new Map([[to, { color: this.chess.turn, role, promoted: true }]])); - this.cg.set(this.cgOpts(true)); + userMove(uci: Uci) { + this.move(uci); + this.getBotMove(); } - cgOpts(withFen = true): CgConfig { - return { - fen: withFen ? this.fen : undefined, - orientation: this.flipped ? 'black' : 'white', - turnColor: this.chess.turn, - check: this.chess.isCheck() ? this.chess.turn : false, - movable: { - color: this.chess.turn, - dests: - this.players[this.chess.turn] !== 'human' - ? new Map() - : new Map([...this.chess.allDests()].map(([s, ds]) => [sq2key(s), [...ds].map(sq2key)])), - }, - }; + async getBotMove() { + const uci = (await this.zf!.goFish(this.fen, { depth: 10 }))[0].moves[0]; + this.move(uci); } fifty(move?: Chops.Move) { @@ -185,43 +115,15 @@ export class Ctrl { ); } - flip = () => { - this.flipped = !this.flipped; - this.cg.toggleOrientation(); - this.redraw(); - }; - - dropHandler(color: 'white' | 'black', el: HTMLElement) { - const $el = $(el); - $el.on('dragenter dragover dragleave drop', e => { - e.preventDefault(); - e.stopPropagation(); - }); - $el.on('dragenter dragover', () => this.zf[color] && $el.addClass('hilite')); - $el.on('dragleave drop', () => this.zf[color] && $el.removeClass('hilite')); - $el.on('drop', e => { - if (!this.zf[color]) return; - const reader = new FileReader(); - const weights = e.dataTransfer.files.item(0) as File; - reader.onload = e => { - this.zf[color]!.setZeroWeights(new Uint8Array(e.target!.result as ArrayBuffer)); - $(`#${color} p`).first().text(weights.name); - this.players[color] = 'zero'; - if (this.players[Chops.opposite(color)] !== 'human') $('#go').removeClass('disabled'); - }; - reader.readAsArrayBuffer(weights); - }); + get dests() { + const dests: { [from: string]: string } = {}; + [...this.chess.allDests()] + .filter(([, to]) => !to.isEmpty()) + .forEach(([s, ds]) => (dests[Chops.makeSquare(s)] = [...ds].map(Chops.makeSquare).join(''))); + return dests; } } -function sq2key(sq: number): Key { - return Chops.makeSquare(sq); -} - -function splitUci(uci: Uci): { from: Key; to: Key; role?: Role } { - return { from: uci.slice(0, 2) as Key, to: uci.slice(2, 4) as Key, role: Chops.charToRole(uci.slice(4)) }; -} - function linesWithin(move: string, lines: PV[], bias = 0, threshold = 50) { const zeroScore = lines.find(line => line.moves[0] === move)?.score ?? Number.NaN; return lines.filter(fish => Math.abs(fish.score - bias - zeroScore) < threshold && fish.moves.length); diff --git a/ui/localPlay/src/data.ts b/ui/localPlay/src/data.ts index 3e986b865c9f9..a22ddfc47c05a 100644 --- a/ui/localPlay/src/data.ts +++ b/ui/localPlay/src/data.ts @@ -1,4 +1,5 @@ -import { /*RoundOpts,*/ RoundData } from 'round'; +import { RoundOpts, RoundData } from 'round'; +import { Ctrl } from './ctrl'; //import { Player, GameData } from 'game'; /*interface RoundApi { @@ -10,8 +11,8 @@ const data: RoundData = { game: { id: 'x7hgwoir', variant: { key: 'standard', name: 'Standard', short: 'Std' }, - speed: 'correspondence', - perf: 'correspondence', + speed: 'classical', + perf: 'classical', rated: false, fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1', turns: 0, @@ -38,16 +39,17 @@ const data: RoundData = { color: 'black', user: { id: 'anonymous', - username: 'Anonymous', + username: 'Baby Howard', online: true, perfs: {}, }, id: '', isGone: false, - name: 'Anonymous', + name: 'Baby Howard', onGame: true, - rating: 1500, + rating: 800, version: 0, + image: '/assets/images/bots/baby-howard.webp', }, pref: { animationDuration: 300, @@ -67,51 +69,41 @@ const data: RoundData = { showCaptured: true, blindfold: false, is3d: false, - keyboardMove: true, - voiceMove: true, + keyboardMove: false, + voiceMove: false, ratings: true, submitMove: false, }, steps: [{ ply: 0, san: '', uci: '', fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1' }], - correspondence: { + /*correspondence: { daysPerTurn: 2, increment: 0, white: 0, black: 0, showBar: true, - }, + },*/ takebackable: true, moretimeable: true, }; -gah(); -function gah() { - const socketUrl = /*opts.data.player.spectator - ? `/watch/${data.game.id}/${data.player.color}/v6` - :*/ `/play/${data.game.id}${data.player.id}/v6`; - lichess.socket = new lichess.StrongSocket(socketUrl, data.player.version, { - params: { userTv: false }, - receive(t: string, d: any) { - t, d; - //round.socketReceive(t, d); - }, - events: {}, - }); - if (location.pathname.lastIndexOf('/round-next/', 0) === 0) history.replaceState(null, '', '/' + data.game.id); - $('#zentog').on('click', () => lichess.pubsub.emit('zen')); - lichess.storage.make('reload-round-tabs').listen(lichess.reload); - - if (!data.player.spectator && location.hostname != (document as any)['Location'.toLowerCase()].hostname) { - alert(`Games cannot be played through a web proxy. Please use ${location.hostname} instead.`); - lichess.socket.destroy(); +export async function makeRounds(ctrl: Ctrl): Promise { + const moves: string[] = []; + console.log(ctrl.dests); + for (const from in ctrl.dests) { + moves.push(from + ctrl.dests[from]); } -} - -export function makeRounds() { - const opts /*: RoundOpts*/ = { + const opts: RoundOpts = { element: document.querySelector('.round__app') as HTMLElement, - data, - socketSend: lichess.socket.send, + data: { ...data, possibleMoves: moves.join(' ') }, + socketSend: (t: string, d: any) => { + if (t === 'move') { + ctrl.userMove(d.u); + } + }, + crosstableEl: document.querySelector('.cross-table') as HTMLElement, + i18n: {}, + onChange: (d: RoundData) => console.log(d), + local: true, }; - lichess.loadEsm('round', { init: opts }); + return lichess.loadEsm('round', { init: opts }); } diff --git a/ui/localPlay/src/main.ts b/ui/localPlay/src/main.ts index 7966295a3d86a..9f7518b5bf730 100644 --- a/ui/localPlay/src/main.ts +++ b/ui/localPlay/src/main.ts @@ -2,17 +2,15 @@ import { attributesModule, classModule, init } from 'snabbdom'; import { Ctrl } from './ctrl'; import view from './view'; import { LocalPlayOpts } from './interfaces'; -import menuHover from 'common/menuHover'; -import { Chessground } from 'chessground'; const patch = init([classModule, attributesModule]); export async function initModule(opts: LocalPlayOpts) { // make a StrongSocket - const ctrl = new Ctrl(opts, redraw); - + const ctrl = new Ctrl(opts, () => {}); + ctrl; const blueprint = view(ctrl); - const element = document.querySelector('main') as HTMLElement; + const element = document.querySelector('#bot-view') as HTMLElement; element.innerHTML = ''; let vnode = patch(element, blueprint); @@ -20,9 +18,4 @@ export async function initModule(opts: LocalPlayOpts) { vnode = patch(vnode, view(ctrl)); } redraw(); - menuHover(); } - -// that's for the rest of lichess to access chessground -// without having to include it a second time -window.Chessground = Chessground; diff --git a/ui/localPlay/src/socket.ts b/ui/localPlay/src/socket.ts deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/ui/localPlay/src/view.ts b/ui/localPlay/src/view.ts index ee2ce7801f2db..8a395b01f3204 100644 --- a/ui/localPlay/src/view.ts +++ b/ui/localPlay/src/view.ts @@ -1,48 +1,26 @@ -import { Chessground } from 'chessground'; import { h, VNode } from 'snabbdom'; -import { makeConfig as makeCgConfig } from './chessground'; -import { onInsert } from 'common/snabbdom'; +//import * as licon from 'common/licon'; +//import { bind } from 'common/snabbdom'; +import { bots } from './bots'; import { Ctrl } from './ctrl'; -export default function render(ctrl: Ctrl): VNode { - return h('div#local-play', [ - h('div.puz-board.main-board', [chessground(ctrl), ctrl.promotion.view()]), - h('div.puz-side', [ - h('div', bot(ctrl, 'black')), - h('div#pgn'), - h('div', [bot(ctrl, 'white'), h('hr'), controls(ctrl)]), - ]), - ]); -} - -function chessground(ctrl: Ctrl): VNode { - return h('div.cg-wrap', { - hook: { - insert: vnode => (ctrl.cg = Chessground(vnode.elm as HTMLElement, makeCgConfig(ctrl))), - }, - }); -} - -function bot(ctrl: Ctrl, color: Color): VNode { - return h(`div#${color}.puz-bot`, { hook: onInsert(el => ctrl.dropHandler(color, el)) }, [ - h('p', 'Drop weights here (otherwise stockfish)'), - h(`p#${color}-totals.totals`), +export default function (ctrl: Ctrl): VNode { + return h('section#bot-view', {}, [ + h('div#bot-tabs', { attrs: { role: 'tablist' } }), + h( + 'div#bot-content', + h( + 'div#bot-list', + bots.map(bot => botView(ctrl, bot)) + ) + ), ]); } -function controls(ctrl: Ctrl) { - return h('span', [ - h( - 'button#go.button.disabled', - { - hook: onInsert(el => - el.addEventListener('click', () => ctrl.go(parseInt($('#num-games').val() as string) || 1)) - ), - }, - 'GO' - ), - h('input#num-games', { - attrs: { type: 'number', min: '1', max: '1000', value: '1' }, - }), +function botView(ctrl: Ctrl, bot: any): VNode { + return h('div.fancy-bot', [ + h('h1', bot.name), + h('p', bot.description), + h('img', { attrs: { src: bot.image } }), ]); } diff --git a/ui/round/src/ctrl.ts b/ui/round/src/ctrl.ts index c9c58e6b13396..204250155f1a7 100644 --- a/ui/round/src/ctrl.ts +++ b/ui/round/src/ctrl.ts @@ -76,7 +76,7 @@ export default class RoundController { loading = false; loadingTimeout: number; redirecting = false; - transientMove: TransientMove; + transientMove?: TransientMove; moveToSubmit?: SocketMove; dropToSubmit?: SocketDrop; goneBerserk: GoneBerserk = {}; @@ -106,7 +106,7 @@ export default class RoundController { this.firstSeconds = false; this.redraw(); }, 3000); - + if (!opts.local) lichess.socket.sign(this.sign); this.socket = makeSocket(opts.socketSend, this); if (d.clock) @@ -133,7 +133,7 @@ export default class RoundController { this.setQuietMode(); this.moveOn = new MoveOn(this, 'move-on'); - this.transientMove = new TransientMove(this.socket); + if (!opts.local) this.transientMove = new TransientMove(this.socket); this.menu = toggle(false, redraw); @@ -331,7 +331,7 @@ export default class RoundController { this.justDropped = meta.justDropped; this.justCaptured = meta.justCaptured; this.preDrop = undefined; - this.transientMove.register(); + this.transientMove?.register(); this.redraw(); }; @@ -478,7 +478,7 @@ export default class RoundController { } this.redraw(); if (playing && playedColor == d.player.color) { - this.transientMove.clear(); + this.transientMove?.clear(); this.moveOn.next(); cevalSub.publish(d, o); } @@ -525,7 +525,7 @@ export default class RoundController { this.clearJust(); this.shouldSendMoveTime = false; if (this.clock) this.clock.setClock(d, d.clock!.white, d.clock!.black); - if (this.corresClock) this.corresClock.update(d.correspondence.white, d.correspondence.black); + if (this.corresClock) this.corresClock.update(d.correspondence!.white, d.correspondence!.black); if (!this.replaying()) ground.reload(this); this.setTitle(); this.moveOn.next(); diff --git a/ui/round/src/interfaces.ts b/ui/round/src/interfaces.ts index 72b2e627d7f47..db03ec43d3a86 100644 --- a/ui/round/src/interfaces.ts +++ b/ui/round/src/interfaces.ts @@ -52,7 +52,7 @@ export interface RoundData extends GameData { possibleDrops?: string; forecastCount?: number; crazyhouse?: CrazyData; - correspondence: CorresClockData; + correspondence?: CorresClockData; tv?: Tv; userTv?: { id: string; diff --git a/ui/round/src/main.ts b/ui/round/src/main.ts index c7765625e0587..274bfadb362b8 100644 --- a/ui/round/src/main.ts +++ b/ui/round/src/main.ts @@ -8,14 +8,15 @@ import { RoundOpts, NvuiPlugin } from './interfaces'; const patch = init([classModule, attributesModule]); -export function initModule(opts: RoundOpts) { - if (opts.local) app(opts); +export function initModule(opts: RoundOpts): SocketSend | undefined { + if (opts.local) return app(opts).socketReceive; else boot(opts, app); + return undefined; } function app(opts: RoundOpts, nvui?: NvuiPlugin) { const ctrl = new RoundController(opts, redraw, nvui); - + console.log('main', opts.element); const blueprint = view(ctrl); opts.element.innerHTML = ''; let vnode = patch(opts.element, blueprint); diff --git a/ui/round/src/socket.ts b/ui/round/src/socket.ts index 19cb05e47ed56..20f8a94a48269 100644 --- a/ui/round/src/socket.ts +++ b/ui/round/src/socket.ts @@ -47,7 +47,7 @@ function backoff(delay: number, factor: number, callback: Callback): Callback { } export function make(send: SocketSend, ctrl: RoundController): RoundSocket { - lichess.socket.sign(ctrl.sign); + //lichess.socket.sign(ctrl.sign); // doing this in ctrl constructor const reload = (o?: Incoming, isRetry?: boolean) => { // avoid reload if possible! @@ -84,8 +84,8 @@ export function make(send: SocketSend, ctrl: RoundController): RoundSocket { }, cclock(o: { white: number; black: number }) { if (ctrl.corresClock) { - ctrl.data.correspondence.white = o.white; - ctrl.data.correspondence.black = o.black; + ctrl.data.correspondence!.white = o.white; + ctrl.data.correspondence!.black = o.black; ctrl.corresClock.update(o.white, o.black); ctrl.redraw(); } diff --git a/ui/round/src/view/table.ts b/ui/round/src/view/table.ts index 06d9eb3a2693f..21254e50056a7 100644 --- a/ui/round/src/view/table.ts +++ b/ui/round/src/view/table.ts @@ -17,6 +17,8 @@ function renderPlayer(ctrl: RoundController, position: Position) { const player = ctrl.playerAt(position); return ctrl.nvui ? undefined + : player.image + ? renderUser.botHtml(ctrl, player, position) : player.ai ? h('div.user-link.online.ruser.ruser-' + position, [ h('i.line'), diff --git a/ui/round/src/view/user.ts b/ui/round/src/view/user.ts index 4ae9343f519bf..5329fb8eb59b5 100644 --- a/ui/round/src/view/user.ts +++ b/ui/round/src/view/user.ts @@ -7,6 +7,23 @@ import RoundController from '../ctrl'; export const aiName = (ctrl: RoundController, level: number) => ctrl.trans('aiNameLevelAiLevel', 'Stockfish', level); +export function botHtml(ctrl: RoundController, player: Player, position: Position) { + ctrl; + return h( + `div.ruser-${position}.ruser.fancy-bot`, + { + class: { + online: true, + offline: false, + }, + }, + [ + h('span', [h('img', { attrs: { src: player.image!, width: 48, height: 48 } }), h('name', player.name)]), + h('rating', player.rating), + //h('rating', player.ratingDiff), + ] + ); +} export function userHtml(ctrl: RoundController, player: Player, position: Position) { const d = ctrl.data, user = player.user, From 6b09faae63138ca14146371c86ebecd9671ee9fd Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Mon, 7 Aug 2023 16:53:34 -0500 Subject: [PATCH 017/174] . --- app/controllers/LocalPlay.scala | 4 + app/views/localPlay/botVsBot.scala | 8 +- app/views/localPlay/vsBot.scala | 14 +-- public/font/lichess.sfd | 65 +++++----- ui/localPlay/css/_bot-vs-bot.scss | 116 ++++++++++++++++++ .../css/{_local-play.scss => _vs-bot.scss} | 39 ------ .../{_local-play.scss => _bot-vs-bot.scss} | 3 +- ui/localPlay/css/build/_vs-bot.scss | 11 ++ ...ltr.dark.scss => bot-vs-bot.ltr.dark.scss} | 2 +- ...r.light.scss => bot-vs-bot.ltr.light.scss} | 2 +- ...transp.scss => bot-vs-bot.ltr.transp.scss} | 2 +- ...rtl.dark.scss => bot-vs-bot.rtl.dark.scss} | 2 +- ...l.light.scss => bot-vs-bot.rtl.light.scss} | 2 +- ...transp.scss => bot-vs-bot.rtl.transp.scss} | 2 +- ui/localPlay/css/build/vs-bot.ltr.dark.scss | 3 + ui/localPlay/css/build/vs-bot.ltr.light.scss | 3 + ui/localPlay/css/build/vs-bot.ltr.transp.scss | 3 + ui/localPlay/css/build/vs-bot.rtl.dark.scss | 3 + ui/localPlay/css/build/vs-bot.rtl.light.scss | 3 + ui/localPlay/css/build/vs-bot.rtl.transp.scss | 3 + .../{chessground.ts => bvbChessground.ts} | 4 +- .../src/botVsBot/{ctrl.ts => bvbCtrl.ts} | 6 +- ui/localPlay/src/botVsBot/bvbInterfaces.ts | 1 + .../src/botVsBot/{main.ts => bvbMain.ts} | 17 ++- .../src/botVsBot/{view.ts => bvbView.ts} | 12 +- ui/localPlay/src/botVsBot/interfaces.ts | 2 - ui/localPlay/src/interfaces.ts | 7 +- ui/localPlay/src/main.ts | 5 +- ui/localPlay/tsconfig.json | 2 +- ui/round/src/view/user.ts | 3 +- 30 files changed, 228 insertions(+), 121 deletions(-) create mode 100644 ui/localPlay/css/_bot-vs-bot.scss rename ui/localPlay/css/{_local-play.scss => _vs-bot.scss} (55%) rename ui/localPlay/css/build/{_local-play.scss => _bot-vs-bot.scss} (87%) create mode 100644 ui/localPlay/css/build/_vs-bot.scss rename ui/localPlay/css/build/{local-play.ltr.dark.scss => bot-vs-bot.ltr.dark.scss} (78%) rename ui/localPlay/css/build/{local-play.ltr.light.scss => bot-vs-bot.ltr.light.scss} (78%) rename ui/localPlay/css/build/{local-play.ltr.transp.scss => bot-vs-bot.ltr.transp.scss} (79%) rename ui/localPlay/css/build/{local-play.rtl.dark.scss => bot-vs-bot.rtl.dark.scss} (78%) rename ui/localPlay/css/build/{local-play.rtl.light.scss => bot-vs-bot.rtl.light.scss} (78%) rename ui/localPlay/css/build/{local-play.rtl.transp.scss => bot-vs-bot.rtl.transp.scss} (79%) create mode 100644 ui/localPlay/css/build/vs-bot.ltr.dark.scss create mode 100644 ui/localPlay/css/build/vs-bot.ltr.light.scss create mode 100644 ui/localPlay/css/build/vs-bot.ltr.transp.scss create mode 100644 ui/localPlay/css/build/vs-bot.rtl.dark.scss create mode 100644 ui/localPlay/css/build/vs-bot.rtl.light.scss create mode 100644 ui/localPlay/css/build/vs-bot.rtl.transp.scss rename ui/localPlay/src/botVsBot/{chessground.ts => bvbChessground.ts} (94%) rename ui/localPlay/src/botVsBot/{ctrl.ts => bvbCtrl.ts} (98%) create mode 100644 ui/localPlay/src/botVsBot/bvbInterfaces.ts rename ui/localPlay/src/botVsBot/{main.ts => bvbMain.ts} (57%) rename ui/localPlay/src/botVsBot/{view.ts => bvbView.ts} (78%) delete mode 100644 ui/localPlay/src/botVsBot/interfaces.ts diff --git a/app/controllers/LocalPlay.scala b/app/controllers/LocalPlay.scala index 4a0e60a12d69b..ea988bd5ce46a 100644 --- a/app/controllers/LocalPlay.scala +++ b/app/controllers/LocalPlay.scala @@ -21,3 +21,7 @@ final class LocalPlay(env: Env) extends LilaController(env): def botVsBot = Open: NoBot: Ok.page(views.html.localPlay.botVsBot.index).map(_.enableSharedArrayBuffer) + + def setup = Open: + NoBot: + Ok diff --git a/app/views/localPlay/botVsBot.scala b/app/views/localPlay/botVsBot.scala index a5a533b59cdfb..eacb58c33dc34 100644 --- a/app/views/localPlay/botVsBot.scala +++ b/app/views/localPlay/botVsBot.scala @@ -12,13 +12,13 @@ object botVsBot: def index(using ctx: PageContext) = views.html.base.layout( title = "Play vs Bots", - moreJs = jsModuleInit("localPlay"), - moreCss = cssTag("local-play"), + moreJs = jsModuleInit("localPlay", Json.obj("mode" -> "botVsBot")), + moreCss = cssTag("bot-vs-bot"), csp = analysisCsp.some, openGraph = lila.app.ui .OpenGraph( - title = "Play vs Bots", - description = "Play vs Bots", + title = "Bots vs Bots", + description = "Bots vs Bots", url = s"$netBaseUrl${controllers.routes.LocalPlay.botVsBot}" ) .some, diff --git a/app/views/localPlay/vsBot.scala b/app/views/localPlay/vsBot.scala index 02e904f646e1e..40be6c6aefec6 100644 --- a/app/views/localPlay/vsBot.scala +++ b/app/views/localPlay/vsBot.scala @@ -20,14 +20,15 @@ object vsBot: jsModuleInit( "localPlay", Json.obj( - "pref" -> lila.pref.JsonView.write(ctx.pref, false), - "i18n" -> i18n, - "userId" -> ctx.userId + "mode" -> "vsBot", + "pref" -> lila.pref.JsonView.write(ctx.pref, false), + "i18n" -> i18n ) - ) + ), + jsModule("round") ), moreCss = frag( - cssTag("local-play"), + cssTag("vs-bot"), cssTag("round"), ctx.pref.hasKeyboardMove option cssTag("keyboardMove"), ctx.pref.hasVoice option cssTag("voice") @@ -47,9 +48,6 @@ object vsBot: main(cls := "round")( st.aside(cls := "round__side")( st.section(id := "bot-view")( - div(id := "bot-tabs")( - div(cls := "bot-tab")(nbsp) - ), div(id := "bot-content") ) ), diff --git a/public/font/lichess.sfd b/public/font/lichess.sfd index 8ac7fa320a2be..eeee4347b6728 100644 --- a/public/font/lichess.sfd +++ b/public/font/lichess.sfd @@ -21,7 +21,7 @@ OS2Version: 3 OS2_WeightWidthSlopeOnly: 0 OS2_UseTypoMetrics: 0 CreationTime: 1554434404 -ModificationTime: 1688492729 +ModificationTime: 1691445074 PfmFamily: 17 TTFWeight: 400 TTFWidth: 5 @@ -87,7 +87,7 @@ NameList: AGL For New Fonts DisplaySize: -128 AntiAlias: 1 FitToEm: 0 -WinInfo: 57393 9 8 +WinInfo: 57366 9 8 BeginChars: 65539 122 StartChar: .notdef @@ -2711,41 +2711,40 @@ StartChar: x Encoding: 57407 57407 44 Width: 512 GlyphClass: 2 -Flags: W +Flags: WO LayerCount: 2 Fore SplineSet -426 134 m 0,0,1 - 426 123 426 123 418 115 c 2,2,-1 - 379 76 l 2,3,4 - 371 68 371 68 359 68 c 0,5,6 - 348 68 348 68 340 76 c 2,7,-1 - 256 160 l 1,8,-1 - 172 76 l 2,9,10 - 164 68 164 68 153 68 c 0,11,12 - 141 68 141 68 133 76 c 2,13,-1 - 94 115 l 2,14,15 - 86 123 86 123 86 134 c 0,16,17 - 86 144 86 144 94 154 c 2,18,-1 - 178 238 l 1,19,-1 - 94 322 l 2,20,21 - 86 330 86 330 86 341 c 0,22,23 - 86 351 86 351 94 361 c 2,24,-1 - 133 399 l 2,25,26 - 140 407 140 407 153 407 c 0,27,28 - 165 407 165 407 172 399 c 2,29,-1 - 256 315 l 1,30,-1 - 340 399 l 2,31,32 - 347 407 347 407 359 407 c 0,33,34 - 372 407 372 407 379 399 c 2,35,-1 - 418 361 l 2,36,37 - 426 351 426 351 426 341 c 0,38,39 - 426 330 426 330 418 322 c 2,40,-1 - 334 238 l 1,41,-1 - 418 154 l 2,42,43 - 426 144 426 144 426 134 c 0,0,1 +426 152 m 4,0,1 + 426 141 426 141 418 133 c 6,2,-1 + 379 94 l 6,3,4 + 371 86 371 86 359 86 c 4,5,6 + 348 86 348 86 340 94 c 6,7,-1 + 256 178 l 5,8,-1 + 172 94 l 6,9,10 + 164 86 164 86 153 86 c 4,11,12 + 141 86 141 86 133 94 c 6,13,-1 + 94 133 l 6,14,15 + 86 141 86 141 86 152 c 4,16,17 + 86 162 86 162 94 172 c 6,18,-1 + 178 256 l 5,19,-1 + 94 340 l 6,20,21 + 86 348 86 348 86 359 c 4,22,23 + 86 369 86 369 94 379 c 6,24,-1 + 133 417 l 6,25,26 + 140 425 140 425 153 425 c 4,27,28 + 165 425 165 425 172 417 c 6,29,-1 + 256 333 l 5,30,-1 + 340 417 l 6,31,32 + 347 425 347 425 359 425 c 4,33,34 + 372 425 372 425 379 417 c 6,35,-1 + 418 379 l 6,36,37 + 426 369 426 369 426 359 c 4,38,39 + 426 348 426 348 418 340 c 6,40,-1 + 334 256 l 5,41,-1 + 418 172 l 6,42,43 + 426 162 426 162 426 152 c 4,0,1 EndSplineSet -Validated: 513 EndChar StartChar: arrow-down-right diff --git a/ui/localPlay/css/_bot-vs-bot.scss b/ui/localPlay/css/_bot-vs-bot.scss new file mode 100644 index 0000000000000..6872bb765a33c --- /dev/null +++ b/ui/localPlay/css/_bot-vs-bot.scss @@ -0,0 +1,116 @@ +$mq-col2: $mq-col2-uniboard; + +#local-play { + display: grid; + + grid-row-gap: $block-gap; + grid-column-gap: $block-gap; + grid-template-areas: 'board' 'side'; + + @include breakpoint($mq-col2) { + grid-template-columns: var(--col2-uniboard-width) auto; + grid-template-rows: fit-content(0); + grid-template-areas: 'board side'; + } + + .about__link { + margin-top: 2vh; + text-align: center; + font-size: 0.8em; + } + #moves { + flex: 2 1 0; + display: flex; + flex-direction: column; + justify-content: space-between; + + // 0 size forces vertical scrollbar + overflow-y: auto; + overflow-x: hidden; + + // else a scrollbar appears sometimes + border-top: $border; + position: relative; + + /* required so line::before scrolls along the moves! */ + .result, + .status { + background: $c-bg-zebra; + text-align: center; + } + + .result { + border-top: $border; + font-weight: bold; + font-size: 1.2em; + padding: 5px 0 3px 0; + } + + .status { + font-size: 1em; + font-style: italic; + padding-bottom: 7px; + } + + button.next { + border: 0; + background: $c-bg-box; + color: $c-link; + padding: 0.5em; + width: 100%; + + @include transition; + + &:hover { + color: $c-link-hover; + } + + &::before { + margin-#{$end-direction}: 0.3em; + } + + &.highlighted { + background: mix($c-primary, $c-bg-box, 80%); + color: $c-primary-over; + + &:hover { + background: $c-primary; + } + } + } + } + + .puz-side { + .puz-bot { + @extend %flex-column; + align-items: center; + width: 300px; + height: 100px; + border: 2px dashed #888; + } + .totals { + font-size: xx-large; + text-align: center; + } + + .puz-bot.hilite { + background-color: #888; + } + #pgn { + height: 400px; + } + #num-games { + width: 120px; + } + span { + @extend %flex-between; + flex-wrap: nowrap; + * { + margin: 0 15px; + } + } + #go { + flex: auto; + } + } +} diff --git a/ui/localPlay/css/_local-play.scss b/ui/localPlay/css/_vs-bot.scss similarity index 55% rename from ui/localPlay/css/_local-play.scss rename to ui/localPlay/css/_vs-bot.scss index c565974de576f..e656339ba0dc7 100644 --- a/ui/localPlay/css/_local-play.scss +++ b/ui/localPlay/css/_vs-bot.scss @@ -1,52 +1,13 @@ -$mq-col2: $mq-col2-uniboard; - #bot-view { display: flex; flex-flow: column nowrap; - #bot-tabs { - flex: 0 0 auto; - display: flex; - } - - .bot-tab { - @extend %roboto, %nowrap-hidden, %box-radius-top; - - flex: 1 1 auto; - text-align: center; - padding: 0.4em 10px; - - cursor: pointer; - color: $c-font-page; - - @include transition; - - &:hover { - background: mix($c-accent, $c-bg-box, 15%); - } - - &-active { - color: $c-font; - background: $c-bg-box !important; - } - - span { - @extend %nowrap-ellipsis; - } - - &:last-child { - border-#{$end-direction}: none; - } - } #bot-content { flex: 1 1 auto; overflow: hidden; } } -#bot-list { -} - .fancy-bot { display: flex; align-items: center; diff --git a/ui/localPlay/css/build/_local-play.scss b/ui/localPlay/css/build/_bot-vs-bot.scss similarity index 87% rename from ui/localPlay/css/build/_local-play.scss rename to ui/localPlay/css/build/_bot-vs-bot.scss index 95ca68f206df0..9fadc86b3c0e5 100644 --- a/ui/localPlay/css/build/_local-play.scss +++ b/ui/localPlay/css/build/_bot-vs-bot.scss @@ -6,6 +6,5 @@ @import '../../../common/css/component/fbt'; @import '../../../common/css/vendor/chessground/coords'; @import '../../../chess/css/promotion'; -@import '../../../puz/css/puz'; -@import '../local-play'; +@import '../bot-vs-bot'; diff --git a/ui/localPlay/css/build/_vs-bot.scss b/ui/localPlay/css/build/_vs-bot.scss new file mode 100644 index 0000000000000..3210e197359a9 --- /dev/null +++ b/ui/localPlay/css/build/_vs-bot.scss @@ -0,0 +1,11 @@ +@import '../../../common/css/plugin'; +@import '../../../common/css/layout/uniboard'; +// @import '../../../common/css/component/board-resize'; +// @import '../../../common/css/component/bar-glider'; +// @import '../../../common/css/component/zen-toggle'; +// @import '../../../common/css/component/fbt'; +// @import '../../../common/css/vendor/chessground/coords'; +// @import '../../../chess/css/promotion'; +// @import '../../../puz/css/puz'; + +@import '../vs-bot'; diff --git a/ui/localPlay/css/build/local-play.ltr.dark.scss b/ui/localPlay/css/build/bot-vs-bot.ltr.dark.scss similarity index 78% rename from ui/localPlay/css/build/local-play.ltr.dark.scss rename to ui/localPlay/css/build/bot-vs-bot.ltr.dark.scss index 0234b74a7a162..ae227dc61cf4b 100644 --- a/ui/localPlay/css/build/local-play.ltr.dark.scss +++ b/ui/localPlay/css/build/bot-vs-bot.ltr.dark.scss @@ -1,3 +1,3 @@ @import '../../../common/css/dir/ltr'; @import '../../../common/css/theme/dark'; -@import 'local-play'; +@import 'bot-vs-bot'; diff --git a/ui/localPlay/css/build/local-play.ltr.light.scss b/ui/localPlay/css/build/bot-vs-bot.ltr.light.scss similarity index 78% rename from ui/localPlay/css/build/local-play.ltr.light.scss rename to ui/localPlay/css/build/bot-vs-bot.ltr.light.scss index b128e239a7df0..519a950bc4562 100644 --- a/ui/localPlay/css/build/local-play.ltr.light.scss +++ b/ui/localPlay/css/build/bot-vs-bot.ltr.light.scss @@ -1,3 +1,3 @@ @import '../../../common/css/dir/ltr'; @import '../../../common/css/theme/light'; -@import 'local-play'; +@import 'bot-vs-bot'; diff --git a/ui/localPlay/css/build/local-play.ltr.transp.scss b/ui/localPlay/css/build/bot-vs-bot.ltr.transp.scss similarity index 79% rename from ui/localPlay/css/build/local-play.ltr.transp.scss rename to ui/localPlay/css/build/bot-vs-bot.ltr.transp.scss index 5834c8012e52b..f60467b9ecd1b 100644 --- a/ui/localPlay/css/build/local-play.ltr.transp.scss +++ b/ui/localPlay/css/build/bot-vs-bot.ltr.transp.scss @@ -1,3 +1,3 @@ @import '../../../common/css/dir/ltr'; @import '../../../common/css/theme/transp'; -@import 'local-play'; +@import 'bot-vs-bot'; diff --git a/ui/localPlay/css/build/local-play.rtl.dark.scss b/ui/localPlay/css/build/bot-vs-bot.rtl.dark.scss similarity index 78% rename from ui/localPlay/css/build/local-play.rtl.dark.scss rename to ui/localPlay/css/build/bot-vs-bot.rtl.dark.scss index 52e615ea96824..b39639e4fbb09 100644 --- a/ui/localPlay/css/build/local-play.rtl.dark.scss +++ b/ui/localPlay/css/build/bot-vs-bot.rtl.dark.scss @@ -1,3 +1,3 @@ @import '../../../common/css/dir/rtl'; @import '../../../common/css/theme/dark'; -@import 'local-play'; +@import 'bot-vs-bot'; diff --git a/ui/localPlay/css/build/local-play.rtl.light.scss b/ui/localPlay/css/build/bot-vs-bot.rtl.light.scss similarity index 78% rename from ui/localPlay/css/build/local-play.rtl.light.scss rename to ui/localPlay/css/build/bot-vs-bot.rtl.light.scss index 92ea951ace40a..624260995be6a 100644 --- a/ui/localPlay/css/build/local-play.rtl.light.scss +++ b/ui/localPlay/css/build/bot-vs-bot.rtl.light.scss @@ -1,3 +1,3 @@ @import '../../../common/css/dir/rtl'; @import '../../../common/css/theme/light'; -@import 'local-play'; +@import 'bot-vs-bot'; diff --git a/ui/localPlay/css/build/local-play.rtl.transp.scss b/ui/localPlay/css/build/bot-vs-bot.rtl.transp.scss similarity index 79% rename from ui/localPlay/css/build/local-play.rtl.transp.scss rename to ui/localPlay/css/build/bot-vs-bot.rtl.transp.scss index 84017ff85aca0..f50dd3dcb843c 100644 --- a/ui/localPlay/css/build/local-play.rtl.transp.scss +++ b/ui/localPlay/css/build/bot-vs-bot.rtl.transp.scss @@ -1,3 +1,3 @@ @import '../../../common/css/dir/rtl'; @import '../../../common/css/theme/transp'; -@import 'local-play'; +@import 'bot-vs-bot'; diff --git a/ui/localPlay/css/build/vs-bot.ltr.dark.scss b/ui/localPlay/css/build/vs-bot.ltr.dark.scss new file mode 100644 index 0000000000000..514d6be754cbb --- /dev/null +++ b/ui/localPlay/css/build/vs-bot.ltr.dark.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/ltr'; +@import '../../../common/css/theme/dark'; +@import 'vs-bot'; diff --git a/ui/localPlay/css/build/vs-bot.ltr.light.scss b/ui/localPlay/css/build/vs-bot.ltr.light.scss new file mode 100644 index 0000000000000..05b2739c3880f --- /dev/null +++ b/ui/localPlay/css/build/vs-bot.ltr.light.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/ltr'; +@import '../../../common/css/theme/light'; +@import 'vs-bot'; diff --git a/ui/localPlay/css/build/vs-bot.ltr.transp.scss b/ui/localPlay/css/build/vs-bot.ltr.transp.scss new file mode 100644 index 0000000000000..53319f547686d --- /dev/null +++ b/ui/localPlay/css/build/vs-bot.ltr.transp.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/ltr'; +@import '../../../common/css/theme/transp'; +@import 'vs-bot'; diff --git a/ui/localPlay/css/build/vs-bot.rtl.dark.scss b/ui/localPlay/css/build/vs-bot.rtl.dark.scss new file mode 100644 index 0000000000000..367dc8f142120 --- /dev/null +++ b/ui/localPlay/css/build/vs-bot.rtl.dark.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/rtl'; +@import '../../../common/css/theme/dark'; +@import 'vs-bot'; diff --git a/ui/localPlay/css/build/vs-bot.rtl.light.scss b/ui/localPlay/css/build/vs-bot.rtl.light.scss new file mode 100644 index 0000000000000..0a809db6ef68b --- /dev/null +++ b/ui/localPlay/css/build/vs-bot.rtl.light.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/rtl'; +@import '../../../common/css/theme/light'; +@import 'vs-bot'; diff --git a/ui/localPlay/css/build/vs-bot.rtl.transp.scss b/ui/localPlay/css/build/vs-bot.rtl.transp.scss new file mode 100644 index 0000000000000..0a60a2f4b44e4 --- /dev/null +++ b/ui/localPlay/css/build/vs-bot.rtl.transp.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/rtl'; +@import '../../../common/css/theme/transp'; +@import 'vs-bot'; diff --git a/ui/localPlay/src/botVsBot/chessground.ts b/ui/localPlay/src/botVsBot/bvbChessground.ts similarity index 94% rename from ui/localPlay/src/botVsBot/chessground.ts rename to ui/localPlay/src/botVsBot/bvbChessground.ts index 5732cf5398eb9..558dd7fbc3092 100644 --- a/ui/localPlay/src/botVsBot/chessground.ts +++ b/ui/localPlay/src/botVsBot/bvbChessground.ts @@ -1,7 +1,7 @@ import resizeHandle from 'common/resize'; import { Config as CgConfig } from 'chessground/config'; import * as Prefs from 'common/prefs'; -import { Ctrl } from './ctrl'; +import { BvbCtrl } from './bvbCtrl'; const pref = { coords: Prefs.Coords.Hidden, @@ -13,7 +13,7 @@ const pref = { animation: 0, }; -export function makeConfig(ctrl: Ctrl): CgConfig { +export function makeBvbConfig(ctrl: BvbCtrl): CgConfig { const opts = ctrl.cgOpts(); return { fen: opts.fen, diff --git a/ui/localPlay/src/botVsBot/ctrl.ts b/ui/localPlay/src/botVsBot/bvbCtrl.ts similarity index 98% rename from ui/localPlay/src/botVsBot/ctrl.ts rename to ui/localPlay/src/botVsBot/bvbCtrl.ts index 4ff85944dbf9a..4f0af552a811a 100644 --- a/ui/localPlay/src/botVsBot/ctrl.ts +++ b/ui/localPlay/src/botVsBot/bvbCtrl.ts @@ -1,4 +1,4 @@ -import { LocalPlayOpts } from './interfaces'; +import { BvbOpts } from './bvbInterfaces'; import { PromotionCtrl } from 'chess/promotion'; import { makeFen /*, parseFen*/ } from 'chessops/fen'; import { Chess, Role } from 'chessops'; @@ -10,7 +10,7 @@ import { Key } from 'chessground/types'; type Player = 'human' | 'zero' | 'fish'; -export class Ctrl { +export class BvbCtrl { cg: CgApi; path = ''; chess = Chess.default(); @@ -24,7 +24,7 @@ export class Ctrl { fiftyMovePly = 0; threefoldFens: Map = new Map(); - constructor(readonly opts: LocalPlayOpts, readonly redraw: () => void) { + constructor(readonly opts: BvbOpts, readonly redraw: () => void) { this.promotion = new PromotionCtrl( f => f(this.cg), () => this.cg.set(this.cgOpts()), diff --git a/ui/localPlay/src/botVsBot/bvbInterfaces.ts b/ui/localPlay/src/botVsBot/bvbInterfaces.ts new file mode 100644 index 0000000000000..14cc08e7219e0 --- /dev/null +++ b/ui/localPlay/src/botVsBot/bvbInterfaces.ts @@ -0,0 +1 @@ +export interface BvbOpts {} diff --git a/ui/localPlay/src/botVsBot/main.ts b/ui/localPlay/src/botVsBot/bvbMain.ts similarity index 57% rename from ui/localPlay/src/botVsBot/main.ts rename to ui/localPlay/src/botVsBot/bvbMain.ts index 7966295a3d86a..a713963733a00 100644 --- a/ui/localPlay/src/botVsBot/main.ts +++ b/ui/localPlay/src/botVsBot/bvbMain.ts @@ -1,26 +1,23 @@ import { attributesModule, classModule, init } from 'snabbdom'; -import { Ctrl } from './ctrl'; -import view from './view'; -import { LocalPlayOpts } from './interfaces'; -import menuHover from 'common/menuHover'; +import { BvbCtrl } from './bvbCtrl'; +import bvbView from './bvbView'; +import { BvbOpts } from './bvbInterfaces'; import { Chessground } from 'chessground'; const patch = init([classModule, attributesModule]); -export async function initModule(opts: LocalPlayOpts) { - // make a StrongSocket - const ctrl = new Ctrl(opts, redraw); +export default async function (opts: BvbOpts) { + const ctrl = new BvbCtrl(opts, redraw); - const blueprint = view(ctrl); + const blueprint = bvbView(ctrl); const element = document.querySelector('main') as HTMLElement; element.innerHTML = ''; let vnode = patch(element, blueprint); function redraw() { - vnode = patch(vnode, view(ctrl)); + vnode = patch(vnode, bvbView(ctrl)); } redraw(); - menuHover(); } // that's for the rest of lichess to access chessground diff --git a/ui/localPlay/src/botVsBot/view.ts b/ui/localPlay/src/botVsBot/bvbView.ts similarity index 78% rename from ui/localPlay/src/botVsBot/view.ts rename to ui/localPlay/src/botVsBot/bvbView.ts index ee2ce7801f2db..c30ddf3adec6c 100644 --- a/ui/localPlay/src/botVsBot/view.ts +++ b/ui/localPlay/src/botVsBot/bvbView.ts @@ -1,10 +1,10 @@ import { Chessground } from 'chessground'; import { h, VNode } from 'snabbdom'; -import { makeConfig as makeCgConfig } from './chessground'; +import { makeBvbConfig as makeCgConfig } from './bvbChessground'; import { onInsert } from 'common/snabbdom'; -import { Ctrl } from './ctrl'; +import { BvbCtrl } from './bvbCtrl'; -export default function render(ctrl: Ctrl): VNode { +export default function render(ctrl: BvbCtrl): VNode { return h('div#local-play', [ h('div.puz-board.main-board', [chessground(ctrl), ctrl.promotion.view()]), h('div.puz-side', [ @@ -15,7 +15,7 @@ export default function render(ctrl: Ctrl): VNode { ]); } -function chessground(ctrl: Ctrl): VNode { +function chessground(ctrl: BvbCtrl): VNode { return h('div.cg-wrap', { hook: { insert: vnode => (ctrl.cg = Chessground(vnode.elm as HTMLElement, makeCgConfig(ctrl))), @@ -23,14 +23,14 @@ function chessground(ctrl: Ctrl): VNode { }); } -function bot(ctrl: Ctrl, color: Color): VNode { +function bot(ctrl: BvbCtrl, color: Color): VNode { return h(`div#${color}.puz-bot`, { hook: onInsert(el => ctrl.dropHandler(color, el)) }, [ h('p', 'Drop weights here (otherwise stockfish)'), h(`p#${color}-totals.totals`), ]); } -function controls(ctrl: Ctrl) { +function controls(ctrl: BvbCtrl) { return h('span', [ h( 'button#go.button.disabled', diff --git a/ui/localPlay/src/botVsBot/interfaces.ts b/ui/localPlay/src/botVsBot/interfaces.ts deleted file mode 100644 index 6fb6c6b5844d2..0000000000000 --- a/ui/localPlay/src/botVsBot/interfaces.ts +++ /dev/null @@ -1,2 +0,0 @@ -export interface LocalPlayOpts {} -export interface Controller {} diff --git a/ui/localPlay/src/interfaces.ts b/ui/localPlay/src/interfaces.ts index 6fb6c6b5844d2..8fccec7b5a5c0 100644 --- a/ui/localPlay/src/interfaces.ts +++ b/ui/localPlay/src/interfaces.ts @@ -1,2 +1,5 @@ -export interface LocalPlayOpts {} -export interface Controller {} +export interface LocalPlayOpts { + mode: 'vsBot' | 'botVsBot'; + pref: any; + i18n: any; +} diff --git a/ui/localPlay/src/main.ts b/ui/localPlay/src/main.ts index 9f7518b5bf730..1e16d4152b196 100644 --- a/ui/localPlay/src/main.ts +++ b/ui/localPlay/src/main.ts @@ -1,14 +1,15 @@ import { attributesModule, classModule, init } from 'snabbdom'; import { Ctrl } from './ctrl'; import view from './view'; +import initBvb from './botVsBot/bvbMain'; import { LocalPlayOpts } from './interfaces'; const patch = init([classModule, attributesModule]); export async function initModule(opts: LocalPlayOpts) { - // make a StrongSocket + if (opts.mode === 'botVsBot') return initBvb(opts); + const ctrl = new Ctrl(opts, () => {}); - ctrl; const blueprint = view(ctrl); const element = document.querySelector('#bot-view') as HTMLElement; element.innerHTML = ''; diff --git a/ui/localPlay/tsconfig.json b/ui/localPlay/tsconfig.json index 04dbe488c3337..56ec36ef3aa73 100644 --- a/ui/localPlay/tsconfig.json +++ b/ui/localPlay/tsconfig.json @@ -5,7 +5,7 @@ "noEmit": true, "noUnusedLocals": false, "noUnusedParameters": false, - "allowUnreachableCode": true, + "allowUnreachableCode": true }, "references": [ { "path": "../chess/tsconfig.json" }, diff --git a/ui/round/src/view/user.ts b/ui/round/src/view/user.ts index 5329fb8eb59b5..d92e865b0811c 100644 --- a/ui/round/src/view/user.ts +++ b/ui/round/src/view/user.ts @@ -18,12 +18,13 @@ export function botHtml(ctrl: RoundController, player: Player, position: Positio }, }, [ - h('span', [h('img', { attrs: { src: player.image!, width: 48, height: 48 } }), h('name', player.name)]), + h('span', [h('img', { attrs: { src: player.image!, width: 64, height: 64 } }), h('name', player.name)]), h('rating', player.rating), //h('rating', player.ratingDiff), ] ); } + export function userHtml(ctrl: RoundController, player: Player, position: Position) { const d = ctrl.data, user = player.user, From dcd4e1a4056b3813d8f557b166d5c413e67a0225 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble <101470903+schlawg@users.noreply.github.com> Date: Tue, 8 Aug 2023 10:50:56 -0500 Subject: [PATCH 018/174] gah --- ui/localPlay/src/{bots.ts => bot.ts} | 10 +++++ ui/localPlay/src/bots/coral.ts | 19 ++++++++++ ui/localPlay/src/ctrl.ts | 56 +++++++++++----------------- ui/round/src/main.ts | 1 - 4 files changed, 50 insertions(+), 36 deletions(-) rename ui/localPlay/src/{bots.ts => bot.ts} (70%) create mode 100644 ui/localPlay/src/bots/coral.ts diff --git a/ui/localPlay/src/bots.ts b/ui/localPlay/src/bot.ts similarity index 70% rename from ui/localPlay/src/bots.ts rename to ui/localPlay/src/bot.ts index 550e95ab8331a..e1dafa7651464 100644 --- a/ui/localPlay/src/bots.ts +++ b/ui/localPlay/src/bot.ts @@ -1,3 +1,13 @@ +import makeZerofish, { Zerofish, PV } from 'zerofish'; + +export interface Bot { + readonly name: string; + readonly description: string; + readonly image: string; + readonly ratings: Map; + move: (fen: string) => Promise; +} + export const bots = [ { name: 'Coral', diff --git a/ui/localPlay/src/bots/coral.ts b/ui/localPlay/src/bots/coral.ts new file mode 100644 index 0000000000000..3548e5592fbe8 --- /dev/null +++ b/ui/localPlay/src/bots/coral.ts @@ -0,0 +1,19 @@ +import makeZerofish, { Zerofish, PV } from 'zerofish'; +import { Bot } from '../bot'; + +export class CoralBot implements Bot { + name: 'Coral'; + description: 'Coral is a simple bot that plays random moves.'; + image: '/lifat/bots/images/coral.webp'; + weightsUrl: '/lifat/bots/weights/maia1100.pb'; + + zf: Zerofish; + constructor() { + makeZerofish({ weightsUrl }).then(zf => this.setZf(zf)); + } + setZf(zf: Zerofish) { + this.zf = zf; + this.zf; + } + move(fen: string) {} +} diff --git a/ui/localPlay/src/ctrl.ts b/ui/localPlay/src/ctrl.ts index 7bfb98d8948e6..5f6d4be3ffdca 100644 --- a/ui/localPlay/src/ctrl.ts +++ b/ui/localPlay/src/ctrl.ts @@ -4,25 +4,22 @@ import { makeFen /*, parseFen*/ } from 'chessops/fen'; import { makeSanAndPlay } from 'chessops/san'; import { Chess } from 'chessops'; import * as Chops from 'chessops'; -import makeZerofish, { Zerofish, PV } from 'zerofish'; -type Tab = string; export class Ctrl { path = ''; chess = Chess.default(); - zf: Zerofish | undefined; - round: SocketSend; - fen = ''; + tellRound: SocketSend; fiftyMovePly = 0; threefoldFens: Map = new Map(); constructor(readonly opts: LocalPlayOpts, readonly redraw: () => void) { - makeZerofish().then(zf => { - this.zf = zf; - // fetch model as arrraybuffer and - //this.zf!.setZeroWeights(new Uint8Array()); - }); - makeRounds(this).then(round => (this.round = round)); + makeRounds(this).then(sender => (this.tellRound = sender)); + } + + set(/*fen: string*/) { + this.fiftyMovePly = 0; + this.threefoldFens.clear(); + this.chess.reset(); } checkGameOver(userEnd?: 'whiteResign' | 'blackResign' | 'mutualDraw'): { @@ -53,48 +50,29 @@ export class Ctrl { move(uci: Uci) { const move = Chops.parseUci(uci); - if (!move || !this.chess.isLegal(move)) - throw new Error(`illegal move ${uci}, ${makeFen(this.chess.toSetup())}}`); - console.log( - `before - turn ${this.chess.turn}, half ${this.chess.halfmoves}, full ${this.chess.fullmoves}, fen '${this.fen}'` - ); + if (!move || !this.chess.isLegal(move)) throw new Error(`illegal move ${uci}, ${this.fen}}`); const san = makeSanAndPlay(this.chess, move); - console.log(this.chess.fullmoves); - this.fen = makeFen(this.chess.toSetup()); - console.log( - `after - turn ${this.chess.turn}, half ${this.chess.halfmoves}, full ${this.chess.fullmoves}, fen '${this.fen}'` - ); this.fifty(move); this.threefold('update'); const { end, result, reason } = this.checkGameOver(); if (end) this.doGameOver(result!, reason!); - this.round('move', { - uci, - fen: this.fen, - ply: 2 * (this.chess.fullmoves - 1) + (this.chess.turn === 'black' ? 1 : 0), - dests: this.dests, - san, - }); + this.tellRound('move', { uci, san, fen: this.fen, ply: this.ply, dests: this.dests }); } userMove(uci: Uci) { this.move(uci); - this.getBotMove(); + this.botMove(); } - async getBotMove() { + async botMove() { const uci = (await this.zf!.goFish(this.fen, { depth: 10 }))[0].moves[0]; this.move(uci); } fifty(move?: Chops.Move) { if (move) - if ( - !('from' in move) || - this.chess.board.getRole(move.from) === 'pawn' || - this.chess.board.get(move.to) - ) + if (!('from' in move) || this.chess.board.getRole(move.from) === 'pawn' || this.chess.board.get(move.to)) this.fiftyMovePly = 0; else this.fiftyMovePly++; return this.fiftyMovePly >= 100; @@ -122,6 +100,14 @@ export class Ctrl { .forEach(([s, ds]) => (dests[Chops.makeSquare(s)] = [...ds].map(Chops.makeSquare).join(''))); return dests; } + + get fen() { + return makeFen(this.chess.toSetup()); + } + + get ply() { + return 2 * (this.chess.fullmoves - 1) + (this.chess.turn === 'black' ? 1 : 0); + } } function linesWithin(move: string, lines: PV[], bias = 0, threshold = 50) { diff --git a/ui/round/src/main.ts b/ui/round/src/main.ts index 274bfadb362b8..1284d85f4c528 100644 --- a/ui/round/src/main.ts +++ b/ui/round/src/main.ts @@ -16,7 +16,6 @@ export function initModule(opts: RoundOpts): SocketSend | undefined { function app(opts: RoundOpts, nvui?: NvuiPlugin) { const ctrl = new RoundController(opts, redraw, nvui); - console.log('main', opts.element); const blueprint = view(ctrl); opts.element.innerHTML = ''; let vnode = patch(opts.element, blueprint); From 90ec1796a11240dd58097367b60f8f2df3e871b0 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Wed, 9 Aug 2023 13:14:34 -0500 Subject: [PATCH 019/174] gah --- app/controllers/Round.scala | 2 +- app/views/round/bits.scala | 2 +- modules/common/src/main/AutoConfig.scala | 10 ++++++ modules/fishnet/src/main/Env.scala | 1 + modules/fishnet/src/main/FishnetPlayer.scala | 7 ++-- pnpm-lock.yaml | 20 ++++++----- public/images/bots/baby-howard.webp | Bin 23494 -> 0 bytes public/images/bots/baby-robot.webp | Bin 23224 -> 0 bytes public/images/bots/beatrice.webp | Bin 21930 -> 0 bytes public/images/bots/coral.webp | Bin 17228 -> 0 bytes public/images/bots/floyd.webp | Bin 19460 -> 0 bytes ui/localPlay/package.json | 2 +- ui/localPlay/src/bot.ts | 34 +++++++++++++++---- ui/localPlay/src/bots/coral.ts | 19 ++++++----- ui/localPlay/src/ctrl.ts | 31 ++++++----------- ui/localPlay/src/data.ts | 2 +- ui/localPlay/src/view.ts | 4 +-- ui/round/package.json | 3 +- ui/round/src/ctrl.ts | 21 +++++++++++- ui/round/src/transientMove.ts | 3 +- 20 files changed, 107 insertions(+), 54 deletions(-) delete mode 100644 public/images/bots/baby-howard.webp delete mode 100644 public/images/bots/baby-robot.webp delete mode 100644 public/images/bots/beatrice.webp delete mode 100644 public/images/bots/coral.webp delete mode 100644 public/images/bots/floyd.webp diff --git a/app/controllers/Round.scala b/app/controllers/Round.scala index ff7076e82690c..65658b83433f6 100644 --- a/app/controllers/Round.scala +++ b/app/controllers/Round.scala @@ -64,7 +64,7 @@ final class Round( chat <- getPlayerChat(pov.game, none) yield Ok(data.add("chat", chat.flatMap(_.game).map(c => lila.chat.JsonView(c.chat)))).noCache ) - yield res + yield res.enableSharedArrayBuffer def player(fullId: GameFullId) = Open: env.round.proxyRepo.pov(fullId) flatMap { diff --git a/app/views/round/bits.scala b/app/views/round/bits.scala index da49ce8263aba..19b9d001fc97c 100644 --- a/app/views/round/bits.scala +++ b/app/views/round/bits.scala @@ -41,7 +41,7 @@ object bits: zenable = zenable, robots = robots, zoomable = true, - csp = defaultCsp.withPeer.withWebAssembly.some, + csp = analysisCsp.some, // defaultCsp.withPeer.withWebAssembly.some, withHrefLangs = withHrefLangs )(body) diff --git a/modules/common/src/main/AutoConfig.scala b/modules/common/src/main/AutoConfig.scala index e0e3e359a5dc9..8e4df5282b10d 100644 --- a/modules/common/src/main/AutoConfig.scala +++ b/modules/common/src/main/AutoConfig.scala @@ -65,6 +65,16 @@ object AutoConfig: // Get the type of this class member TypeRepr.of[T].memberType(param).asType match + case '[Option[t]] => + Expr.summon[ConfigLoader[t]] match + case None => + report.errorAndAbort( + s"Could not find an instance of ConfigLoader for type ${TypeRepr.of[t].show}" + ) + case Some(value) => + '{ + optionalConfig[t](using $value).load($confTerm, $nameOverride) + } case '[t] => // summon ConfigLoader for the type of this parameter Expr.summon[ConfigLoader[t]] match diff --git a/modules/fishnet/src/main/Env.scala b/modules/fishnet/src/main/Env.scala index fb85947eb9cec..07ae0a6436f2d 100644 --- a/modules/fishnet/src/main/Env.scala +++ b/modules/fishnet/src/main/Env.scala @@ -19,6 +19,7 @@ private class FishnetConfig( @ConfigName("offline_mode") val offlineMode: Boolean, @ConfigName("analysis.nodes") val analysisNodes: Int, @ConfigName("move.plies") val movePlies: Int, + @ConfigName("move.delay") val moveDelay: Option[FiniteDuration], @ConfigName("client_min_version") val clientMinVersion: String, @ConfigName("redis.uri") val redisUri: String, val explorerEndpoint: String diff --git a/modules/fishnet/src/main/FishnetPlayer.scala b/modules/fishnet/src/main/FishnetPlayer.scala index c28dd5023c6e0..ea46371c83de5 100644 --- a/modules/fishnet/src/main/FishnetPlayer.scala +++ b/modules/fishnet/src/main/FishnetPlayer.scala @@ -13,7 +13,7 @@ final class FishnetPlayer( openingBook: FishnetOpeningBook, gameRepo: GameRepo, uciMemo: UciMemo, - val maxPlies: Int + config: FishnetConfig )(using ec: Executor, scheduler: Scheduler @@ -34,10 +34,12 @@ final class FishnetPlayer( logger.info(e.getMessage) } + lazy val maxPlies = config.pp.movePlies.pp + private val delayFactor = 0.011f private val defaultClock = Clock(Clock.LimitSeconds(300), Clock.IncrementSeconds(0)) - private def delayFor(g: Game): Option[FiniteDuration] = + private def delayFor(g: Game): Option[FiniteDuration] = config.moveDelay.fold { if !g.bothPlayersHaveMoved then 2.seconds.some else for @@ -53,6 +55,7 @@ final class FishnetPlayer( randomized = millis + millis * (ThreadLocalRandom.nextDouble() - 0.5) divided = randomized / (if g.ply > 9 then 1 else 2) yield divided.toInt.millis + }(_.some) private def makeWork(game: Game, level: Int): Fu[Work.Move] = if game.situation playable true then diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 351b85e11315e..b5a9fcf7e6cb4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.1' +lockfileVersion: '6.0' settings: autoInstallPeers: true @@ -494,8 +494,8 @@ importers: specifier: workspace:* version: link:../tree zerofish: - specifier: ^0.0.8 - version: 0.0.8 + specifier: ^0.0.11 + version: 0.0.11 ui/mod: dependencies: @@ -745,6 +745,9 @@ importers: voice: specifier: workspace:* version: link:../voice + zerofish: + specifier: 0.0.11 + version: 0.0.11 ui/serviceWorker: dependencies: @@ -3161,8 +3164,8 @@ packages: resolution: {integrity: sha512-GXZxEtOxYGFchyUzxvKI14iff9KZ2DI+A6a37o6EQevtg6uO9t+aUZKcaC1Te5Ng1OnLM7K9NVVj+FbecD9cJg==} dev: false - /@types/node@20.4.5: - resolution: {integrity: sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg==} + /@types/node@20.4.9: + resolution: {integrity: sha512-8e2HYcg7ohnTUbHk8focoklEQYvemQmu9M/f43DZVx43kHn0tE3BY/6gSDxS7k0SprtS0NHvj+L80cGLnoOUcQ==} dev: false /@types/parse5@6.0.3: @@ -6427,13 +6430,14 @@ packages: engines: {node: '>=10'} dev: false - /zerofish@0.0.8: - resolution: {integrity: sha512-PwOmIMRcSx+K87WFVAdwrDgy4tEUXX1q+LSUdslsnVkslaBtkwQHoWLrLzWHRzXXHNruQAnKgKTVEvemiYZOtQ==} + /zerofish@0.0.11: + resolution: {integrity: sha512-kNM+OonZ4RfFmSX2rCwR1DNXbRnKhscabChK+fosWD+wBULVV9HidjFgFCHQVJyreewZnOvfwSMU47pgikpyzw==} dependencies: '@types/emscripten': 1.39.7 - '@types/node': 20.4.5 + '@types/node': 20.4.9 '@types/web': 0.0.84 esbuild: 0.18.4 + prettier: 2.8.1 typescript: 5.1.6 dev: false diff --git a/public/images/bots/baby-howard.webp b/public/images/bots/baby-howard.webp deleted file mode 100644 index 53e0e4751d0eb4dc9b333b71317cca57f37bbf97..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23494 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 diff --git a/public/images/bots/baby-robot.webp b/public/images/bots/baby-robot.webp deleted file mode 100644 index 9c56d595d66ff3c34d65f96cbcb879736bc34b60..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23224 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 diff --git a/public/images/bots/beatrice.webp b/public/images/bots/beatrice.webp deleted file mode 100644 index 807e259f459204a73ef64d01b26ee4158d5ccef0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21930 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 J1^=-0zW}k>)`0*3 diff --git a/public/images/bots/floyd.webp b/public/images/bots/floyd.webp deleted file mode 100644 index 30c7df4c648c4d18ad8d9bdb6ff8830aa1c1edab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19460 zcmV)#K##vtNk&HeO8@{@MM6+kP&il$0000G0001-0RWr<06|PpNG~M-00Hm^0DvN= z+5f#WE9a6+a=ExC1mck37Tn$4THM_!PjO01k@5ib_Nh>)P;)*C=ZYYq*Mh)T&V z7&K-6{F%=U?2wZfW3%X0l;Q^&y)C`R3x^+8IX#%vh}~7iXI>hWAFX2)Jw_XvI&k}W zHzq>jvZD1}lZ~umr!pmXntSjHBEq|#?i!Y=V-z{o*kHu=JLQ;&i@6QmqSch*M(g5w zt-D!;F%kXgsWIthT3Lf?YxC(pmmnfZ=Y_4!!&oJaHD?UlUsTBviQ?-!dWN!!8Kh%6 zZ@ya2VIo;nwWn=3t(0l3J>S3N;;@7W`E6ZuwL%7MEmz#B@E}a2TybSgs7fJ=Xt(lm zv0Gx8d#RO8Aq#7^@Xr#2iNw!tZwwVNU~RhWUNw=tyS$%{QNmz!&sQ9r^ax&(ZBf8z zZQk}1$e|Odaz%ZYo>TvS}uC%5eIqBzP4u0+(nx|C~{#x2x&z_MGLY?t53U$NbbG;S~O~q z+PM3gpGy4dYGf5H*4pkHClSlJrCo#YU~@=Jizz3OKa*07ik2a7&Rsj*@($Ml@Za=zJ94X6{FJX66XC{?Z@e@X)IK%7+agSomf0_bVGB4 zidLyr*7hrIa=y9$>>jRGthB8+UL;a1zA!jkPq7-EHGR}?)xP0}E8B3 zkZVi#IE~I|jmsKwvfMY~IK94Em`a(_CoFR!Nf1)>dH>XexP-L)aVN{LZ`f0`saK2+ zDo|8Rx7RU|C~|(hbY!QzCI!<^mm=Q?cHVtwSVJA9JkgE+b%ID1m*4zm=lmHf-n>zX zux|u&cfOgD5kfVXAZGy4u z$q#GWM5z@bO4Xs*UYihtxvEQB`o!ocMTpV$aB{T_MwOR$_f6J7C5Scl_TbuuV9Yr# zzdJ0!2$dmJ+Yo}0qxg+ssVYkOQJMQ;QVU~r?X!VVdgVtQJg8P8#8v)k-x%db8!?j9 z&X~LSK}#K@^ca&S*HQ#mKOPmQS9*f;77K;S?%jRpz|Rmto|pSZK!wNJZmmFE#on3E z&p-UY{R|O&Zf};FQg*^RZ4d?*ebUYvY|9w(T_t{YNcH9ZFRaQW6AA5$Mp|IHCN7mV&i+1j1e(y*DsO$x(+6 z7O3#O2GRhGxy2DAjCXtCtPmwf6FNko(z6*70Wj*hk2n!LPnLx$IqKj+0y(Z^Nd@R) zhn8`|A^U2plA|*B6Ucp2Iux7r_Ics3YmK7A8hZ+Z zJ15M==Q4H54q*E^I3ZBs<~Rufw$GhvVa^On)F?bmmvbdTkm^qwvQR?pKD;K(kyaxd!P#OV(yVJO|QPsPF)7%o1Uq z92g!f9iWa{iUmM7Uyg%7>7jzAaaah?;};?+>Co1BoB;NBM%jSk0}SJfT|yA_W)ll7 z87g>|8~ax>J;$T~Y1{885`_G|Bws5T7&OC){Hng#&de%6Y-0Z}upkwe#s*PR0p=NQ z?AO^H4H>8aQO5LL9xMcQZOXPlshFm7e&HPhB0()2fC0+3_?AluLJIe{W~GCOpPXZC zyj~ds<{rE53gf997ow63hy6IUGlzx>ktT7_X<-O?wM&$Ah|ieY(AEZO<$#RV=YGKn z^!P$Ai&`4fJlLm_^ZiV$5&$8r4FHv}^DYtwd)9RblZ0{NeWwqY+uJpo0s$Wt6yKz6 zv-IdtlTO9Z@L2-Z{yVFLA>`4SmO5Gjx-R=~_;Bs5K2|jd_$YP5QF{uno_Krp#Qq&y zHE9rKQd7^2CbI8+ED*NO&kvG-w#Dl6HE^Y`=7rEe0Z=s@f56FkoE48RpZM;ht#bzE zN9d_%1k$;e-2xGGYJDReEt;;)rb2J1XmuMC1PTD4BHF&lT zl+?`WGd)-!;w)N|q=6#Q=8UDk-n#eC?lEyR1hu)m0;-fD|9R~8;uTlje|z@uu5Fs; zWhF-hX(Ywyx)eKvBKW6KX&R9LfUL$~(6JEI@-ho*V}`zQ7yCd6u6%In;I4IR7Ec@1 zCp}0lApo=Y{~;KuzP2#g07a@RWflOewblMxoG%1(_NNbT-@1C~x1%e2CTMBtfUwpZ zuwVrLFgr`7!~htSH}i(e7eX*boZIbkIqzMV-6~u~Nru)9D0T`)xaxUHMuh+}Dm6<} zQUSCkZQ&Qb5kfG=2w~2B{*B%#7BxdbCZI8OH${x-wNR!Ro(pdrHOqyv}oKg-b6#OpmEswGT{h5 z{qGz#tpKPaIv>98sw%mB;@kJPzWmPDXD?j(`n61rFzGcKwMJ(QOKCZD-m3>s z7u~*l`}#vSCL$u&;|pKEzG=zq@hxKYlt@68I^t^)2y*?alZ}P}AFWGU@I|#7A;h_= zDl6?S4~H;92yq<8A%rpU9b+Oka@)&G?%yeVZ9;Y^D-zTsY_ucc2&p_dC{Y*i0T|M9 z-sKX62_cwIo_&lw9(U!R+k4woP!s?xqrQG3263EQnXI7#J-`?;=YH(uNNtKSMjrcz z1Cmrw98E(1pIu@Qd}>;b9tL^<1~q*CYALQwygxoMAyzLApkr75<`#oE@2+WNrUE^n z4r;ymS0}1gS1K? z)}Ygj_s>7BsCKwL2xC$Ud+gIQ&7y#aLA#JRgmcWx2nw_ytF@+fS@gm6ryhg}AsAy! zYG?fQ(o_{Ch9<1vqiS&o`Fdp<0|P0j3TfDT$=5|L4wKs7b9GdzUJOWOZaXHH_j;i7F~UqVzEXcK+o+nADlBANs_r z#h`V4Zd8cnyf7qD7Z@?d%rPH4av)6V(Bi|RZHx$j%2@P+SmfzDBO@6Y_&_SS(dc(B zyAdXJDfYb6F^UlZ7!np%b0Tq1-fthy20pYpw%w9n?s@7MLjK;@#YBq$wA~Ng7KxzC z>k7HEqF2S@7Q?-ypP zDB%EYTX^3k680P%lFSA+tf|rH*B(}5QWx7FJfC2M!U03UrfVV*^4FdM9TmXPn#eYD z&pkkx)X|4#XX;r9g`&-QA5>$JoR4S5>H`)^ZOtBi_y#6*x2t?vn>dqR!%$EN0K%ty zRV@-h?+r_Y0STnFp_v0-IZ=g3U5*Psp46^MgGh^x6-3`)%x$sAjW64)slWqLI&0Rz zt;Zj`F{#^KPk-INdh);yjic1G0F);G0v5|%IVHguXi$v7nlpIa4>zh@TwO=7tMY!~ zFYm7JlWZ0MfYbx`#KLI*h}gh_)&$2jAGP79YgI0UF{%5p$5~Z!d++#E6(vB#u)Sgt zQus+$KtY*OJ51U3>)&P7E>0d0f)VE~JlHo@FMuv)qZLrED3^^ z$uY{DGWerQh{*2ZbCYs{83+Jl=t?At<(SISWce3Pj>vE9I=ZoKl$N5bZB_*=kg>L& zf2kA`S?;>AePBX}wZ&V1NoKk1(!_S&@w*cfd5)boKAAph@~e-y0EN;gO+4@z6ZwuD zj~+V^k*wUJjk!~PbYddwF(wk0S(LTOl0Tl3Kw4I@#+GZ3ak#!Br18AMrzEgm5h|Nl zLyK+4Jh;BX+ImH~iv-%Ti81EyIZFcX__T)v;;3>_u>vSp(W+JkSp$PJ$9{1I;Xv)gh*XW7Ve}2g?7CeZxP8?wjFmUZ zeHVTFdr7qi2W03!jWm>;fwUnav+Kl--`uw201Q{{iKBp=0f1sG2`xtLyIw^CaMy(u zkwE4EsE=$m@j$sJ5PSMs{}9;&%*g|GUqB=fzc;IiN%p|-R;&N1B7r!%+&x?^cW8C` zw1b!go#RMAj?up1v7iopHq?>bj@U_H zZhtsFS}%8ud9$zL0PXtc?KQN_fguC-Jtl!UJ}^Fo0`dl`;#dCV49r}mi_^?N=CDzn z4>)i@hX34AV4&rWuKDtFm;~a;{&x3R2FM&}9&zLe3CK&2Oo`P(xx#(|jY%C_7f2FM*canQ$@1mO7L=I)^skUebkt$&bbA7lAdb*g8S2FM?E z-@nSFf-x2barUxDkE%RaUg3i?6BsCeXyd3#k3>9nhl|7h-8=sB;tNpn1;gARo1^ARIqQ|BOvZ?g1fuR~uH%l&M@k!2Nc|s2>fw}t`@8mGECAuXwqa89 zC$r;$@0#@SCy4CD+$Yl$RTQYxB}Oq_t2`1o&`<>dFs4o>K8}gh`38eVA1#$V=-fN) zL)D;;ml%|W^PPAA0HC#9eCb3(pQxa$ZEoxnMsA1G<>9cLxb%J}iyGAVlI!N+M8jqK z;&=eI@udLx&gRTNLVR_X{jh#qr%v5QEIRzyB@^(~4+h2Pfee%w#ddci(cFKmj`9Lp z`O@AGb=NO$`+%zUcZy~JpVG$k-+s$o_o3^r4~)`6AiHQyAB07#es@$@4OlQu@6*LK zp&th(sezAFaI?ApRN}e}JMX+TCP_~Lc_=ZJX%Hr&KO7pb^VXu=C0PGUWi?d&Ff|PL ztBdOQ?KL;9n>gpqqt7)7rXi4v64O{m67m0Ot!KQYEc^o3PyqcHe5bmG+jH`aegQ!1 zTde)5!j0<~%vC*peQbt-1#(hi`j|-)xSmTxZ^79MN}S%hsVJYc!z(>CoNaEP1polB zQEfMzE_UKN1G&ond}nHl2pY&ti5U~7N+Fv9-f3-xblpQV__}TIgmoHB|!uOhmg~4_t&Cdnv`Y;daiO{o7P= z_>{_;J?V|YryQqF+vA{e3h6vZMFpN_leN>0Dv}Sz&pQR-Z?JTw-FBH zjr%1HiUt6Hv?X`KPq%9i%-Ku-x%B0(;rX#92FP29nUbeVpk!LM(I)_CT}WK0mho-K zLx(qVwz+|k0NsX?&|V69h6?VurZqB~Z0zpw$lm0EEDIRolIn zYq+zZnMoo5M0QzsyXu+3TNfvlF8E^<5;k2v3r7Y#sT!P>*;uRQV~f_6no2LMg> z)Xz&i&j2d@`ciVD{nxk%T1o&iam!D;q~$Ez)-N0anJzK4 zWhjRwP}hAlyp{YABIsG-mn%331K^ z85SVlB}Qv{yCp%s8=0nowU{Akf`p3C^)j-7Q!k0QN){&>VC}`|x?t&iGd~@IK#5|z zJ2}CStGv>Ky%F-qt8Mg5tp({e(m^{rMuLC|(w!dLg~G0@AAI!316%|4?CGDRp=&7s zTS*2vm!y~iCjeW1ds8U)*u98WiY#^07 z^^j0yC#FZ6BYNG(J`m#iX8p)ct()aHYuz;4%2KrmkWS^*p?VlVfzZ4cu|SVM8y-xv znIHV^6Cnh79OaMh-T8c7i%@lK(n2!#zU`p_fs`hE8i$1`KR&?%fF*aHgY(TRM(5sY zZer`AORu(82UN6ivesx64X82fc_Kq3=ZsXBgot0u&GI9vlETN5=906?b< z-(6IJeL?Os|8A*c>H=4#6Zx|zKa>irn6&YSaSa}a-NE_r#h@e>@YQJpS0B7mhA<%n zyGqaOAKN$-)UqwG5SY7v;ZBvuC+uDmt%89S6`Z@xjlH?5D=OW%rl-FTGf_MMRlyDW zKEGi33#(VHS~9s)yomy}ZF4vB50@0axoG##w|%l-n-&Z~Uaa80EA*6>&ZtOHb*k=QqP z$+ng@3IL#Lwe!y!?8oOtLJ)WXDs1EjPrOuKe1GB6{Up`aby=tGX zywb@80RR9RQa5_L^X-U+dJu>KP5glKm{(o2cUb@Db`?8)A=MvF&9JCxSi1q3^Wrfd zxW{|)gBjpGXr8y1`$SM6FrzftpOs@mi2LE5F3memJ@}`K^MP+B6_Q10ZVZCk{jicD~oWS*z|N*B`x&eIVF*^`o^D zdo)dpNlI&!Vl&XS5;Tk{b$h`l)@ABxAIvk#JiK;lI&jm)_5SQK!i4*5Xq%?FnH?v+ z`N)oIB7|Vh_3+Z?yWiNiY)Ec6TPp!g_b=~yv5!kJ0AG#6elFoP0|DW)0_pH}%dV9X zeCgG(Ez_bBGl#wU<6XNigb<9}_Ttk^JH_g$+DYB)U%&INf0a!`pY-iE74ez|asV*; zoOz!)x$>e--5Q2k!y6S$`S)Kg>=VHr`(N+$O)*fQwtCUawjSZYcU`L&3KakV3^85T zestpW%L8-5t)b!ZIlb0=b>E47K<=AI*7S+f(xA3#zqg1tEJW>xq31id6#)PZk-1%m z^k^J!F&hmAvn{pts0CX-I8}7#>iJ(jduQY9!R;DY)u7ha^f~sJ*O`KTm?vGcD+2&P zMq@VX)ig~}5JFm|4~}lwqJ5VRt@E=QM42@d1hu$!#1j{<1MoL!jteUVb-H%60|_(_ zllp+M92Sby2k6FP!Y`<=AcPR?sjnEvtGeH|KI5+sn$%C|$MfD86eko`_SCjnyg6|O zfjDQrk_V%^@8gX&(gIc9ZKCM4{^}-gs0Ipj?b|FRkLuyUyum6U(4l#W${z75$b8jV1mw&eyxf7E?0K2`o`%>dqt z7!B}i-~)&9r?pMs%@3vh8&c`vby4(e1)F)ZEzrNAPNe*$ZT^)v%MAP*=Elljliwfm zW}1Y;NZD&v<`m9@3Gq~QR`gnMm^VX+;T$HU@}CaM9^oTlFd^i|mo1b(Pw8aAeq5fe z{M~%Lt6m5~2-(#+QqgP4`>bm;E7ZzHK?)wETJv@$sGv}DqIC)$YwpS08UY0KHYHT0 z;8{Da<828SZjyyj@KWBoBGv>Gt>85}#|fty!5e3!6uk65iC~mrtl|eVF9~N(Th5yp z#3*)<&M6cwjjFP+ScQyXvp0~vC}1uxXZ(vMIc1EGdEvU%p+Sw>8jX;jrp)j0^Oie7ro^lP69(r zDn$;`N$+@w1XkpQC~~TZ&OZ_I#OUNg-hO|hks=qm z!i~jf^7;jDKdeht=2jviMrhtTZ{M#?QRY^7uo#qKjJLlJXDV_b3m&<}0O}#-9$v-W zCW@S?+s@mfFb&3j$wQueMNXGB`J5<#jab1Wq^TlDoAN#v2L#Rc)|^t}0F2vj$KuTM z_I#RAi38T*Z&!*l7kN3mIYE)rHyC$M6l325_j!&V8z^$Dx&2j9C~exCJnon>2kGoW zQ2>bC!6PTr6gdFWFNyW4ms5(}pCU!P;^Cdmq+xwT@PPd#gpfNka*c{yBe4LL_A?=b zd$=jGUIL{2L_;zer?60g)5nkF z9h;n~7ApBr1+Q><3%vqIsS0?Nm&X`|3hTE65kin-p+$iMuoVw;(@jD_riC37LdcR} zB@VRU5i(OS0J2U$sL!x<+{jy6Php6k%v%%5C~{adTHTZP<)~CvfotwYVgb;R=Xx?+ ztHf1%Btpm-C9dg%D&8!!DA>yc161BJ-a5ydA4-eRzQn;>oM2Gif^(Mhwq{wNaJhSa z=e<2JUU^elM)H1Hl+6fd$ymwzV|N}@+$eQFUd6TEtZW#s=$opUqFIj{wjz zm-nQoiSpJ%3cx&@huqETDQ58qX$k8!x*C}J2-9>s;?2zsQrfz^kOT~ae=6pEIwzh| z*g6(Hm4LR%yt9}P;=I^B(#TR?pdtoyH=mM#I&JjNJYxTLZO`;DlR>XH7<3wzp(zSN zMJuw?Zr)I}IH|*a@Ip}8A8*ea*{4U>u3g&XXQU>^$3{nl+d_jamLRi9uVyKzECEB} z65gZ`l^EKbX(zl9f;nf!<0qBnPwrkhck1}Dqd$Im@V)J8S1nsKYhs@k4Wq3=2CbT< zDX0+9mdU&=ZEPYi!F}GkMf}GYdFMPXr^8-dS$6&88>=P{Y~MI7F5F_!Fq9GmjT3p_ zFK;Lu9g@55kxKxdF~$fX&Q<;J?;qY=HFadauC20@BZD*yRDKLzZvWIwFqNg%@*^HB zj#rE^a#!CyyKlwRF~j=z=+G)V*{WlpGW6(=mIBe5_>OCf$}o{6#vbQ`-*(Iy*{@f( zjvYI7DrlT!QB#Ug^}}{h2uKH~57~Rh;gO8Hy6E%y1G{$U*r9!=ZasQ*ZkZaUp%fj; zG>OMK2bfiUK}KsyAF=7ot@0`-hcU+DA$#$c8-})T)3!rFo3`!Swr=09X=U|#2pExLW{ZePO3%*8O3%#6Z`rbSLHjl> zn-=t&vHe)7TPTd()fW#=>zJL|uwiO)QiF_)jGQLYzneQ$Hc`aCdOEeDu#lJ z45TAFZgwL-ykg{WJpJeNfn96oO&T)bjYG}C!-BMo7e1v8NohTL{RgLRlvFxgj;gZz zzkj*^Uo%GZY>^&kvxbCN3o7n+T3E=kgG*f6VC8JTuWk z!5a9KF)F=H-zl5l{o&}TUk+`YIlODL+?bt+ax3a08@d*F`X|*LGr=Zuc;X}K(Z__k0HQr{{vJAs8G)++y1^EC#2xOoP zgLH%4=P{N9Ap}?6os(tphYuNzG1zJg3k$bdEk+GPLGM6r$}uM6@O?}OAs9=55OQ=| z1`YiK06<7l6iw3<1%Z5(VSq|p_EnXOI_C=RSjdBM7fQ%tLt?j7Q z2MZk@Tm$mhpWME5;@Hu5R<(&z_Pnx&h=#2POy79u_xt58kEiO5@eP3Vve8~Tc>dW-+{!ei4)?YmjT>fMKX@6|J-@oGe$Nv-R9sOth=UT7s zf3Y9#N2gD-C-$HFewTm$^@sfv=EueyZ1qb!E7kV9sh5Iz)c(J}-T@xo-v_4*p@n7WmQTo%^bIV`ZPwwZmNA5`U!?L51$W39ZAp-{^ zkeb6*LIw^*U;kYfc%#+Bk;qM9t04miA^N=@`jGD-`pWf#qs}N{slC4U@UA!}u+@-( zgOSKglG`!Xy@BKyXd<}u4K}lN(l4>ndHOI*r@YF06VJ!>PNo{N5HNB%396@2w;i0t zSsmMc9$Y8JIwE=s8}s*{Ki_qLZ5maq$<-B&0+?#ZK*7l5CaP_W411m8tx0L(QGgU< zQN?&bl^}J1kAg4$6Ig1;-kV zfMESG=1}?lVoJ#nEWXMQx|YVL(TgIo-!N0z2mqgV!M(hA4&kPtSv(cpvEuDNy*7lu zdc`&)m{1vDu0`c+7;W%!IS1B;@v4v0l?jveAtj0KfV3=x0?ZNC5qw{w`oA5Q`#f!8 zI%pSTft@k|5C@3vvWV*wv9^(FQ7QFNAVqLbfx7#dr5{ms|Js@;bDP!?L{K_RgJXPr zNN)TbjzRcarM41v;s5k6$Q9CYQKEGljQNhoEW>PRQO%CfLznUH)vl@{UrEIUl zn?LK0UsJI)Qgh&}BDjQ#<-Sh+$cw6rnU@ZYEv+lG(gApNjr}W28Suft2vwfzc{jG3 z7D5ILRJrptCV0yh*ITWEIY*H`{WrX$Z25oLv-UHf72Mnt=)48+-GSU2I%V0=wtd6? zcdbT!zR}aac`VF8C2FKm^>{GIURNjWS+;|Fg(6-$V{JXWIi0G zz8bR}Zlc#C>0!_B92s9+*UpVKmN3Wtif4eS=u?)5T+?rEkQqVo7Npk{!$Mv*8QMz_ z8=dJwnV0`#Y+3jCX$Wau8y)OQtch2M$=OK zkjh~hY*ySzZn);7U-ScYpSzezniclCB|RYpepWnBR5Y|2S`wVMG zkeb6(WW|4%})*m z2r!bw)*7-9FmgEwl)yj;2=b+7P_7eLYREvr$mAxlWfJY)e(HyvKhrVvk?7A`Dmfg4 z)*7-9FGK@l9JX?Pf=sA87!e(DBd#2dLTe3K2pBmMb8&SqeDiSi7gXEeyjRIfP$=x4DiTLt7S(ckczp{%B|MYmgZ5i|$1if;t1by{=OU*moy!cughR(AI7YFB8=YoN;5()CuJp!QbpA?Bhh$Fj5fWOSVxO z$KX6}i<&|8aBG?a`s+LcUb-)hAR)J^bn)DZrbZMD$YFpo^NYS(gE~Kd0knBkAV>fo zfhxPwDqNUp!qL&wlaYPLzmw5CKGp) zA(RdN4bb=nleJgYl7Tr_{4mO6IgJcupmvq7{rai|I6movD6la+Iu`Tsa|s;~_` zK7~V^O3dAYK9Q9Au5Lu&jkk(&y8FwArX=21@j`SQ1nJiJ-5BG)3;Sgux`xEff8V(cBc1L=5Sm7^MBWwW33c?`2~EVq0GmJRH;$;5ccO|NAg^;L_bXztVNynG~mEM-MXXhVIkMaWCN$5o1dXG3R;ljwb03gPfu>@1Pup0o~m%j$c0`^X!MuiIz8`v~vbm}o6OnR#TKJM5@dlG+J;TE2Bsjz3x%?&e<7ZQOamy%7J-_>aj~wfg9iw+NB;C zxU%Mkdv&~F{*R5hwCh?3^9f+;Xsr`}L!ii2EHm4$L<>9%mhCpl;rjB$9ns_G(W~WC zsv(^eWy0P~3&{N&U;qP@eT4eb^F9Ro2?D%Vfkx_as(kQP3FwZR)Ip!>xLh&rH}=dK zcfdX8LpT;^YPXlIzEx7eDNQQ6Z2;;;G=l!QaUs>U5b0H9i-zs4-Y-5o^GC4%D~j0D6%Zk zoeP}AlIb>gzzb}qMtCq~Uey0LbtK27wyc43w$ha78tb|oxGS|mG$JMWC+du`d)td! zgEHlS-%>}y`OkJ7q0Hv64v=Mzh;G&V9)#tODF+>QJS(u2@~J`8N+{&3j{lcmh_!`` zd=5h}W6XcpktKkAbsFmG>2i<9LQhM|@`zsKECL}Ke@|du-eSR0z%wP;5yRXHP^NTV z*SDVkKYB%xaRkHLhgVN+XV2HZ-(kIWwidJuDgP-u&;5+dlFL_S>{;1SP43WL|PTef)8QS>*KWv${k0o7> z<`u-fJQ^=UX`Bxf^|c#F8Y8_g4~CJN=;E|D+%SgFM1so{?+bS31qr-pZgE~(p@>Q= zh0!~=DZ*E_(ICy?hr=~pmtw)4K0Cf!FIPm;N9oM_rP9;(;`blCF=j_DU5uIN9i|k? zb%tiSjjmi`z$J<7804(ao2&C&Wd<8<2nr&LUMd)SVgIe^G7C_Z7FyTAOw-13epU={ zu4QI0g2d1a{qp{y&(ahqHN9Z};b;<2XZxGf@+J$M-BVyD!ua3JZeC`-U$*~C9ey~~ zh!?ocK@`S}ng^XMvK^8@q_7u}F`=WYS`AT_NHUGMki}FA%n>=G10w6_6f8>>C;I@~1Gbdf8q@u|AP&CPniNQR$5}Ncj`7z~i;G-EqNr_;$oRS5@gs`l z@_6)F+mjyFI+!Pty5jOcz-Kh3U@|EK_P$cDAoxM|f<5SDa}522mfS?{(&aiD;mLw~ zZzFus#}t&DG{so|xTaW0et9V231(}La_$#q6Uhgessmv0i1$ylZyTxiY#Qxnz`fy% z7lOU6j<>ls^(a+`cR5_y4%*k_xdN7<20JUGpQOrvG64!PvU35r?I93Tc{*BN%8?fF zsMP(9%3gwP&Ib%f_!lRzR)C_%7Cjgu7&2NPgN*3RR%-L2# zrLiV?abDHnH7aUf5T9M3S?3KxgT>K|BceyNVk8QJX2Nzr)b->OAzrLs`Rvnf;;DpC zWF7Uxz9oD)WgBER%UH-e!LvWOKS&h@-Nm zB<5EI-_`jf?1kRH2QD`XLuk%SHrD6~X8Vn6?rrK$kiU{iyS;~fH_ z++;P?>cX0BG$5c6rljJo#1$|iel;LuO0et*wAJ?8$KP_3E*dPUGd^Zfsq(7YoXhey z-+g9}>HzlfOyv7#DH`0@!Q-j7mr?N-VHh#|+Q5j1G4B>tr3xBZRYvKD`mgQw9g(Xw zln99Y9o)|oyO!M%(JSWVJ80rHp9T;%f>z+LT(NGPEDu~s-VWS)TC}Avtxb=&s<&_W zO|S~Uqx*{)cHG=Ed_kWV^;aJe;_*~pR53*Sw?x0;LX(sdwtbKoUMQuhkO*p|%e(?t zDTNH}fc%~ek1YjO+;8^HXSfXPDs6P5q`qT^dBHVlS9~|Sb~*EY-L3CW48CnjzV`Ql zsgcxfacQR(&k@2}*DB+_*fM4iOopgiltf7&n^xyp#f?j@_yY8~~oa%`3a_S4_j z3>_pe8|&p%{;$2Q5=fQp697@R+hKUvijxUwAtwPtEanwt8_#x0k{#x*fJbrN%x$0f zuVpROG2$Kx@-sGKJ~OUesuVA;uRV{hJd6okh3soHr=y{CeJO&+(?RD01&cP@Y4tQw}`fdJ0%#LgUkKZ0b)7x@+{7^z|fg*WXqk@SJ$ zlLu~QtL!tGuXv^cZJdU(&oF_kWP}*;rUkzOrG(NbX*3Z0;cEa1d_qeXnS=8Xs>Zhc zbfM7dqI|2MV_x8<0d_+4tIu5?Ybyug~|siB!dOiBDBPSDdlPKk4; zcE>9)XzPkGLfDlc5N-!_?iG}`K$&dymsn{tqvBiGX=wfF%h#?`#f8AQfOz%jy6$07 zWO!A+pp_7FrzLS}shcY#eD3eB`Q|l`=MU>qpPR7&vRZedL0kzwGun4vSr8`CugJ@u zmD>1T(Cgjp#0>Dhw+ z331|*Ow+5MTrhN`tXYTN;+=GY=dxY`XzW#oCibogQ&kj_vu3bY))xepl}g>oX_l2P z3mmjKDlTnpAO6%V+iy{{W2o{7vp&Y`Bg*vWM+@1chk+t)N zel)l9+5N#->Z}OSSaW=hN;DPPo&_-;77W5maL+ZTg{}P~?^?CG(B8bS14*3iabOe< z8R=z};7>Wr??-_Dq<|@SA2VFCmAzpvVQ2bg-1Gq}1lqMIPZYKYwk?67$W&j3sxN&EE~!4QoRVjK&m61Wq68s+K)ejsmEU6>TI*Cn*+q#wSWW zb>Qg^nPMZ2j8F!W ze%S!p0dKhz;xdv>f`Ac+ zFJi#|8%2dGiR?`6fT*Mq^(q(|M?7#Z=!9;J%sKs?2F}!1F9(AP(wvl(#)BC!J0+r>ZIT6Y=`Kq>x#sgdpUJM6g!K zHn2-;PMKNQ@5@bLa<)TF&dH4qDj4kL8AZ@wvx{oRL75JrjHeve2hTNW=QyG=PboS|QCmb@Y0nn`tR!CwyCPr58YtM5W+IsQ{Z7~jc$Mb~5 zw#Z5a+DS?O7m&vvk^D992y9+#Uak9E0h8iD!hMpCfY?%Nv+6@Qc-(D@zV;W`efSWFdE^<+ORFRxDHtX$0_0n` zeCdMn_z-jb6L)3+K3^1sl8HnEECcHZyhKT}DIBfQ*a1^tM!^+pavQH*1jqr!ftp6v zTC!9hZ|$)7gIOxglzMiH4_>hJ(<)e?C31}(p(1-R9sDNWtEl&><)h(rL2uoJsbo5T zhy?&u8d(nEj|y(QVuDiNL-gY&1JfS@TOKa^rL;bX%EZteypB`FqL%<(Sp-B1MV{mF@%X(ni^WHWzLzY%x z@jpu390DyF#U6UUeOjnMEC zfcN|l?M7T27Ng(CZn)+I|7?$EbVYmQ0{hq1Nzfc;w$U~~iqEU9-YfIi?_+#nJX(o< zjqij4VJ0$Vzf^rqid+e>M+K zw5i6(-zoqAu&n8N^B00Bbc7kv21{|GXB$=dzIKLqL@IO*^RKfb@>P~wR(9C$yZ*Mg z=xEJJgp{Xk#Bp8W?Yc+Yp?B(}qQ0fK3$pU)j3ACZNSZCgvEioSD8DU9Ez4{7@c%KE z zFL7!)UTYOKWV*>j$;=5u2Iq@!*^jJCrW?Y*els4>ZBS9@cYar=(yKwCHXU%)hsQA| zWrWC6VEbT{asgWi1{)^@4tq6;f;7NR*<;Hjs*d}Q0JfI8(x|<~3UPJ8szWHk&M&#BKdcT#zRH|a%?{uwaSlUPzh7Gq+HpTv5mCadwt(Mrrp zl9l4IdrJ~nHhKy)7+U^Dj#B$LMxay9D>*ks#a8jcjtr5Z^QoaqMY;^!&Tct_eaKy2Sh{(K<)Lq&xy(`C zI1f>*!vq13#{fO*SF?y6CboDDy>rgaKJ4AUIUoTk`{p^hTEJS_w(Ij!7$ulaYys@h*-i{Z9%1R_S~=cn9><3V5C$**zJ9QgG2gLUG< zB#Ou#7an-kNR-{o5H?}CX0^ymlDg)BmTuXZZ{~(c%QG+LkhG0hff#_Dn|dB6z3w-? z;1_@Z2DurSE@E8|r`Ber*obQ6uDO5O)o#xR5~Z&oFY?<{`ggf$*9?R%5&`?f!e= z-0c^llUIKSd?H_3%#}~poyCFeT ztQpGgQ5B9mUph2#GN_D>E*#b}$?k=*g_tf-C#-TiQQ^$sEA^N+X(!}$=@;KsxP?p5FOh_hf(YL3O1oO=KZJ$*6jF#dhn(qxuC03j_B9C{xeY-k*c z0e*{nyZ{%Uw!|8-A1}7KeJ<&*ol)26S--944_+NmH>iSZ-6S1RBlLj(B z87ZO8(_Ozn83Sd({+uM~t>W2?@J=txr30N78E|=)~)1yd8rgARL$@XVV-W#U&MG5(PKb#bdJ-8{`{?T7_wGa zNv}5fZ@V!kmY#P!keVCvz~7O?39%0V17TnSykF{~ESaOCR)oJEnU7f2VzpYM335W3 zbMZ0{Ygw*^T9FJfV^FN!1*R#PvTA3SC1_nw#Qg(>BO@jX)0eEubBw~d^iMqUqQS>6 ziiP<6gI~)oi9)v8=T4GtqeXD$x-BIWX<{_!M5QA#6xKmk{&UGHGgbJ8Z~y=cQUQ}0 zx~ODseOTTVZJ1s$q}KnaaCO~gyH*l!^=VBGP))-H854CWn7Z!^J;$N3;HJ0J^nPHJlv}-!z3`hArM(@vlVn=d^UPm@b8(qj!~AqdHn?_i zSdQz`%e3?6R!8$(Z~Mebtr>}zC_bcnGA;=teC`5fXbdGb8d zl9Jak;%yITN0iB?{nSpmj@0l>tJ(oqw^tZ(5ZTNgZmq|j=m;Zcw0&w>e-Qie7$j~x=>Id{9jY%g8Nm)0^L%u{XidlznJ0ux;TRYf zl8+$=)MKgLPQPk0g^A&Fh+}q4i*@A{4pLI%U8iWL(?Dk4ctVj6x{ReR`GNroZ{wh~ zT$pX1SH!GV@8G($UXG-@qkuZ&@;sm92w%-wOYCcxq~rt#DHfFeYWQK&82A27@jf%% zG!T%u{YLI#jiFmxfBQyYd(ixB(-)GTJDhI3xn!D4<*{U~1s;b=s-30Y8fKt=^6k0k zAUe+@%uo-=3T1HCAZ<^_TEDN$NN*@vk?NMjHf#p*=HD&21S-7un}SM&2GPeEabMUb*`KRQv_MtGI>j#DvVAsUwGz z6~Z9d>lpDH84vNM0XI3}8~aVgUTUAPq%y50w6rcp{*BW}9E>@IzP9N1;DiNLR{19E z718WAt_93{x(-E^ZDvXBk!K^r4t}ubwNt)nQ?2YlykIY1a7)HLt=g&Oo!vn5Gk`+n zqY!`qZohf5ap=q@X@HxC8IN@{NwJ6CtEc?^rh#;MHFW2CPWY2?YIpd#@rZ_Ya3(Qa zndX<; move: (fen: string) => Promise; } +export function netUrl(name: string) { + return `/assets/lifat/bots/weights/${name}`; +} + +export function imageUrl(name: string) { + return `/assets/lifat/bots/images/${name}`; +} + export const bots = [ { name: 'Coral', description: 'Coral is a simple bot that plays random moves.', - image: '/assets/images/bots/coral.webp', + image: '/lifat/bots/images/coral.webp', }, { name: 'Baby Howard', description: 'Baby Howard is a bot that plays random moves.', - image: '/assets/images/bots/babyHoward.webp', + image: '/lifat/bots/images/baby-howard.webp', }, { name: 'Baby Bot', description: 'Baby Bot is a bot that plays random moves.', - image: '/assets/images/bots/babyBot.webp', + image: '/lifat/bots/images/baby-robot.webp', }, { name: 'Beatrice', description: 'Beatrice is a bot that plays random moves.', - image: '/assets/images/bots/beatrice.webp', + image: '/lifat/bots/images/beatrice.webp', }, ]; + +/*function linesWithin(move: string, lines: PV[], bias = 0, threshold = 50) { + const zeroScore = lines.find(line => line.moves[0] === move)?.score ?? Number.NaN; + return lines.filter(fish => Math.abs(fish.score - bias - zeroScore) < threshold && fish.moves.length); +} + +function randomSprinkle(move: string, lines: PV[]) { + lines = linesWithin(move, lines, 0, 20); + if (!lines.length) return move; + return lines[Math.floor(Math.random() * lines.length)].moves[0] ?? move; +} + +function occurs(chance: number) { + return Math.random() < chance; +}*/ diff --git a/ui/localPlay/src/bots/coral.ts b/ui/localPlay/src/bots/coral.ts index 3548e5592fbe8..56b24e33a89a1 100644 --- a/ui/localPlay/src/bots/coral.ts +++ b/ui/localPlay/src/bots/coral.ts @@ -1,19 +1,22 @@ -import makeZerofish, { Zerofish, PV } from 'zerofish'; -import { Bot } from '../bot'; +import makeZerofish, { type Zerofish } from 'zerofish'; +import { Bot, netUrl } from '../bot'; export class CoralBot implements Bot { - name: 'Coral'; - description: 'Coral is a simple bot that plays random moves.'; - image: '/lifat/bots/images/coral.webp'; - weightsUrl: '/lifat/bots/weights/maia1100.pb'; + name = 'Coral'; + description = 'Coral is a simple bot that plays random moves.'; + image = 'coral.webp'; + net = 'maia-1100.pb'; + ratings = new Map(); zf: Zerofish; constructor() { - makeZerofish({ weightsUrl }).then(zf => this.setZf(zf)); + makeZerofish({ pbUrl: netUrl(this.net) }).then(zf => this.setZf(zf)); } setZf(zf: Zerofish) { this.zf = zf; this.zf; } - move(fen: string) {} + async move(fen: string) { + return await this.zf.goZero(fen); + } } diff --git a/ui/localPlay/src/ctrl.ts b/ui/localPlay/src/ctrl.ts index 5f6d4be3ffdca..d470b6ec71b8c 100644 --- a/ui/localPlay/src/ctrl.ts +++ b/ui/localPlay/src/ctrl.ts @@ -1,4 +1,6 @@ import { LocalPlayOpts } from './interfaces'; +import { Bot } from './bot'; +import { CoralBot } from './bots/coral'; import { makeRounds } from './data'; import { makeFen /*, parseFen*/ } from 'chessops/fen'; import { makeSanAndPlay } from 'chessops/san'; @@ -6,7 +8,7 @@ import { Chess } from 'chessops'; import * as Chops from 'chessops'; export class Ctrl { - path = ''; + bot?: Bot = new CoralBot(); chess = Chess.default(); tellRound: SocketSend; fiftyMovePly = 0; @@ -16,7 +18,7 @@ export class Ctrl { makeRounds(this).then(sender => (this.tellRound = sender)); } - set(/*fen: string*/) { + reset(/*fen: string*/) { this.fiftyMovePly = 0; this.threefoldFens.clear(); this.chess.reset(); @@ -66,13 +68,16 @@ export class Ctrl { } async botMove() { - const uci = (await this.zf!.goFish(this.fen, { depth: 10 }))[0].moves[0]; - this.move(uci); + this.move(await this.bot!.move(this.fen)); } fifty(move?: Chops.Move) { if (move) - if (!('from' in move) || this.chess.board.getRole(move.from) === 'pawn' || this.chess.board.get(move.to)) + if ( + !('from' in move) || + this.chess.board.getRole(move.from) === 'pawn' || + this.chess.board.get(move.to) + ) this.fiftyMovePly = 0; else this.fiftyMovePly++; return this.fiftyMovePly >= 100; @@ -109,19 +114,3 @@ export class Ctrl { return 2 * (this.chess.fullmoves - 1) + (this.chess.turn === 'black' ? 1 : 0); } } - -function linesWithin(move: string, lines: PV[], bias = 0, threshold = 50) { - const zeroScore = lines.find(line => line.moves[0] === move)?.score ?? Number.NaN; - return lines.filter(fish => Math.abs(fish.score - bias - zeroScore) < threshold && fish.moves.length); -} - -function randomSprinkle(move: string, lines: PV[]) { - lines = linesWithin(move, lines, 0, 20); - if (!lines.length) return move; - return lines[Math.floor(Math.random() * lines.length)].moves[0] ?? move; -} - -/* -function occurs(chance: number) { - return Math.random() < chance; -}*/ diff --git a/ui/localPlay/src/data.ts b/ui/localPlay/src/data.ts index a22ddfc47c05a..28aa593e3e6f8 100644 --- a/ui/localPlay/src/data.ts +++ b/ui/localPlay/src/data.ts @@ -49,7 +49,7 @@ const data: RoundData = { onGame: true, rating: 800, version: 0, - image: '/assets/images/bots/baby-howard.webp', + image: '/assets/lifat/bots/images/coral.webp', }, pref: { animationDuration: 300, diff --git a/ui/localPlay/src/view.ts b/ui/localPlay/src/view.ts index 8a395b01f3204..d9dc0fd5e03d2 100644 --- a/ui/localPlay/src/view.ts +++ b/ui/localPlay/src/view.ts @@ -1,7 +1,7 @@ import { h, VNode } from 'snabbdom'; //import * as licon from 'common/licon'; //import { bind } from 'common/snabbdom'; -import { bots } from './bots'; +import { bots } from './bot'; import { Ctrl } from './ctrl'; export default function (ctrl: Ctrl): VNode { @@ -21,6 +21,6 @@ function botView(ctrl: Ctrl, bot: any): VNode { return h('div.fancy-bot', [ h('h1', bot.name), h('p', bot.description), - h('img', { attrs: { src: bot.image } }), + h('img', { attrs: { src: lichess.assetUrl(bot.image, { noVersion: true }) } }), ]); } diff --git a/ui/round/package.json b/ui/round/package.json index f9be4cd97ba75..1e6ff59492395 100644 --- a/ui/round/package.json +++ b/ui/round/package.json @@ -23,7 +23,8 @@ "voice": "workspace:*", "nvui": "workspace:*", "board": "workspace:*", - "snabbdom": "^3.5.1" + "snabbdom": "^3.5.1", + "zerofish": "0.0.11" }, "scripts": { "compile": "tsc", diff --git a/ui/round/src/ctrl.ts b/ui/round/src/ctrl.ts index 204250155f1a7..48bfc9668b5bf 100644 --- a/ui/round/src/ctrl.ts +++ b/ui/round/src/ctrl.ts @@ -34,6 +34,9 @@ import * as wakeLock from 'common/wakeLock'; import { opposite, uciToMove } from 'chessground/util'; import * as Prefs from 'common/prefs'; +import makeZerofish, { Zerofish } from 'zerofish'; +import * as Ch from 'chess'; + import { RoundOpts, RoundData, @@ -92,8 +95,9 @@ export default class RoundController { sign: string = Math.random().toString(36); keyboardHelp: boolean = location.hash === '#keyboard'; + zerofish?: Zerofish; + constructor(readonly opts: RoundOpts, readonly redraw: Redraw, readonly nvui?: NvuiPlugin) { - console.log('rounds', JSON.stringify(opts.i18n, undefined, 2)); round.massage(opts.data); const d = (this.data = opts.data); @@ -163,6 +167,16 @@ export default class RoundController { if (!this.opts.noab && this.isPlaying()) ab.init(this); lichess.sound.move(); + + makeZerofish({ pbUrl: '/assets/lifat/bots/weights/maia-1100.pb' }).then(zf => (this.zerofish = zf)); + } + + private async updateZero(fen: string, canMove: boolean) { + if (!canMove) return; + if (fen.split(' ')[0] === fen) fen += this.ply % 2 === 0 ? ' w' : ' b'; + console.trace('updateZero', fen, canMove); + const uci = await this.zerofish?.goZero(fen); + this.auxMove(uci?.slice(0, 2) as Key, uci?.slice(2, 4) as Key, Ch.charRole(uci!.slice(4))); } private showExpiration = () => { @@ -279,6 +293,8 @@ export default class RoundController { } this.autoScroll(); const canMove = ply === this.lastPly() && this.data.player.color === config.turnColor; + + this.updateZero(s.fen, canMove); this.voiceMove?.update(s.fen, canMove); this.keyboardMove?.update(s), canMove; lichess.pubsub.emit('ply', ply); @@ -497,6 +513,8 @@ export default class RoundController { } this.autoScroll(); this.onChange(); + + this.updateZero(step.fen, playedColor != d.player.color); this.keyboardMove?.update(step, playedColor != d.player.color); this.voiceMove?.update(step.fen, playedColor != d.player.color); lichess.sound.move(o); @@ -534,6 +552,7 @@ export default class RoundController { this.autoScroll(); this.onChange(); this.setLoading(false); + this.updateZero(d.steps[d.steps.length - 1].fen, true); this.keyboardMove?.update(d.steps[d.steps.length - 1]); this.voiceMove?.update(d.steps[d.steps.length - 1].fen, true); }; diff --git a/ui/round/src/transientMove.ts b/ui/round/src/transientMove.ts index fd42671bb3fc7..bbc26896096f3 100644 --- a/ui/round/src/transientMove.ts +++ b/ui/round/src/transientMove.ts @@ -24,7 +24,8 @@ export default class TransientMove { }; expire = () => { - xhr.text('/statlog?e=roundTransientExpire', { method: 'post' }); + if (window.location.href.startsWith('https://lichess')) + xhr.text('/statlog?e=roundTransientExpire', { method: 'post' }); this.socket.reload(); }; } From decdd74951fa2921d110580db46f7f2d240721e7 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Wed, 9 Aug 2023 17:27:36 -0500 Subject: [PATCH 020/174] gah --- conf/base.conf | 1 + ui/round/src/ctrl.ts | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/conf/base.conf b/conf/base.conf index d4367519b46d3..1a06c08916d17 100644 --- a/conf/base.conf +++ b/conf/base.conf @@ -388,6 +388,7 @@ fishnet { actor.name = fishnet analysis.nodes = 1500000 # sf 15+ move.plies = 300 + # move.delay = 0 seconds - specify in application.conf to override delay calculation client_min_version = "2.1.3" explorerEndpoint = ${explorer.endpoint} } diff --git a/ui/round/src/ctrl.ts b/ui/round/src/ctrl.ts index 48bfc9668b5bf..44065a7aec0e1 100644 --- a/ui/round/src/ctrl.ts +++ b/ui/round/src/ctrl.ts @@ -168,15 +168,18 @@ export default class RoundController { lichess.sound.move(); - makeZerofish({ pbUrl: '/assets/lifat/bots/weights/maia-1100.pb' }).then(zf => (this.zerofish = zf)); + makeZerofish({ pbUrl: '/assets/lifat/bots/weights/maia-1900.pb' }).then(zf => (this.zerofish = zf)); } - private async updateZero(fen: string, canMove: boolean) { - if (!canMove) return; + private async updateZero(fen: string) { + if (this.ply !== this.lastPly() || this.data.player.color !== (this.ply % 2 === 0 ? 'white' : 'black')) + return; if (fen.split(' ')[0] === fen) fen += this.ply % 2 === 0 ? ' w' : ' b'; - console.trace('updateZero', fen, canMove); const uci = await this.zerofish?.goZero(fen); - this.auxMove(uci?.slice(0, 2) as Key, uci?.slice(2, 4) as Key, Ch.charRole(uci!.slice(4))); + if (uci) + this.sendMove(uci?.slice(0, 2) as Key, uci?.slice(2, 4) as Key, Ch.charRole(uci!.slice(4)), { + premove: false, + }); } private showExpiration = () => { @@ -294,7 +297,7 @@ export default class RoundController { this.autoScroll(); const canMove = ply === this.lastPly() && this.data.player.color === config.turnColor; - this.updateZero(s.fen, canMove); + this.updateZero(s.fen); this.voiceMove?.update(s.fen, canMove); this.keyboardMove?.update(s), canMove; lichess.pubsub.emit('ply', ply); @@ -514,7 +517,7 @@ export default class RoundController { this.autoScroll(); this.onChange(); - this.updateZero(step.fen, playedColor != d.player.color); + this.updateZero(step.fen); //, playedColor != d.player.color); this.keyboardMove?.update(step, playedColor != d.player.color); this.voiceMove?.update(step.fen, playedColor != d.player.color); lichess.sound.move(o); @@ -552,7 +555,7 @@ export default class RoundController { this.autoScroll(); this.onChange(); this.setLoading(false); - this.updateZero(d.steps[d.steps.length - 1].fen, true); + this.updateZero(d.steps[d.steps.length - 1].fen); //, true); this.keyboardMove?.update(d.steps[d.steps.length - 1]); this.voiceMove?.update(d.steps[d.steps.length - 1].fen, true); }; @@ -912,7 +915,7 @@ export default class RoundController { location.href = '/page/play-extensions'; } }, 1000); - + this.updateZero(d.game.fen); this.onChange(); }, 800); }; From e16c2ae680604023e622a5edeec51e2d0302dedf Mon Sep 17 00:00:00 2001 From: Jonathan Gamble <101470903+schlawg@users.noreply.github.com> Date: Wed, 9 Aug 2023 21:39:11 -0500 Subject: [PATCH 021/174] improve puzzle speech (english only) --- ui/puzzle/src/ctrl.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ui/puzzle/src/ctrl.ts b/ui/puzzle/src/ctrl.ts index 8fa13fa96b71f..d2e723d8c76eb 100644 --- a/ui/puzzle/src/ctrl.ts +++ b/ui/puzzle/src/ctrl.ts @@ -151,7 +151,6 @@ export default function (opts: PuzzleOpts, redraw: Redraw): Controller { }); instanciateCeval(); - lichess.sound.move(); } function position(): Chess { @@ -261,11 +260,12 @@ export default function (opts: PuzzleOpts, redraw: Redraw): Controller { withGround(g => g.playPremove()); const progress = moveTest(vm, data.puzzle); + if (progress === 'fail') lichess.sound.say('incorrect'); if (progress) applyProgress(progress); reorderChildren(path); redraw(); - lichess.sound.saySan(node.san, false); - lichess.sound.move(node); + //lichess.sound.saySan(node.san, false); + //lichess.sound.move(node); } function reorderChildren(path: Tree.Path, recursive?: boolean): void { @@ -280,8 +280,11 @@ export default function (opts: PuzzleOpts, redraw: Redraw): Controller { } function instantRevertUserMove(): void { - withGround(g => g.cancelPremove()); - userJump(treePath.init(vm.path)); + withGround(g => { + g.cancelPremove(); + g.selectSquare(null); + }); + jump(treePath.init(vm.path)); redraw(); } @@ -477,6 +480,9 @@ export default function (opts: PuzzleOpts, redraw: Redraw): Controller { withGround(showGround); if (pathChanged) { if (isForwardStep) { + lichess.sound.saySan(vm.node.san); + lichess.sound.move({ uci: vm.node.uci }); + // TODO - consolidate piece move handling for music & other sound sets if (!vm.node.uci) sound.move(); // initial position else if (!vm.justPlayed || vm.node.uci.includes(vm.justPlayed)) { @@ -501,12 +507,6 @@ export default function (opts: PuzzleOpts, redraw: Redraw): Controller { if (tree.nodeAtPath(path)?.puzzle == 'fail' && vm.mode != 'view') return; withGround(g => g.selectSquare(null)); jump(path); - lichess.sound.move(vm.node); - if (vm.mode !== 'view') lichess.sound.say('Is not the move, try something else!'); - else { - lichess.sound.saySan(vm.node.san, true); - lichess.sound.say('Is the correct move!'); - } } function userJumpPlyDelta(plyDelta: Ply) { From eb0fcd82a0798976da9973f8748e7c636d3a77e5 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Thu, 10 Aug 2023 10:25:00 -0500 Subject: [PATCH 022/174] audio context resume - increase our odds of getting one by 0.001% --- ui/site/src/component/sound.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ui/site/src/component/sound.ts b/ui/site/src/component/sound.ts index e7dcaf579b8fc..a29575b9b0ae5 100644 --- a/ui/site/src/component/sound.ts +++ b/ui/site/src/component/sound.ts @@ -200,20 +200,21 @@ export default new (class implements SoundI { } } // if suspended, try audioContext.resume() with a timeout (sometimes it never resolves) - if (this.ctx?.state === 'suspended') + if (this.ctx?.state === 'suspended') { + const ctxResume = this.ctx.resume(); await new Promise(resolve => { const resumeTimer = setTimeout(() => { $('#warn-no-autoplay').addClass('shown'); resolve(); }, 400); - this.ctx - ?.resume() + ctxResume .then(() => { clearTimeout(resumeTimer); resolve(); }) .catch(resolve); }); + } return this.ctx?.state === 'running'; } From 3109cc4a48732cc2d905255038867c0cace04985 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Wed, 16 Aug 2023 12:49:29 -0500 Subject: [PATCH 023/174] ephemeral help tooltip link to schlawg npm packages so things build fix jest config, only test js --- app/controllers/Main.scala | 9 +- app/controllers/Puzzle.scala | 2 +- app/controllers/Round.scala | 2 +- app/controllers/UserAnalysis.scala | 2 +- .../site/{helpModal.scala => help.scala} | 24 +- conf/routes | 1 + package.json | 2 +- pnpm-lock.yaml | 2598 ++++++----------- ui/analyse/css/build/_analyse.keyboard.scss | 2 +- ui/analyse/src/autoShape.ts | 2 +- ui/analyse/src/ctrl.ts | 1 + ui/analyse/src/keyboard.ts | 28 +- .../{_help-modal.scss => _help.scss} | 92 +- ui/common/css/theme/_default.scss | 5 + ui/common/package.json | 2 +- ui/jest.config.js | 12 +- .../css/build/_keyboardMove.help.scss | 2 +- ui/opening/package.json | 2 +- ui/opening/src/main.ts | 4 +- ui/package.json | 13 +- ui/puzzle/css/build/_puzzle.keyboard.scss | 2 +- ui/round/css/build/_round.keyboard.scss | 2 +- ui/site/package.json | 2 +- ui/tutor/package.json | 2 +- ui/voice/css/build/_voiceMove.help.scss | 2 +- 25 files changed, 984 insertions(+), 1831 deletions(-) rename app/views/site/{helpModal.scala => help.scala} (93%) rename ui/common/css/component/{_help-modal.scss => _help.scss} (55%) diff --git a/app/controllers/Main.scala b/app/controllers/Main.scala index 9704b62fd3702..098c1b6519998 100644 --- a/app/controllers/Main.scala +++ b/app/controllers/Main.scala @@ -114,13 +114,16 @@ final class Main( pageHit NotImplemented.page(html.site.message.temporarilyDisabled) + def analyseShiftKeyHelp = Open: + Ok.page(html.site.help.analyseShiftKey) + def keyboardMoveHelp = Open: - Ok.page(html.site.helpModal.keyboardMove) + Ok.page(html.site.help.keyboardMove) def voiceHelp(module: String) = Open: module match - case "move" => Ok.page(html.site.helpModal.voiceMove) - case "coords" => Ok.page(html.site.helpModal.voiceCoords) + case "move" => Ok.page(html.site.help.voiceMove) + case "coords" => Ok.page(html.site.help.voiceCoords) case _ => NotFound(s"Unknown voice help module: $module") def movedPermanently(to: String) = Anon: diff --git a/app/controllers/Puzzle.scala b/app/controllers/Puzzle.scala index 4323d54015cd5..7ff289f202c1c 100644 --- a/app/controllers/Puzzle.scala +++ b/app/controllers/Puzzle.scala @@ -500,7 +500,7 @@ final class Puzzle(env: Env, apiC: => Api) extends LilaController(env): } def help = Open: - Ok.page(html.site.helpModal.puzzle) + Ok.page(html.site.help.puzzle) private def DashboardPage(username: Option[UserStr])(f: Context ?=> User => Fu[Result]) = Auth { ctx ?=> me ?=> diff --git a/app/controllers/Round.scala b/app/controllers/Round.scala index ff7076e82690c..fc6f4f348c0df 100644 --- a/app/controllers/Round.scala +++ b/app/controllers/Round.scala @@ -305,4 +305,4 @@ final class Round( } def help = Open: - Ok.page(html.site.helpModal.round) + Ok.page(html.site.help.round) diff --git a/app/controllers/UserAnalysis.scala b/app/controllers/UserAnalysis.scala index 562504548761b..89dbb2fd7af49 100644 --- a/app/controllers/UserAnalysis.scala +++ b/app/controllers/UserAnalysis.scala @@ -180,4 +180,4 @@ final class UserAnalysis( def help = Open: Ok.page: - html.site.helpModal.analyse(getBool("study")) + html.site.help.analyse(getBool("study")) diff --git a/app/views/site/helpModal.scala b/app/views/site/help.scala similarity index 93% rename from app/views/site/helpModal.scala rename to app/views/site/help.scala index 61acbb16d8489..4b73b2b5f68f8 100644 --- a/app/views/site/helpModal.scala +++ b/app/views/site/help.scala @@ -5,7 +5,7 @@ import play.api.i18n.Lang import lila.app.templating.Environment.{ *, given } import lila.app.ui.ScalatagsTemplate.{ *, given } -object helpModal: +object help: private def header(text: Frag) = tr(th(colspan := 2)(p(text))) private def row(keys: Frag, desc: Frag) = tr(td(cls := "keys")(keys), td(cls := "desc")(desc)) @@ -66,8 +66,8 @@ object helpModal: table( tbody( navigateMoves, - row(frag(kbd("shift"), kbd("←"), or, kbd("shift"), kbd("→")), trans.keyEnterOrExitVariation()), - row(frag(kbd("shift"), kbd("J"), or, kbd("shift"), kbd("K")), trans.keyEnterOrExitVariation()), + row(kbd("shift"), "Cycle selected move arrow"), + row(frag(kbd("shift"), kbd("←"), or, kbd("shift"), kbd("J")), "Rewind to mainline"), header(trans.analysisOptions()), flip, row(frag(kbd("shift"), kbd("I")), trans.inlineNotation()), @@ -106,6 +106,24 @@ object helpModal: ) ) + def analyseShiftKey(using Lang) = + frag( + div(cls := "help-ephemeral")( + ul( + li("Purple arrow is mainline move"), + li("Pink arrows are variations"), + li("Blue arrow is eval best move") + ), + table( + tbody( + row(kbd("shift"), "cycle selected move arrow"), + row(kbd("→"), "play selected move"), + row(span(kbd("shift"), or, kbd("←")), "return to previous mainline move") + ) + ) + ) + ) + def keyboardMove(using Lang) = import trans.keyboardMove.* frag( diff --git a/conf/routes b/conf/routes index b91056685674d..d5ff01a887f24 100644 --- a/conf/routes +++ b/conf/routes @@ -850,6 +850,7 @@ GET /variant/:key controllers.ContentPage.variant(key) # Help GET /help/contribute controllers.ContentPage.help GET /help/master controllers.ContentPage.master +GET /help/analyse/shift-key controllers.Main.analyseShiftKeyHelp GET /help/keyboard-move controllers.Main.keyboardMoveHelp GET /help/voice/:module controllers.Main.voiceHelp(module) # DGT diff --git a/package.json b/package.json index e36d773dcf8fd..19ddf8d048848 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@typescript-eslint/eslint-plugin": "^6.4.0", "@typescript-eslint/parser": "^6.4.0", "ab": "github:lichess-org/ab-stub", - "chessground": "^9.0.1", + "chessground": "npm:schlawground@9.0.1", "eslint": "^8.47.0", "lint-staged": "^14.0.0", "prettier": "^3.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25a893868d271..00b723ff03548 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: github:lichess-org/ab-stub version: github.com/lichess-org/ab-stub/94236bf34dbc9c05daf50f4c9842d859b9142be0 chessground: - specifier: link:../chessground - version: link:../chessground + specifier: npm:schlawground@9.0.1 + version: /schlawground@9.0.1 eslint: specifier: ^8.47.0 version: 8.47.0 @@ -47,27 +47,18 @@ importers: ui: dependencies: - '@babel/core': - specifier: ^7.17.10 - version: 7.20.5 - '@babel/preset-env': - specifier: ^7.17.10 - version: 7.20.2(@babel/core@7.20.5) '@types/jest': - specifier: ^28.1.6 - version: 28.1.8 + specifier: ^29.5.3 + version: 29.5.3 breakpoint-sass: - specifier: ^2.7.1 - version: 2.7.1 + specifier: ^3.0.0 + version: 3.0.0(sass@1.65.1) jest: - specifier: ^28.1.3 - version: 28.1.3(@types/node@20.5.0) + specifier: ^29.6.2 + version: 29.6.2(@types/node@20.5.0) jest-environment-jsdom: - specifier: ^28.1.3 - version: 28.1.3 - ts-jest: - specifier: ^28.0.7 - version: 28.0.8(@babel/core@7.20.5)(jest@28.1.3)(typescript@5.1.6) + specifier: ^29.6.2 + version: 29.6.2 ui/@build: dependencies: @@ -272,8 +263,8 @@ importers: specifier: workspace:* version: link:../@types/lichess lichess-pgn-viewer: - specifier: '>1.6.0' - version: 1.6.1 + specifier: npm:schlawg-pgn-viewer@2.0.0 + version: /schlawg-pgn-viewer@2.0.0 snabbdom: specifier: ^3.5.1 version: 3.5.1 @@ -563,8 +554,8 @@ importers: specifier: ^3.1.2 version: 3.1.2 lichess-pgn-viewer: - specifier: '>1.6.0' - version: 1.6.1 + specifier: npm:schlawg-pgn-viewer@2.0.0 + version: /schlawg-pgn-viewer@2.0.0 ui/palantir: dependencies: @@ -791,8 +782,8 @@ importers: specifier: ^0.3.1 version: 0.3.1 lichess-pgn-viewer: - specifier: '>1.6.0' - version: 1.6.1 + specifier: npm:schlawg-pgn-viewer@2.0.0 + version: /schlawg-pgn-viewer@2.0.0 prop-types: specifier: ^15.8.1 version: 15.8.1 @@ -950,8 +941,8 @@ importers: specifier: workspace:* version: link:../common lichess-pgn-viewer: - specifier: '>1.6.0' - version: 1.6.1 + specifier: npm:schlawg-pgn-viewer@2.0.0 + version: /schlawg-pgn-viewer@2.0.0 ui/voice: dependencies: @@ -992,7 +983,7 @@ packages: engines: {node: '>=6.0.0'} dependencies: '@jridgewell/gen-mapping': 0.1.1 - '@jridgewell/trace-mapping': 0.3.17 + '@jridgewell/trace-mapping': 0.3.19 dev: false /@babel/code-frame@7.18.6: @@ -1002,30 +993,38 @@ packages: '@babel/highlight': 7.18.6 dev: false - /@babel/compat-data@7.20.5: - resolution: {integrity: sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g==} + /@babel/code-frame@7.22.10: + resolution: {integrity: sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.22.10 + chalk: 2.4.2 + dev: false + + /@babel/compat-data@7.22.9: + resolution: {integrity: sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==} engines: {node: '>=6.9.0'} dev: false - /@babel/core@7.20.5: - resolution: {integrity: sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ==} + /@babel/core@7.22.10: + resolution: {integrity: sha512-fTmqbbUBAwCcre6zPzNngvsI0aNrPZe77AeqvDxWM9Nm+04RrJ3CAmGHA9f7lJQY6ZMhRztNemy4uslDxTX4Qw==} engines: {node: '>=6.9.0'} dependencies: '@ampproject/remapping': 2.2.0 - '@babel/code-frame': 7.18.6 - '@babel/generator': 7.20.5 - '@babel/helper-compilation-targets': 7.20.0(@babel/core@7.20.5) - '@babel/helper-module-transforms': 7.20.2 - '@babel/helpers': 7.20.6 - '@babel/parser': 7.20.5 - '@babel/template': 7.18.10 - '@babel/traverse': 7.20.5 - '@babel/types': 7.20.5 + '@babel/code-frame': 7.22.10 + '@babel/generator': 7.22.10 + '@babel/helper-compilation-targets': 7.22.10 + '@babel/helper-module-transforms': 7.22.9(@babel/core@7.22.10) + '@babel/helpers': 7.22.10 + '@babel/parser': 7.22.10 + '@babel/template': 7.22.5 + '@babel/traverse': 7.22.10 + '@babel/types': 7.22.10 convert-source-map: 1.9.0 debug: 4.3.4 gensync: 1.0.0-beta.2 json5: 2.2.2 - semver: 6.3.0 + semver: 6.3.1 transitivePeerDependencies: - supports-color dev: false @@ -1034,204 +1033,99 @@ packages: resolution: {integrity: sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.10 '@jridgewell/gen-mapping': 0.3.2 jsesc: 2.5.2 dev: false - /@babel/helper-annotate-as-pure@7.18.6: - resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.20.5 - dev: false - - /@babel/helper-builder-binary-assignment-operator-visitor@7.18.9: - resolution: {integrity: sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-explode-assignable-expression': 7.18.6 - '@babel/types': 7.20.5 - dev: false - - /@babel/helper-compilation-targets@7.20.0(@babel/core@7.20.5): - resolution: {integrity: sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/compat-data': 7.20.5 - '@babel/core': 7.20.5 - '@babel/helper-validator-option': 7.18.6 - browserslist: 4.21.4 - semver: 6.3.0 - dev: false - - /@babel/helper-create-class-features-plugin@7.20.5(@babel/core@7.20.5): - resolution: {integrity: sha512-3RCdA/EmEaikrhayahwToF0fpweU/8o2p8vhc1c/1kftHOdTKuC65kik/TLc+qfbS8JKw4qqJbne4ovICDhmww==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.19.0 - '@babel/helper-member-expression-to-functions': 7.18.9 - '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/helper-replace-supers': 7.19.1 - '@babel/helper-split-export-declaration': 7.18.6 - transitivePeerDependencies: - - supports-color - dev: false - - /@babel/helper-create-regexp-features-plugin@7.20.5(@babel/core@7.20.5): - resolution: {integrity: sha512-m68B1lkg3XDGX5yCvGO0kPx3v9WIYLnzjKfPcQiwntEQa5ZeRkPmo2X/ISJc8qxWGfwUr+kvZAeEzAwLec2r2w==} + /@babel/generator@7.22.10: + resolution: {integrity: sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-annotate-as-pure': 7.18.6 - regexpu-core: 5.2.2 - dev: false - - /@babel/helper-define-polyfill-provider@0.3.3(@babel/core@7.20.5): - resolution: {integrity: sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==} - peerDependencies: - '@babel/core': ^7.4.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-compilation-targets': 7.20.0(@babel/core@7.20.5) - '@babel/helper-plugin-utils': 7.20.2 - debug: 4.3.4 - lodash.debounce: 4.0.8 - resolve: 1.22.1 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: false - - /@babel/helper-environment-visitor@7.18.9: - resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==} - engines: {node: '>=6.9.0'} - dev: false - - /@babel/helper-explode-assignable-expression@7.18.6: - resolution: {integrity: sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.20.5 - dev: false - - /@babel/helper-function-name@7.19.0: - resolution: {integrity: sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.18.10 - '@babel/types': 7.20.5 + '@babel/types': 7.22.10 + '@jridgewell/gen-mapping': 0.3.2 + '@jridgewell/trace-mapping': 0.3.19 + jsesc: 2.5.2 dev: false - /@babel/helper-hoist-variables@7.18.6: - resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} + /@babel/helper-compilation-targets@7.22.10: + resolution: {integrity: sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/compat-data': 7.22.9 + '@babel/helper-validator-option': 7.22.5 + browserslist: 4.21.10 + lru-cache: 5.1.1 + semver: 6.3.1 dev: false - /@babel/helper-member-expression-to-functions@7.18.9: - resolution: {integrity: sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==} + /@babel/helper-environment-visitor@7.22.5: + resolution: {integrity: sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.20.5 dev: false - /@babel/helper-module-imports@7.18.6: - resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} + /@babel/helper-function-name@7.22.5: + resolution: {integrity: sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/template': 7.22.5 + '@babel/types': 7.22.10 dev: false - /@babel/helper-module-transforms@7.20.2: - resolution: {integrity: sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==} + /@babel/helper-hoist-variables@7.22.5: + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-simple-access': 7.20.2 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/helper-validator-identifier': 7.19.1 - '@babel/template': 7.18.10 - '@babel/traverse': 7.20.5 - '@babel/types': 7.20.5 - transitivePeerDependencies: - - supports-color + '@babel/types': 7.22.10 dev: false - /@babel/helper-optimise-call-expression@7.18.6: - resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==} + /@babel/helper-module-imports@7.22.5: + resolution: {integrity: sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.10 dev: false - /@babel/helper-plugin-utils@7.20.2: - resolution: {integrity: sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==} - engines: {node: '>=6.9.0'} - dev: false - - /@babel/helper-remap-async-to-generator@7.18.9(@babel/core@7.20.5): - resolution: {integrity: sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==} + /@babel/helper-module-transforms@7.22.9(@babel/core@7.22.10): + resolution: {integrity: sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-wrap-function': 7.20.5 - '@babel/types': 7.20.5 - transitivePeerDependencies: - - supports-color + '@babel/core': 7.22.10 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-module-imports': 7.22.5 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.5 dev: false - /@babel/helper-replace-supers@7.19.1: - resolution: {integrity: sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw==} + /@babel/helper-plugin-utils@7.20.2: + resolution: {integrity: sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-member-expression-to-functions': 7.18.9 - '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/traverse': 7.20.5 - '@babel/types': 7.20.5 - transitivePeerDependencies: - - supports-color dev: false - /@babel/helper-simple-access@7.20.2: - resolution: {integrity: sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==} + /@babel/helper-plugin-utils@7.22.5: + resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.20.5 dev: false - /@babel/helper-skip-transparent-expression-wrappers@7.20.0: - resolution: {integrity: sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==} + /@babel/helper-simple-access@7.22.5: + resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.10 dev: false - /@babel/helper-split-export-declaration@7.18.6: - resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} + /@babel/helper-split-export-declaration@7.22.6: + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.10 dev: false - /@babel/helper-string-parser@7.19.4: - resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==} + /@babel/helper-string-parser@7.22.5: + resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} engines: {node: '>=6.9.0'} dev: false @@ -1240,921 +1134,221 @@ packages: engines: {node: '>=6.9.0'} dev: false - /@babel/helper-validator-option@7.18.6: - resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==} - engines: {node: '>=6.9.0'} - dev: false - - /@babel/helper-wrap-function@7.20.5: - resolution: {integrity: sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-function-name': 7.19.0 - '@babel/template': 7.18.10 - '@babel/traverse': 7.20.5 - '@babel/types': 7.20.5 - transitivePeerDependencies: - - supports-color - dev: false - - /@babel/helpers@7.20.6: - resolution: {integrity: sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.18.10 - '@babel/traverse': 7.20.5 - '@babel/types': 7.20.5 - transitivePeerDependencies: - - supports-color - dev: false - - /@babel/highlight@7.18.6: - resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-validator-identifier': 7.19.1 - chalk: 2.4.2 - js-tokens: 4.0.0 - dev: false - - /@babel/parser@7.20.5: - resolution: {integrity: sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==} - engines: {node: '>=6.0.0'} - hasBin: true - dependencies: - '@babel/types': 7.20.5 - dev: false - - /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.18.9(@babel/core@7.20.5): - resolution: {integrity: sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.13.0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 - '@babel/plugin-proposal-optional-chaining': 7.18.9(@babel/core@7.20.5) - dev: false - - /@babel/plugin-proposal-async-generator-functions@7.20.1(@babel/core@7.20.5): - resolution: {integrity: sha512-Gh5rchzSwE4kC+o/6T8waD0WHEQIsDmjltY8WnWRXHUdH8axZhuH86Ov9M72YhJfDrZseQwuuWaaIT/TmePp3g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-remap-async-to-generator': 7.18.9(@babel/core@7.20.5) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.20.5) - transitivePeerDependencies: - - supports-color - dev: false - - /@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-create-class-features-plugin': 7.20.5(@babel/core@7.20.5) - '@babel/helper-plugin-utils': 7.20.2 - transitivePeerDependencies: - - supports-color - dev: false - - /@babel/plugin-proposal-class-static-block@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.12.0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-create-class-features-plugin': 7.20.5(@babel/core@7.20.5) - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.20.5) - transitivePeerDependencies: - - supports-color - dev: false - - /@babel/plugin-proposal-dynamic-import@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.20.5) - dev: false - - /@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.20.5): - resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.20.5) - dev: false - - /@babel/plugin-proposal-json-strings@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.20.5) - dev: false - - /@babel/plugin-proposal-logical-assignment-operators@7.18.9(@babel/core@7.20.5): - resolution: {integrity: sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.20.5) - dev: false - - /@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.20.5) - dev: false - - /@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.20.5) - dev: false - - /@babel/plugin-proposal-object-rest-spread@7.20.2(@babel/core@7.20.5): - resolution: {integrity: sha512-Ks6uej9WFK+fvIMesSqbAto5dD8Dz4VuuFvGJFKgIGSkJuRGcrwGECPA1fDgQK3/DbExBJpEkTeYeB8geIFCSQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/compat-data': 7.20.5 - '@babel/core': 7.20.5 - '@babel/helper-compilation-targets': 7.20.0(@babel/core@7.20.5) - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-transform-parameters': 7.20.5(@babel/core@7.20.5) - dev: false - - /@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.20.5) - dev: false - - /@babel/plugin-proposal-optional-chaining@7.18.9(@babel/core@7.20.5): - resolution: {integrity: sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.20.5) - dev: false - - /@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-create-class-features-plugin': 7.20.5(@babel/core@7.20.5) - '@babel/helper-plugin-utils': 7.20.2 - transitivePeerDependencies: - - supports-color - dev: false - - /@babel/plugin-proposal-private-property-in-object@7.20.5(@babel/core@7.20.5): - resolution: {integrity: sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-create-class-features-plugin': 7.20.5(@babel/core@7.20.5) - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.20.5) - transitivePeerDependencies: - - supports-color - dev: false - - /@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} - engines: {node: '>=4'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-create-regexp-features-plugin': 7.20.5(@babel/core@7.20.5) - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.20.5): - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.20.5): - resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.20.5): - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.20.5): - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.20.5): - resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.20.5): - resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-syntax-import-assertions@7.20.0(@babel/core@7.20.5): - resolution: {integrity: sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.20.5): - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.20.5): - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-syntax-jsx@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.20.5): - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.20.5): - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.20.5): - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.20.5): - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.20.5): - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.20.5): - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.20.5): - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.20.5): - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-syntax-typescript@7.20.0(@babel/core@7.20.5): - resolution: {integrity: sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-transform-arrow-functions@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-transform-async-to-generator@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-remap-async-to-generator': 7.18.9(@babel/core@7.20.5) - transitivePeerDependencies: - - supports-color - dev: false - - /@babel/plugin-transform-block-scoped-functions@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-transform-block-scoping@7.20.5(@babel/core@7.20.5): - resolution: {integrity: sha512-WvpEIW9Cbj9ApF3yJCjIEEf1EiNJLtXagOrL5LNWEZOo3jv8pmPoYTSNJQvqej8OavVlgOoOPw6/htGZro6IkA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-transform-classes@7.20.2(@babel/core@7.20.5): - resolution: {integrity: sha512-9rbPp0lCVVoagvtEyQKSo5L8oo0nQS/iif+lwlAz29MccX2642vWDlSZK+2T2buxbopotId2ld7zZAzRfz9j1g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-compilation-targets': 7.20.0(@babel/core@7.20.5) - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.19.0 - '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-replace-supers': 7.19.1 - '@babel/helper-split-export-declaration': 7.18.6 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - dev: false - - /@babel/plugin-transform-computed-properties@7.18.9(@babel/core@7.20.5): - resolution: {integrity: sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-transform-destructuring@7.20.2(@babel/core@7.20.5): - resolution: {integrity: sha512-mENM+ZHrvEgxLTBXUiQ621rRXZes3KWUv6NdQlrnr1TkWVw+hUjQBZuP2X32qKlrlG2BzgR95gkuCRSkJl8vIw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-transform-dotall-regex@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-create-regexp-features-plugin': 7.20.5(@babel/core@7.20.5) - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-transform-duplicate-keys@7.18.9(@babel/core@7.20.5): - resolution: {integrity: sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-transform-exponentiation-operator@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-transform-for-of@7.18.8(@babel/core@7.20.5): - resolution: {integrity: sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-transform-function-name@7.18.9(@babel/core@7.20.5): - resolution: {integrity: sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-compilation-targets': 7.20.0(@babel/core@7.20.5) - '@babel/helper-function-name': 7.19.0 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-transform-literals@7.18.9(@babel/core@7.20.5): - resolution: {integrity: sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-transform-member-expression-literals@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: false - - /@babel/plugin-transform-modules-amd@7.19.6(@babel/core@7.20.5): - resolution: {integrity: sha512-uG3od2mXvAtIFQIh0xrpLH6r5fpSQN04gIVovl+ODLdUMANokxQLZnPBHcjmv3GxRjnqwLuHvppjjcelqUFZvg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-module-transforms': 7.20.2 - '@babel/helper-plugin-utils': 7.20.2 - transitivePeerDependencies: - - supports-color + /@babel/helper-validator-identifier@7.22.5: + resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} + engines: {node: '>=6.9.0'} dev: false - /@babel/plugin-transform-modules-commonjs@7.19.6(@babel/core@7.20.5): - resolution: {integrity: sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ==} + /@babel/helper-validator-option@7.22.5: + resolution: {integrity: sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-module-transforms': 7.20.2 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-simple-access': 7.20.2 - transitivePeerDependencies: - - supports-color dev: false - /@babel/plugin-transform-modules-systemjs@7.19.6(@babel/core@7.20.5): - resolution: {integrity: sha512-fqGLBepcc3kErfR9R3DnVpURmckXP7gj7bAlrTQyBxrigFqszZCkFkcoxzCp2v32XmwXLvbw+8Yq9/b+QqksjQ==} + /@babel/helpers@7.22.10: + resolution: {integrity: sha512-a41J4NW8HyZa1I1vAndrraTlPZ/eZoga2ZgS7fEr0tZJGVU4xqdE80CEm0CcNjha5EZ8fTBYLKHF0kqDUuAwQw==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-module-transforms': 7.20.2 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-identifier': 7.19.1 + '@babel/template': 7.22.5 + '@babel/traverse': 7.22.10 + '@babel/types': 7.22.10 transitivePeerDependencies: - supports-color dev: false - /@babel/plugin-transform-modules-umd@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==} + /@babel/highlight@7.18.6: + resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-module-transforms': 7.20.2 - '@babel/helper-plugin-utils': 7.20.2 - transitivePeerDependencies: - - supports-color + '@babel/helper-validator-identifier': 7.19.1 + chalk: 2.4.2 + js-tokens: 4.0.0 dev: false - /@babel/plugin-transform-named-capturing-groups-regex@7.20.5(@babel/core@7.20.5): - resolution: {integrity: sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==} + /@babel/highlight@7.22.10: + resolution: {integrity: sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-create-regexp-features-plugin': 7.20.5(@babel/core@7.20.5) - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-validator-identifier': 7.22.5 + chalk: 2.4.2 + js-tokens: 4.0.0 dev: false - /@babel/plugin-transform-new-target@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + /@babel/parser@7.22.10: + resolution: {integrity: sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==} + engines: {node: '>=6.0.0'} + hasBin: true dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/types': 7.22.10 dev: false - /@babel/plugin-transform-object-super@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==} - engines: {node: '>=6.9.0'} + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.22.10): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-replace-supers': 7.19.1 - transitivePeerDependencies: - - supports-color + '@babel/core': 7.22.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-parameters@7.20.5(@babel/core@7.20.5): - resolution: {integrity: sha512-h7plkOmcndIUWXZFLgpbrh2+fXAi47zcUX7IrOQuZdLD0I0KvjJ6cvo3BEcAOsDOcZhVKGJqv07mkSqK0y2isQ==} - engines: {node: '>=6.9.0'} + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.22.10): + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.22.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-property-literals@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==} - engines: {node: '>=6.9.0'} + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.22.10): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.22.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-regenerator@7.20.5(@babel/core@7.20.5): - resolution: {integrity: sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==} - engines: {node: '>=6.9.0'} + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.22.10): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - regenerator-transform: 0.15.1 + '@babel/core': 7.22.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-reserved-words@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==} - engines: {node: '>=6.9.0'} + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.22.10): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.22.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-shorthand-properties@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==} + /@babel/plugin-syntax-jsx@7.18.6(@babel/core@7.22.10): + resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 + '@babel/core': 7.22.10 '@babel/helper-plugin-utils': 7.20.2 dev: false - /@babel/plugin-transform-spread@7.19.0(@babel/core@7.20.5): - resolution: {integrity: sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==} - engines: {node: '>=6.9.0'} + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.22.10): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + '@babel/core': 7.22.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-sticky-regex@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==} - engines: {node: '>=6.9.0'} + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.22.10): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.22.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-template-literals@7.18.9(@babel/core@7.20.5): - resolution: {integrity: sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==} - engines: {node: '>=6.9.0'} + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.22.10): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.22.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-typeof-symbol@7.18.9(@babel/core@7.20.5): - resolution: {integrity: sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==} - engines: {node: '>=6.9.0'} + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.22.10): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.22.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-unicode-escapes@7.18.10(@babel/core@7.20.5): - resolution: {integrity: sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==} - engines: {node: '>=6.9.0'} + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.22.10): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.22.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-unicode-regex@7.18.6(@babel/core@7.20.5): - resolution: {integrity: sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==} - engines: {node: '>=6.9.0'} + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.22.10): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 - '@babel/helper-create-regexp-features-plugin': 7.20.5(@babel/core@7.20.5) - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.22.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/preset-env@7.20.2(@babel/core@7.20.5): - resolution: {integrity: sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==} + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.22.10): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.20.5 - '@babel/core': 7.20.5 - '@babel/helper-compilation-targets': 7.20.0(@babel/core@7.20.5) - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-option': 7.18.6 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.18.9(@babel/core@7.20.5) - '@babel/plugin-proposal-async-generator-functions': 7.20.1(@babel/core@7.20.5) - '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-proposal-class-static-block': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-proposal-dynamic-import': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-proposal-export-namespace-from': 7.18.9(@babel/core@7.20.5) - '@babel/plugin-proposal-json-strings': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-proposal-logical-assignment-operators': 7.18.9(@babel/core@7.20.5) - '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-proposal-numeric-separator': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-proposal-object-rest-spread': 7.20.2(@babel/core@7.20.5) - '@babel/plugin-proposal-optional-catch-binding': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-proposal-optional-chaining': 7.18.9(@babel/core@7.20.5) - '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-proposal-private-property-in-object': 7.20.5(@babel/core@7.20.5) - '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.20.5) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.20.5) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.20.5) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-import-assertions': 7.20.0(@babel/core@7.20.5) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.20.5) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.20.5) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.20.5) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.20.5) - '@babel/plugin-transform-arrow-functions': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-async-to-generator': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-block-scoped-functions': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-block-scoping': 7.20.5(@babel/core@7.20.5) - '@babel/plugin-transform-classes': 7.20.2(@babel/core@7.20.5) - '@babel/plugin-transform-computed-properties': 7.18.9(@babel/core@7.20.5) - '@babel/plugin-transform-destructuring': 7.20.2(@babel/core@7.20.5) - '@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-duplicate-keys': 7.18.9(@babel/core@7.20.5) - '@babel/plugin-transform-exponentiation-operator': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-for-of': 7.18.8(@babel/core@7.20.5) - '@babel/plugin-transform-function-name': 7.18.9(@babel/core@7.20.5) - '@babel/plugin-transform-literals': 7.18.9(@babel/core@7.20.5) - '@babel/plugin-transform-member-expression-literals': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-modules-amd': 7.19.6(@babel/core@7.20.5) - '@babel/plugin-transform-modules-commonjs': 7.19.6(@babel/core@7.20.5) - '@babel/plugin-transform-modules-systemjs': 7.19.6(@babel/core@7.20.5) - '@babel/plugin-transform-modules-umd': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-named-capturing-groups-regex': 7.20.5(@babel/core@7.20.5) - '@babel/plugin-transform-new-target': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-object-super': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-parameters': 7.20.5(@babel/core@7.20.5) - '@babel/plugin-transform-property-literals': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-regenerator': 7.20.5(@babel/core@7.20.5) - '@babel/plugin-transform-reserved-words': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-shorthand-properties': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-spread': 7.19.0(@babel/core@7.20.5) - '@babel/plugin-transform-sticky-regex': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-template-literals': 7.18.9(@babel/core@7.20.5) - '@babel/plugin-transform-typeof-symbol': 7.18.9(@babel/core@7.20.5) - '@babel/plugin-transform-unicode-escapes': 7.18.10(@babel/core@7.20.5) - '@babel/plugin-transform-unicode-regex': 7.18.6(@babel/core@7.20.5) - '@babel/preset-modules': 0.1.5(@babel/core@7.20.5) - '@babel/types': 7.20.5 - babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.20.5) - babel-plugin-polyfill-corejs3: 0.6.0(@babel/core@7.20.5) - babel-plugin-polyfill-regenerator: 0.4.1(@babel/core@7.20.5) - core-js-compat: 3.26.1 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color + '@babel/core': 7.22.10 + '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/preset-modules@0.1.5(@babel/core@7.20.5): - resolution: {integrity: sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==} + /@babel/plugin-syntax-typescript@7.20.0(@babel/core@7.22.10): + resolution: {integrity: sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.5 + '@babel/core': 7.22.10 '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.20.5) - '@babel/types': 7.20.5 - esutils: 2.0.3 dev: false - /@babel/runtime@7.20.6: - resolution: {integrity: sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==} + /@babel/template@7.22.5: + resolution: {integrity: sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==} engines: {node: '>=6.9.0'} dependencies: - regenerator-runtime: 0.13.11 + '@babel/code-frame': 7.22.10 + '@babel/parser': 7.22.10 + '@babel/types': 7.22.10 dev: false - /@babel/template@7.18.10: - resolution: {integrity: sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==} + /@babel/traverse@7.20.5: + resolution: {integrity: sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.18.6 - '@babel/parser': 7.20.5 - '@babel/types': 7.20.5 + '@babel/code-frame': 7.22.10 + '@babel/generator': 7.22.10 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-function-name': 7.22.5 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.22.10 + '@babel/types': 7.22.10 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color dev: false - /@babel/traverse@7.20.5: - resolution: {integrity: sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==} + /@babel/traverse@7.22.10: + resolution: {integrity: sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.18.6 - '@babel/generator': 7.20.5 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.19.0 - '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/parser': 7.20.5 - '@babel/types': 7.20.5 + '@babel/code-frame': 7.22.10 + '@babel/generator': 7.22.10 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-function-name': 7.22.5 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.22.10 + '@babel/types': 7.22.10 debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: @@ -2165,8 +1359,17 @@ packages: resolution: {integrity: sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-string-parser': 7.19.4 - '@babel/helper-validator-identifier': 7.19.1 + '@babel/helper-string-parser': 7.22.5 + '@babel/helper-validator-identifier': 7.22.5 + to-fast-properties: 2.0.0 + dev: false + + /@babel/types@7.22.10: + resolution: {integrity: sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.22.5 + '@babel/helper-validator-identifier': 7.22.5 to-fast-properties: 2.0.0 dev: false @@ -2453,71 +1656,61 @@ packages: engines: {node: '>=8'} dev: false - /@jest/console@28.1.3: - resolution: {integrity: sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /@jest/console@29.6.2: + resolution: {integrity: sha512-0N0yZof5hi44HAR2pPS+ikJ3nzKNoZdVu8FffRf3wy47I7Dm7etk/3KetMdRUqzVd16V4O2m2ISpNTbnIuqy1w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 28.1.3 + '@jest/types': 29.6.1 '@types/node': 20.5.0 chalk: 4.1.2 - jest-message-util: 28.1.3 - jest-util: 28.1.3 + jest-message-util: 29.6.2 + jest-util: 29.6.2 slash: 3.0.0 dev: false - /@jest/core@28.1.3: - resolution: {integrity: sha512-CIKBrlaKOzA7YG19BEqCw3SLIsEwjZkeJzf5bdooVnW4bH5cktqe3JX+G2YV1aK5vP8N9na1IGWFzYaTp6k6NA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /@jest/core@29.6.2: + resolution: {integrity: sha512-Oj+5B+sDMiMWLhPFF+4/DvHOf+U10rgvCLGPHP8Xlsy/7QxS51aU/eBngudHlJXnaWD5EohAgJ4js+T6pa+zOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: node-notifier: optional: true dependencies: - '@jest/console': 28.1.3 - '@jest/reporters': 28.1.3 - '@jest/test-result': 28.1.3 - '@jest/transform': 28.1.3 - '@jest/types': 28.1.3 + '@jest/console': 29.6.2 + '@jest/reporters': 29.6.2 + '@jest/test-result': 29.6.2 + '@jest/transform': 29.6.2 + '@jest/types': 29.6.1 '@types/node': 20.5.0 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.7.0 exit: 0.1.2 graceful-fs: 4.2.10 - jest-changed-files: 28.1.3 - jest-config: 28.1.3(@types/node@20.5.0) - jest-haste-map: 28.1.3 - jest-message-util: 28.1.3 - jest-regex-util: 28.0.2 - jest-resolve: 28.1.3 - jest-resolve-dependencies: 28.1.3 - jest-runner: 28.1.3 - jest-runtime: 28.1.3 - jest-snapshot: 28.1.3 - jest-util: 28.1.3 - jest-validate: 28.1.3 - jest-watcher: 28.1.3 + jest-changed-files: 29.5.0 + jest-config: 29.6.2(@types/node@20.5.0) + jest-haste-map: 29.6.2 + jest-message-util: 29.6.2 + jest-regex-util: 29.4.3 + jest-resolve: 29.6.2 + jest-resolve-dependencies: 29.6.2 + jest-runner: 29.6.2 + jest-runtime: 29.6.2 + jest-snapshot: 29.6.2 + jest-util: 29.6.2 + jest-validate: 29.6.2 + jest-watcher: 29.6.2 micromatch: 4.0.5 - pretty-format: 28.1.3 - rimraf: 3.0.2 + pretty-format: 29.6.2 slash: 3.0.0 strip-ansi: 6.0.1 transitivePeerDependencies: + - babel-plugin-macros - supports-color - ts-node dev: false - /@jest/environment@28.1.3: - resolution: {integrity: sha512-1bf40cMFTEkKyEf585R9Iz1WayDjHoHqvts0XFYEqyKM3cFWDpeMoqKKTAF9LSYQModPUlh8FKptoM2YcMWAXA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - '@jest/fake-timers': 28.1.3 - '@jest/types': 28.1.3 - '@types/node': 20.5.0 - jest-mock: 28.1.3 - dev: false - /@jest/environment@29.3.1: resolution: {integrity: sha512-pMmvfOPmoa1c1QpfFW0nXYtNLpofqo4BrCIk6f2kW4JFeNlHV2t3vd+3iDLf31e2ot2Mec0uqZfmI+U0K2CFag==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2528,11 +1721,14 @@ packages: jest-mock: 29.3.1 dev: false - /@jest/expect-utils@28.1.3: - resolution: {integrity: sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /@jest/environment@29.6.2: + resolution: {integrity: sha512-AEcW43C7huGd/vogTddNNTDRpO6vQ2zaQNrttvWV18ArBx9Z56h7BIsXkNFJVOO4/kblWEQz30ckw0+L3izc+Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - jest-get-type: 28.0.2 + '@jest/fake-timers': 29.6.2 + '@jest/types': 29.6.1 + '@types/node': 20.5.0 + jest-mock: 29.6.2 dev: false /@jest/expect-utils@29.3.1: @@ -2542,14 +1738,11 @@ packages: jest-get-type: 29.2.0 dev: false - /@jest/expect@28.1.3: - resolution: {integrity: sha512-lzc8CpUbSoE4dqT0U+g1qODQjBRHPpCPXissXD4mS9+sWQdmmpeJ9zSH1rS1HEkrsMN0fb7nKrJ9giAR1d3wBw==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /@jest/expect-utils@29.6.2: + resolution: {integrity: sha512-6zIhM8go3RV2IG4aIZaZbxwpOzz3ZiM23oxAlkquOIole+G6TrbeXnykxWYlqF7kz2HlBjdKtca20x9atkEQYg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - expect: 28.1.3 - jest-snapshot: 28.1.3 - transitivePeerDependencies: - - supports-color + jest-get-type: 29.4.3 dev: false /@jest/expect@29.3.1: @@ -2562,16 +1755,14 @@ packages: - supports-color dev: false - /@jest/fake-timers@28.1.3: - resolution: {integrity: sha512-D/wOkL2POHv52h+ok5Oj/1gOG9HSywdoPtFsRCUmlCILXNn5eIWmcnd3DIiWlJnpGvQtmajqBP95Ei0EimxfLw==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /@jest/expect@29.6.2: + resolution: {integrity: sha512-m6DrEJxVKjkELTVAztTLyS/7C92Y2b0VYqmDROYKLLALHn8T/04yPs70NADUYPrV3ruI+H3J0iUIuhkjp7vkfg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 28.1.3 - '@sinonjs/fake-timers': 9.1.2 - '@types/node': 20.5.0 - jest-message-util: 28.1.3 - jest-mock: 28.1.3 - jest-util: 28.1.3 + expect: 29.6.2 + jest-snapshot: 29.6.2 + transitivePeerDependencies: + - supports-color dev: false /@jest/fake-timers@29.3.1: @@ -2586,15 +1777,16 @@ packages: jest-util: 29.3.1 dev: false - /@jest/globals@28.1.3: - resolution: {integrity: sha512-XFU4P4phyryCXu1pbcqMO0GSQcYe1IsalYCDzRNyhetyeyxMcIxa11qPNDpVNLeretItNqEmYYQn1UYz/5x1NA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /@jest/fake-timers@29.6.2: + resolution: {integrity: sha512-euZDmIlWjm1Z0lJ1D0f7a0/y5Kh/koLFMUBE5SUYWrmy8oNhJpbTBDAP6CxKnadcMLDoDf4waRYCe35cH6G6PA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 28.1.3 - '@jest/expect': 28.1.3 - '@jest/types': 28.1.3 - transitivePeerDependencies: - - supports-color + '@jest/types': 29.6.1 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 20.5.0 + jest-message-util: 29.6.2 + jest-mock: 29.6.2 + jest-util: 29.6.2 dev: false /@jest/globals@29.3.1: @@ -2609,9 +1801,21 @@ packages: - supports-color dev: false - /@jest/reporters@28.1.3: - resolution: {integrity: sha512-JuAy7wkxQZVNU/V6g9xKzCGC5LVXx9FDcABKsSXp5MiKPEE2144a/vXTEDoyzjUpZKfVwp08Wqg5A4WfTMAzjg==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /@jest/globals@29.6.2: + resolution: {integrity: sha512-cjuJmNDjs6aMijCmSa1g2TNG4Lby/AeU7/02VtpW+SLcZXzOLK2GpN2nLqcFjmhy3B3AoPeQVx7BnyOf681bAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.6.2 + '@jest/expect': 29.6.2 + '@jest/types': 29.6.1 + jest-mock: 29.6.2 + transitivePeerDependencies: + - supports-color + dev: false + + /@jest/reporters@29.6.2: + resolution: {integrity: sha512-sWtijrvIav8LgfJZlrGCdN0nP2EWbakglJY49J1Y5QihcQLfy7ovyxxjJBRXMNltgt4uPtEcFmIMbVshEDfFWw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: @@ -2619,11 +1823,11 @@ packages: optional: true dependencies: '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 28.1.3 - '@jest/test-result': 28.1.3 - '@jest/transform': 28.1.3 - '@jest/types': 28.1.3 - '@jridgewell/trace-mapping': 0.3.17 + '@jest/console': 29.6.2 + '@jest/test-result': 29.6.2 + '@jest/transform': 29.6.2 + '@jest/types': 29.6.1 + '@jridgewell/trace-mapping': 0.3.19 '@types/node': 20.5.0 chalk: 4.1.2 collect-v8-coverage: 1.0.1 @@ -2635,76 +1839,75 @@ packages: istanbul-lib-report: 3.0.0 istanbul-lib-source-maps: 4.0.1 istanbul-reports: 3.1.5 - jest-message-util: 28.1.3 - jest-util: 28.1.3 - jest-worker: 28.1.3 + jest-message-util: 29.6.2 + jest-util: 29.6.2 + jest-worker: 29.6.2 slash: 3.0.0 string-length: 4.0.2 strip-ansi: 6.0.1 - terminal-link: 2.1.1 v8-to-istanbul: 9.0.1 transitivePeerDependencies: - supports-color dev: false - /@jest/schemas@28.1.3: - resolution: {integrity: sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /@jest/schemas@29.0.0: + resolution: {integrity: sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@sinclair/typebox': 0.24.51 dev: false - /@jest/schemas@29.0.0: - resolution: {integrity: sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==} + /@jest/schemas@29.6.0: + resolution: {integrity: sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@sinclair/typebox': 0.24.51 + '@sinclair/typebox': 0.27.8 dev: false - /@jest/source-map@28.1.2: - resolution: {integrity: sha512-cV8Lx3BeStJb8ipPHnqVw/IM2VCMWO3crWZzYodSIkxXnRcXJipCdx1JCK0K5MsJJouZQTH73mzf4vgxRaH9ww==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /@jest/source-map@29.6.0: + resolution: {integrity: sha512-oA+I2SHHQGxDCZpbrsCQSoMLb3Bz547JnM+jUr9qEbuw0vQlWZfpPS7CO9J7XiwKicEz9OFn/IYoLkkiUD7bzA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jridgewell/trace-mapping': 0.3.17 + '@jridgewell/trace-mapping': 0.3.19 callsites: 3.1.0 graceful-fs: 4.2.10 dev: false - /@jest/test-result@28.1.3: - resolution: {integrity: sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /@jest/test-result@29.6.2: + resolution: {integrity: sha512-3VKFXzcV42EYhMCsJQURptSqnyjqCGbtLuX5Xxb6Pm6gUf1wIRIl+mandIRGJyWKgNKYF9cnstti6Ls5ekduqw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/console': 28.1.3 - '@jest/types': 28.1.3 + '@jest/console': 29.6.2 + '@jest/types': 29.6.1 '@types/istanbul-lib-coverage': 2.0.4 collect-v8-coverage: 1.0.1 dev: false - /@jest/test-sequencer@28.1.3: - resolution: {integrity: sha512-NIMPEqqa59MWnDi1kvXXpYbqsfQmSJsIbnd85mdVGkiDfQ9WQQTXOLsvISUfonmnBT+w85WEgneCigEEdHDFxw==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /@jest/test-sequencer@29.6.2: + resolution: {integrity: sha512-GVYi6PfPwVejO7slw6IDO0qKVum5jtrJ3KoLGbgBWyr2qr4GaxFV6su+ZAjdTX75Sr1DkMFRk09r2ZVa+wtCGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/test-result': 28.1.3 + '@jest/test-result': 29.6.2 graceful-fs: 4.2.10 - jest-haste-map: 28.1.3 + jest-haste-map: 29.6.2 slash: 3.0.0 dev: false - /@jest/transform@28.1.3: - resolution: {integrity: sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /@jest/transform@29.3.1: + resolution: {integrity: sha512-8wmCFBTVGYqFNLWfcOWoVuMuKYPUBTnTMDkdvFtAYELwDOl9RGwOsvQWGPFxDJ8AWY9xM/8xCXdqmPK3+Q5Lug==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/core': 7.20.5 - '@jest/types': 28.1.3 - '@jridgewell/trace-mapping': 0.3.17 + '@babel/core': 7.22.10 + '@jest/types': 29.3.1 + '@jridgewell/trace-mapping': 0.3.19 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 - convert-source-map: 1.9.0 + convert-source-map: 2.0.0 fast-json-stable-stringify: 2.1.0 graceful-fs: 4.2.10 - jest-haste-map: 28.1.3 - jest-regex-util: 28.0.2 - jest-util: 28.1.3 + jest-haste-map: 29.3.1 + jest-regex-util: 29.2.0 + jest-util: 29.3.1 micromatch: 4.0.5 pirates: 4.0.5 slash: 3.0.0 @@ -2713,21 +1916,21 @@ packages: - supports-color dev: false - /@jest/transform@29.3.1: - resolution: {integrity: sha512-8wmCFBTVGYqFNLWfcOWoVuMuKYPUBTnTMDkdvFtAYELwDOl9RGwOsvQWGPFxDJ8AWY9xM/8xCXdqmPK3+Q5Lug==} + /@jest/transform@29.6.2: + resolution: {integrity: sha512-ZqCqEISr58Ce3U+buNFJYUktLJZOggfyvR+bZMaiV1e8B1SIvJbwZMrYz3gx/KAPn9EXmOmN+uB08yLCjWkQQg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/core': 7.20.5 - '@jest/types': 29.3.1 - '@jridgewell/trace-mapping': 0.3.17 + '@babel/core': 7.22.10 + '@jest/types': 29.6.1 + '@jridgewell/trace-mapping': 0.3.19 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 convert-source-map: 2.0.0 fast-json-stable-stringify: 2.1.0 graceful-fs: 4.2.10 - jest-haste-map: 29.3.1 - jest-regex-util: 29.2.0 - jest-util: 29.3.1 + jest-haste-map: 29.6.2 + jest-regex-util: 29.4.3 + jest-util: 29.6.2 micromatch: 4.0.5 pirates: 4.0.5 slash: 3.0.0 @@ -2736,11 +1939,11 @@ packages: - supports-color dev: false - /@jest/types@28.1.3: - resolution: {integrity: sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /@jest/types@29.3.1: + resolution: {integrity: sha512-d0S0jmmTpjnhCmNpApgX3jrUZgZ22ivKJRvL2lli5hpCRoNnp1f85r2/wpKfXuYu8E7Jjh1hGfhPyup1NM5AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/schemas': 28.1.3 + '@jest/schemas': 29.0.0 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 '@types/node': 20.5.0 @@ -2748,11 +1951,11 @@ packages: chalk: 4.1.2 dev: false - /@jest/types@29.3.1: - resolution: {integrity: sha512-d0S0jmmTpjnhCmNpApgX3jrUZgZ22ivKJRvL2lli5hpCRoNnp1f85r2/wpKfXuYu8E7Jjh1hGfhPyup1NM5AmA==} + /@jest/types@29.6.1: + resolution: {integrity: sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/schemas': 29.0.0 + '@jest/schemas': 29.6.0 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 '@types/node': 20.5.0 @@ -2774,7 +1977,7 @@ packages: dependencies: '@jridgewell/set-array': 1.1.2 '@jridgewell/sourcemap-codec': 1.4.14 - '@jridgewell/trace-mapping': 0.3.17 + '@jridgewell/trace-mapping': 0.3.19 dev: false /@jridgewell/resolve-uri@3.1.0: @@ -2791,8 +1994,8 @@ packages: resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} dev: false - /@jridgewell/trace-mapping@0.3.17: - resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==} + /@jridgewell/trace-mapping@0.3.19: + resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} dependencies: '@jridgewell/resolve-uri': 3.1.0 '@jridgewell/sourcemap-codec': 1.4.14 @@ -2823,12 +2026,28 @@ packages: resolution: {integrity: sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==} dev: false + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: false + /@sinonjs/commons@1.8.6: resolution: {integrity: sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==} dependencies: type-detect: 4.0.8 dev: false + /@sinonjs/commons@3.0.0: + resolution: {integrity: sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==} + dependencies: + type-detect: 4.0.8 + dev: false + + /@sinonjs/fake-timers@10.3.0: + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + dependencies: + '@sinonjs/commons': 3.0.0 + dev: false + /@sinonjs/fake-timers@9.1.2: resolution: {integrity: sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==} dependencies: @@ -2860,8 +2079,8 @@ packages: /@types/babel__core@7.1.20: resolution: {integrity: sha512-PVb6Bg2QuscZ30FvOU7z4guG6c926D9YRvOxEaelzndpMsvP+YM74Q/dAFASpg2l6+XLalxSGxcq/lrgYWZtyQ==} dependencies: - '@babel/parser': 7.20.5 - '@babel/types': 7.20.5 + '@babel/parser': 7.22.10 + '@babel/types': 7.22.10 '@types/babel__generator': 7.6.4 '@types/babel__template': 7.4.1 '@types/babel__traverse': 7.18.3 @@ -2870,20 +2089,20 @@ packages: /@types/babel__generator@7.6.4: resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.10 dev: false /@types/babel__template@7.4.1: resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} dependencies: - '@babel/parser': 7.20.5 - '@babel/types': 7.20.5 + '@babel/parser': 7.22.10 + '@babel/types': 7.22.10 dev: false /@types/babel__traverse@7.18.3: resolution: {integrity: sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.10 dev: false /@types/chess.js@0.10.1: @@ -2934,41 +2153,29 @@ packages: '@types/istanbul-lib-report': 3.0.0 dev: false - /@types/jest@28.1.8: - resolution: {integrity: sha512-8TJkV++s7B6XqnDrzR1m/TT0A0h948Pnl/097veySPN67VRAgQ4gZ7n2KfJo2rVq6njQjdxU3GCCyDvAeuHoiw==} + /@types/jest@29.5.3: + resolution: {integrity: sha512-1Nq7YrO/vJE/FYnqYyw0FS8LdrjExSgIiHyKg7xPpn+yi8Q4huZryKnkJatN1ZRH89Kw2v33/8ZMB7DuZeSLlA==} dependencies: - expect: 28.1.3 - pretty-format: 28.1.3 + expect: 29.3.1 + pretty-format: 29.3.1 dev: false - /@types/jsdom@16.2.15: - resolution: {integrity: sha512-nwF87yjBKuX/roqGYerZZM0Nv1pZDMAT5YhOHYeM/72Fic+VEqJh4nyoqoapzJnW3pUlfxPY5FhgsJtM+dRnQQ==} + /@types/jsdom@20.0.1: + resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} dependencies: '@types/node': 20.5.0 - '@types/parse5': 6.0.3 '@types/tough-cookie': 4.0.2 + parse5: 7.1.2 dev: false /@types/json-schema@7.0.12: resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} dev: false - /@types/node@18.16.18: - resolution: {integrity: sha512-/aNaQZD0+iSBAGnvvN2Cx92HqE5sZCPZtx2TsK+4nvV23fFe09jVDvpArXr2j9DnYlzuU9WuoykDDc6wqvpNcw==} - dev: false - - /@types/node@20.3.2: - resolution: {integrity: sha512-vOBLVQeCQfIcF/2Y7eKFTqrMnizK5lRNQ7ykML/5RuwVXVWxYkgwS7xbt4B6fKCUPgbSL5FSsjHQpaGQP/dQmw==} - dev: false - /@types/node@20.5.0: resolution: {integrity: sha512-Mgq7eCtoTjT89FqNoTzzXg2XvCi5VMhRV6+I2aYanc6kQCBImeNaAYRs/DyoVqk1YEUJK5gN9VO7HRIdz4Wo3Q==} dev: false - /@types/parse5@6.0.3: - resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} - dev: false - /@types/prettier@2.7.1: resolution: {integrity: sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==} dev: false @@ -3179,11 +2386,11 @@ packages: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} dev: false - /acorn-globals@6.0.0: - resolution: {integrity: sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==} + /acorn-globals@7.0.1: + resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} dependencies: - acorn: 7.4.1 - acorn-walk: 7.2.0 + acorn: 8.9.0 + acorn-walk: 8.2.0 dev: false /acorn-jsx@5.3.2(acorn@8.9.0): @@ -3194,21 +2401,9 @@ packages: acorn: 8.9.0 dev: false - /acorn-walk@7.2.0: - resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} - engines: {node: '>=0.4.0'} - dev: false - - /acorn@7.4.1: - resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: false - - /acorn@8.8.1: - resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==} + /acorn-walk@8.2.0: + resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} engines: {node: '>=0.4.0'} - hasBin: true dev: false /acorn@8.9.0: @@ -3321,17 +2516,17 @@ packages: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: false - /babel-jest@28.1.3(@babel/core@7.20.5): - resolution: {integrity: sha512-epUaPOEWMk3cWX0M/sPvCHHCe9fMFAa/9hXEgKP8nFfNl/jlGkE9ucq9NqkZGXLDduCJYS0UvSlPUwC0S+rH6Q==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /babel-jest@29.6.2(@babel/core@7.22.10): + resolution: {integrity: sha512-BYCzImLos6J3BH/+HvUCHG1dTf2MzmAB4jaVxHV+29RZLjR29XuYTmsf2sdDwkrb+FczkGo3kOhE7ga6sI0P4A==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.8.0 dependencies: - '@babel/core': 7.20.5 - '@jest/transform': 28.1.3 + '@babel/core': 7.22.10 + '@jest/transform': 29.6.2 '@types/babel__core': 7.1.20 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 28.1.3(@babel/core@7.20.5) + babel-preset-jest: 29.5.0(@babel/core@7.22.10) chalk: 4.1.2 graceful-fs: 4.2.10 slash: 3.0.0 @@ -3343,7 +2538,7 @@ packages: resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} engines: {node: '>=8'} dependencies: - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.22.5 '@istanbuljs/load-nyc-config': 1.1.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-instrument: 5.2.1 @@ -3352,87 +2547,56 @@ packages: - supports-color dev: false - /babel-plugin-jest-hoist@28.1.3: - resolution: {integrity: sha512-Ys3tUKAmfnkRUpPdpa98eYrAR0nV+sSFUZZEGuQ2EbFd1y4SOLtD5QDNHAq+bb9a+bbXvYQC4b+ID/THIMcU6Q==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /babel-plugin-jest-hoist@29.5.0: + resolution: {integrity: sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/template': 7.18.10 - '@babel/types': 7.20.5 + '@babel/template': 7.22.5 + '@babel/types': 7.22.10 '@types/babel__core': 7.1.20 '@types/babel__traverse': 7.18.3 dev: false - /babel-plugin-polyfill-corejs2@0.3.3(@babel/core@7.20.5): - resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/compat-data': 7.20.5 - '@babel/core': 7.20.5 - '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.20.5) - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: false - - /babel-plugin-polyfill-corejs3@0.6.0(@babel/core@7.20.5): - resolution: {integrity: sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.20.5) - core-js-compat: 3.26.1 - transitivePeerDependencies: - - supports-color - dev: false - - /babel-plugin-polyfill-regenerator@0.4.1(@babel/core@7.20.5): - resolution: {integrity: sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.20.5) - transitivePeerDependencies: - - supports-color - dev: false - - /babel-preset-current-node-syntax@1.0.1(@babel/core@7.20.5): + /babel-preset-current-node-syntax@1.0.1(@babel/core@7.22.10): resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.20.5 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.20.5) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.20.5) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.20.5) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.20.5) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.20.5) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.20.5) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.20.5) - dev: false - - /babel-preset-jest@28.1.3(@babel/core@7.20.5): - resolution: {integrity: sha512-L+fupJvlWAHbQfn74coNX3zf60LXMJsezNvvx8eIh7iOR1luJ1poxYgQk1F8PYtNq/6QODDHCqsSnTFSWC491A==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + '@babel/core': 7.22.10 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.22.10) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.22.10) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.22.10) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.22.10) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.22.10) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.22.10) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.22.10) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.22.10) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.22.10) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.22.10) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.22.10) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.22.10) + dev: false + + /babel-preset-jest@29.5.0(@babel/core@7.22.10): + resolution: {integrity: sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.20.5 - babel-plugin-jest-hoist: 28.1.3 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.20.5) + '@babel/core': 7.22.10 + babel-plugin-jest-hoist: 29.5.0 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.10) dev: false /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: false + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: false + /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -3447,30 +2611,23 @@ packages: fill-range: 7.0.1 dev: false - /breakpoint-sass@2.7.1: - resolution: {integrity: sha512-99tYVacptnG6v3VnX62W07TnifrroDnWql+1wuTOfPCEGeNoMvpd0Mw+o+JZk50mAZ1CIHAr1I3GatHEZ2VZeQ==} - dev: false - - /browser-process-hrtime@1.0.0: - resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==} + /breakpoint-sass@3.0.0(sass@1.65.1): + resolution: {integrity: sha512-qxJqSfTaOHI+RCGzvKWVRwwC2hMIaS0KV1b+asqWUFxdLv/yKNADF7AtT1uNnkt2VxSMZ2csM22CSc+Hez+EIg==} + peerDependencies: + sass: ^1.25 + dependencies: + sass: 1.65.1 dev: false - /browserslist@4.21.4: - resolution: {integrity: sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==} + /browserslist@4.21.10: + resolution: {integrity: sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001439 - electron-to-chromium: 1.4.284 - node-releases: 2.0.8 - update-browserslist-db: 1.0.10(browserslist@4.21.4) - dev: false - - /bs-logger@0.2.6: - resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} - engines: {node: '>= 6'} - dependencies: - fast-json-stable-stringify: 2.1.0 + caniuse-lite: 1.0.30001521 + electron-to-chromium: 1.4.494 + node-releases: 2.0.13 + update-browserslist-db: 1.0.11(browserslist@4.21.10) dev: false /bser@2.1.1: @@ -3498,8 +2655,8 @@ packages: engines: {node: '>=10'} dev: false - /caniuse-lite@1.0.30001439: - resolution: {integrity: sha512-1MgUzEkoMO6gKfXflStpYgZDlFM7M/ck/bgfVCACO5vnAf0fXoNVHdWtqGU+MYca+4bL9Z5bpOVmR33cWW9G2A==} + /caniuse-lite@1.0.30001521: + resolution: {integrity: sha512-fnx1grfpEOvDGH+V17eccmNjucGUnCbP6KL+l5KqBIerp26WK/+RQ7CIDE37KGJjaPyqWXXlFUyKiWmvdNNKmQ==} dev: false /chalk@2.4.2: @@ -3548,16 +2705,27 @@ packages: mithril: github.com/ornicar/mithril.js/d99d36d445fad1b22f4b285963ea065291d99ad4 dev: false - /chessground@8.3.13: - resolution: {integrity: sha512-H063phgP0OWXNOwu24Zm+HmW8aDCjBobdbZDVM3N3exDZ3cClfttH6nr4vc8XEcNUFt8mM+2L09OZvWtIwaAlA==} - dev: false - /chessops@0.12.7: resolution: {integrity: sha512-ylWwKOudgTNJCDq7vx/Rs9fkZwPB3CEk0AJ8NtAfEYOoA3DGRQdS/Z/oQRsnQvs92rUu0yEobn4ZIPzLh8y9/w==} dependencies: '@badrap/result': 0.2.13 dev: false + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: false + /ci-info@3.7.0: resolution: {integrity: sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog==} engines: {node: '>=8'} @@ -3649,12 +2817,6 @@ packages: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} dev: false - /core-js-compat@3.26.1: - resolution: {integrity: sha512-622/KzTudvXCDLRw70iHW4KKs1aGpcRcowGWyYJr2DEBfRrd6hNJybxSWJFuZYD4ma86xhrwDDHxmDaIq4EA8A==} - dependencies: - browserslist: 4.21.4 - dev: false - /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -3717,8 +2879,13 @@ packages: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} dev: false - /dedent@0.7.0: - resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + /dedent@1.5.1: + resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true dev: false /deep-is@0.1.4: @@ -3740,16 +2907,16 @@ packages: engines: {node: '>=8'} dev: false - /diff-sequences@28.1.1: - resolution: {integrity: sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dev: false - /diff-sequences@29.3.1: resolution: {integrity: sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: false + /diff-sequences@29.4.3: + resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: false + /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -3783,12 +2950,12 @@ packages: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: false - /electron-to-chromium@1.4.284: - resolution: {integrity: sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==} + /electron-to-chromium@1.4.494: + resolution: {integrity: sha512-KF7wtsFFDu4ws1ZsSOt4pdmO1yWVNWCFtijVYZPUeW4SV7/hy/AESjLn/+qIWgq7mHscNOKAwN5AIM1+YAy+Ww==} dev: false - /emittery@0.10.2: - resolution: {integrity: sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==} + /emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} dev: false @@ -3800,6 +2967,11 @@ packages: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} dev: false + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: false + /error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: @@ -4006,17 +3178,6 @@ packages: engines: {node: '>= 0.8.0'} dev: false - /expect@28.1.3: - resolution: {integrity: sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - '@jest/expect-utils': 28.1.3 - jest-get-type: 28.0.2 - jest-matcher-utils: 28.1.3 - jest-message-util: 28.1.3 - jest-util: 28.1.3 - dev: false - /expect@29.3.1: resolution: {integrity: sha512-gGb1yTgU30Q0O/tQq+z30KBWv24ApkMgFUpvKBkyLUBL68Wv8dHdJxTBZFl/iT8K/bqDHvUYRH6IIN3rToopPA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4028,6 +3189,18 @@ packages: jest-util: 29.3.1 dev: false + /expect@29.6.2: + resolution: {integrity: sha512-iAErsLxJ8C+S02QbLAwgSGSezLQK+XXRDt8IuFXFpwCNw2ECmzZSmjKcCaFVp5VRMk+WAvz6h6jokzEzBFZEuA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.6.2 + '@types/node': 20.5.0 + jest-get-type: 29.4.3 + jest-matcher-utils: 29.6.2 + jest-message-util: 29.6.2 + jest-util: 29.6.2 + dev: false + /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: false @@ -4296,6 +3469,10 @@ packages: engines: {node: '>= 4'} dev: false + /immutable@4.3.2: + resolution: {integrity: sha512-oGXzbEDem9OOpDWZu88jGiYCvIsLHMvGw+8OXlpsvTFvIQplQbjg1B1cvKg8f7Hoch6+NGjpPsH1Fr+Mc2D1aA==} + dev: false + /import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -4333,6 +3510,13 @@ packages: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} dev: false + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: false + /is-core-module@2.11.0: resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} dependencies: @@ -4403,11 +3587,11 @@ packages: resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} engines: {node: '>=8'} dependencies: - '@babel/core': 7.20.5 - '@babel/parser': 7.20.5 + '@babel/core': 7.22.10 + '@babel/parser': 7.22.10 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.0 - semver: 6.3.0 + semver: 6.3.1 transitivePeerDependencies: - supports-color dev: false @@ -4440,44 +3624,46 @@ packages: istanbul-lib-report: 3.0.0 dev: false - /jest-changed-files@28.1.3: - resolution: {integrity: sha512-esaOfUWJXk2nfZt9SPyC8gA1kNfdKLkQWyzsMlqq8msYSlNKfmZxfRgZn4Cd4MGVUF+7v6dBs0d5TOAKa7iIiA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-changed-files@29.5.0: + resolution: {integrity: sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: execa: 5.1.1 p-limit: 3.1.0 dev: false - /jest-circus@28.1.3: - resolution: {integrity: sha512-cZ+eS5zc79MBwt+IhQhiEp0OeBddpc1n8MBo1nMB8A7oPMKEO+Sre+wHaLJexQUj9Ya/8NOBY0RESUgYjB6fow==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-circus@29.6.2: + resolution: {integrity: sha512-G9mN+KOYIUe2sB9kpJkO9Bk18J4dTDArNFPwoZ7WKHKel55eKIS/u2bLthxgojwlf9NLCVQfgzM/WsOVvoC6Fw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 28.1.3 - '@jest/expect': 28.1.3 - '@jest/test-result': 28.1.3 - '@jest/types': 28.1.3 + '@jest/environment': 29.6.2 + '@jest/expect': 29.6.2 + '@jest/test-result': 29.6.2 + '@jest/types': 29.6.1 '@types/node': 20.5.0 chalk: 4.1.2 co: 4.6.0 - dedent: 0.7.0 + dedent: 1.5.1 is-generator-fn: 2.1.0 - jest-each: 28.1.3 - jest-matcher-utils: 28.1.3 - jest-message-util: 28.1.3 - jest-runtime: 28.1.3 - jest-snapshot: 28.1.3 - jest-util: 28.1.3 + jest-each: 29.6.2 + jest-matcher-utils: 29.6.2 + jest-message-util: 29.6.2 + jest-runtime: 29.6.2 + jest-snapshot: 29.6.2 + jest-util: 29.6.2 p-limit: 3.1.0 - pretty-format: 28.1.3 + pretty-format: 29.6.2 + pure-rand: 6.0.2 slash: 3.0.0 stack-utils: 2.0.6 transitivePeerDependencies: + - babel-plugin-macros - supports-color dev: false - /jest-cli@28.1.3(@types/node@20.5.0): - resolution: {integrity: sha512-roY3kvrv57Azn1yPgdTebPAXvdR2xfezaKKYzVxZ6It/5NCxzJym6tUI5P1zkdWhfUYkxEI9uZWcQdaFLo8mJQ==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-cli@29.6.2(@types/node@20.5.0): + resolution: {integrity: sha512-TT6O247v6dCEX2UGHGyflMpxhnrL0DNqP2fRTKYm3nJJpCTfXX3GCMQPGFjXDoj0i5/Blp3jriKXFgdfmbYB6Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -4485,27 +3671,28 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 28.1.3 - '@jest/test-result': 28.1.3 - '@jest/types': 28.1.3 + '@jest/core': 29.6.2 + '@jest/test-result': 29.6.2 + '@jest/types': 29.6.1 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.10 import-local: 3.1.0 - jest-config: 28.1.3(@types/node@20.5.0) - jest-util: 28.1.3 - jest-validate: 28.1.3 + jest-config: 29.6.2(@types/node@20.5.0) + jest-util: 29.6.2 + jest-validate: 29.6.2 prompts: 2.4.2 yargs: 17.6.2 transitivePeerDependencies: - '@types/node' + - babel-plugin-macros - supports-color - ts-node dev: false - /jest-config@28.1.3(@types/node@20.5.0): - resolution: {integrity: sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-config@29.6.2(@types/node@20.5.0): + resolution: {integrity: sha512-VxwFOC8gkiJbuodG9CPtMRjBUNZEHxwfQXmIudSTzFWxaci3Qub1ddTRbFNQlD/zUeaifLndh/eDccFX4wCMQw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@types/node': '*' ts-node: '>=9.0.0' @@ -4515,43 +3702,34 @@ packages: ts-node: optional: true dependencies: - '@babel/core': 7.20.5 - '@jest/test-sequencer': 28.1.3 - '@jest/types': 28.1.3 + '@babel/core': 7.22.10 + '@jest/test-sequencer': 29.6.2 + '@jest/types': 29.6.1 '@types/node': 20.5.0 - babel-jest: 28.1.3(@babel/core@7.20.5) + babel-jest: 29.6.2(@babel/core@7.22.10) chalk: 4.1.2 ci-info: 3.7.0 deepmerge: 4.2.2 glob: 7.2.3 graceful-fs: 4.2.10 - jest-circus: 28.1.3 - jest-environment-node: 28.1.3 - jest-get-type: 28.0.2 - jest-regex-util: 28.0.2 - jest-resolve: 28.1.3 - jest-runner: 28.1.3 - jest-util: 28.1.3 - jest-validate: 28.1.3 + jest-circus: 29.6.2 + jest-environment-node: 29.6.2 + jest-get-type: 29.4.3 + jest-regex-util: 29.4.3 + jest-resolve: 29.6.2 + jest-runner: 29.6.2 + jest-util: 29.6.2 + jest-validate: 29.6.2 micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 28.1.3 + pretty-format: 29.6.2 slash: 3.0.0 strip-json-comments: 3.1.1 transitivePeerDependencies: + - babel-plugin-macros - supports-color dev: false - /jest-diff@28.1.3: - resolution: {integrity: sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - chalk: 4.1.2 - diff-sequences: 28.1.1 - jest-get-type: 28.0.2 - pretty-format: 28.1.3 - dev: false - /jest-diff@29.3.1: resolution: {integrity: sha512-vU8vyiO7568tmin2lA3r2DP8oRvzhvRcD4DjpXc6uGveQodyk7CKLhQlCSiwgx3g0pFaE88/KLZ0yaTWMc4Uiw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4562,58 +3740,67 @@ packages: pretty-format: 29.3.1 dev: false - /jest-docblock@28.1.1: - resolution: {integrity: sha512-3wayBVNiOYx0cwAbl9rwm5kKFP8yHH3d/fkEaL02NPTkDojPtheGB7HZSFY4wzX+DxyrvhXz0KSCVksmCknCuA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-diff@29.6.2: + resolution: {integrity: sha512-t+ST7CB9GX5F2xKwhwCf0TAR17uNDiaPTZnVymP9lw0lssa9vG+AFyDZoeIHStU3WowFFwT+ky+er0WVl2yGhA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.4.3 + jest-get-type: 29.4.3 + pretty-format: 29.6.2 + dev: false + + /jest-docblock@29.4.3: + resolution: {integrity: sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: detect-newline: 3.1.0 dev: false - /jest-each@28.1.3: - resolution: {integrity: sha512-arT1z4sg2yABU5uogObVPvSlSMQlDA48owx07BDPAiasW0yYpYHYOo4HHLz9q0BVzDVU4hILFjzJw0So9aCL/g==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-each@29.6.2: + resolution: {integrity: sha512-MsrsqA0Ia99cIpABBc3izS1ZYoYfhIy0NNWqPSE0YXbQjwchyt6B1HD2khzyPe1WiJA7hbxXy77ZoUQxn8UlSw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 28.1.3 + '@jest/types': 29.6.1 chalk: 4.1.2 - jest-get-type: 28.0.2 - jest-util: 28.1.3 - pretty-format: 28.1.3 - dev: false - - /jest-environment-jsdom@28.1.3: - resolution: {integrity: sha512-HnlGUmZRdxfCByd3GM2F100DgQOajUBzEitjGqIREcb45kGjZvRrKUdlaF6escXBdcXNl0OBh+1ZrfeZT3GnAg==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - '@jest/environment': 28.1.3 - '@jest/fake-timers': 28.1.3 - '@jest/types': 28.1.3 - '@types/jsdom': 16.2.15 - '@types/node': 20.3.2 - jest-mock: 28.1.3 - jest-util: 28.1.3 - jsdom: 19.0.0 + jest-get-type: 29.4.3 + jest-util: 29.6.2 + pretty-format: 29.6.2 + dev: false + + /jest-environment-jsdom@29.6.2: + resolution: {integrity: sha512-7oa/+266AAEgkzae8i1awNEfTfjwawWKLpiw2XesZmaoVVj9u9t8JOYx18cG29rbPNtkUlZ8V4b5Jb36y/VxoQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + '@jest/environment': 29.6.2 + '@jest/fake-timers': 29.6.2 + '@jest/types': 29.6.1 + '@types/jsdom': 20.0.1 + '@types/node': 20.5.0 + jest-mock: 29.6.2 + jest-util: 29.6.2 + jsdom: 20.0.3 transitivePeerDependencies: - bufferutil - - canvas - supports-color - utf-8-validate dev: false - /jest-environment-node@28.1.3: - resolution: {integrity: sha512-ugP6XOhEpjAEhGYvp5Xj989ns5cB1K6ZdjBYuS30umT4CQEETaxSiPcZ/E1kFktX4GkrcM4qu07IIlDYX1gp+A==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-environment-node@29.6.2: + resolution: {integrity: sha512-YGdFeZ3T9a+/612c5mTQIllvWkddPbYcN2v95ZH24oWMbGA4GGS2XdIF92QMhUhvrjjuQWYgUGW2zawOyH63MQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 28.1.3 - '@jest/fake-timers': 28.1.3 - '@jest/types': 28.1.3 + '@jest/environment': 29.6.2 + '@jest/fake-timers': 29.6.2 + '@jest/types': 29.6.1 '@types/node': 20.5.0 - jest-mock: 28.1.3 - jest-util: 28.1.3 - dev: false - - /jest-get-type@28.0.2: - resolution: {integrity: sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + jest-mock: 29.6.2 + jest-util: 29.6.2 dev: false /jest-get-type@29.2.0: @@ -4621,60 +3808,55 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: false - /jest-haste-map@28.1.3: - resolution: {integrity: sha512-3S+RQWDXccXDKSWnkHa/dPwt+2qwA8CJzR61w3FoYCvoo3Pn8tvGcysmMF0Bj0EX5RYvAI2EIvC57OmotfdtKA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-get-type@29.4.3: + resolution: {integrity: sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: false + + /jest-haste-map@29.3.1: + resolution: {integrity: sha512-/FFtvoG1xjbbPXQLFef+WSU4yrc0fc0Dds6aRPBojUid7qlPqZvxdUBA03HW0fnVHXVCnCdkuoghYItKNzc/0A==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 28.1.3 + '@jest/types': 29.3.1 '@types/graceful-fs': 4.1.5 '@types/node': 20.5.0 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.10 - jest-regex-util: 28.0.2 - jest-util: 28.1.3 - jest-worker: 28.1.3 + jest-regex-util: 29.2.0 + jest-util: 29.3.1 + jest-worker: 29.3.1 micromatch: 4.0.5 walker: 1.0.8 optionalDependencies: fsevents: 2.3.2 dev: false - /jest-haste-map@29.3.1: - resolution: {integrity: sha512-/FFtvoG1xjbbPXQLFef+WSU4yrc0fc0Dds6aRPBojUid7qlPqZvxdUBA03HW0fnVHXVCnCdkuoghYItKNzc/0A==} + /jest-haste-map@29.6.2: + resolution: {integrity: sha512-+51XleTDAAysvU8rT6AnS1ZJ+WHVNqhj1k6nTvN2PYP+HjU3kqlaKQ1Lnw3NYW3bm2r8vq82X0Z1nDDHZMzHVA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.3.1 + '@jest/types': 29.6.1 '@types/graceful-fs': 4.1.5 '@types/node': 20.5.0 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.10 - jest-regex-util: 29.2.0 - jest-util: 29.3.1 - jest-worker: 29.3.1 + jest-regex-util: 29.4.3 + jest-util: 29.6.2 + jest-worker: 29.6.2 micromatch: 4.0.5 walker: 1.0.8 optionalDependencies: fsevents: 2.3.2 dev: false - /jest-leak-detector@28.1.3: - resolution: {integrity: sha512-WFVJhnQsiKtDEo5lG2mM0v40QWnBM+zMdHHyJs8AWZ7J0QZJS59MsyKeJHWhpBZBH32S48FOVvGyOFT1h0DlqA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - jest-get-type: 28.0.2 - pretty-format: 28.1.3 - dev: false - - /jest-matcher-utils@28.1.3: - resolution: {integrity: sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-leak-detector@29.6.2: + resolution: {integrity: sha512-aNqYhfp5uYEO3tdWMb2bfWv6f0b4I0LOxVRpnRLAeque2uqOVVMLh6khnTcE2qJ5wAKop0HcreM1btoysD6bPQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - chalk: 4.1.2 - jest-diff: 28.1.3 - jest-get-type: 28.0.2 - pretty-format: 28.1.3 + jest-get-type: 29.4.3 + pretty-format: 29.6.2 dev: false /jest-matcher-utils@29.3.1: @@ -4687,19 +3869,14 @@ packages: pretty-format: 29.3.1 dev: false - /jest-message-util@28.1.3: - resolution: {integrity: sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-matcher-utils@29.6.2: + resolution: {integrity: sha512-4LiAk3hSSobtomeIAzFTe+N8kL6z0JtF3n6I4fg29iIW7tt99R7ZcIFW34QkX+DuVrf+CUe6wuVOpm7ZKFJzZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/code-frame': 7.18.6 - '@jest/types': 28.1.3 - '@types/stack-utils': 2.0.1 chalk: 4.1.2 - graceful-fs: 4.2.10 - micromatch: 4.0.5 - pretty-format: 28.1.3 - slash: 3.0.0 - stack-utils: 2.0.6 + jest-diff: 29.6.2 + jest-get-type: 29.4.3 + pretty-format: 29.6.2 dev: false /jest-message-util@29.3.1: @@ -4717,12 +3894,19 @@ packages: stack-utils: 2.0.6 dev: false - /jest-mock@28.1.3: - resolution: {integrity: sha512-o3J2jr6dMMWYVH4Lh/NKmDXdosrsJgi4AviS8oXLujcjpCMBb1FMsblDnOXKZKfSiHLxYub1eS0IHuRXsio9eA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-message-util@29.6.2: + resolution: {integrity: sha512-vnIGYEjoPSuRqV8W9t+Wow95SDp6KPX2Uf7EoeG9G99J2OVh7OSwpS4B6J0NfpEIpfkBNHlBZpA2rblEuEFhZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 28.1.3 - '@types/node': 20.5.0 + '@babel/code-frame': 7.18.6 + '@jest/types': 29.6.1 + '@types/stack-utils': 2.0.1 + chalk: 4.1.2 + graceful-fs: 4.2.10 + micromatch: 4.0.5 + pretty-format: 29.6.2 + slash: 3.0.0 + stack-utils: 2.0.6 dev: false /jest-mock@29.3.1: @@ -4734,7 +3918,16 @@ packages: jest-util: 29.3.1 dev: false - /jest-pnp-resolver@1.2.3(jest-resolve@28.1.3): + /jest-mock@29.6.2: + resolution: {integrity: sha512-hoSv3lb3byzdKfwqCuT6uTscan471GUECqgNYykg6ob0yiAw3zYc7OrPnI9Qv8Wwoa4lC7AZ9hyS4AiIx5U2zg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.1 + '@types/node': 20.5.0 + jest-util: 29.6.2 + dev: false + + /jest-pnp-resolver@1.2.3(jest-resolve@29.6.2): resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} engines: {node: '>=6'} peerDependencies: @@ -4743,12 +3936,7 @@ packages: jest-resolve: optional: true dependencies: - jest-resolve: 28.1.3 - dev: false - - /jest-regex-util@28.0.2: - resolution: {integrity: sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + jest-resolve: 29.6.2 dev: false /jest-regex-util@29.2.0: @@ -4756,129 +3944,103 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: false - /jest-resolve-dependencies@28.1.3: - resolution: {integrity: sha512-qa0QO2Q0XzQoNPouMbCc7Bvtsem8eQgVPNkwn9LnS+R2n8DaVDPL/U1gngC0LTl1RYXJU0uJa2BMC2DbTfFrHA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-regex-util@29.4.3: + resolution: {integrity: sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: false + + /jest-resolve-dependencies@29.6.2: + resolution: {integrity: sha512-LGqjDWxg2fuQQm7ypDxduLu/m4+4Lb4gczc13v51VMZbVP5tSBILqVx8qfWcsdP8f0G7aIqByIALDB0R93yL+w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - jest-regex-util: 28.0.2 - jest-snapshot: 28.1.3 + jest-regex-util: 29.4.3 + jest-snapshot: 29.6.2 transitivePeerDependencies: - supports-color dev: false - /jest-resolve@28.1.3: - resolution: {integrity: sha512-Z1W3tTjE6QaNI90qo/BJpfnvpxtaFTFw5CDgwpyE/Kz8U/06N1Hjf4ia9quUhCh39qIGWF1ZuxFiBiJQwSEYKQ==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-resolve@29.6.2: + resolution: {integrity: sha512-G/iQUvZWI5e3SMFssc4ug4dH0aZiZpsDq9o1PtXTV1210Ztyb2+w+ZgQkB3iOiC5SmAEzJBOHWz6Hvrd+QnNPw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: chalk: 4.1.2 graceful-fs: 4.2.10 - jest-haste-map: 28.1.3 - jest-pnp-resolver: 1.2.3(jest-resolve@28.1.3) - jest-util: 28.1.3 - jest-validate: 28.1.3 + jest-haste-map: 29.6.2 + jest-pnp-resolver: 1.2.3(jest-resolve@29.6.2) + jest-util: 29.6.2 + jest-validate: 29.6.2 resolve: 1.22.1 - resolve.exports: 1.1.0 + resolve.exports: 2.0.2 slash: 3.0.0 dev: false - /jest-runner@28.1.3: - resolution: {integrity: sha512-GkMw4D/0USd62OVO0oEgjn23TM+YJa2U2Wu5zz9xsQB1MxWKDOlrnykPxnMsN0tnJllfLPinHTka61u0QhaxBA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-runner@29.6.2: + resolution: {integrity: sha512-wXOT/a0EspYgfMiYHxwGLPCZfC0c38MivAlb2lMEAlwHINKemrttu1uSbcGbfDV31sFaPWnWJPmb2qXM8pqZ4w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/console': 28.1.3 - '@jest/environment': 28.1.3 - '@jest/test-result': 28.1.3 - '@jest/transform': 28.1.3 - '@jest/types': 28.1.3 + '@jest/console': 29.6.2 + '@jest/environment': 29.6.2 + '@jest/test-result': 29.6.2 + '@jest/transform': 29.6.2 + '@jest/types': 29.6.1 '@types/node': 20.5.0 chalk: 4.1.2 - emittery: 0.10.2 + emittery: 0.13.1 graceful-fs: 4.2.10 - jest-docblock: 28.1.1 - jest-environment-node: 28.1.3 - jest-haste-map: 28.1.3 - jest-leak-detector: 28.1.3 - jest-message-util: 28.1.3 - jest-resolve: 28.1.3 - jest-runtime: 28.1.3 - jest-util: 28.1.3 - jest-watcher: 28.1.3 - jest-worker: 28.1.3 + jest-docblock: 29.4.3 + jest-environment-node: 29.6.2 + jest-haste-map: 29.6.2 + jest-leak-detector: 29.6.2 + jest-message-util: 29.6.2 + jest-resolve: 29.6.2 + jest-runtime: 29.6.2 + jest-util: 29.6.2 + jest-watcher: 29.6.2 + jest-worker: 29.6.2 p-limit: 3.1.0 source-map-support: 0.5.13 transitivePeerDependencies: - supports-color dev: false - /jest-runtime@28.1.3: - resolution: {integrity: sha512-NU+881ScBQQLc1JHG5eJGU7Ui3kLKrmwCPPtYsJtBykixrM2OhVQlpMmFWJjMyDfdkGgBMNjXCGB/ebzsgNGQw==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-runtime@29.6.2: + resolution: {integrity: sha512-2X9dqK768KufGJyIeLmIzToDmsN0m7Iek8QNxRSI/2+iPFYHF0jTwlO3ftn7gdKd98G/VQw9XJCk77rbTGZnJg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 28.1.3 - '@jest/fake-timers': 28.1.3 - '@jest/globals': 28.1.3 - '@jest/source-map': 28.1.2 - '@jest/test-result': 28.1.3 - '@jest/transform': 28.1.3 - '@jest/types': 28.1.3 + '@jest/environment': 29.6.2 + '@jest/fake-timers': 29.6.2 + '@jest/globals': 29.6.2 + '@jest/source-map': 29.6.0 + '@jest/test-result': 29.6.2 + '@jest/transform': 29.6.2 + '@jest/types': 29.6.1 + '@types/node': 20.5.0 chalk: 4.1.2 cjs-module-lexer: 1.2.2 collect-v8-coverage: 1.0.1 - execa: 5.1.1 glob: 7.2.3 graceful-fs: 4.2.10 - jest-haste-map: 28.1.3 - jest-message-util: 28.1.3 - jest-mock: 28.1.3 - jest-regex-util: 28.0.2 - jest-resolve: 28.1.3 - jest-snapshot: 28.1.3 - jest-util: 28.1.3 + jest-haste-map: 29.6.2 + jest-message-util: 29.6.2 + jest-mock: 29.6.2 + jest-regex-util: 29.4.3 + jest-resolve: 29.6.2 + jest-snapshot: 29.6.2 + jest-util: 29.6.2 slash: 3.0.0 strip-bom: 4.0.0 transitivePeerDependencies: - supports-color dev: false - /jest-snapshot@28.1.3: - resolution: {integrity: sha512-4lzMgtiNlc3DU/8lZfmqxN3AYD6GGLbl+72rdBpXvcV+whX7mDrREzkPdp2RnmfIiWBg1YbuFSkXduF2JcafJg==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - '@babel/core': 7.20.5 - '@babel/generator': 7.20.5 - '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.20.5) - '@babel/traverse': 7.20.5 - '@babel/types': 7.20.5 - '@jest/expect-utils': 28.1.3 - '@jest/transform': 28.1.3 - '@jest/types': 28.1.3 - '@types/babel__traverse': 7.18.3 - '@types/prettier': 2.7.1 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.20.5) - chalk: 4.1.2 - expect: 28.1.3 - graceful-fs: 4.2.10 - jest-diff: 28.1.3 - jest-get-type: 28.0.2 - jest-haste-map: 28.1.3 - jest-matcher-utils: 28.1.3 - jest-message-util: 28.1.3 - jest-util: 28.1.3 - natural-compare: 1.4.0 - pretty-format: 28.1.3 - semver: 7.5.4 - transitivePeerDependencies: - - supports-color - dev: false - /jest-snapshot@29.3.1: resolution: {integrity: sha512-+3JOc+s28upYLI2OJM4PWRGK9AgpsMs/ekNryUV0yMBClT9B1DF2u2qay8YxcQd338PPYSFNb0lsar1B49sLDA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/core': 7.20.5 + '@babel/core': 7.22.10 '@babel/generator': 7.20.5 - '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.20.5) - '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.20.5) + '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.22.10) + '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.22.10) '@babel/traverse': 7.20.5 '@babel/types': 7.20.5 '@jest/expect-utils': 29.3.1 @@ -4886,7 +4048,7 @@ packages: '@jest/types': 29.3.1 '@types/babel__traverse': 7.18.3 '@types/prettier': 2.7.1 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.20.5) + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.10) chalk: 4.1.2 expect: 29.3.1 graceful-fs: 4.2.10 @@ -4903,16 +4065,32 @@ packages: - supports-color dev: false - /jest-util@28.1.3: - resolution: {integrity: sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-snapshot@29.6.2: + resolution: {integrity: sha512-1OdjqvqmRdGNvWXr/YZHuyhh5DeaLp1p/F8Tht/MrMw4Kr1Uu/j4lRG+iKl1DAqUJDWxtQBMk41Lnf/JETYBRA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 28.1.3 - '@types/node': 20.5.0 + '@babel/core': 7.22.10 + '@babel/generator': 7.20.5 + '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.22.10) + '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.22.10) + '@babel/types': 7.20.5 + '@jest/expect-utils': 29.6.2 + '@jest/transform': 29.6.2 + '@jest/types': 29.6.1 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.10) chalk: 4.1.2 - ci-info: 3.7.0 + expect: 29.6.2 graceful-fs: 4.2.10 - picomatch: 2.3.1 + jest-diff: 29.6.2 + jest-get-type: 29.4.3 + jest-matcher-utils: 29.6.2 + jest-message-util: 29.6.2 + jest-util: 29.6.2 + natural-compare: 1.4.0 + pretty-format: 29.6.2 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color dev: false /jest-util@29.3.1: @@ -4927,54 +4105,67 @@ packages: picomatch: 2.3.1 dev: false - /jest-validate@28.1.3: - resolution: {integrity: sha512-SZbOGBWEsaTxBGCOpsRWlXlvNkvTkY0XxRfh7zYmvd8uL5Qzyg0CHAXiXKROflh801quA6+/DsT4ODDthOC/OA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-util@29.6.2: + resolution: {integrity: sha512-3eX1qb6L88lJNCFlEADKOkjpXJQyZRiavX1INZ4tRnrBVr2COd3RgcTLyUiEXMNBlDU/cgYq6taUS0fExrWW4w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.1 + '@types/node': 20.5.0 + chalk: 4.1.2 + ci-info: 3.7.0 + graceful-fs: 4.2.10 + picomatch: 2.3.1 + dev: false + + /jest-validate@29.6.2: + resolution: {integrity: sha512-vGz0yMN5fUFRRbpJDPwxMpgSXW1LDKROHfBopAvDcmD6s+B/s8WJrwi+4bfH4SdInBA5C3P3BI19dBtKzx1Arg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 28.1.3 + '@jest/types': 29.6.1 camelcase: 6.3.0 chalk: 4.1.2 - jest-get-type: 28.0.2 + jest-get-type: 29.4.3 leven: 3.1.0 - pretty-format: 28.1.3 + pretty-format: 29.6.2 dev: false - /jest-watcher@28.1.3: - resolution: {integrity: sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-watcher@29.6.2: + resolution: {integrity: sha512-GZitlqkMkhkefjfN/p3SJjrDaxPflqxEAv3/ik10OirZqJGYH5rPiIsgVcfof0Tdqg3shQGdEIxDBx+B4tuLzA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/test-result': 28.1.3 - '@jest/types': 28.1.3 + '@jest/test-result': 29.6.2 + '@jest/types': 29.6.1 '@types/node': 20.5.0 ansi-escapes: 4.3.2 chalk: 4.1.2 - emittery: 0.10.2 - jest-util: 28.1.3 + emittery: 0.13.1 + jest-util: 29.6.2 string-length: 4.0.2 dev: false - /jest-worker@28.1.3: - resolution: {integrity: sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest-worker@29.3.1: + resolution: {integrity: sha512-lY4AnnmsEWeiXirAIA0c9SDPbuCBq8IYuDVL8PMm0MZ2PEs2yPvRA/J64QBXuZp7CYKrDM/rmNrc9/i3KJQncw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@types/node': 20.5.0 + jest-util: 29.3.1 merge-stream: 2.0.0 supports-color: 8.1.1 dev: false - /jest-worker@29.3.1: - resolution: {integrity: sha512-lY4AnnmsEWeiXirAIA0c9SDPbuCBq8IYuDVL8PMm0MZ2PEs2yPvRA/J64QBXuZp7CYKrDM/rmNrc9/i3KJQncw==} + /jest-worker@29.6.2: + resolution: {integrity: sha512-l3ccBOabTdkng8I/ORCkADz4eSMKejTYv1vB/Z83UiubqhC1oQ5Li6dWCyqOIvSifGjUBxuvxvlm6KGK2DtuAQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@types/node': 20.5.0 - jest-util: 29.3.1 + jest-util: 29.6.2 merge-stream: 2.0.0 supports-color: 8.1.1 dev: false - /jest@28.1.3(@types/node@20.5.0): - resolution: {integrity: sha512-N4GT5on8UkZgH0O5LUavMRV1EDEhNTL0KEfRmDIeZHSV7p2XgLoY9t9VDUgL6o+yfdgYHVxuz81G8oB9VG5uyA==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /jest@29.6.2(@types/node@20.5.0): + resolution: {integrity: sha512-8eQg2mqFbaP7CwfsTpCxQ+sHzw1WuNWL5UUvjnWP4hx2riGz9fPSzYOaU5q8/GqWn1TfgZIVTqYJygbGbWAANg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -4982,12 +4173,13 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 28.1.3 - '@jest/types': 28.1.3 + '@jest/core': 29.6.2 + '@jest/types': 29.6.1 import-local: 3.1.0 - jest-cli: 28.1.3(@types/node@20.5.0) + jest-cli: 29.6.2(@types/node@20.5.0) transitivePeerDependencies: - '@types/node' + - babel-plugin-macros - supports-color - ts-node dev: false @@ -5011,9 +4203,9 @@ packages: argparse: 2.0.1 dev: false - /jsdom@19.0.0: - resolution: {integrity: sha512-RYAyjCbxy/vri/CfnjUWJQQtZ3LKlLnDqj+9XLNnJPgEGeirZs3hllKR20re8LUZ6o1b1X4Jat+Qd26zmP41+A==} - engines: {node: '>=12'} + /jsdom@20.0.3: + resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} + engines: {node: '>=14'} peerDependencies: canvas: ^2.5.0 peerDependenciesMeta: @@ -5021,8 +4213,8 @@ packages: optional: true dependencies: abab: 2.0.6 - acorn: 8.8.1 - acorn-globals: 6.0.0 + acorn: 8.9.0 + acorn-globals: 7.0.1 cssom: 0.5.0 cssstyle: 2.3.0 data-urls: 3.0.2 @@ -5035,16 +4227,15 @@ packages: https-proxy-agent: 5.0.1 is-potential-custom-element-name: 1.0.1 nwsapi: 2.2.2 - parse5: 6.0.1 - saxes: 5.0.1 + parse5: 7.1.2 + saxes: 6.0.0 symbol-tree: 3.2.4 tough-cookie: 4.1.2 - w3c-hr-time: 1.0.2 - w3c-xmlserializer: 3.0.0 + w3c-xmlserializer: 4.0.0 webidl-conversions: 7.0.0 whatwg-encoding: 2.0.0 whatwg-mimetype: 3.0.0 - whatwg-url: 10.0.0 + whatwg-url: 11.0.0 ws: 8.11.0 xml-name-validator: 4.0.0 transitivePeerDependencies: @@ -5053,11 +4244,6 @@ packages: - utf-8-validate dev: false - /jsesc@0.5.0: - resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} - hasBin: true - dev: false - /jsesc@2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} @@ -5108,15 +4294,6 @@ packages: type-check: 0.4.0 dev: false - /lichess-pgn-viewer@1.6.1: - resolution: {integrity: sha512-rKGRbpqy7xgcQ8S5Wneu86UFuJnsTPyVw0TJYCUHxO/+x5eIN8GF8TCxf2Q/aZ6YR4kec2SLjIBTik4KejHeZw==} - dependencies: - '@types/node': 18.16.18 - chessground: 8.3.13 - chessops: 0.12.7 - snabbdom: 3.5.1 - dev: false - /lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} @@ -5177,14 +4354,6 @@ packages: p-locate: 5.0.0 dev: false - /lodash.debounce@4.0.8: - resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - dev: false - - /lodash.memoize@4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - dev: false - /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: false @@ -5207,6 +4376,12 @@ packages: js-tokens: 4.0.0 dev: false + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: false + /lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -5218,11 +4393,7 @@ packages: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} dependencies: - semver: 6.3.0 - dev: false - - /make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + semver: 6.3.1 dev: false /makeerror@1.0.12: @@ -5292,8 +4463,8 @@ packages: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} dev: false - /node-releases@2.0.8: - resolution: {integrity: sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==} + /node-releases@2.0.13: + resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} dev: false /normalize-path@3.0.0: @@ -5420,14 +4591,16 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.22.10 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 dev: false - /parse5@6.0.1: - resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + /parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + dependencies: + entities: 4.5.0 dev: false /path-exists@4.0.0: @@ -5502,21 +4675,20 @@ packages: hasBin: true dev: false - /pretty-format@28.1.3: - resolution: {integrity: sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + /pretty-format@29.3.1: + resolution: {integrity: sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/schemas': 28.1.3 - ansi-regex: 5.0.1 + '@jest/schemas': 29.0.0 ansi-styles: 5.2.0 react-is: 18.2.0 dev: false - /pretty-format@29.3.1: - resolution: {integrity: sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg==} + /pretty-format@29.6.2: + resolution: {integrity: sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/schemas': 29.0.0 + '@jest/schemas': 29.6.0 ansi-styles: 5.2.0 react-is: 18.2.0 dev: false @@ -5599,16 +4771,15 @@ packages: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} dev: false - /punycode@2.1.1: - resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} - engines: {node: '>=6'} - dev: false - /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} dev: false + /pure-rand@6.0.2: + resolution: {integrity: sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==} + dev: false + /querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} dev: false @@ -5625,48 +4796,11 @@ packages: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: false - /regenerate-unicode-properties@10.1.0: - resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==} - engines: {node: '>=4'} - dependencies: - regenerate: 1.4.2 - dev: false - - /regenerate@1.4.2: - resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} - dev: false - - /regenerator-runtime@0.13.11: - resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} - dev: false - - /regenerator-transform@0.15.1: - resolution: {integrity: sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==} - dependencies: - '@babel/runtime': 7.20.6 - dev: false - - /regexpu-core@5.2.2: - resolution: {integrity: sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw==} - engines: {node: '>=4'} - dependencies: - regenerate: 1.4.2 - regenerate-unicode-properties: 10.1.0 - regjsgen: 0.7.1 - regjsparser: 0.9.1 - unicode-match-property-ecmascript: 2.0.0 - unicode-match-property-value-ecmascript: 2.1.0 - dev: false - - /regjsgen@0.7.1: - resolution: {integrity: sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==} - dev: false - - /regjsparser@0.9.1: - resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} - hasBin: true + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} dependencies: - jsesc: 0.5.0 + picomatch: 2.3.1 dev: false /require-directory@2.1.1: @@ -5695,8 +4829,8 @@ packages: engines: {node: '>=8'} dev: false - /resolve.exports@1.1.0: - resolution: {integrity: sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==} + /resolve.exports@2.0.2: + resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} engines: {node: '>=10'} dev: false @@ -5751,28 +4885,43 @@ packages: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} dev: false + /sass@1.65.1: + resolution: {integrity: sha512-9DINwtHmA41SEd36eVPQ9BJKpn7eKDQmUHmpI0y5Zv2Rcorrh0zS+cFrt050hdNbmmCNKTW3hV5mWfuegNRsEA==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + chokidar: 3.5.3 + immutable: 4.3.2 + source-map-js: 1.0.2 + dev: false + /sax@1.2.4: resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} dev: false - /saxes@5.0.1: - resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} - engines: {node: '>=10'} + /saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} dependencies: xmlchars: 2.2.0 dev: false - /semver@6.3.0: - resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} - hasBin: true + /schlawg-pgn-viewer@2.0.0: + resolution: {integrity: sha512-SjuwlF+xB3xSn5fnOzowT2+22SShjdNEU/AbCc9oN9zZhn0qjrP5hfrZA0F0KGsJaHgRQe/T34HoH0boUYstnw==} + dependencies: + '@types/node': 20.5.0 + chessground: /schlawground@9.0.1 + chessops: 0.12.7 + snabbdom: 3.5.1 dev: false - /semver@7.3.8: - resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} - engines: {node: '>=10'} + /schlawground@9.0.1: + resolution: {integrity: sha512-nIlOhNk4InEN1aNXDazF2fqia3b9jBmYT+YphoTMYS2f43P6MaFPze7dz1NDCr9oTDNUupdDnwGu6h6TtpAAYQ==} + dev: false + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - dependencies: - lru-cache: 6.0.0 dev: false /semver@7.5.3: @@ -5829,6 +4978,11 @@ packages: engines: {node: '>=8.3.0'} dev: false + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: false + /source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} dependencies: @@ -5954,14 +5108,6 @@ packages: has-flag: 4.0.0 dev: false - /supports-hyperlinks@2.3.0: - resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} - engines: {node: '>=8'} - dependencies: - has-flag: 4.0.0 - supports-color: 7.2.0 - dev: false - /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -6029,14 +5175,6 @@ packages: resolution: {integrity: sha512-WkfcZBHsp47gVH9CBHG0ZXopriG01IA87arGrchvIe868d4RiXVvoYPS1zMq9IdW05kBs5iGsqxTABqLyWonbg==} dev: false - /terminal-link@2.1.1: - resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==} - engines: {node: '>=8'} - dependencies: - ansi-escapes: 4.3.2 - supports-hyperlinks: 2.3.0 - dev: false - /test-exclude@6.0.0: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} @@ -6071,7 +5209,7 @@ packages: engines: {node: '>=6'} dependencies: psl: 1.9.0 - punycode: 2.1.1 + punycode: 2.3.0 universalify: 0.2.0 url-parse: 1.5.10 dev: false @@ -6092,40 +5230,6 @@ packages: typescript: 5.1.6 dev: false - /ts-jest@28.0.8(@babel/core@7.20.5)(jest@28.1.3)(typescript@5.1.6): - resolution: {integrity: sha512-5FaG0lXmRPzApix8oFG8RKjAz4ehtm8yMKOTy5HX3fY6W8kmvOrmcY0hKDElW52FJov+clhUbrKAqofnj4mXTg==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/types': ^28.0.0 - babel-jest: ^28.0.0 - esbuild: '*' - jest: ^28.0.0 - typescript: '>=4.3' - peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - dependencies: - '@babel/core': 7.20.5 - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - jest: 28.1.3(@types/node@20.5.0) - jest-util: 28.1.3 - json5: 2.2.2 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.3.8 - typescript: 5.1.6 - yargs-parser: 21.1.1 - dev: false - /type-check@0.3.2: resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} engines: {node: '>= 0.8.0'} @@ -6170,41 +5274,18 @@ packages: hasBin: true dev: false - /unicode-canonical-property-names-ecmascript@2.0.0: - resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} - engines: {node: '>=4'} - dev: false - - /unicode-match-property-ecmascript@2.0.0: - resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} - engines: {node: '>=4'} - dependencies: - unicode-canonical-property-names-ecmascript: 2.0.0 - unicode-property-aliases-ecmascript: 2.1.0 - dev: false - - /unicode-match-property-value-ecmascript@2.1.0: - resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} - engines: {node: '>=4'} - dev: false - - /unicode-property-aliases-ecmascript@2.1.0: - resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} - engines: {node: '>=4'} - dev: false - /universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} dev: false - /update-browserslist-db@1.0.10(browserslist@4.21.4): - resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==} + /update-browserslist-db@1.0.11(browserslist@4.21.10): + resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' dependencies: - browserslist: 4.21.4 + browserslist: 4.21.10 escalade: 3.1.1 picocolors: 1.0.0 dev: false @@ -6231,7 +5312,7 @@ packages: resolution: {integrity: sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==} engines: {node: '>=10.12.0'} dependencies: - '@jridgewell/trace-mapping': 0.3.17 + '@jridgewell/trace-mapping': 0.3.19 '@types/istanbul-lib-coverage': 2.0.4 convert-source-map: 1.9.0 dev: false @@ -6242,20 +5323,13 @@ packages: uuid: 9.0.0 dev: false - /w3c-hr-time@1.0.2: - resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} - deprecated: Use your platform's native performance.now() and performance.timeOrigin. - dependencies: - browser-process-hrtime: 1.0.0 - dev: false - /w3c-keyname@2.2.6: resolution: {integrity: sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==} dev: false - /w3c-xmlserializer@3.0.0: - resolution: {integrity: sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==} - engines: {node: '>=12'} + /w3c-xmlserializer@4.0.0: + resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} + engines: {node: '>=14'} dependencies: xml-name-validator: 4.0.0 dev: false @@ -6283,14 +5357,6 @@ packages: engines: {node: '>=12'} dev: false - /whatwg-url@10.0.0: - resolution: {integrity: sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w==} - engines: {node: '>=12'} - dependencies: - tr46: 3.0.0 - webidl-conversions: 7.0.0 - dev: false - /whatwg-url@11.0.0: resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} engines: {node: '>=12'} @@ -6382,6 +5448,10 @@ packages: engines: {node: '>=10'} dev: false + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: false + /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} dev: false diff --git a/ui/analyse/css/build/_analyse.keyboard.scss b/ui/analyse/css/build/_analyse.keyboard.scss index 33b738be30d73..4939f1a00afe5 100644 --- a/ui/analyse/css/build/_analyse.keyboard.scss +++ b/ui/analyse/css/build/_analyse.keyboard.scss @@ -1,3 +1,3 @@ @import '../../../common/css/plugin'; -@import '../../../common/css/component/help-modal'; +@import '../../../common/css/component/help'; @import '../keyboard'; diff --git a/ui/analyse/src/autoShape.ts b/ui/analyse/src/autoShape.ts index 53addeec4191f..ab1f796f33606 100644 --- a/ui/analyse/src/autoShape.ts +++ b/ui/analyse/src/autoShape.ts @@ -136,7 +136,7 @@ export function compute(ctrl: AnalyseCtrl): DrawShape[] { existing.modifiers ??= {}; existing.modifiers.hilite = true; } - if (symbol) existing.label = { text: symbol }; + if (symbol) existing.label = { text: symbol, fill: glyphColors[symbol] }; } else shapes.push({ orig: node.uci!.slice(0, 2) as Key, diff --git a/ui/analyse/src/ctrl.ts b/ui/analyse/src/ctrl.ts index b1e4cd46c86ff..0325f3eaea8d5 100644 --- a/ui/analyse/src/ctrl.ts +++ b/ui/analyse/src/ctrl.ts @@ -632,6 +632,7 @@ export default class AnalyseCtrl { setAutoShapes = (): void => { this.withCg(cg => cg.setAutoShapes(computeAutoShapes(this))); + if (this.node.children.length > 1) keyboard.maybeShowShiftKeyHelp(); }; private onNewCeval = (ev: Tree.ClientEval, path: Tree.Path, isThreat?: boolean): void => { diff --git a/ui/analyse/src/keyboard.ts b/ui/analyse/src/keyboard.ts index 16ef7cc2f210c..26344e8d4c3a7 100644 --- a/ui/analyse/src/keyboard.ts +++ b/ui/analyse/src/keyboard.ts @@ -1,5 +1,6 @@ import * as control from './control'; import * as xhr from 'common/xhr'; +import { isTouchDevice } from 'common/mobile'; import AnalyseCtrl from './ctrl'; import { h, VNode } from 'snabbdom'; import { snabModal } from 'common/modal'; @@ -26,17 +27,16 @@ export const bind = (ctrl: AnalyseCtrl) => { .bind(['shift+right', 'shift+j'], () => {}) .bind(['right', 'j'], () => { if (!ctrl.fork.proceed()) control.next(ctrl); - ctrl.redraw(); }) .bind(['up', '0', 'home'], () => { - if (!ctrl.fork.prev()) control.first(ctrl); - else ctrl.setAutoShapes(); + /*if (!ctrl.fork.prev())*/ control.first(ctrl); + //else ctrl.setAutoShapes(); ctrl.redraw(); }) .bind(['down', '$', 'end'], () => { - if (!ctrl.fork.next()) control.last(ctrl); - else ctrl.setAutoShapes(); + /*if (!ctrl.fork.next())*/ control.last(ctrl); + //else ctrl.setAutoShapes(); ctrl.redraw(); }) .bind('shift+c', () => { @@ -139,3 +139,21 @@ export function view(ctrl: AnalyseCtrl): VNode { content: [h('div.scrollable', spinner())], }); } + +export function maybeShowShiftKeyHelp() { + // we can probably delete this after a month or so + if (isTouchDevice() || !lichess.once('help.analyse.shift-key')) return; + Promise.all([lichess.loadCssPath('analyse.keyboard'), xhr.text('/help/analyse/shift-key')]).then( + ([, html]) => { + $('.cg-wrap').append($(html).attr('id', 'analyse-shift-key-tooltip')); + $(document).on('mousedown keydown wheel', () => { + setTimeout(() => { + $(document).off('mousedown keydown wheel'); + $('#analyse-shift-key-tooltip').addClass('fade-out'); + + setTimeout(() => $('#analyse-shift-key-tooltip').remove(), 500); + }, 700); + }); + }, + ); +} diff --git a/ui/common/css/component/_help-modal.scss b/ui/common/css/component/_help.scss similarity index 55% rename from ui/common/css/component/_help-modal.scss rename to ui/common/css/component/_help.scss index dc2234dab6c0f..0304105acd912 100644 --- a/ui/common/css/component/_help-modal.scss +++ b/ui/common/css/component/_help.scss @@ -1,23 +1,6 @@ -%help-modal { +%help { @extend %flex-column; - > div { - padding: 0 !important; - } - - h2 { - margin: 0.5em 0 0 0; - } - - .scrollable { - overflow-y: auto; - padding: 0.8em 0; - } - - table { - width: 100%; - } - th p { margin: 1.2em 0 0.6em 0; background: $c-brag; @@ -31,16 +14,16 @@ text-align: left; } + .desc:first-letter { + text-transform: uppercase; + } + .keys { padding-#{$end-direction}: 1em; text-align: right; white-space: nowrap; } - .desc:first-letter { - text-transform: uppercase; - } - or { margin-#{$start-direction}: 0.2em; opacity: 0.5; @@ -61,3 +44,68 @@ box-shadow: inset 0 -1px 0 #bbb; } } + +%help-modal { + @extend %help; + + table { + width: 100%; + } + + > div { + padding: 0 !important; + } + + h2 { + margin: 0.5em 0 0 0; + } + + .scrollable { + overflow-y: auto; + padding: 0.8em 0; + } +} + +.help-ephemeral { + @extend %help; + @include back-blur(); + position: absolute; + z-index: 100; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: auto; + align-items: center; + background: set-alpha($c-bg-popup, 0.8); + box-shadow: 0 0 1em rgba(0, 0, 0, 0.4); + color: $c-font-clear; + border-radius: 0.5em; + padding: 1em 2em; + user-select: none; + pointer-events: none; + + kbd { + font-family: roboto, sans-serif; + } + + table { + display: inline-block; + width: auto; + } + + ul { + margin-bottom: 1em; + display: inline-block; + width: auto; + font-size: larger; + li { + padding-left: 0.5em; + list-style: disc; + } + } + + &.fade-out { + transition: opacity 0.5s linear; + opacity: 0; + } +} diff --git a/ui/common/css/theme/_default.scss b/ui/common/css/theme/_default.scss index 338cdb9c779ef..dcb75e6c73fbd 100644 --- a/ui/common/css/theme/_default.scss +++ b/ui/common/css/theme/_default.scss @@ -10,6 +10,11 @@ @return mix(#000, $color, $weight); } +@function set-alpha($color, $alpha) { + // set alpha channel if it already exists, add it if not + @return rgba(red($color), green($color), blue($color), $alpha); +} + $c-site-hue: 37; $c-bg-page: hsl($c-site-hue, 10%, 92%); diff --git a/ui/common/package.json b/ui/common/package.json index a0975e5e913dc..fe4ced59f3430 100644 --- a/ui/common/package.json +++ b/ui/common/package.json @@ -26,7 +26,7 @@ "dependencies": { "@types/cash": "workspace:*", "@types/lichess": "workspace:*", - "lichess-pgn-viewer": "^2.0.0", + "lichess-pgn-viewer": "npm:schlawg-pgn-viewer@2.0.0", "snabbdom": "^3.5.1", "tablesort": "^5.3.0" }, diff --git a/ui/jest.config.js b/ui/jest.config.js index aa94ae8b73cac..7e8423a370eb5 100644 --- a/ui/jest.config.js +++ b/ui/jest.config.js @@ -1,17 +1,9 @@ -/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ /* eslint-env node */ module.exports = { - preset: 'ts-jest', + testMatch: ['**/dist/**/*.test.js'], testEnvironment: 'jsdom', - transform: { - '^.+\\.(ts|tsx)?$': 'ts-jest', - '^.+\\.(js|jsx)$': 'babel-jest', - }, - transformIgnorePatterns: ['/node_modules/.pnpm/(?!chessground)'], + transform: {}, globals: { lichess: {}, - 'ts-jest': { - tsconfig: 'tsconfig.base.json', - }, }, }; diff --git a/ui/keyboardMove/css/build/_keyboardMove.help.scss b/ui/keyboardMove/css/build/_keyboardMove.help.scss index 4f6579d1eab52..abea3dff3ed9b 100644 --- a/ui/keyboardMove/css/build/_keyboardMove.help.scss +++ b/ui/keyboardMove/css/build/_keyboardMove.help.scss @@ -1,4 +1,4 @@ @import '../../../common/css/plugin'; @import '../../../common/css/component/modal'; -@import '../../../common/css/component/help-modal'; +@import '../../../common/css/component/help'; @import '../keyboardMove.help'; diff --git a/ui/opening/package.json b/ui/opening/package.json index 26405b274126d..90be7d1d19e53 100644 --- a/ui/opening/package.json +++ b/ui/opening/package.json @@ -14,7 +14,7 @@ "common": "workspace:*", "date-fns": "^2.29.3", "debounce-promise": "^3.1.2", - "lichess-pgn-viewer": "^2.0.0" + "lichess-pgn-viewer": "npm:schlawg-pgn-viewer@2.0.0" }, "scripts": { "compile": "tsc", diff --git a/ui/opening/src/main.ts b/ui/opening/src/main.ts index 450d0aeec7a3e..283c69ac7cee1 100644 --- a/ui/opening/src/main.ts +++ b/ui/opening/src/main.ts @@ -19,7 +19,7 @@ function page(data: OpeningPage) { showMoves: 'bottom', showClocks: false, showPlayers: false, - chessground: cgConfig as any, // 'as any' temporary + chessground: cgConfig, menu: { getPgn: { enabled: true, @@ -54,7 +54,7 @@ const loadExampleGames = () => showMoves: 'bottom', showClocks: false, showPlayers: true, - chessground: cgConfig as any, // as any temporary + chessground: cgConfig, menu: { getPgn: { enabled: true, diff --git a/ui/package.json b/ui/package.json index ac50a84b05b8e..25a3ce8a6cf03 100644 --- a/ui/package.json +++ b/ui/package.json @@ -9,15 +9,12 @@ "author": "Thibault Duplessis", "license": "AGPL-3.0-or-later", "dependencies": { - "breakpoint-sass": "^2.7.1", - "@babel/core": "^7.17.10", - "@babel/preset-env": "^7.17.10", - "@types/jest": "^28.1.6", - "jest": "^28.1.3", - "jest-environment-jsdom": "^28.1.3", - "ts-jest": "^28.0.7" + "breakpoint-sass": "^3.0.0", + "@types/jest": "^29.5.3", + "jest": "^29.6.2", + "jest-environment-jsdom": "^29.6.2" }, "scripts": { - "test": "jest" + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" } } diff --git a/ui/puzzle/css/build/_puzzle.keyboard.scss b/ui/puzzle/css/build/_puzzle.keyboard.scss index 33b738be30d73..4939f1a00afe5 100644 --- a/ui/puzzle/css/build/_puzzle.keyboard.scss +++ b/ui/puzzle/css/build/_puzzle.keyboard.scss @@ -1,3 +1,3 @@ @import '../../../common/css/plugin'; -@import '../../../common/css/component/help-modal'; +@import '../../../common/css/component/help'; @import '../keyboard'; diff --git a/ui/round/css/build/_round.keyboard.scss b/ui/round/css/build/_round.keyboard.scss index 33b738be30d73..4939f1a00afe5 100644 --- a/ui/round/css/build/_round.keyboard.scss +++ b/ui/round/css/build/_round.keyboard.scss @@ -1,3 +1,3 @@ @import '../../../common/css/plugin'; -@import '../../../common/css/component/help-modal'; +@import '../../../common/css/component/help'; @import '../keyboard'; diff --git a/ui/site/package.json b/ui/site/package.json index 062654eb2332b..e1ef41262c237 100644 --- a/ui/site/package.json +++ b/ui/site/package.json @@ -23,7 +23,7 @@ "flatpickr": "^4.6.13", "highcharts": "=4.2.5", "hopscotch": "^0.3.1", - "lichess-pgn-viewer": "^2.0.0", + "lichess-pgn-viewer": "npm:schlawg-pgn-viewer@2.0.0", "prop-types": "^15.8.1", "stockfish-mv.wasm": "^0.6.1", "stockfish-nnue.wasm": "1.0.0-1946a675.smolnet", diff --git a/ui/tutor/package.json b/ui/tutor/package.json index aaa357fb10813..aca02a6d40f1d 100644 --- a/ui/tutor/package.json +++ b/ui/tutor/package.json @@ -9,7 +9,7 @@ "@types/cash": "workspace:*", "@types/lichess": "workspace:*", "common": "workspace:*", - "lichess-pgn-viewer": "^2.0.0" + "lichess-pgn-viewer": "npm:schlawg-pgn-viewer@2.0.0" }, "scripts": { "compile": "tsc", diff --git a/ui/voice/css/build/_voiceMove.help.scss b/ui/voice/css/build/_voiceMove.help.scss index c4b0ebba6b4f3..026eea20c8892 100644 --- a/ui/voice/css/build/_voiceMove.help.scss +++ b/ui/voice/css/build/_voiceMove.help.scss @@ -1,4 +1,4 @@ @import '../../../common/css/plugin'; @import '../../../common/css/component/modal'; -@import '../../../common/css/component/help-modal'; +@import '../../../common/css/component/help'; @import '../voiceMove.help'; From 86c4bad54caf323ce50778ac0f4341ea2f784665 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Sat, 19 Aug 2023 03:16:30 -0500 Subject: [PATCH 024/174] merge --- bin/git-hooks/pre-push | 3 --- 1 file changed, 3 deletions(-) delete mode 100755 bin/git-hooks/pre-push diff --git a/bin/git-hooks/pre-push b/bin/git-hooks/pre-push deleted file mode 100755 index 15edc4903d922..0000000000000 --- a/bin/git-hooks/pre-push +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -pnpm check-format && pnpm lint \ No newline at end of file From e2c2bff429f4b9847cadd96507073f8f99caea97 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Sat, 19 Aug 2023 09:44:45 -0500 Subject: [PATCH 025/174] . --- bin/git-hooks/pre-commit | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 bin/git-hooks/pre-commit diff --git a/bin/git-hooks/pre-commit b/bin/git-hooks/pre-commit old mode 100644 new mode 100755 From c3fd25d46c6b15d1824d077c7491ba5f52dd1959 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Sun, 20 Aug 2023 06:57:38 -0500 Subject: [PATCH 026/174] . --- app/views/localPlay/botVsBot.scala | 3 +-- app/views/localPlay/vsBot.scala | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/views/localPlay/botVsBot.scala b/app/views/localPlay/botVsBot.scala index eacb58c33dc34..c07a72b32a93e 100644 --- a/app/views/localPlay/botVsBot.scala +++ b/app/views/localPlay/botVsBot.scala @@ -22,8 +22,7 @@ object botVsBot: url = s"$netBaseUrl${controllers.routes.LocalPlay.botVsBot}" ) .some, - zoomable = true, - chessground = false + zoomable = true ) { main } diff --git a/app/views/localPlay/vsBot.scala b/app/views/localPlay/vsBot.scala index 40be6c6aefec6..c12954f6ca22f 100644 --- a/app/views/localPlay/vsBot.scala +++ b/app/views/localPlay/vsBot.scala @@ -42,8 +42,7 @@ object vsBot: url = s"$netBaseUrl${controllers.routes.LocalPlay.vsBot}" ) .some, - zoomable = true, - chessground = false + zoomable = true ) { main(cls := "round")( st.aside(cls := "round__side")( From f06864895d1ff50187689be30b38a3682b003d40 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Tue, 22 Aug 2023 09:46:08 -0500 Subject: [PATCH 027/174] move game setup modal out of lobby module --- modules/setup/src/main/LocalConfig.scala | 112 +++++++++++ pnpm-lock.yaml | 12 ++ ui/gameSetup/css/_setup.scss | 185 ++++++++++++++++++ ui/gameSetup/css/build/_setup.scss | 5 + ui/gameSetup/css/build/setup.ltr.dark.scss | 3 + ui/gameSetup/css/build/setup.ltr.light.scss | 3 + ui/gameSetup/css/build/setup.ltr.transp.scss | 3 + ui/gameSetup/css/build/setup.rtl.dark.scss | 3 + ui/gameSetup/css/build/setup.rtl.light.scss | 3 + ui/gameSetup/css/build/setup.rtl.transp.scss | 3 + ui/gameSetup/package.json | 25 +++ .../setupCtrl.ts => gameSetup/src/ctrl.ts} | 61 +++--- ui/gameSetup/src/interfaces.ts | 56 ++++++ ui/gameSetup/src/main.ts | 6 + ui/{lobby => gameSetup}/src/options.ts | 0 ui/gameSetup/src/types.ts | 2 + .../setup => gameSetup/src/view}/aiContent.ts | 6 +- .../src/view/components/colorButtons.ts | 55 ++++++ .../src/view}/components/fenInput.ts | 6 +- .../src/view}/components/gameModeButtons.ts | 12 +- .../src/view}/components/levelButtons.ts | 6 +- .../src/view}/components/option.ts | 0 .../components/ratingDifferenceSliders.ts | 24 +-- .../src/view}/components/ratingView.ts | 18 +- .../view}/components/timePickerAndSliders.ts | 26 +-- .../src/view}/components/variantPicker.ts | 14 +- .../src/view}/friendContent.ts | 8 +- .../src/view}/hookContent.ts | 6 +- .../setup => gameSetup/src/view}/modal.ts | 12 +- ui/gameSetup/tsconfig.json | 12 ++ ui/lobby/package.json | 1 + ui/lobby/src/ctrl.ts | 109 +++++++---- ui/lobby/src/interfaces.ts | 6 - ui/lobby/src/main.ts | 2 + ui/lobby/src/view/correspondence.ts | 5 +- ui/lobby/src/view/pools.ts | 6 +- .../src/view/setup/components/colorButtons.ts | 61 ------ ui/lobby/src/view/table.ts | 15 +- ui/lobby/tsconfig.json | 6 +- 39 files changed, 674 insertions(+), 224 deletions(-) create mode 100644 modules/setup/src/main/LocalConfig.scala create mode 100644 ui/gameSetup/css/_setup.scss create mode 100644 ui/gameSetup/css/build/_setup.scss create mode 100644 ui/gameSetup/css/build/setup.ltr.dark.scss create mode 100644 ui/gameSetup/css/build/setup.ltr.light.scss create mode 100644 ui/gameSetup/css/build/setup.ltr.transp.scss create mode 100644 ui/gameSetup/css/build/setup.rtl.dark.scss create mode 100644 ui/gameSetup/css/build/setup.rtl.light.scss create mode 100644 ui/gameSetup/css/build/setup.rtl.transp.scss create mode 100644 ui/gameSetup/package.json rename ui/{lobby/src/setupCtrl.ts => gameSetup/src/ctrl.ts} (89%) create mode 100644 ui/gameSetup/src/interfaces.ts create mode 100644 ui/gameSetup/src/main.ts rename ui/{lobby => gameSetup}/src/options.ts (100%) create mode 100644 ui/gameSetup/src/types.ts rename ui/{lobby/src/view/setup => gameSetup/src/view}/aiContent.ts (83%) create mode 100644 ui/gameSetup/src/view/components/colorButtons.ts rename ui/{lobby/src/view/setup => gameSetup/src/view}/components/fenInput.ts (91%) rename ui/{lobby/src/view/setup => gameSetup/src/view}/components/gameModeButtons.ts (75%) rename ui/{lobby/src/view/setup => gameSetup/src/view}/components/levelButtons.ts (91%) rename ui/{lobby/src/view/setup => gameSetup/src/view}/components/option.ts (100%) rename ui/{lobby/src/view/setup => gameSetup/src/view}/components/ratingDifferenceSliders.ts (66%) rename ui/{lobby/src/view/setup => gameSetup/src/view}/components/ratingView.ts (58%) rename ui/{lobby/src/view/setup => gameSetup/src/view}/components/timePickerAndSliders.ts (85%) rename ui/{lobby/src/view/setup => gameSetup/src/view}/components/variantPicker.ts (56%) rename ui/{lobby/src/view/setup => gameSetup/src/view}/friendContent.ts (76%) rename ui/{lobby/src/view/setup => gameSetup/src/view}/hookContent.ts (83%) rename ui/{lobby/src/view/setup => gameSetup/src/view}/modal.ts (61%) create mode 100644 ui/gameSetup/tsconfig.json delete mode 100644 ui/lobby/src/view/setup/components/colorButtons.ts diff --git a/modules/setup/src/main/LocalConfig.scala b/modules/setup/src/main/LocalConfig.scala new file mode 100644 index 0000000000000..7f7b2b3afe5e9 --- /dev/null +++ b/modules/setup/src/main/LocalConfig.scala @@ -0,0 +1,112 @@ +package lila.setup + +import chess.format.Fen +import chess.{ Clock, ByColor } +import chess.variant.Variant + +import lila.common.Days +import lila.game.{ Game, IdGenerator, Player, Pov, Source } +import lila.lobby.Color +import lila.user.{ User, GameUser } +import lila.rating.PerfType + +case class LocalConfig( + opponent: String, + timeMode: TimeMode, + time: Double, + increment: Clock.IncrementSeconds, + days: Days, + color: Color, + fen: Option[Fen.Epd] = None, + priv: Boolean = true +) extends Config + with Positional: + + val variant = Variant.default + val strictFen = true + + def >> = (opponent, timeMode.id, time, increment, days, color.name, fen).some + + /*private def game(user: GameUser)(using IdGenerator): Fu[Game] = + fenGame: chessGame => + val pt = PerfType(chessGame.situation.board.variant, chess.Speed(chessGame.clock.map(_.config))) + Game + .make( + chess = chessGame, + players = ByColor: c => + if creatorColor == c + then Player.make(c, user) + else Player.makeAnon(c, level.some), + mode = chess.Mode.Casual, + source = if chessGame.board.variant.fromPosition then Source.Position else Source.Ai, + daysPerTurn = makeDaysPerTurn, + pgnImport = None + ) + .withUniqueId + .dmap(_.start) + + def pov(user: GameUser)(using IdGenerator) = game(user) dmap { Pov(_, creatorColor) } + */ + def timeControlFromPosition = + timeMode != TimeMode.RealTime || time >= 1 + +object LocalConfig extends BaseConfig: + + def from( + o: String, + tm: Int, + t: Double, + i: Clock.IncrementSeconds, + d: Days, + c: String, + fen: Option[Fen.Epd], + priv: Boolean + ) = + new LocalConfig( + opponent = o, + timeMode = TimeMode(tm) err s"Invalid time mode $tm", + time = t, + increment = i, + days = d, + color = Color(c) err "Invalid color " + c, + fen = fen, + priv = priv + ) + + val default = LocalConfig( + opponent = "coral", + timeMode = TimeMode.Unlimited, + time = 5d, + increment = Clock.IncrementSeconds(8), + days = Days(2), + color = Color.default, + priv = true + ) + + import lila.db.BSON + import lila.db.dsl.{ *, given } + + private[setup] given BSON[LocalConfig] with + + def reads(r: BSON.Reader): LocalConfig = + LocalConfig( + opponent = r.get("o"), + timeMode = TimeMode.orDefault(r int "tm"), + time = r double "t", + increment = r get "i", + days = r.get("d"), + color = Color.White, + fen = r.getO[Fen.Epd]("f").filter(_.value.nonEmpty), + priv = r bool "p" + ) + + def writes(w: BSON.Writer, o: LocalConfig) = + $doc( + "o" -> o.opponent, + "tm" -> o.timeMode.id, + "t" -> o.time, + "i" -> o.increment, + "d" -> o.days, + "f" -> o.fen, + "p" -> o.priv + ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b3cb792d7c2f..3e0978047346b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -278,6 +278,15 @@ importers: specifier: ^3.5.1 version: 3.5.1 + ui/gameSetup: + dependencies: + common: + specifier: workspace:* + version: link:../common + snabbdom: + specifier: ^3.5.1 + version: 3.5.1 + ui/insight: dependencies: '@types/highcharts': @@ -340,6 +349,9 @@ importers: debounce-promise: specifier: ^3.1.2 version: 3.1.2 + gameSetup: + specifier: workspace:* + version: link:../gameSetup snabbdom: specifier: ^3.5.1 version: 3.5.1 diff --git a/ui/gameSetup/css/_setup.scss b/ui/gameSetup/css/_setup.scss new file mode 100644 index 0000000000000..04e8ca5355b2a --- /dev/null +++ b/ui/gameSetup/css/_setup.scss @@ -0,0 +1,185 @@ +$c-setup: $c-secondary; +$c-slider: $c-setup; + +.game-setup#modal-wrap { + display: block; + width: 30em; + text-align: center; + + > div { + padding: 0; + max-height: 96vh; + } + + h2 { + margin: 1.5rem 0; + } + + .setup-content > div { + padding: 0.5em 1em; + } + + group.radio { + margin: 0 auto 1em auto; + width: 70%; + + .disabled { + opacity: 0.4; + cursor: default; + } + + input:checked + label { + background: $c-setup; + } + } + + .optional-config { + border-bottom: $border; + } + + .optional-config.disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .optional-config, + .ratings { + background: $c-bg-zebra; + border-top: $border; + } + + .mode-choice { + margin-top: 1em; + } + + .label-select { + @extend %flex-center; + + &.variant { + margin-bottom: 1em; + padding-bottom: 0; + } + + label { + flex: 0 0 33%; + text-align: right; + } + + select { + margin-#{$start-direction}: 0.8em; + font-weight: bold; + } + } + + .fen__form { + @extend %flex-center-nowrap; + } + + .fen__board { + display: block; + width: 50%; + margin: 0.5em auto 0 auto; + } + + #fen-input { + flex: 1 1 100%; + } + + .failure { + background: mix($c-bg-box, $c-bad, 80%); + box-shadow: 0 0 13px 6px mix($c-bg-box, $c-bad, 80%); + border-radius: 0.5em; + } + + .range { + padding-top: 1em; + + span { + font-weight: bold; + } + + input { + font-size: 1.5em; + margin-top: 0.5em; + padding: 0; + width: 90%; + } + } + + .rating-range { + @extend %flex-center-nowrap; + justify-content: center; + .rating-min, + .rating-max { + flex: 0 0 7ch; + } + input { + width: 30%; + padding: 0.6em 0; + } + } + + .ratings { + padding: 1em; + width: 100%; + text-align: center; + strong { + margin-#{$end-direction}: 0.25em; + } + } + + .color-submits { + display: flex; + align-items: flex-end; + justify-content: center; + margin: 1em auto; + text-align: center; + + &__button { + margin: 0 0.5em; + width: 64px; + height: 64px; + padding: 7px; + + i { + display: block; + padding: 0; + width: 50px; + height: 50px; + background-size: 50px 50px; + } + + &.white i { + background-image: img-url('../piece/cburnett/wK.svg'); + } + + &.black i { + background-image: img-url('../piece/cburnett/bK.svg'); + } + + &.random { + width: 85px; + height: 85px; + padding: 10px; + + i { + background-image: img-url('wbK.svg'); + background-size: 65px 65px; + width: 65px; + height: 65px; + } + } + + &:disabled { + opacity: 0.3; + cursor: not-allowed; + } + } + + .spinner { + width: 85px; + height: 85px; + margin: 10px auto 20px auto; + } + } +} diff --git a/ui/gameSetup/css/build/_setup.scss b/ui/gameSetup/css/build/_setup.scss new file mode 100644 index 0000000000000..1c6c483138581 --- /dev/null +++ b/ui/gameSetup/css/build/_setup.scss @@ -0,0 +1,5 @@ +@import '../../../common/css/plugin'; +@import '../../../common/css/form/range'; +@import '../../../common/css/form/radio'; +@import '../../../common/css/component/modal'; +@import '../setup'; diff --git a/ui/gameSetup/css/build/setup.ltr.dark.scss b/ui/gameSetup/css/build/setup.ltr.dark.scss new file mode 100644 index 0000000000000..40f701f4d9077 --- /dev/null +++ b/ui/gameSetup/css/build/setup.ltr.dark.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/ltr'; +@import '../../../common/css/theme/dark'; +@import 'setup'; diff --git a/ui/gameSetup/css/build/setup.ltr.light.scss b/ui/gameSetup/css/build/setup.ltr.light.scss new file mode 100644 index 0000000000000..7e1586c3337d9 --- /dev/null +++ b/ui/gameSetup/css/build/setup.ltr.light.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/ltr'; +@import '../../../common/css/theme/light'; +@import 'setup'; diff --git a/ui/gameSetup/css/build/setup.ltr.transp.scss b/ui/gameSetup/css/build/setup.ltr.transp.scss new file mode 100644 index 0000000000000..668028c4030b7 --- /dev/null +++ b/ui/gameSetup/css/build/setup.ltr.transp.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/ltr'; +@import '../../../common/css/theme/transp'; +@import 'setup'; diff --git a/ui/gameSetup/css/build/setup.rtl.dark.scss b/ui/gameSetup/css/build/setup.rtl.dark.scss new file mode 100644 index 0000000000000..1ff61bfda3c77 --- /dev/null +++ b/ui/gameSetup/css/build/setup.rtl.dark.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/rtl'; +@import '../../../common/css/theme/dark'; +@import 'setup'; diff --git a/ui/gameSetup/css/build/setup.rtl.light.scss b/ui/gameSetup/css/build/setup.rtl.light.scss new file mode 100644 index 0000000000000..fc4f23ba99a31 --- /dev/null +++ b/ui/gameSetup/css/build/setup.rtl.light.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/rtl'; +@import '../../../common/css/theme/light'; +@import 'setup'; diff --git a/ui/gameSetup/css/build/setup.rtl.transp.scss b/ui/gameSetup/css/build/setup.rtl.transp.scss new file mode 100644 index 0000000000000..4e4da524419a8 --- /dev/null +++ b/ui/gameSetup/css/build/setup.rtl.transp.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/rtl'; +@import '../../../common/css/theme/transp'; +@import 'setup'; diff --git a/ui/gameSetup/package.json b/ui/gameSetup/package.json new file mode 100644 index 0000000000000..bd2b63736d0f0 --- /dev/null +++ b/ui/gameSetup/package.json @@ -0,0 +1,25 @@ +{ + "name": "gameSetup", + "version": "2.0.0", + "private": true, + "author": "Thibault Duplessis", + "license": "AGPL-3.0-or-later", + "type": "module", + "types": "./dist/types.d.ts", + "dependencies": { + "common": "workspace:*", + "snabbdom": "^3.5.1" + }, + "scripts": { + "compile": "tsc", + "dev": "tsc", + "prod": "tsc" + }, + "lichess": { + "modules": { + "esm": { + "src/main.ts": "gameSetup" + } + } + } +} diff --git a/ui/lobby/src/setupCtrl.ts b/ui/gameSetup/src/ctrl.ts similarity index 89% rename from ui/lobby/src/setupCtrl.ts rename to ui/gameSetup/src/ctrl.ts index 6fd4c1ddb429e..35e4b40d9d2a8 100644 --- a/ui/lobby/src/setupCtrl.ts +++ b/ui/gameSetup/src/ctrl.ts @@ -2,16 +2,15 @@ import { Prop, propWithEffect } from 'common'; import debounce from 'common/debounce'; import * as xhr from 'common/xhr'; import { storedJsonProp, StoredJsonProp } from 'common/storage'; -import LobbyController from './ctrl'; import { ForceSetupOptions, GameMode, GameType, InputValue, - PoolMember, RealValue, SetupStore, TimeMode, + ParentCtrl, } from './interfaces'; import { daysVToDays, @@ -22,6 +21,7 @@ import { timeVToTime, variants, } from './options'; +import renderSetup from './view/modal'; const getPerf = (variant: VariantKey, timeMode: TimeMode, time: RealValue, increment: RealValue): Perf => { if (!['standard', 'fromPosition'].includes(variant)) return variant as Perf; @@ -39,8 +39,8 @@ const getPerf = (variant: VariantKey, timeMode: TimeMode, time: RealValue, incre : 'classical'; }; -export default class SetupController { - root: LobbyController; +export class SetupCtrl { + root: ParentCtrl; store: Record>; gameType: GameType | null = null; lastValidFen = ''; @@ -69,7 +69,7 @@ export default class SetupController { increment: () => RealValue = () => incrementVToIncrement(this.incrementV()); days: () => RealValue = () => daysVToDays(this.daysV()); - constructor(ctrl: LobbyController) { + constructor(ctrl: ParentCtrl) { this.root = ctrl; this.blindModeColor = propWithEffect('random', this.onPropChange); // Initialize stores with default props as necessary @@ -77,11 +77,12 @@ export default class SetupController { hook: this.makeSetupStore('hook'), friend: this.makeSetupStore('friend'), ai: this.makeSetupStore('ai'), + local: this.makeSetupStore('local'), }; } // Namespace the store by username for user specific modal settings - private storeKey = (gameType: GameType) => `lobby.setup.${this.root.me?.username || 'anon'}.${gameType}`; + private storeKey = (gameType: GameType) => `lobby.setup.${this.root.user || 'anon'}.${gameType}`; makeSetupStore = (gameType: GameType) => storedJsonProp(this.storeKey(gameType), () => ({ @@ -91,7 +92,7 @@ export default class SetupController { time: 5, increment: 3, days: 2, - gameMode: gameType === 'ai' || !this.root.me ? 'casual' : 'rated', + gameMode: gameType === 'ai' || !this.root.user ? 'casual' : 'rated', ratingMin: -500, ratingMax: 500, aiLevel: 1, @@ -159,7 +160,7 @@ export default class SetupController { }); private isProvisional = () => { - const rating = this.root.data.ratingMap && this.root.data.ratingMap[this.selectedPerf()]; + const rating = this.root.ratingMap && this.root.ratingMap[this.selectedPerf()]; return rating ? !!rating.prov : true; }; @@ -189,15 +190,20 @@ export default class SetupController { private propWithApply = (value: A) => propWithEffect(value, this.onPropChange); openModal = (gameType: GameType, forceOptions?: ForceSetupOptions, friendUser?: string) => { - this.root.leavePool(); + window.alert('wtf'); this.gameType = gameType; this.loading = false; this.fenError = false; this.lastValidFen = ''; this.friendUser = friendUser || ''; this.loadPropsFromStore(forceOptions); + console.log('heyo'); + this.root.redraw(); + console.log('hey now'); }; + renderModal = () => renderSetup(this); + closeModal = () => { this.gameType = null; this.root.redraw(); @@ -228,7 +234,7 @@ export default class SetupController { ratedModeDisabled = (): boolean => // anonymous games cannot be rated - !this.root.me || + !this.root.user || // unlimited games cannot be rated (this.gameType === 'hook' && this.timeMode() === 'unlimited') || // variants with very low time cannot be rated @@ -240,27 +246,11 @@ export default class SetupController { selectedPerf = (): Perf => getPerf(this.variant(), this.timeMode(), this.time(), this.increment()); ratingRange = (): string => { - if (!this.root.data.ratingMap) return ''; - const rating = this.root.data.ratingMap[this.selectedPerf()].rating; + if (!this.root.ratingMap) return ''; + const rating = this.root.ratingMap[this.selectedPerf()].rating; return `${Math.max(100, rating + this.ratingMin())}-${rating + this.ratingMax()}`; }; - hookToPoolMember = (color: Color | 'random'): PoolMember | null => { - const valid = - color == 'random' && - this.gameType === 'hook' && - this.variant() == 'standard' && - this.gameMode() == 'rated' && - this.timeMode() == 'realTime'; - const id = `${this.time()}+${this.increment()}`; - return valid && this.root.pools.find(p => p.id === id) - ? { - id, - range: this.ratingRange(), - } - : null; - }; - propsToFormData = (color: Color | 'random'): FormData => xhr.form({ variant: keyToId(this.variant(), variants).toString(), @@ -290,14 +280,21 @@ export default class SetupController { valid = (): boolean => this.validFen() && this.validTime() && this.validAiTime(); submit = async (color: Color | 'random') => { - const poolMember = this.hookToPoolMember(color); - if (poolMember) { - this.root.enterPool(poolMember); + if ( + this.root.acquire?.({ + id: `${this.time()}+${this.increment()}`, + gameType: this.gameType, + color, + variant: this.variant(), + gameMode: this.gameMode(), + timeMode: this.timeMode(), + range: this.ratingRange(), + }) + ) { this.closeModal(); return; } - if (this.gameType === 'hook') this.root.setTab(this.timeMode() === 'realTime' ? 'real_time' : 'seeks'); this.loading = true; this.root.redraw(); diff --git a/ui/gameSetup/src/interfaces.ts b/ui/gameSetup/src/interfaces.ts new file mode 100644 index 0000000000000..3cd9b344c5423 --- /dev/null +++ b/ui/gameSetup/src/interfaces.ts @@ -0,0 +1,56 @@ +export type GameType = 'hook' | 'friend' | 'ai' | 'local'; +export type TimeMode = 'realTime' | 'correspondence' | 'unlimited'; +export type GameMode = 'casual' | 'rated'; + +export type InputValue = number; +export type RealValue = number; + +export interface Variant { + id: number; + key: VariantKey; + name: string; + icon: string; +} + +export interface SetupStore { + variant: VariantKey; + fen: string; + timeMode: TimeMode; + gameMode: GameMode; + ratingMin: number; + ratingMax: number; + aiLevel: number; + time: number; + increment: number; + days: number; +} + +export interface ForceSetupOptions { + variant?: VariantKey; + fen?: string; + timeMode?: TimeMode; +} + +export interface RatingWithProvisional { + rating: number; + prov?: boolean; +} + +export interface GameSetup { + id: string; + gameType: GameType | null; + color: Color | 'random'; + variant: VariantKey; + timeMode: TimeMode; + gameMode: GameMode; + range: string; +} + +export interface ParentCtrl { + readonly user?: string; + readonly ratingMap?: Record; + redraw: () => void; + acquire?: (candidate: GameSetup) => boolean; + trans: Trans; + opts: { hideRatings: boolean }; +} diff --git a/ui/gameSetup/src/main.ts b/ui/gameSetup/src/main.ts new file mode 100644 index 0000000000000..e43aa3b229f3a --- /dev/null +++ b/ui/gameSetup/src/main.ts @@ -0,0 +1,6 @@ +import { SetupCtrl } from './ctrl'; +import { ParentCtrl } from './interfaces'; + +export function initModule(root: ParentCtrl): SetupCtrl { + return new SetupCtrl(root); +} diff --git a/ui/lobby/src/options.ts b/ui/gameSetup/src/options.ts similarity index 100% rename from ui/lobby/src/options.ts rename to ui/gameSetup/src/options.ts diff --git a/ui/gameSetup/src/types.ts b/ui/gameSetup/src/types.ts new file mode 100644 index 0000000000000..b89461235f82f --- /dev/null +++ b/ui/gameSetup/src/types.ts @@ -0,0 +1,2 @@ +export * from './interfaces'; +export * from './ctrl'; diff --git a/ui/lobby/src/view/setup/aiContent.ts b/ui/gameSetup/src/view/aiContent.ts similarity index 83% rename from ui/lobby/src/view/setup/aiContent.ts rename to ui/gameSetup/src/view/aiContent.ts index 7a0efae07c1aa..dfcf350547013 100644 --- a/ui/lobby/src/view/setup/aiContent.ts +++ b/ui/gameSetup/src/view/aiContent.ts @@ -1,6 +1,6 @@ import { h } from 'snabbdom'; import { MaybeVNodes } from 'common/snabbdom'; -import LobbyController from '../../ctrl'; +import { SetupCtrl } from '../ctrl'; import { variantPicker } from './components/variantPicker'; import { fenInput } from './components/fenInput'; import { timePickerAndSliders } from './components/timePickerAndSliders'; @@ -8,8 +8,8 @@ import { levelButtons } from './components/levelButtons'; import { colorButtons } from './components/colorButtons'; import { ratingView } from './components/ratingView'; -export default function aiContent(ctrl: LobbyController): MaybeVNodes { - const { trans } = ctrl; +export default function aiContent(ctrl: SetupCtrl): MaybeVNodes { + const trans = ctrl.root.trans; return [ h('h2', trans('playWithTheMachine')), h('div.setup-content', [ diff --git a/ui/gameSetup/src/view/components/colorButtons.ts b/ui/gameSetup/src/view/components/colorButtons.ts new file mode 100644 index 0000000000000..c2560dedd8b2a --- /dev/null +++ b/ui/gameSetup/src/view/components/colorButtons.ts @@ -0,0 +1,55 @@ +import { h } from 'snabbdom'; +import { spinnerVdom } from 'common/spinner'; +import { SetupCtrl } from '../../ctrl'; +import { colors, variantsWhereWhiteIsBetter } from '../../options'; +import { option } from './option'; + +const renderBlindModeColorPicker = (ctrl: SetupCtrl) => [ + ...(ctrl.gameType === 'hook' + ? [] + : [ + h('label', { attrs: { for: 'sf_color' } }, ctrl.root.trans('side')), + h( + 'select#sf_color', + { + on: { + change: (e: Event) => + ctrl.blindModeColor((e.target as HTMLSelectElement).value as Color | 'random'), + }, + }, + colors(ctrl.root.trans).map(color => option(color, ctrl.blindModeColor())), + ), + ]), + h('button', { on: { click: () => ctrl.submit(ctrl.blindModeColor()) } }, 'Create the game'), +]; + +export const colorButtons = (ctrl: SetupCtrl) => { + const enabledColors: (Color | 'random')[] = []; + if (ctrl.valid()) { + enabledColors.push('random'); + + const randomColorOnly = + ctrl.gameType !== 'ai' && + ctrl.gameMode() === 'rated' && + variantsWhereWhiteIsBetter.includes(ctrl.variant()); + if (!randomColorOnly) enabledColors.push('white', 'black'); + } + + return h( + 'div.color-submits', + lichess.blindMode + ? renderBlindModeColorPicker(ctrl) + : ctrl.loading + ? spinnerVdom() + : colors(ctrl.root.trans).map(({ key, name }) => + h( + `button.button.button-metal.color-submits__button.${key}`, + { + attrs: { disabled: !enabledColors.includes(key), title: name, value: key }, + on: { click: () => ctrl.submit(key) }, + }, + h('i'), + ), + ), + ); +}; diff --git a/ui/lobby/src/view/setup/components/fenInput.ts b/ui/gameSetup/src/view/components/fenInput.ts similarity index 91% rename from ui/lobby/src/view/setup/components/fenInput.ts rename to ui/gameSetup/src/view/components/fenInput.ts index c6c652e935bad..2dc213f747217 100644 --- a/ui/lobby/src/view/setup/components/fenInput.ts +++ b/ui/gameSetup/src/view/components/fenInput.ts @@ -1,9 +1,9 @@ import { h } from 'snabbdom'; import * as licon from 'common/licon'; -import LobbyController from '../../../ctrl'; +import { SetupCtrl } from '../../ctrl'; -export const fenInput = (ctrl: LobbyController) => { - const { trans, setupCtrl } = ctrl; +export const fenInput = (setupCtrl: SetupCtrl) => { + const trans = setupCtrl.root.trans; if (setupCtrl.variant() !== 'fromPosition') return null; const fen = setupCtrl.fen(); return h('div.fen.optional-config', [ diff --git a/ui/lobby/src/view/setup/components/gameModeButtons.ts b/ui/gameSetup/src/view/components/gameModeButtons.ts similarity index 75% rename from ui/lobby/src/view/setup/components/gameModeButtons.ts rename to ui/gameSetup/src/view/components/gameModeButtons.ts index 3205141548ebc..e7b872bd9c494 100644 --- a/ui/lobby/src/view/setup/components/gameModeButtons.ts +++ b/ui/gameSetup/src/view/components/gameModeButtons.ts @@ -1,13 +1,13 @@ import { MaybeVNode } from 'common/snabbdom'; import { h } from 'snabbdom'; -import LobbyController from '../../../ctrl'; -import { GameMode } from '../../../interfaces'; -import { gameModes } from '../../../options'; +import { SetupCtrl } from '../../ctrl'; +import { GameMode } from '../../interfaces'; +import { gameModes } from '../../options'; -export const gameModeButtons = (ctrl: LobbyController): MaybeVNode => { - if (!ctrl.me) return null; +export const gameModeButtons = (setupCtrl: SetupCtrl): MaybeVNode => { + if (!setupCtrl.root.user) return null; - const { trans, setupCtrl } = ctrl; + const trans = setupCtrl.root.trans; return h( 'div.mode-choice.buttons', h( diff --git a/ui/lobby/src/view/setup/components/levelButtons.ts b/ui/gameSetup/src/view/components/levelButtons.ts similarity index 91% rename from ui/lobby/src/view/setup/components/levelButtons.ts rename to ui/gameSetup/src/view/components/levelButtons.ts index 40ab67b36ab0f..768738e69a894 100644 --- a/ui/lobby/src/view/setup/components/levelButtons.ts +++ b/ui/gameSetup/src/view/components/levelButtons.ts @@ -1,9 +1,9 @@ import { h } from 'snabbdom'; -import LobbyController from '../../../ctrl'; +import { SetupCtrl } from '../../ctrl'; import { option } from './option'; -export const levelButtons = (ctrl: LobbyController) => { - const { trans, setupCtrl } = ctrl; +export const levelButtons = (setupCtrl: SetupCtrl) => { + const trans = setupCtrl.root.trans; return lichess.blindMode ? [ h('label', { attrs: { for: 'sf_level' } }, trans('strength')), diff --git a/ui/lobby/src/view/setup/components/option.ts b/ui/gameSetup/src/view/components/option.ts similarity index 100% rename from ui/lobby/src/view/setup/components/option.ts rename to ui/gameSetup/src/view/components/option.ts diff --git a/ui/lobby/src/view/setup/components/ratingDifferenceSliders.ts b/ui/gameSetup/src/view/components/ratingDifferenceSliders.ts similarity index 66% rename from ui/lobby/src/view/setup/components/ratingDifferenceSliders.ts rename to ui/gameSetup/src/view/components/ratingDifferenceSliders.ts index 0e09a7656c2d2..0b3418dd86318 100644 --- a/ui/lobby/src/view/setup/components/ratingDifferenceSliders.ts +++ b/ui/gameSetup/src/view/components/ratingDifferenceSliders.ts @@ -1,17 +1,17 @@ import { h } from 'snabbdom'; -import LobbyController from '../../../ctrl'; +import { SetupCtrl } from '../../ctrl'; -export const ratingDifferenceSliders = (ctrl: LobbyController) => { - if (!ctrl.me || lichess.blindMode || !ctrl.data.ratingMap) return null; +export const ratingDifferenceSliders = (ctrl: SetupCtrl) => { + if (!ctrl.root.user || lichess.blindMode || !ctrl.root.ratingMap) return null; - const { trans, setupCtrl } = ctrl; - const selectedPerf = ctrl.setupCtrl.selectedPerf(); - const isProvisional = !!ctrl.data.ratingMap[selectedPerf].prov; + const trans = ctrl.root.trans; + const selectedPerf = ctrl.selectedPerf(); + const isProvisional = !!ctrl.root.ratingMap[selectedPerf].prov; const disabled = isProvisional ? '.disabled' : ''; // Get current rating values or use default values if isProvisional - const currentRatingMin = isProvisional ? -500 : setupCtrl.ratingMin(); - const currentRatingMax = isProvisional ? 500 : setupCtrl.ratingMax(); + const currentRatingMin = isProvisional ? -500 : ctrl.ratingMin(); + const currentRatingMax = isProvisional ? 500 : ctrl.ratingMax(); return h( `div.rating-range-config.optional-config${disabled}`, @@ -37,8 +37,8 @@ export const ratingDifferenceSliders = (ctrl: LobbyController) => { on: { input: (e: Event) => { const newVal = parseInt((e.target as HTMLInputElement).value); - if (newVal === 0 && setupCtrl.ratingMax() === 0) setupCtrl.ratingMax(50); - setupCtrl.ratingMin(newVal); + if (newVal === 0 && ctrl.ratingMax() === 0) ctrl.ratingMax(50); + ctrl.ratingMin(newVal); }, }, }), @@ -57,8 +57,8 @@ export const ratingDifferenceSliders = (ctrl: LobbyController) => { on: { input: (e: Event) => { const newVal = parseInt((e.target as HTMLInputElement).value); - if (newVal === 0 && setupCtrl.ratingMin() === 0) setupCtrl.ratingMin(-50); - setupCtrl.ratingMax(newVal); + if (newVal === 0 && ctrl.ratingMin() === 0) ctrl.ratingMin(-50); + ctrl.ratingMax(newVal); }, }, }), diff --git a/ui/lobby/src/view/setup/components/ratingView.ts b/ui/gameSetup/src/view/components/ratingView.ts similarity index 58% rename from ui/lobby/src/view/setup/components/ratingView.ts rename to ui/gameSetup/src/view/components/ratingView.ts index db2ce65484281..4fbd90a02aee7 100644 --- a/ui/lobby/src/view/setup/components/ratingView.ts +++ b/ui/gameSetup/src/view/components/ratingView.ts @@ -1,13 +1,12 @@ import { MaybeVNode } from 'common/snabbdom'; import { h } from 'snabbdom'; -import LobbyController from '../../../ctrl'; -import { speeds, variants } from '../../../options'; +import { SetupCtrl } from '../../ctrl'; +import { speeds, variants } from '../../options'; -export const ratingView = (ctrl: LobbyController): MaybeVNode => { - const { opts, data } = ctrl; - if (lichess.blindMode || !data.ratingMap) return null; +export const ratingView = (ctrl: SetupCtrl): MaybeVNode => { + if (lichess.blindMode || !ctrl.root.ratingMap) return null; - const selectedPerf = ctrl.setupCtrl.selectedPerf(); + const selectedPerf = ctrl.selectedPerf(); const perfOrSpeed: { key: string; icon: string; name: string } | undefined = variants.find(({ key }) => key === selectedPerf) || speeds.find(({ key }) => key === selectedPerf); @@ -16,15 +15,16 @@ export const ratingView = (ctrl: LobbyController): MaybeVNode => { const perfIconAttrs = { attrs: { 'data-icon': perfOrSpeed.icon } }; return h( 'div.ratings', - opts.hideRatings + ctrl.root.opts.hideRatings ? [h('i', perfIconAttrs), perfOrSpeed.name] : [ - ...ctrl.trans.vdom( + ...ctrl.root.trans.vdom( 'perfRatingX', h( 'strong', perfIconAttrs, - data.ratingMap[selectedPerf].rating + (data.ratingMap[selectedPerf].prov ? '?' : ''), + ctrl.root.ratingMap[selectedPerf].rating + + (ctrl.root.ratingMap[selectedPerf].prov ? '?' : ''), ), ), perfOrSpeed.name, diff --git a/ui/lobby/src/view/setup/components/timePickerAndSliders.ts b/ui/gameSetup/src/view/components/timePickerAndSliders.ts similarity index 85% rename from ui/lobby/src/view/setup/components/timePickerAndSliders.ts rename to ui/gameSetup/src/view/components/timePickerAndSliders.ts index eeac588af2e2e..0195335348e2a 100644 --- a/ui/lobby/src/view/setup/components/timePickerAndSliders.ts +++ b/ui/gameSetup/src/view/components/timePickerAndSliders.ts @@ -1,8 +1,8 @@ import { Prop } from 'common'; import { h } from 'snabbdom'; -import LobbyController from '../../../ctrl'; -import { InputValue, TimeMode } from '../../../interfaces'; -import { daysVToDays, incrementVToIncrement, sliderTimes, timeModes } from '../../../options'; +import { SetupCtrl } from '../../ctrl'; +import { InputValue, TimeMode } from '../../interfaces'; +import { daysVToDays, incrementVToIncrement, sliderTimes, timeModes } from '../../options'; import { option } from './option'; const showTime = (v: number) => { @@ -12,10 +12,10 @@ const showTime = (v: number) => { return v.toString(); }; -const renderBlindModeTimePickers = (ctrl: LobbyController, allowAnonymous: boolean) => { - const { trans, setupCtrl } = ctrl; +const renderBlindModeTimePickers = (setupCtrl: SetupCtrl, allowAnonymous: boolean) => { + const trans = setupCtrl.root.trans; return [ - renderTimeModePicker(ctrl, allowAnonymous), + renderTimeModePicker(setupCtrl, allowAnonymous), setupCtrl.timeMode() === 'realTime' ? h('div.time-choice', [ h('label', { attrs: { for: 'sf_time' } }, trans('minutesPerSide')), @@ -75,9 +75,9 @@ const renderBlindModeTimePickers = (ctrl: LobbyController, allowAnonymous: boole ]; }; -const renderTimeModePicker = (ctrl: LobbyController, allowAnonymous = false) => { - const { trans, setupCtrl } = ctrl; - return ctrl.me || allowAnonymous +const renderTimeModePicker = (setupCtrl: SetupCtrl, allowAnonymous = false) => { + const trans = setupCtrl.root.trans; + return setupCtrl.root.user || allowAnonymous ? h('div.label-select', [ h('label', { attrs: { for: 'sf_timeMode' } }, trans('timeControl')), h( @@ -102,14 +102,14 @@ const inputRange = (min: number, max: number, prop: Prop, classes?: }, }); -export const timePickerAndSliders = (ctrl: LobbyController, allowAnonymous = false) => { - const { trans, setupCtrl } = ctrl; +export const timePickerAndSliders = (setupCtrl: SetupCtrl, allowAnonymous = false) => { + const trans = setupCtrl.root.trans; return h( 'div.time-mode-config.optional-config', lichess.blindMode - ? renderBlindModeTimePickers(ctrl, allowAnonymous) + ? renderBlindModeTimePickers(setupCtrl, allowAnonymous) : [ - renderTimeModePicker(ctrl, allowAnonymous), + renderTimeModePicker(setupCtrl, allowAnonymous), setupCtrl.timeMode() === 'realTime' ? h('div.time-choice.range', [ `${trans('minutesPerSide')}: `, diff --git a/ui/lobby/src/view/setup/components/variantPicker.ts b/ui/gameSetup/src/view/components/variantPicker.ts similarity index 56% rename from ui/lobby/src/view/setup/components/variantPicker.ts rename to ui/gameSetup/src/view/components/variantPicker.ts index f11d3a1383bdc..6bbded9046756 100644 --- a/ui/lobby/src/view/setup/components/variantPicker.ts +++ b/ui/gameSetup/src/view/components/variantPicker.ts @@ -1,11 +1,11 @@ import { onInsert } from 'common/snabbdom'; import { h } from 'snabbdom'; -import LobbyController from '../../../ctrl'; -import { variantsBlindMode, variants, variantsForGameType } from '../../../options'; +import { SetupCtrl } from '../../ctrl'; +import { variantsBlindMode, variants, variantsForGameType } from '../../options'; import { option } from './option'; -export const variantPicker = (ctrl: LobbyController) => { - const { trans, setupCtrl } = ctrl; +export const variantPicker = (ctrl: SetupCtrl) => { + const trans = ctrl.root.trans; const baseVariants = lichess.blindMode ? variantsBlindMode : variants; return h('div.variant.label-select', [ h('label', { attrs: { for: 'sf_variant' } }, trans('variant')), @@ -13,13 +13,11 @@ export const variantPicker = (ctrl: LobbyController) => { 'select#sf_variant', { on: { - change: (e: Event) => setupCtrl.variant((e.target as HTMLSelectElement).value as VariantKey), + change: (e: Event) => ctrl.variant((e.target as HTMLSelectElement).value as VariantKey), }, hook: onInsert(element => element.focus()), }, - variantsForGameType(baseVariants, setupCtrl.gameType!).map(variant => - option(variant, setupCtrl.variant()), - ), + variantsForGameType(baseVariants, ctrl.gameType!).map(variant => option(variant, ctrl.variant())), ), ]); }; diff --git a/ui/lobby/src/view/setup/friendContent.ts b/ui/gameSetup/src/view/friendContent.ts similarity index 76% rename from ui/lobby/src/view/setup/friendContent.ts rename to ui/gameSetup/src/view/friendContent.ts index 1b95a4502b2c9..6cd32fcb8126e 100644 --- a/ui/lobby/src/view/setup/friendContent.ts +++ b/ui/gameSetup/src/view/friendContent.ts @@ -1,7 +1,7 @@ import { h } from 'snabbdom'; import { MaybeVNodes } from 'common/snabbdom'; import userLink from 'common/userLink'; -import LobbyController from '../../ctrl'; +import { SetupCtrl } from '../ctrl'; import { variantPicker } from './components/variantPicker'; import { fenInput } from './components/fenInput'; import { timePickerAndSliders } from './components/timePickerAndSliders'; @@ -9,12 +9,12 @@ import { gameModeButtons } from './components/gameModeButtons'; import { colorButtons } from './components/colorButtons'; import { ratingView } from './components/ratingView'; -export default function friendContent(ctrl: LobbyController): MaybeVNodes { - const { trans } = ctrl; +export default function friendContent(ctrl: SetupCtrl): MaybeVNodes { + const trans = ctrl.root.trans; return [ h('h2', trans('playWithAFriend')), h('div.setup-content', [ - ctrl.setupCtrl.friendUser ? userLink(ctrl.setupCtrl.friendUser) : null, + ctrl.friendUser ? userLink(ctrl.friendUser) : null, variantPicker(ctrl), fenInput(ctrl), timePickerAndSliders(ctrl, true), diff --git a/ui/lobby/src/view/setup/hookContent.ts b/ui/gameSetup/src/view/hookContent.ts similarity index 83% rename from ui/lobby/src/view/setup/hookContent.ts rename to ui/gameSetup/src/view/hookContent.ts index 460003056fa0f..16cbff62185d4 100644 --- a/ui/lobby/src/view/setup/hookContent.ts +++ b/ui/gameSetup/src/view/hookContent.ts @@ -1,6 +1,6 @@ import { h } from 'snabbdom'; import { MaybeVNodes } from 'common/snabbdom'; -import LobbyController from '../../ctrl'; +import { SetupCtrl } from '../ctrl'; import { variantPicker } from './components/variantPicker'; import { timePickerAndSliders } from './components/timePickerAndSliders'; @@ -9,8 +9,8 @@ import { ratingDifferenceSliders } from './components/ratingDifferenceSliders'; import { colorButtons } from './components/colorButtons'; import { ratingView } from './components/ratingView'; -export default function hookContent(ctrl: LobbyController): MaybeVNodes { - const { trans } = ctrl; +export default function hookContent(ctrl: SetupCtrl): MaybeVNodes { + const trans = ctrl.root.trans; return [ h('h2', trans('createAGame')), h('div.setup-content', [ diff --git a/ui/lobby/src/view/setup/modal.ts b/ui/gameSetup/src/view/modal.ts similarity index 61% rename from ui/lobby/src/view/setup/modal.ts rename to ui/gameSetup/src/view/modal.ts index 8dab0dd5c3c03..318b6b618029b 100644 --- a/ui/lobby/src/view/setup/modal.ts +++ b/ui/gameSetup/src/view/modal.ts @@ -1,6 +1,6 @@ import { MaybeVNode } from 'common/snabbdom'; import { snabModal } from 'common/modal'; -import LobbyController from '../../ctrl'; +import { SetupCtrl } from '../ctrl'; import hookContent from './hookContent'; import friendContent from './friendContent'; import aiContent from './aiContent'; @@ -9,16 +9,16 @@ const gameTypeToRenderer = { hook: hookContent, friend: friendContent, ai: aiContent, + local: aiContent, }; -export default function setupModal(ctrl: LobbyController): MaybeVNode { - const { setupCtrl } = ctrl; - if (!setupCtrl.gameType) return null; - const renderContent = gameTypeToRenderer[setupCtrl.gameType]; +export default function setupModal(ctrl: SetupCtrl): MaybeVNode { + if (!ctrl.gameType) return null; + const renderContent = gameTypeToRenderer[ctrl.gameType]; return snabModal({ class: 'game-setup', onInsert: () => lichess.loadCssPath('lobby.setup'), - onClose: setupCtrl.closeModal, + onClose: ctrl.closeModal, content: renderContent(ctrl), }); } diff --git a/ui/gameSetup/tsconfig.json b/ui/gameSetup/tsconfig.json new file mode 100644 index 0000000000000..62ac5dc7b1ab6 --- /dev/null +++ b/ui/gameSetup/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig_module.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "esModuleInterop": true, + "emitDeclarationOnly": true, + "isolatedModules": true, + "composite": true + }, + "references": [{ "path": "../common/tsconfig.json" }] +} diff --git a/ui/lobby/package.json b/ui/lobby/package.json index ff58d2a0453d9..5bb9850281a2d 100644 --- a/ui/lobby/package.json +++ b/ui/lobby/package.json @@ -18,6 +18,7 @@ "common": "workspace:*", "dasher": "workspace:*", "debounce-promise": "^3.1.2", + "gameSetup": "workspace:*", "snabbdom": "^3.5.1" }, "scripts": { diff --git a/ui/lobby/src/ctrl.ts b/ui/lobby/src/ctrl.ts index 5d7b05be74c3d..31af0d78146e7 100644 --- a/ui/lobby/src/ctrl.ts +++ b/ui/lobby/src/ctrl.ts @@ -5,25 +5,12 @@ import * as seekRepo from './seekRepo'; import { make as makeStores, Stores } from './store'; import * as xhr from './xhr'; import * as poolRangeStorage from './poolRangeStorage'; -import { - LobbyOpts, - LobbyData, - Tab, - Mode, - Sort, - Hook, - Seek, - Pool, - PoolMember, - GameType, - ForceSetupOptions, - LobbyMe, -} from './interfaces'; +import { LobbyOpts, LobbyData, Tab, Mode, Sort, Hook, Seek, Pool, PoolMember, LobbyMe } from './interfaces'; +import { ParentCtrl, ForceSetupOptions, GameType, GameSetup, SetupCtrl } from 'gameSetup'; import LobbySocket from './socket'; import Filter from './filter'; -import SetupController from './setupCtrl'; -export default class LobbyController { +export default class LobbyController implements ParentCtrl { data: LobbyData; playban: any; me?: LobbyMe; @@ -39,7 +26,7 @@ export default class LobbyController { trans: Trans; pools: Pool[]; filter: Filter; - setupCtrl: SetupController; + setupCtrl: SetupCtrl; private poolInStorage: LichessStorage; private flushHooksTimeout?: number; @@ -55,7 +42,7 @@ export default class LobbyController { this.pools = opts.pools; this.playban = opts.playban; this.filter = new Filter(lichess.storage.make('lobby.filter'), this); - this.setupCtrl = new SetupController(this); + this.redraw = redraw; hookRepo.initAll(this); seekRepo.initAll(this); @@ -67,30 +54,7 @@ export default class LobbyController { this.sort = this.stores.sort.get(); this.trans = opts.trans; - const locationHash = location.hash.replace('#', ''); - if (['ai', 'friend', 'hook'].includes(locationHash)) { - let friendUser; - const forceOptions: ForceSetupOptions = {}; - const urlParams = new URLSearchParams(location.search); - if (locationHash === 'hook') { - if (urlParams.get('time') === 'realTime') { - this.tab = 'real_time'; - forceOptions.timeMode = 'realTime'; - } else if (urlParams.get('time') === 'correspondence') { - this.tab = 'seeks'; - forceOptions.timeMode = 'correspondence'; - } - } else if (urlParams.get('fen')) { - forceOptions.fen = urlParams.get('fen')!; - forceOptions.variant = 'fromPosition'; - } else { - friendUser = urlParams.get('user')!; - } - - this.setupCtrl.openModal(locationHash as GameType, forceOptions, friendUser); - history.replaceState(null, '', '/'); - } - + this.locationHashSetupModal(); this.poolInStorage = lichess.storage.make('lobby.pool-in'); this.poolInStorage.listen(_ => { // when another tab joins a pool @@ -118,7 +82,6 @@ export default class LobbyController { this.socket.realTimeIn(); } else if (this.tab === 'pools' && this.poolMember) this.poolIn(); }); - window.addEventListener('beforeunload', () => this.leavePool()); } @@ -236,6 +199,24 @@ export default class LobbyController { this.socket.poolIn(this.poolMember); }; + acquire = ({ id, gameType, color, variant, timeMode, gameMode, range }: GameSetup) => { + const pool = + color == 'random' && + gameType === 'hook' && + variant == 'standard' && + gameMode == 'rated' && + timeMode == 'realTime' && + this.pools.find(p => p.id === id) + ? { + id, + range: range, + } + : null; + if (!pool && gameType === 'hook') this.setTab(timeMode === 'realTime' ? 'real_time' : 'seeks'); + if (pool) this.enterPool(pool); + return pool !== null; + }; + hasOngoingRealTimeGame = () => !!this.data.nowPlaying.find(nowPlaying => nowPlaying.isMyTurn && nowPlaying.speed !== 'correspondence'); @@ -277,6 +258,48 @@ export default class LobbyController { } }; + get user() { + return this.me?.username.toLowerCase(); + } + + get ratingMap() { + return this.data.ratingMap ? this.data.ratingMap : undefined; + } + + hasPool = (id: string) => this.pools.some(p => p.id === id); + + loadSetupCtrl = async () => { + return (this.setupCtrl = await lichess.loadEsm('gameSetup', { init: this })); + }; + + private locationHashSetupModal = async () => { + const locationHash = location.hash.replace('#', ''); + if (!['ai', 'friend', 'hook', 'local'].includes(locationHash)) return; + + await this.loadSetupCtrl(); + + let friendUser; + const forceOptions: ForceSetupOptions = {}; + const urlParams = new URLSearchParams(location.search); + if (locationHash === 'hook') { + if (urlParams.get('time') === 'realTime') { + this.tab = 'real_time'; + forceOptions.timeMode = 'realTime'; + } else if (urlParams.get('time') === 'correspondence') { + this.tab = 'seeks'; + forceOptions.timeMode = 'correspondence'; + } + } else if (urlParams.get('fen')) { + forceOptions.fen = urlParams.get('fen')!; + forceOptions.variant = 'fromPosition'; + } else { + friendUser = urlParams.get('user')!; + } + this.leavePool(); + this.setupCtrl.openModal(locationHash as GameType, forceOptions, friendUser); + history.replaceState(null, '', '/'); + }; + // after click on round "new opponent" button // also handles onboardink link for anon users private joinPoolFromLocationHash = () => { diff --git a/ui/lobby/src/interfaces.ts b/ui/lobby/src/interfaces.ts index f2b5e906c4d51..803e67e65f8de 100644 --- a/ui/lobby/src/interfaces.ts +++ b/ui/lobby/src/interfaces.ts @@ -136,9 +136,3 @@ export interface SetupStore { increment: number; days: number; } - -export interface ForceSetupOptions { - variant?: VariantKey; - fen?: string; - timeMode?: TimeMode; -} diff --git a/ui/lobby/src/main.ts b/ui/lobby/src/main.ts index ebf58d4b37b53..fddff5cf19b8d 100644 --- a/ui/lobby/src/main.ts +++ b/ui/lobby/src/main.ts @@ -16,7 +16,9 @@ export default function main(opts: LobbyOpts) { let tableVNode = patch(opts.tableElement, tableView(ctrl)); function redraw() { + console.log('wtf'); appVNode = patch(appVNode, appView(ctrl)); + console.log('patching table view'); tableVNode = patch(tableVNode, tableView(ctrl)); } diff --git a/ui/lobby/src/view/correspondence.ts b/ui/lobby/src/view/correspondence.ts index 186e27646beec..882aed8fd2676 100644 --- a/ui/lobby/src/view/correspondence.ts +++ b/ui/lobby/src/view/correspondence.ts @@ -51,7 +51,10 @@ function createSeek(ctrl: LobbyController): VNode | undefined { { hook: bind( 'click', - () => ctrl.setupCtrl.openModal('hook', { variant: 'standard', timeMode: 'correspondence' }), + () => { + ctrl.leavePool(); + ctrl.setupCtrl.openModal('hook', { variant: 'standard', timeMode: 'correspondence' }); + }, ctrl.redraw, ), }, diff --git a/ui/lobby/src/view/pools.ts b/ui/lobby/src/view/pools.ts index 524c44394bacf..675d159b45e8b 100644 --- a/ui/lobby/src/view/pools.ts +++ b/ui/lobby/src/view/pools.ts @@ -10,8 +10,10 @@ export function hooks(ctrl: LobbyController): Hooks { const id = (e.target as HTMLElement).getAttribute('data-id') || ((e.target as HTMLElement).parentNode as HTMLElement).getAttribute('data-id'); - if (id === 'custom') ctrl.setupCtrl.openModal('hook'); - else if (id) ctrl.clickPool(id); + if (id === 'custom') { + ctrl.leavePool(); + ctrl.setupCtrl.openModal('hook'); + } else if (id) ctrl.clickPool(id); }, ctrl.redraw, ); diff --git a/ui/lobby/src/view/setup/components/colorButtons.ts b/ui/lobby/src/view/setup/components/colorButtons.ts deleted file mode 100644 index e15e524e7ff7e..0000000000000 --- a/ui/lobby/src/view/setup/components/colorButtons.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { h } from 'snabbdom'; -import { spinnerVdom } from 'common/spinner'; -import LobbyController from '../../../ctrl'; -import { colors, variantsWhereWhiteIsBetter } from '../../../options'; -import { option } from './option'; - -const renderBlindModeColorPicker = (ctrl: LobbyController) => [ - ...(ctrl.setupCtrl.gameType === 'hook' - ? [] - : [ - h('label', { attrs: { for: 'sf_color' } }, ctrl.trans('side')), - h( - 'select#sf_color', - { - on: { - change: (e: Event) => - ctrl.setupCtrl.blindModeColor((e.target as HTMLSelectElement).value as Color | 'random'), - }, - }, - colors(ctrl.trans).map(color => option(color, ctrl.setupCtrl.blindModeColor())), - ), - ]), - h( - 'button', - { on: { click: () => ctrl.setupCtrl.submit(ctrl.setupCtrl.blindModeColor()) } }, - 'Create the game', - ), -]; - -export const colorButtons = (ctrl: LobbyController) => { - const { setupCtrl } = ctrl; - - const enabledColors: (Color | 'random')[] = []; - if (setupCtrl.valid()) { - enabledColors.push('random'); - - const randomColorOnly = - setupCtrl.gameType !== 'ai' && - setupCtrl.gameMode() === 'rated' && - variantsWhereWhiteIsBetter.includes(setupCtrl.variant()); - if (!randomColorOnly) enabledColors.push('white', 'black'); - } - - return h( - 'div.color-submits', - lichess.blindMode - ? renderBlindModeColorPicker(ctrl) - : setupCtrl.loading - ? spinnerVdom() - : colors(ctrl.trans).map(({ key, name }) => - h( - `button.button.button-metal.color-submits__button.${key}`, - { - attrs: { disabled: !enabledColors.includes(key), title: name, value: key }, - on: { click: () => ctrl.setupCtrl.submit(key) }, - }, - h('i'), - ), - ), - ); -}; diff --git a/ui/lobby/src/view/table.ts b/ui/lobby/src/view/table.ts index 47617c03d8e56..3aee2165fb7a0 100644 --- a/ui/lobby/src/view/table.ts +++ b/ui/lobby/src/view/table.ts @@ -2,7 +2,6 @@ import { h, thunk } from 'snabbdom'; import { bind, onInsert } from 'common/snabbdom'; import LobbyController from '../ctrl'; import { GameType } from '../interfaces'; -import renderSetupModal from './setup/modal'; import { numberFormat } from 'common/number'; export default function table(ctrl: LobbyController) { @@ -27,22 +26,22 @@ export default function table(ctrl: LobbyController) { h( `button.button.button-metal.config_${gameType}`, { - class: { active: ctrl.setupCtrl.gameType === gameType, disabled }, + class: { active: ctrl.setupCtrl?.gameType === gameType, disabled }, attrs: { type: 'button' }, hook: disabled ? {} - : bind( - lichess.blindMode ? 'click' : 'mousedown', - () => ctrl.setupCtrl.openModal(gameType), - ctrl.redraw, - ), + : bind(lichess.blindMode ? 'click' : 'mousedown', async () => { + await ctrl.loadSetupCtrl(); + ctrl.leavePool(); + ctrl.setupCtrl.openModal(gameType); + }), }, trans(transKey), ), ), ), ), - renderSetupModal(ctrl), + ctrl.setupCtrl?.renderModal(), // Use a thunk here so that snabbdom does not rerender; we will do so manually after insert thunk( 'div.lobby__counters', diff --git a/ui/lobby/tsconfig.json b/ui/lobby/tsconfig.json index c78b63b2f2805..47588ec98645e 100644 --- a/ui/lobby/tsconfig.json +++ b/ui/lobby/tsconfig.json @@ -5,5 +5,9 @@ "noEmit": true }, "isolatedModules": true, - "references": [{ "path": "../common/tsconfig.json" }, { "path": "../dasher/tsconfig.json" }] + "references": [ + { "path": "../common/tsconfig.json" }, + { "path": "../dasher/tsconfig.json" }, + { "path": "../gameSetup/tsconfig.json" } + ] } From 3295f15420d3e0c67f0ae400cf6f672ad4ebb931 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Tue, 22 Aug 2023 10:18:19 -0500 Subject: [PATCH 028/174] fix assorted buttons --- ui/gameSetup/src/ctrl.ts | 5 +- ui/gameSetup/src/view/aiContent.ts | 3 +- ui/gameSetup/src/view/components/fenInput.ts | 23 +++---- .../src/view/components/gameModeButtons.ts | 15 ++--- .../src/view/components/levelButtons.ts | 19 +++--- .../components/ratingDifferenceSliders.ts | 3 +- .../view/components/timePickerAndSliders.ts | 67 +++++++++---------- .../src/view/components/variantPicker.ts | 3 +- ui/gameSetup/src/view/friendContent.ts | 3 +- ui/gameSetup/src/view/hookContent.ts | 3 +- ui/lobby/src/ctrl.ts | 13 ++-- ui/lobby/src/main.ts | 2 - ui/lobby/src/view/correspondence.ts | 2 +- ui/lobby/src/view/pools.ts | 2 +- ui/lobby/src/view/table.ts | 6 +- 15 files changed, 75 insertions(+), 94 deletions(-) diff --git a/ui/gameSetup/src/ctrl.ts b/ui/gameSetup/src/ctrl.ts index 35e4b40d9d2a8..bc9517729310f 100644 --- a/ui/gameSetup/src/ctrl.ts +++ b/ui/gameSetup/src/ctrl.ts @@ -190,16 +190,13 @@ export class SetupCtrl { private propWithApply = (value: A) => propWithEffect(value, this.onPropChange); openModal = (gameType: GameType, forceOptions?: ForceSetupOptions, friendUser?: string) => { - window.alert('wtf'); this.gameType = gameType; this.loading = false; this.fenError = false; this.lastValidFen = ''; this.friendUser = friendUser || ''; this.loadPropsFromStore(forceOptions); - console.log('heyo'); this.root.redraw(); - console.log('hey now'); }; renderModal = () => renderSetup(this); @@ -299,7 +296,7 @@ export class SetupCtrl { this.root.redraw(); let urlPath = `/setup/${this.gameType}`; - if (this.gameType === 'hook') urlPath += `/${lichess.sri}`; + if (this.gameType === 'hook') urlPath += '/' + lichess.sri; const urlParams = { user: this.friendUser || undefined }; let response; try { diff --git a/ui/gameSetup/src/view/aiContent.ts b/ui/gameSetup/src/view/aiContent.ts index dfcf350547013..5296fe2778b1c 100644 --- a/ui/gameSetup/src/view/aiContent.ts +++ b/ui/gameSetup/src/view/aiContent.ts @@ -9,9 +9,8 @@ import { colorButtons } from './components/colorButtons'; import { ratingView } from './components/ratingView'; export default function aiContent(ctrl: SetupCtrl): MaybeVNodes { - const trans = ctrl.root.trans; return [ - h('h2', trans('playWithTheMachine')), + h('h2', ctrl.root.trans('playWithTheMachine')), h('div.setup-content', [ variantPicker(ctrl), fenInput(ctrl), diff --git a/ui/gameSetup/src/view/components/fenInput.ts b/ui/gameSetup/src/view/components/fenInput.ts index 2dc213f747217..85782457b2c4e 100644 --- a/ui/gameSetup/src/view/components/fenInput.ts +++ b/ui/gameSetup/src/view/components/fenInput.ts @@ -2,27 +2,26 @@ import { h } from 'snabbdom'; import * as licon from 'common/licon'; import { SetupCtrl } from '../../ctrl'; -export const fenInput = (setupCtrl: SetupCtrl) => { - const trans = setupCtrl.root.trans; - if (setupCtrl.variant() !== 'fromPosition') return null; - const fen = setupCtrl.fen(); +export const fenInput = (ctrl: SetupCtrl) => { + if (ctrl.variant() !== 'fromPosition') return null; + const fen = ctrl.fen(); return h('div.fen.optional-config', [ h('div.fen__form', [ h('input#fen-input', { - attrs: { placeholder: trans('pasteTheFenStringHere'), value: fen }, + attrs: { placeholder: ctrl.root.trans('pasteTheFenStringHere'), value: fen }, on: { input: (e: InputEvent) => { - setupCtrl.fen((e.target as HTMLInputElement).value); - setupCtrl.validateFen(); + ctrl.fen((e.target as HTMLInputElement).value); + ctrl.validateFen(); }, }, - hook: { insert: setupCtrl.validateFen }, - class: { failure: setupCtrl.fenError }, + hook: { insert: ctrl.validateFen }, + class: { failure: ctrl.fenError }, }), h('a.button.button-empty', { attrs: { 'data-icon': licon.Pencil, - title: trans('boardEditor'), + title: ctrl.root.trans('boardEditor'), href: '/editor' + (fen ? `/${fen.replace(' ', '_')}` : ''), }, }), @@ -30,12 +29,12 @@ export const fenInput = (setupCtrl: SetupCtrl) => { h( 'a.fen__board', { attrs: { href: '/editor' } }, - setupCtrl.fenError || !setupCtrl.lastValidFen + ctrl.fenError || !ctrl.lastValidFen ? null : h( 'span.preview', h('div.position.mini-board.cg-wrap.is2d', { - attrs: { 'data-state': `${setupCtrl.lastValidFen},white` }, + attrs: { 'data-state': `${ctrl.lastValidFen},white` }, hook: { insert: vnode => lichess.miniBoard.init(vnode.elm as HTMLElement), update: vnode => lichess.miniBoard.init(vnode.elm as HTMLElement), diff --git a/ui/gameSetup/src/view/components/gameModeButtons.ts b/ui/gameSetup/src/view/components/gameModeButtons.ts index e7b872bd9c494..b919187b1d76a 100644 --- a/ui/gameSetup/src/view/components/gameModeButtons.ts +++ b/ui/gameSetup/src/view/components/gameModeButtons.ts @@ -4,27 +4,26 @@ import { SetupCtrl } from '../../ctrl'; import { GameMode } from '../../interfaces'; import { gameModes } from '../../options'; -export const gameModeButtons = (setupCtrl: SetupCtrl): MaybeVNode => { - if (!setupCtrl.root.user) return null; +export const gameModeButtons = (ctrl: SetupCtrl): MaybeVNode => { + if (!ctrl.root.user) return null; - const trans = setupCtrl.root.trans; return h( 'div.mode-choice.buttons', h( 'group.radio', - gameModes(trans).map(({ key, name }) => { - const disabled = key === 'rated' && setupCtrl.ratedModeDisabled(); + gameModes(ctrl.root.trans).map(({ key, name }) => { + const disabled = key === 'rated' && ctrl.ratedModeDisabled(); return h('div', [ - h(`input#sf_mode_${key}.checked_${key === setupCtrl.gameMode()}`, { + h(`input#sf_mode_${key}.checked_${key === ctrl.gameMode()}`, { attrs: { name, type: 'radio', value: key, - checked: key === setupCtrl.gameMode(), + checked: key === ctrl.gameMode(), disabled, }, on: { - change: (e: Event) => setupCtrl.gameMode((e.target as HTMLInputElement).value as GameMode), + change: (e: Event) => ctrl.gameMode((e.target as HTMLInputElement).value as GameMode), }, }), h('label', { class: { disabled }, attrs: { for: `sf_mode_${key}` } }, name), diff --git a/ui/gameSetup/src/view/components/levelButtons.ts b/ui/gameSetup/src/view/components/levelButtons.ts index 768738e69a894..1ff7d80d9239d 100644 --- a/ui/gameSetup/src/view/components/levelButtons.ts +++ b/ui/gameSetup/src/view/components/levelButtons.ts @@ -2,22 +2,21 @@ import { h } from 'snabbdom'; import { SetupCtrl } from '../../ctrl'; import { option } from './option'; -export const levelButtons = (setupCtrl: SetupCtrl) => { - const trans = setupCtrl.root.trans; +export const levelButtons = (ctrl: SetupCtrl) => { return lichess.blindMode ? [ - h('label', { attrs: { for: 'sf_level' } }, trans('strength')), + h('label', { attrs: { for: 'sf_level' } }, ctrl.root.trans('strength')), h( 'select#sf_level', { - on: { change: (e: Event) => setupCtrl.aiLevel(parseInt((e.target as HTMLSelectElement).value)) }, + on: { change: (e: Event) => ctrl.aiLevel(parseInt((e.target as HTMLSelectElement).value)) }, }, - '12345678'.split('').map(key => option({ key, name: key }, setupCtrl.aiLevel().toString())), + '12345678'.split('').map(key => option({ key, name: key }, ctrl.aiLevel().toString())), ), ] : [ h('br'), - trans('strength'), + ctrl.root.trans('strength'), h('div.level.buttons', [ h( 'div.config_level', @@ -30,10 +29,10 @@ export const levelButtons = (setupCtrl: SetupCtrl) => { name: 'level', type: 'radio', value: level, - checked: level === setupCtrl.aiLevel(), + checked: level === ctrl.aiLevel(), }, on: { - change: (e: Event) => setupCtrl.aiLevel(parseInt((e.target as HTMLInputElement).value)), + change: (e: Event) => ctrl.aiLevel(parseInt((e.target as HTMLInputElement).value)), }, }), h('label', { attrs: { for: `sf_level_${level}` } }, level), @@ -44,8 +43,8 @@ export const levelButtons = (setupCtrl: SetupCtrl) => { h( 'div.ai_info', h( - `div.sf_level_${setupCtrl.aiLevel()}`, - trans('aiNameLevelAiLevel', 'Fairy-Stockfish 14', setupCtrl.aiLevel()), + `div.sf_level_${ctrl.aiLevel()}`, + ctrl.root.trans('aiNameLevelAiLevel', 'Fairy-Stockfish 14', ctrl.aiLevel()), ), ), ]), diff --git a/ui/gameSetup/src/view/components/ratingDifferenceSliders.ts b/ui/gameSetup/src/view/components/ratingDifferenceSliders.ts index 0b3418dd86318..16d32b5a0b328 100644 --- a/ui/gameSetup/src/view/components/ratingDifferenceSliders.ts +++ b/ui/gameSetup/src/view/components/ratingDifferenceSliders.ts @@ -4,7 +4,6 @@ import { SetupCtrl } from '../../ctrl'; export const ratingDifferenceSliders = (ctrl: SetupCtrl) => { if (!ctrl.root.user || lichess.blindMode || !ctrl.root.ratingMap) return null; - const trans = ctrl.root.trans; const selectedPerf = ctrl.selectedPerf(); const isProvisional = !!ctrl.root.ratingMap[selectedPerf].prov; const disabled = isProvisional ? '.disabled' : ''; @@ -23,7 +22,7 @@ export const ratingDifferenceSliders = (ctrl: SetupCtrl) => { : undefined, }, [ - trans('ratingRange'), + ctrl.root.trans('ratingRange'), h('div.rating-range', [ h('input.range.rating-range__min', { attrs: { diff --git a/ui/gameSetup/src/view/components/timePickerAndSliders.ts b/ui/gameSetup/src/view/components/timePickerAndSliders.ts index 0195335348e2a..8bf302e4727a8 100644 --- a/ui/gameSetup/src/view/components/timePickerAndSliders.ts +++ b/ui/gameSetup/src/view/components/timePickerAndSliders.ts @@ -12,61 +12,60 @@ const showTime = (v: number) => { return v.toString(); }; -const renderBlindModeTimePickers = (setupCtrl: SetupCtrl, allowAnonymous: boolean) => { - const trans = setupCtrl.root.trans; +const renderBlindModeTimePickers = (ctrl: SetupCtrl, allowAnonymous: boolean) => { return [ - renderTimeModePicker(setupCtrl, allowAnonymous), - setupCtrl.timeMode() === 'realTime' + renderTimeModePicker(ctrl, allowAnonymous), + ctrl.timeMode() === 'realTime' ? h('div.time-choice', [ - h('label', { attrs: { for: 'sf_time' } }, trans('minutesPerSide')), + h('label', { attrs: { for: 'sf_time' } }, ctrl.root.trans('minutesPerSide')), h( 'select#sf_time', { on: { - change: (e: Event) => setupCtrl.timeV(parseFloat((e.target as HTMLSelectElement).value)), + change: (e: Event) => ctrl.timeV(parseFloat((e.target as HTMLSelectElement).value)), }, }, sliderTimes.map((sliderTime, timeV) => - option({ key: timeV.toString(), name: showTime(sliderTime) }, setupCtrl.timeV().toString()), + option({ key: timeV.toString(), name: showTime(sliderTime) }, ctrl.timeV().toString()), ), ), ]) : null, - setupCtrl.timeMode() === 'realTime' + ctrl.timeMode() === 'realTime' ? h('div.increment-choice', [ - h('label', { attrs: { for: 'sf_increment' } }, trans('incrementInSeconds')), + h('label', { attrs: { for: 'sf_increment' } }, ctrl.root.trans('incrementInSeconds')), h( 'select#sf_increment', { on: { - change: (e: Event) => setupCtrl.incrementV(parseInt((e.target as HTMLSelectElement).value)), + change: (e: Event) => ctrl.incrementV(parseInt((e.target as HTMLSelectElement).value)), }, }, // 31 because the range below goes from 0 to 30 Array.from(Array(31).keys()).map(incrementV => option( { key: incrementV.toString(), name: incrementVToIncrement(incrementV).toString() }, - setupCtrl.incrementV().toString(), + ctrl.incrementV().toString(), ), ), ), ]) : null, - setupCtrl.timeMode() === 'correspondence' + ctrl.timeMode() === 'correspondence' ? h('div.days-choice', [ - h('label', { attrs: { for: 'sf_days' } }, trans('daysPerTurn')), + h('label', { attrs: { for: 'sf_days' } }, ctrl.root.trans('daysPerTurn')), h( 'select#sf_days', { on: { - change: (e: Event) => setupCtrl.daysV(parseInt((e.target as HTMLSelectElement).value)), + change: (e: Event) => ctrl.daysV(parseInt((e.target as HTMLSelectElement).value)), }, }, // 7 because the range below goes from 1 to 7 Array.from(Array(7).keys()).map(daysV => option( { key: (daysV + 1).toString(), name: daysVToDays(daysV + 1).toString() }, - setupCtrl.daysV().toString(), + ctrl.daysV().toString(), ), ), ), @@ -75,19 +74,19 @@ const renderBlindModeTimePickers = (setupCtrl: SetupCtrl, allowAnonymous: boolea ]; }; -const renderTimeModePicker = (setupCtrl: SetupCtrl, allowAnonymous = false) => { - const trans = setupCtrl.root.trans; - return setupCtrl.root.user || allowAnonymous +const renderTimeModePicker = (ctrl: SetupCtrl, allowAnonymous = false) => { + const trans = ctrl.root.trans; + return ctrl.root.user || allowAnonymous ? h('div.label-select', [ h('label', { attrs: { for: 'sf_timeMode' } }, trans('timeControl')), h( 'select#sf_timeMode', { on: { - change: (e: Event) => setupCtrl.timeMode((e.target as HTMLSelectElement).value as TimeMode), + change: (e: Event) => ctrl.timeMode((e.target as HTMLSelectElement).value as TimeMode), }, }, - timeModes(trans).map(timeMode => option(timeMode, setupCtrl.timeMode())), + timeModes(trans).map(timeMode => option(timeMode, ctrl.timeMode())), ), ]) : null; @@ -102,36 +101,36 @@ const inputRange = (min: number, max: number, prop: Prop, classes?: }, }); -export const timePickerAndSliders = (setupCtrl: SetupCtrl, allowAnonymous = false) => { - const trans = setupCtrl.root.trans; +export const timePickerAndSliders = (ctrl: SetupCtrl, allowAnonymous = false) => { + const trans = ctrl.root.trans; return h( 'div.time-mode-config.optional-config', lichess.blindMode - ? renderBlindModeTimePickers(setupCtrl, allowAnonymous) + ? renderBlindModeTimePickers(ctrl, allowAnonymous) : [ - renderTimeModePicker(setupCtrl, allowAnonymous), - setupCtrl.timeMode() === 'realTime' + renderTimeModePicker(ctrl, allowAnonymous), + ctrl.timeMode() === 'realTime' ? h('div.time-choice.range', [ `${trans('minutesPerSide')}: `, - h('span', showTime(setupCtrl.time())), - inputRange(0, 38, setupCtrl.timeV, { - failure: !setupCtrl.validTime() || !setupCtrl.validAiTime(), + h('span', showTime(ctrl.time())), + inputRange(0, 38, ctrl.timeV, { + failure: !ctrl.validTime() || !ctrl.validAiTime(), }), ]) : null, - setupCtrl.timeMode() === 'realTime' + ctrl.timeMode() === 'realTime' ? h('div.increment-choice.range', [ `${trans('incrementInSeconds')}: `, - h('span', setupCtrl.increment()), - inputRange(0, 30, setupCtrl.incrementV, { failure: !setupCtrl.validTime() }), + h('span', ctrl.increment()), + inputRange(0, 30, ctrl.incrementV, { failure: !ctrl.validTime() }), ]) - : setupCtrl.timeMode() === 'correspondence' + : ctrl.timeMode() === 'correspondence' ? h( 'div.correspondence', h('div.days-choice.range', [ `${trans('daysPerTurn')}: `, - h('span', setupCtrl.days()), - inputRange(1, 7, setupCtrl.daysV), + h('span', ctrl.days()), + inputRange(1, 7, ctrl.daysV), ]), ) : null, diff --git a/ui/gameSetup/src/view/components/variantPicker.ts b/ui/gameSetup/src/view/components/variantPicker.ts index 6bbded9046756..fbf8819e7e292 100644 --- a/ui/gameSetup/src/view/components/variantPicker.ts +++ b/ui/gameSetup/src/view/components/variantPicker.ts @@ -5,10 +5,9 @@ import { variantsBlindMode, variants, variantsForGameType } from '../../options' import { option } from './option'; export const variantPicker = (ctrl: SetupCtrl) => { - const trans = ctrl.root.trans; const baseVariants = lichess.blindMode ? variantsBlindMode : variants; return h('div.variant.label-select', [ - h('label', { attrs: { for: 'sf_variant' } }, trans('variant')), + h('label', { attrs: { for: 'sf_variant' } }, ctrl.root.trans('variant')), h( 'select#sf_variant', { diff --git a/ui/gameSetup/src/view/friendContent.ts b/ui/gameSetup/src/view/friendContent.ts index 6cd32fcb8126e..1609cae9e3652 100644 --- a/ui/gameSetup/src/view/friendContent.ts +++ b/ui/gameSetup/src/view/friendContent.ts @@ -10,9 +10,8 @@ import { colorButtons } from './components/colorButtons'; import { ratingView } from './components/ratingView'; export default function friendContent(ctrl: SetupCtrl): MaybeVNodes { - const trans = ctrl.root.trans; return [ - h('h2', trans('playWithAFriend')), + h('h2', ctrl.root.trans('playWithAFriend')), h('div.setup-content', [ ctrl.friendUser ? userLink(ctrl.friendUser) : null, variantPicker(ctrl), diff --git a/ui/gameSetup/src/view/hookContent.ts b/ui/gameSetup/src/view/hookContent.ts index 16cbff62185d4..8a4fc926bdd22 100644 --- a/ui/gameSetup/src/view/hookContent.ts +++ b/ui/gameSetup/src/view/hookContent.ts @@ -10,9 +10,8 @@ import { colorButtons } from './components/colorButtons'; import { ratingView } from './components/ratingView'; export default function hookContent(ctrl: SetupCtrl): MaybeVNodes { - const trans = ctrl.root.trans; return [ - h('h2', trans('createAGame')), + h('h2', ctrl.root.trans('createAGame')), h('div.setup-content', [ variantPicker(ctrl), timePickerAndSliders(ctrl), diff --git a/ui/lobby/src/ctrl.ts b/ui/lobby/src/ctrl.ts index 31af0d78146e7..132d3c36ba6b9 100644 --- a/ui/lobby/src/ctrl.ts +++ b/ui/lobby/src/ctrl.ts @@ -268,16 +268,16 @@ export default class LobbyController implements ParentCtrl { hasPool = (id: string) => this.pools.some(p => p.id === id); - loadSetupCtrl = async () => { - return (this.setupCtrl = await lichess.loadEsm('gameSetup', { init: this })); + showSetupModal = async (gameType: GameType, opts?: ForceSetupOptions, friendUser?: string) => { + if (!this.setupCtrl) this.setupCtrl = await lichess.loadEsm('gameSetup', { init: this }); + this.leavePool(); + this.setupCtrl.openModal(gameType, opts, friendUser); }; - private locationHashSetupModal = async () => { + private locationHashSetupModal = () => { const locationHash = location.hash.replace('#', ''); if (!['ai', 'friend', 'hook', 'local'].includes(locationHash)) return; - await this.loadSetupCtrl(); - let friendUser; const forceOptions: ForceSetupOptions = {}; const urlParams = new URLSearchParams(location.search); @@ -295,8 +295,7 @@ export default class LobbyController implements ParentCtrl { } else { friendUser = urlParams.get('user')!; } - this.leavePool(); - this.setupCtrl.openModal(locationHash as GameType, forceOptions, friendUser); + this.showSetupModal(locationHash as GameType, forceOptions, friendUser); history.replaceState(null, '', '/'); }; diff --git a/ui/lobby/src/main.ts b/ui/lobby/src/main.ts index fddff5cf19b8d..ebf58d4b37b53 100644 --- a/ui/lobby/src/main.ts +++ b/ui/lobby/src/main.ts @@ -16,9 +16,7 @@ export default function main(opts: LobbyOpts) { let tableVNode = patch(opts.tableElement, tableView(ctrl)); function redraw() { - console.log('wtf'); appVNode = patch(appVNode, appView(ctrl)); - console.log('patching table view'); tableVNode = patch(tableVNode, tableView(ctrl)); } diff --git a/ui/lobby/src/view/correspondence.ts b/ui/lobby/src/view/correspondence.ts index 882aed8fd2676..1cbcdbec24aa1 100644 --- a/ui/lobby/src/view/correspondence.ts +++ b/ui/lobby/src/view/correspondence.ts @@ -53,7 +53,7 @@ function createSeek(ctrl: LobbyController): VNode | undefined { 'click', () => { ctrl.leavePool(); - ctrl.setupCtrl.openModal('hook', { variant: 'standard', timeMode: 'correspondence' }); + ctrl.showSetupModal('hook', { variant: 'standard', timeMode: 'correspondence' }); }, ctrl.redraw, ), diff --git a/ui/lobby/src/view/pools.ts b/ui/lobby/src/view/pools.ts index 675d159b45e8b..4ea4fdc7a3dc2 100644 --- a/ui/lobby/src/view/pools.ts +++ b/ui/lobby/src/view/pools.ts @@ -12,7 +12,7 @@ export function hooks(ctrl: LobbyController): Hooks { ((e.target as HTMLElement).parentNode as HTMLElement).getAttribute('data-id'); if (id === 'custom') { ctrl.leavePool(); - ctrl.setupCtrl.openModal('hook'); + ctrl.showSetupModal('hook'); } else if (id) ctrl.clickPool(id); }, ctrl.redraw, diff --git a/ui/lobby/src/view/table.ts b/ui/lobby/src/view/table.ts index 3aee2165fb7a0..f565c4200ea82 100644 --- a/ui/lobby/src/view/table.ts +++ b/ui/lobby/src/view/table.ts @@ -30,11 +30,7 @@ export default function table(ctrl: LobbyController) { attrs: { type: 'button' }, hook: disabled ? {} - : bind(lichess.blindMode ? 'click' : 'mousedown', async () => { - await ctrl.loadSetupCtrl(); - ctrl.leavePool(); - ctrl.setupCtrl.openModal(gameType); - }), + : bind(lichess.blindMode ? 'click' : 'mousedown', () => ctrl.showSetupModal(gameType)), }, trans(transKey), ), From 6ab8ebb023e2df3f09851b50e6de179be4c40028 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Wed, 23 Aug 2023 09:12:38 -0500 Subject: [PATCH 029/174] gah --- ui/gameSetup/css/_game-setup.scss | 56 ++++++ ui/gameSetup/css/_local-setup.scss | 54 +++++ ui/gameSetup/css/_setup.scss | 46 +---- .../build/{_setup.scss => _game-setup.scss} | 2 +- ui/gameSetup/css/build/_local-setup.scss | 5 + ...ltr.dark.scss => game-setup.ltr.dark.scss} | 2 +- ...r.light.scss => game-setup.ltr.light.scss} | 2 +- ...transp.scss => game-setup.ltr.transp.scss} | 2 +- ...rtl.dark.scss => game-setup.rtl.dark.scss} | 2 +- ...l.light.scss => game-setup.rtl.light.scss} | 2 +- ...transp.scss => game-setup.rtl.transp.scss} | 2 +- .../css/build/local-setup.ltr.dark.scss | 3 + .../css/build/local-setup.ltr.light.scss | 3 + .../css/build/local-setup.ltr.transp.scss | 3 + .../css/build/local-setup.rtl.dark.scss | 3 + .../css/build/local-setup.rtl.light.scss | 3 + .../css/build/local-setup.rtl.transp.scss | 3 + ui/lobby/css/_lobby.setup.scss | 2 - ui/lobby/css/_setup.scss | 185 ------------------ ui/lobby/css/build/_lobby.setup.scss | 3 +- 20 files changed, 143 insertions(+), 240 deletions(-) create mode 100644 ui/gameSetup/css/_game-setup.scss create mode 100644 ui/gameSetup/css/_local-setup.scss rename ui/gameSetup/css/build/{_setup.scss => _game-setup.scss} (87%) create mode 100644 ui/gameSetup/css/build/_local-setup.scss rename ui/gameSetup/css/build/{setup.ltr.dark.scss => game-setup.ltr.dark.scss} (78%) rename ui/gameSetup/css/build/{setup.ltr.light.scss => game-setup.ltr.light.scss} (78%) rename ui/gameSetup/css/build/{setup.ltr.transp.scss => game-setup.ltr.transp.scss} (79%) rename ui/gameSetup/css/build/{setup.rtl.dark.scss => game-setup.rtl.dark.scss} (78%) rename ui/gameSetup/css/build/{setup.rtl.light.scss => game-setup.rtl.light.scss} (78%) rename ui/gameSetup/css/build/{setup.rtl.transp.scss => game-setup.rtl.transp.scss} (79%) create mode 100644 ui/gameSetup/css/build/local-setup.ltr.dark.scss create mode 100644 ui/gameSetup/css/build/local-setup.ltr.light.scss create mode 100644 ui/gameSetup/css/build/local-setup.ltr.transp.scss create mode 100644 ui/gameSetup/css/build/local-setup.rtl.dark.scss create mode 100644 ui/gameSetup/css/build/local-setup.rtl.light.scss create mode 100644 ui/gameSetup/css/build/local-setup.rtl.transp.scss delete mode 100644 ui/lobby/css/_lobby.setup.scss delete mode 100644 ui/lobby/css/_setup.scss diff --git a/ui/gameSetup/css/_game-setup.scss b/ui/gameSetup/css/_game-setup.scss new file mode 100644 index 0000000000000..6920ef402a7b2 --- /dev/null +++ b/ui/gameSetup/css/_game-setup.scss @@ -0,0 +1,56 @@ +@import 'setup'; + +.game-setup#modal-wrap { + @extend %setup; + width: 30em; + text-align: center; + + > div { + padding: 0; + max-height: 96vh; + } + + .label-select { + &.variant { + margin-bottom: 1em; + padding-bottom: 0; + } + } + + .range { + padding-top: 1em; + + span { + font-weight: bold; + } + + input { + font-size: 1.5em; + margin-top: 0.5em; + padding: 0; + width: 90%; + } + } + + .rating-range { + @extend %flex-center-nowrap; + justify-content: center; + .rating-min, + .rating-max { + flex: 0 0 7ch; + } + input { + width: 30%; + padding: 0.6em 0; + } + } + + .ratings { + padding: 1em; + width: 100%; + text-align: center; + strong { + margin-#{$end-direction}: 0.25em; + } + } +} diff --git a/ui/gameSetup/css/_local-setup.scss b/ui/gameSetup/css/_local-setup.scss new file mode 100644 index 0000000000000..1c3dd45710d50 --- /dev/null +++ b/ui/gameSetup/css/_local-setup.scss @@ -0,0 +1,54 @@ +@import 'setup'; + +.local-setup#modal-wrap { + @extend %setup; + + #bot-view { + display: flex; + flex-flow: column nowrap; + + #bot-content { + flex: 1 1 auto; + overflow: hidden; + } + } + + .fancy-bot { + display: flex; + align-items: center; + + &:nth-child(even) { + background: $c-bg-zebra; + justify-content: space-between; + img.picture { + order: 3; + } + } + + &:hover { + background: mix($c-link, $c-bg-box, 15%); + } + + img.picture { + flex: 0 0 128px; + + display: block; + } + span { + display: flex; + flex-flow: row nowrap; + align-items: center; + * { + margin-right: 6px; + } + } + .overview { + margin: 20px 10px 10px 2.5vw; + display: flex; + flex: auto; + flex-flow: column; + justify-content: space-between; + padding-bottom: 15px; + } + } +} diff --git a/ui/gameSetup/css/_setup.scss b/ui/gameSetup/css/_setup.scss index 04e8ca5355b2a..a41bd149810ba 100644 --- a/ui/gameSetup/css/_setup.scss +++ b/ui/gameSetup/css/_setup.scss @@ -1,10 +1,8 @@ $c-setup: $c-secondary; $c-slider: $c-setup; -.game-setup#modal-wrap { +%setup { display: block; - width: 30em; - text-align: center; > div { padding: 0; @@ -55,11 +53,6 @@ $c-slider: $c-setup; .label-select { @extend %flex-center; - &.variant { - margin-bottom: 1em; - padding-bottom: 0; - } - label { flex: 0 0 33%; text-align: right; @@ -91,43 +84,6 @@ $c-slider: $c-setup; border-radius: 0.5em; } - .range { - padding-top: 1em; - - span { - font-weight: bold; - } - - input { - font-size: 1.5em; - margin-top: 0.5em; - padding: 0; - width: 90%; - } - } - - .rating-range { - @extend %flex-center-nowrap; - justify-content: center; - .rating-min, - .rating-max { - flex: 0 0 7ch; - } - input { - width: 30%; - padding: 0.6em 0; - } - } - - .ratings { - padding: 1em; - width: 100%; - text-align: center; - strong { - margin-#{$end-direction}: 0.25em; - } - } - .color-submits { display: flex; align-items: flex-end; diff --git a/ui/gameSetup/css/build/_setup.scss b/ui/gameSetup/css/build/_game-setup.scss similarity index 87% rename from ui/gameSetup/css/build/_setup.scss rename to ui/gameSetup/css/build/_game-setup.scss index 1c6c483138581..340db309b8bd6 100644 --- a/ui/gameSetup/css/build/_setup.scss +++ b/ui/gameSetup/css/build/_game-setup.scss @@ -2,4 +2,4 @@ @import '../../../common/css/form/range'; @import '../../../common/css/form/radio'; @import '../../../common/css/component/modal'; -@import '../setup'; +@import '../game-setup'; diff --git a/ui/gameSetup/css/build/_local-setup.scss b/ui/gameSetup/css/build/_local-setup.scss new file mode 100644 index 0000000000000..e32fa5c488df9 --- /dev/null +++ b/ui/gameSetup/css/build/_local-setup.scss @@ -0,0 +1,5 @@ +@import '../../../common/css/plugin'; +@import '../../../common/css/form/range'; +@import '../../../common/css/form/radio'; +@import '../../../common/css/component/modal'; +@import '../local-setup'; diff --git a/ui/gameSetup/css/build/setup.ltr.dark.scss b/ui/gameSetup/css/build/game-setup.ltr.dark.scss similarity index 78% rename from ui/gameSetup/css/build/setup.ltr.dark.scss rename to ui/gameSetup/css/build/game-setup.ltr.dark.scss index 40f701f4d9077..55632e2d524a9 100644 --- a/ui/gameSetup/css/build/setup.ltr.dark.scss +++ b/ui/gameSetup/css/build/game-setup.ltr.dark.scss @@ -1,3 +1,3 @@ @import '../../../common/css/dir/ltr'; @import '../../../common/css/theme/dark'; -@import 'setup'; +@import 'game-setup'; diff --git a/ui/gameSetup/css/build/setup.ltr.light.scss b/ui/gameSetup/css/build/game-setup.ltr.light.scss similarity index 78% rename from ui/gameSetup/css/build/setup.ltr.light.scss rename to ui/gameSetup/css/build/game-setup.ltr.light.scss index 7e1586c3337d9..fe6f5e923e977 100644 --- a/ui/gameSetup/css/build/setup.ltr.light.scss +++ b/ui/gameSetup/css/build/game-setup.ltr.light.scss @@ -1,3 +1,3 @@ @import '../../../common/css/dir/ltr'; @import '../../../common/css/theme/light'; -@import 'setup'; +@import 'game-setup'; diff --git a/ui/gameSetup/css/build/setup.ltr.transp.scss b/ui/gameSetup/css/build/game-setup.ltr.transp.scss similarity index 79% rename from ui/gameSetup/css/build/setup.ltr.transp.scss rename to ui/gameSetup/css/build/game-setup.ltr.transp.scss index 668028c4030b7..c70993c0e9a31 100644 --- a/ui/gameSetup/css/build/setup.ltr.transp.scss +++ b/ui/gameSetup/css/build/game-setup.ltr.transp.scss @@ -1,3 +1,3 @@ @import '../../../common/css/dir/ltr'; @import '../../../common/css/theme/transp'; -@import 'setup'; +@import 'game-setup'; diff --git a/ui/gameSetup/css/build/setup.rtl.dark.scss b/ui/gameSetup/css/build/game-setup.rtl.dark.scss similarity index 78% rename from ui/gameSetup/css/build/setup.rtl.dark.scss rename to ui/gameSetup/css/build/game-setup.rtl.dark.scss index 1ff61bfda3c77..ef3e2925695cb 100644 --- a/ui/gameSetup/css/build/setup.rtl.dark.scss +++ b/ui/gameSetup/css/build/game-setup.rtl.dark.scss @@ -1,3 +1,3 @@ @import '../../../common/css/dir/rtl'; @import '../../../common/css/theme/dark'; -@import 'setup'; +@import 'game-setup'; diff --git a/ui/gameSetup/css/build/setup.rtl.light.scss b/ui/gameSetup/css/build/game-setup.rtl.light.scss similarity index 78% rename from ui/gameSetup/css/build/setup.rtl.light.scss rename to ui/gameSetup/css/build/game-setup.rtl.light.scss index fc4f23ba99a31..b2055f0abc0f0 100644 --- a/ui/gameSetup/css/build/setup.rtl.light.scss +++ b/ui/gameSetup/css/build/game-setup.rtl.light.scss @@ -1,3 +1,3 @@ @import '../../../common/css/dir/rtl'; @import '../../../common/css/theme/light'; -@import 'setup'; +@import 'game-setup'; diff --git a/ui/gameSetup/css/build/setup.rtl.transp.scss b/ui/gameSetup/css/build/game-setup.rtl.transp.scss similarity index 79% rename from ui/gameSetup/css/build/setup.rtl.transp.scss rename to ui/gameSetup/css/build/game-setup.rtl.transp.scss index 4e4da524419a8..9fd5332c20791 100644 --- a/ui/gameSetup/css/build/setup.rtl.transp.scss +++ b/ui/gameSetup/css/build/game-setup.rtl.transp.scss @@ -1,3 +1,3 @@ @import '../../../common/css/dir/rtl'; @import '../../../common/css/theme/transp'; -@import 'setup'; +@import 'game-setup'; diff --git a/ui/gameSetup/css/build/local-setup.ltr.dark.scss b/ui/gameSetup/css/build/local-setup.ltr.dark.scss new file mode 100644 index 0000000000000..61100975ca58e --- /dev/null +++ b/ui/gameSetup/css/build/local-setup.ltr.dark.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/ltr'; +@import '../../../common/css/theme/dark'; +@import 'local-setup'; diff --git a/ui/gameSetup/css/build/local-setup.ltr.light.scss b/ui/gameSetup/css/build/local-setup.ltr.light.scss new file mode 100644 index 0000000000000..7da4d3be64f21 --- /dev/null +++ b/ui/gameSetup/css/build/local-setup.ltr.light.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/ltr'; +@import '../../../common/css/theme/light'; +@import 'local-setup'; diff --git a/ui/gameSetup/css/build/local-setup.ltr.transp.scss b/ui/gameSetup/css/build/local-setup.ltr.transp.scss new file mode 100644 index 0000000000000..82a77006ac823 --- /dev/null +++ b/ui/gameSetup/css/build/local-setup.ltr.transp.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/ltr'; +@import '../../../common/css/theme/transp'; +@import 'local-setup'; diff --git a/ui/gameSetup/css/build/local-setup.rtl.dark.scss b/ui/gameSetup/css/build/local-setup.rtl.dark.scss new file mode 100644 index 0000000000000..0bbef2cd1dc0f --- /dev/null +++ b/ui/gameSetup/css/build/local-setup.rtl.dark.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/rtl'; +@import '../../../common/css/theme/dark'; +@import 'local-setup'; diff --git a/ui/gameSetup/css/build/local-setup.rtl.light.scss b/ui/gameSetup/css/build/local-setup.rtl.light.scss new file mode 100644 index 0000000000000..8d0933f082417 --- /dev/null +++ b/ui/gameSetup/css/build/local-setup.rtl.light.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/rtl'; +@import '../../../common/css/theme/light'; +@import 'local-setup'; diff --git a/ui/gameSetup/css/build/local-setup.rtl.transp.scss b/ui/gameSetup/css/build/local-setup.rtl.transp.scss new file mode 100644 index 0000000000000..2fee23538b13e --- /dev/null +++ b/ui/gameSetup/css/build/local-setup.rtl.transp.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/rtl'; +@import '../../../common/css/theme/transp'; +@import 'local-setup'; diff --git a/ui/lobby/css/_lobby.setup.scss b/ui/lobby/css/_lobby.setup.scss deleted file mode 100644 index 357787e66055e..0000000000000 --- a/ui/lobby/css/_lobby.setup.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'setup'; -@import 'app/hook-filter'; diff --git a/ui/lobby/css/_setup.scss b/ui/lobby/css/_setup.scss deleted file mode 100644 index 04e8ca5355b2a..0000000000000 --- a/ui/lobby/css/_setup.scss +++ /dev/null @@ -1,185 +0,0 @@ -$c-setup: $c-secondary; -$c-slider: $c-setup; - -.game-setup#modal-wrap { - display: block; - width: 30em; - text-align: center; - - > div { - padding: 0; - max-height: 96vh; - } - - h2 { - margin: 1.5rem 0; - } - - .setup-content > div { - padding: 0.5em 1em; - } - - group.radio { - margin: 0 auto 1em auto; - width: 70%; - - .disabled { - opacity: 0.4; - cursor: default; - } - - input:checked + label { - background: $c-setup; - } - } - - .optional-config { - border-bottom: $border; - } - - .optional-config.disabled { - opacity: 0.5; - cursor: not-allowed; - } - - .optional-config, - .ratings { - background: $c-bg-zebra; - border-top: $border; - } - - .mode-choice { - margin-top: 1em; - } - - .label-select { - @extend %flex-center; - - &.variant { - margin-bottom: 1em; - padding-bottom: 0; - } - - label { - flex: 0 0 33%; - text-align: right; - } - - select { - margin-#{$start-direction}: 0.8em; - font-weight: bold; - } - } - - .fen__form { - @extend %flex-center-nowrap; - } - - .fen__board { - display: block; - width: 50%; - margin: 0.5em auto 0 auto; - } - - #fen-input { - flex: 1 1 100%; - } - - .failure { - background: mix($c-bg-box, $c-bad, 80%); - box-shadow: 0 0 13px 6px mix($c-bg-box, $c-bad, 80%); - border-radius: 0.5em; - } - - .range { - padding-top: 1em; - - span { - font-weight: bold; - } - - input { - font-size: 1.5em; - margin-top: 0.5em; - padding: 0; - width: 90%; - } - } - - .rating-range { - @extend %flex-center-nowrap; - justify-content: center; - .rating-min, - .rating-max { - flex: 0 0 7ch; - } - input { - width: 30%; - padding: 0.6em 0; - } - } - - .ratings { - padding: 1em; - width: 100%; - text-align: center; - strong { - margin-#{$end-direction}: 0.25em; - } - } - - .color-submits { - display: flex; - align-items: flex-end; - justify-content: center; - margin: 1em auto; - text-align: center; - - &__button { - margin: 0 0.5em; - width: 64px; - height: 64px; - padding: 7px; - - i { - display: block; - padding: 0; - width: 50px; - height: 50px; - background-size: 50px 50px; - } - - &.white i { - background-image: img-url('../piece/cburnett/wK.svg'); - } - - &.black i { - background-image: img-url('../piece/cburnett/bK.svg'); - } - - &.random { - width: 85px; - height: 85px; - padding: 10px; - - i { - background-image: img-url('wbK.svg'); - background-size: 65px 65px; - width: 65px; - height: 65px; - } - } - - &:disabled { - opacity: 0.3; - cursor: not-allowed; - } - } - - .spinner { - width: 85px; - height: 85px; - margin: 10px auto 20px auto; - } - } -} diff --git a/ui/lobby/css/build/_lobby.setup.scss b/ui/lobby/css/build/_lobby.setup.scss index bda1499462d3a..e73971dabc2f4 100644 --- a/ui/lobby/css/build/_lobby.setup.scss +++ b/ui/lobby/css/build/_lobby.setup.scss @@ -2,4 +2,5 @@ @import '../../../common/css/form/range'; @import '../../../common/css/form/radio'; @import '../../../common/css/component/modal'; -@import '../lobby.setup'; +@import '../../../gameSetup/css/game-setup'; +@import '../app/hook-filter'; From 7da0aaf6a476b4371d477e903ee19da790ec1fc7 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble <101470903+schlawg@users.noreply.github.com> Date: Wed, 23 Aug 2023 13:00:19 -0500 Subject: [PATCH 030/174] get modules in order --- pnpm-lock.yaml | 26 ++++++++- ui/gameSetup/css/_local-setup.scss | 10 ++-- ui/gameSetup/package.json | 1 + ui/gameSetup/src/ctrl.ts | 17 +++--- ui/gameSetup/src/interfaces.ts | 7 ++- ui/gameSetup/src/view/components/botPicker.ts | 21 ++++++++ ui/gameSetup/src/view/localContent.ts | 21 ++++++++ ui/gameSetup/src/view/modal.ts | 3 +- ui/gameSetup/tsconfig.json | 2 +- ui/libot/package.json | 37 +++++++++++++ ui/libot/src/behavior.ts | 16 ++++++ ui/libot/src/bots/babyBot.ts | 20 +++++++ ui/libot/src/bots/babyHoward.ts | 20 +++++++ ui/libot/src/bots/beatrice.ts | 20 +++++++ ui/libot/src/bots/coral.ts | 20 +++++++ ui/libot/src/interfaces.ts | 9 ++++ ui/libot/src/main.ts | 38 +++++++++++++ ui/libot/tsconfig.json | 17 ++++++ ui/lobby/src/ctrl.ts | 6 +-- ui/lobby/src/view/table.ts | 1 + ui/localPlay/package.json | 7 +-- ui/localPlay/src/bot.ts | 54 ------------------- ui/localPlay/src/bots/coral.ts | 22 -------- ui/localPlay/src/ctrl.ts | 11 ++-- ui/localPlay/src/view.ts | 6 +-- ui/localPlay/tsconfig.json | 1 + 26 files changed, 302 insertions(+), 111 deletions(-) create mode 100644 ui/gameSetup/src/view/components/botPicker.ts create mode 100644 ui/gameSetup/src/view/localContent.ts create mode 100644 ui/libot/package.json create mode 100644 ui/libot/src/behavior.ts create mode 100644 ui/libot/src/bots/babyBot.ts create mode 100644 ui/libot/src/bots/babyHoward.ts create mode 100644 ui/libot/src/bots/beatrice.ts create mode 100644 ui/libot/src/bots/coral.ts create mode 100644 ui/libot/src/interfaces.ts create mode 100644 ui/libot/src/main.ts create mode 100644 ui/libot/tsconfig.json delete mode 100644 ui/localPlay/src/bot.ts delete mode 100644 ui/localPlay/src/bots/coral.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e0978047346b..406eabea88624 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' settings: autoInstallPeers: true @@ -283,6 +283,9 @@ importers: common: specifier: workspace:* version: link:../common + libot: + specifier: workspace:* + version: link:../libot snabbdom: specifier: ^3.5.1 version: 3.5.1 @@ -335,6 +338,24 @@ importers: specifier: github:ornicar/mithril.js#lila-1 version: github.com/ornicar/mithril.js/dd92827aec63f921149ca9a3460ccd981e7025bb + ui/libot: + dependencies: + chess: + specifier: workspace:* + version: link:../chess + chessops: + specifier: ^0.12.7 + version: 0.12.7 + common: + specifier: workspace:* + version: link:../common + tree: + specifier: workspace:* + version: link:../tree + zerofish: + specifier: ^0.0.11 + version: 0.0.11 + ui/lobby: dependencies: '@types/debounce-promise': @@ -376,6 +397,9 @@ importers: game: specifier: workspace:* version: link:../game + libot: + specifier: workspace:* + version: link:../libot nvui: specifier: workspace:* version: link:../nvui diff --git a/ui/gameSetup/css/_local-setup.scss b/ui/gameSetup/css/_local-setup.scss index 1c3dd45710d50..77d22dc4b022f 100644 --- a/ui/gameSetup/css/_local-setup.scss +++ b/ui/gameSetup/css/_local-setup.scss @@ -3,17 +3,13 @@ .local-setup#modal-wrap { @extend %setup; - #bot-view { + #bot-select { display: flex; flex-flow: column nowrap; - - #bot-content { - flex: 1 1 auto; - overflow: hidden; - } + overflow-y: auto; } - .fancy-bot { + .libot { display: flex; align-items: center; diff --git a/ui/gameSetup/package.json b/ui/gameSetup/package.json index bd2b63736d0f0..ed0c4397cfce7 100644 --- a/ui/gameSetup/package.json +++ b/ui/gameSetup/package.json @@ -8,6 +8,7 @@ "types": "./dist/types.d.ts", "dependencies": { "common": "workspace:*", + "libot": "workspace:*", "snabbdom": "^3.5.1" }, "scripts": { diff --git a/ui/gameSetup/src/ctrl.ts b/ui/gameSetup/src/ctrl.ts index bc9517729310f..d3615f0bcf0c3 100644 --- a/ui/gameSetup/src/ctrl.ts +++ b/ui/gameSetup/src/ctrl.ts @@ -2,8 +2,9 @@ import { Prop, propWithEffect } from 'common'; import debounce from 'common/debounce'; import * as xhr from 'common/xhr'; import { storedJsonProp, StoredJsonProp } from 'common/storage'; + import { - ForceSetupOptions, + SetupConstraints, GameMode, GameType, InputValue, @@ -98,12 +99,12 @@ export class SetupCtrl { aiLevel: 1, })); - private loadPropsFromStore = (forceOptions?: ForceSetupOptions) => { + private init = (opts?: SetupConstraints) => { const storeProps = this.store[this.gameType!](); - // Load props from the store, but override any store values with values found in forceOptions - this.variant = propWithEffect(forceOptions?.variant || storeProps.variant, this.onVariantChange); - this.fen = this.propWithApply(forceOptions?.fen || storeProps.fen); - this.timeMode = this.propWithApply(forceOptions?.timeMode || storeProps.timeMode); + // Load props from the store, but override any store values with values found in opts + this.variant = propWithEffect(opts?.variant || storeProps.variant, this.onVariantChange); + this.fen = this.propWithApply(opts?.fen || storeProps.fen); + this.timeMode = this.propWithApply(opts?.timeMode || storeProps.timeMode); this.timeV = this.propWithApply(sliderInitVal(storeProps.time, timeVToTime, 100)!); this.incrementV = this.propWithApply(sliderInitVal(storeProps.increment, incrementVToIncrement, 100)!); this.daysV = this.propWithApply(sliderInitVal(storeProps.days, daysVToDays, 20)!); @@ -189,13 +190,13 @@ export class SetupCtrl { private propWithApply = (value: A) => propWithEffect(value, this.onPropChange); - openModal = (gameType: GameType, forceOptions?: ForceSetupOptions, friendUser?: string) => { + openModal = (gameType: GameType, opts?: SetupConstraints, friendUser?: string) => { this.gameType = gameType; this.loading = false; this.fenError = false; this.lastValidFen = ''; this.friendUser = friendUser || ''; - this.loadPropsFromStore(forceOptions); + this.init(opts); this.root.redraw(); }; diff --git a/ui/gameSetup/src/interfaces.ts b/ui/gameSetup/src/interfaces.ts index 3cd9b344c5423..f8eeb72ac371b 100644 --- a/ui/gameSetup/src/interfaces.ts +++ b/ui/gameSetup/src/interfaces.ts @@ -1,3 +1,5 @@ +import { type Libot } from 'libot'; + export type GameType = 'hook' | 'friend' | 'ai' | 'local'; export type TimeMode = 'realTime' | 'correspondence' | 'unlimited'; export type GameMode = 'casual' | 'rated'; @@ -25,10 +27,11 @@ export interface SetupStore { days: number; } -export interface ForceSetupOptions { +export interface SetupConstraints { variant?: VariantKey; fen?: string; timeMode?: TimeMode; + bots?: Libot[]; } export interface RatingWithProvisional { @@ -49,6 +52,8 @@ export interface GameSetup { export interface ParentCtrl { readonly user?: string; readonly ratingMap?: Record; + readonly localBots?: Libot[]; + redraw: () => void; acquire?: (candidate: GameSetup) => boolean; trans: Trans; diff --git a/ui/gameSetup/src/view/components/botPicker.ts b/ui/gameSetup/src/view/components/botPicker.ts new file mode 100644 index 0000000000000..f5e33e68e49c3 --- /dev/null +++ b/ui/gameSetup/src/view/components/botPicker.ts @@ -0,0 +1,21 @@ +import { h, VNode } from 'snabbdom'; +import { SetupCtrl } from '../../ctrl'; +import { localBots, type BotInfo } from 'libot'; + +export function botPicker(ctrl: SetupCtrl) { + if (lichess.blindMode) return null; + return h( + 'div#bot-select', + {}, + Object.values(localBots).map(bot => botView(ctrl, bot)), + ); +} + +function botView(ctrl: SetupCtrl, bot: BotInfo): VNode { + ctrl; + return h('div.libot', [ + h('h1', bot.name), + h('p', bot.description), + h('img', { attrs: { src: bot.image } }), + ]); +} diff --git a/ui/gameSetup/src/view/localContent.ts b/ui/gameSetup/src/view/localContent.ts new file mode 100644 index 0000000000000..e5bd21c8157fa --- /dev/null +++ b/ui/gameSetup/src/view/localContent.ts @@ -0,0 +1,21 @@ +import { h } from 'snabbdom'; +import { MaybeVNodes } from 'common/snabbdom'; +import { SetupCtrl } from '../ctrl'; +import { botPicker } from './components/botPicker'; +import { fenInput } from './components/fenInput'; +import { timePickerAndSliders } from './components/timePickerAndSliders'; +import { colorButtons } from './components/colorButtons'; +import { ratingView } from './components/ratingView'; + +export default function localContent(ctrl: SetupCtrl): MaybeVNodes { + return [ + h('h2', 'Local Play'), + h('div.setup-content', [ + botPicker(ctrl), + fenInput(ctrl), + timePickerAndSliders(ctrl, true), + colorButtons(ctrl), + ]), + ratingView(ctrl), + ]; +} diff --git a/ui/gameSetup/src/view/modal.ts b/ui/gameSetup/src/view/modal.ts index 318b6b618029b..9f419d897eaf4 100644 --- a/ui/gameSetup/src/view/modal.ts +++ b/ui/gameSetup/src/view/modal.ts @@ -4,12 +4,13 @@ import { SetupCtrl } from '../ctrl'; import hookContent from './hookContent'; import friendContent from './friendContent'; import aiContent from './aiContent'; +import localContent from './localContent'; const gameTypeToRenderer = { hook: hookContent, friend: friendContent, ai: aiContent, - local: aiContent, + local: localContent, }; export default function setupModal(ctrl: SetupCtrl): MaybeVNode { diff --git a/ui/gameSetup/tsconfig.json b/ui/gameSetup/tsconfig.json index 62ac5dc7b1ab6..811f17adcd8bb 100644 --- a/ui/gameSetup/tsconfig.json +++ b/ui/gameSetup/tsconfig.json @@ -8,5 +8,5 @@ "isolatedModules": true, "composite": true }, - "references": [{ "path": "../common/tsconfig.json" }] + "references": [{ "path": "../common/tsconfig.json" }, { "path": "../libot/tsconfig.json" }] } diff --git a/ui/libot/package.json b/ui/libot/package.json new file mode 100644 index 0000000000000..d750bb10cd0a6 --- /dev/null +++ b/ui/libot/package.json @@ -0,0 +1,37 @@ +{ + "name": "libot", + "private": true, + "author": "T-Bone Duplexus", + "license": "AGPL-3.0-or-later", + "types": "dist/main.d.ts", + "typings": "main", + "exports": { + ".": "./dist/main.js", + "./*": "./dist/*.js" + }, + "typesVersions": { + "*": { + "*": [ + "dist/*" + ] + } + }, + "dependencies": { + "chess": "workspace:*", + "chessops": "^0.12.7", + "common": "workspace:*", + "tree": "workspace:*", + "zerofish": "^0.0.11" + }, + "scripts": { + "compile": "tsc", + "dev": "tsc", + "prod": "tsc" + }, + "lichess": { + "copy": { + "src": "node_modules/zerofish/dist/zerofishEngine.*", + "dest": "../../public/compiled" + } + } +} diff --git a/ui/libot/src/behavior.ts b/ui/libot/src/behavior.ts new file mode 100644 index 0000000000000..c795a88c33dc2 --- /dev/null +++ b/ui/libot/src/behavior.ts @@ -0,0 +1,16 @@ +import { type PV } from 'zerofish'; + +export function linesWithin(move: string, lines: PV[], bias = 0, threshold = 50) { + const zeroScore = lines.find(line => line.moves[0] === move)?.score ?? Number.NaN; + return lines.filter(fish => Math.abs(fish.score - bias - zeroScore) < threshold && fish.moves.length); +} + +export function randomSprinkle(move: string, lines: PV[]) { + lines = linesWithin(move, lines, 0, 20); + if (!lines.length) return move; + return lines[Math.floor(Math.random() * lines.length)].moves[0] ?? move; +} + +export function occurs(chance: number) { + return Math.random() < chance; +} diff --git a/ui/libot/src/bots/babyBot.ts b/ui/libot/src/bots/babyBot.ts new file mode 100644 index 0000000000000..ec3d5a1e42bbe --- /dev/null +++ b/ui/libot/src/bots/babyBot.ts @@ -0,0 +1,20 @@ +import makeZerofish, { type Zerofish } from 'zerofish'; +import { type Libot, botNetUrl, localBots } from '../main'; + +export class BabyBot implements Libot { + name = localBots.babyBot.name; + description = localBots.babyBot.description; + image = localBots.babyBot.image; + net = botNetUrl('maia-1100.pb'); + ratings = new Map(); + + zf: Zerofish; + constructor(opts?: any) { + opts; + makeZerofish({ pbUrl: this.net }).then(zf => (this.zf = zf)); + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} diff --git a/ui/libot/src/bots/babyHoward.ts b/ui/libot/src/bots/babyHoward.ts new file mode 100644 index 0000000000000..59a0402f20c01 --- /dev/null +++ b/ui/libot/src/bots/babyHoward.ts @@ -0,0 +1,20 @@ +import makeZerofish, { type Zerofish } from 'zerofish'; +import { type Libot, botNetUrl, localBots } from '../main'; + +export class BabyHoward implements Libot { + name = localBots.babyHoward.name; + description = localBots.babyHoward.description; + image = localBots.babyHoward.image; + net = botNetUrl('maia-1100.pb'); + ratings = new Map(); + + zf: Zerofish; + constructor(opts?: any) { + opts; + makeZerofish({ pbUrl: this.net }).then(zf => (this.zf = zf)); + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} diff --git a/ui/libot/src/bots/beatrice.ts b/ui/libot/src/bots/beatrice.ts new file mode 100644 index 0000000000000..77122481c1b0b --- /dev/null +++ b/ui/libot/src/bots/beatrice.ts @@ -0,0 +1,20 @@ +import makeZerofish, { type Zerofish } from 'zerofish'; +import { Libot, botNetUrl, localBots } from '../main'; + +export class Beatrice implements Libot { + name = localBots.beatrice.name; + description = localBots.beatrice.description; + image = localBots.beatrice.image; + net = botNetUrl('maia-1100.pb'); + ratings = new Map(); + zf: Zerofish; + + constructor(opts?: any) { + opts; + makeZerofish({ pbUrl: this.net }).then(zf => (this.zf = zf)); + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} diff --git a/ui/libot/src/bots/coral.ts b/ui/libot/src/bots/coral.ts new file mode 100644 index 0000000000000..248e0c93299d9 --- /dev/null +++ b/ui/libot/src/bots/coral.ts @@ -0,0 +1,20 @@ +import makeZerofish, { type Zerofish } from 'zerofish'; +import { Libot, botNetUrl, localBots } from '../main'; + +export class Coral implements Libot { + name = localBots.coral.name; + description = localBots.coral.description; + image = localBots.coral.image; + net = botNetUrl('maia-1100.pb'); + ratings = new Map(); + zf: Zerofish; + + constructor(opts?: any) { + opts; + makeZerofish({ pbUrl: this.net }).then(zf => (this.zf = zf)); + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} diff --git a/ui/libot/src/interfaces.ts b/ui/libot/src/interfaces.ts new file mode 100644 index 0000000000000..71fe62254b7ec --- /dev/null +++ b/ui/libot/src/interfaces.ts @@ -0,0 +1,9 @@ +export interface Libot { + readonly name: string; + readonly description: string; + readonly image: string; + readonly net?: string; + readonly ratings: Map; + + move: (fen: string) => Promise; +} diff --git a/ui/libot/src/main.ts b/ui/libot/src/main.ts new file mode 100644 index 0000000000000..9d0cc7d886abd --- /dev/null +++ b/ui/libot/src/main.ts @@ -0,0 +1,38 @@ +export * from './interfaces'; + +export interface BotInfo { + readonly name: string; + readonly description: string; + readonly image: string; +} + +export const localBots: { [key: string]: BotInfo } = { + coral: { + name: 'Coral', + description: 'Coral is a simple bot that plays random moves.', + image: botImageUrl('coral.webp'), + }, + babyHoward: { + name: 'Baby Howard', + description: 'Baby Howard is a bot that plays random moves.', + image: botImageUrl('baby-howard.webp'), + }, + babyBot: { + name: 'Baby Bot', + description: 'Baby Bot is a bot that plays random moves.', + image: botImageUrl('baby-robot.webp'), + }, + beatrice: { + name: 'Beatrice', + description: 'Beatrice is a bot that plays random moves.', + image: botImageUrl('beatrice.webp'), + }, +}; + +export function botNetUrl(weights: string) { + return lichess.assetUrl(`lifat/bots/weights/${weights}`, { noVersion: true }); +} + +export function botImageUrl(image: string) { + return lichess.assetUrl(`lifat/bots/images/${image}`, { noVersion: true }); +} diff --git a/ui/libot/tsconfig.json b/ui/libot/tsconfig.json new file mode 100644 index 0000000000000..7797a9cc46781 --- /dev/null +++ b/ui/libot/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "noUnusedLocals": false, + "noUnusedParameters": false, + "allowUnreachableCode": true, + "outDir": "dist", + "rootDir": "src", + "composite": true, + "isolatedModules": true + }, + "references": [ + { "path": "../chess/tsconfig.json" }, + { "path": "../common/tsconfig.json" }, + { "path": "../tree/tsconfig.json" } + ] +} diff --git a/ui/lobby/src/ctrl.ts b/ui/lobby/src/ctrl.ts index 132d3c36ba6b9..9aea80c5cd34c 100644 --- a/ui/lobby/src/ctrl.ts +++ b/ui/lobby/src/ctrl.ts @@ -6,7 +6,7 @@ import { make as makeStores, Stores } from './store'; import * as xhr from './xhr'; import * as poolRangeStorage from './poolRangeStorage'; import { LobbyOpts, LobbyData, Tab, Mode, Sort, Hook, Seek, Pool, PoolMember, LobbyMe } from './interfaces'; -import { ParentCtrl, ForceSetupOptions, GameType, GameSetup, SetupCtrl } from 'gameSetup'; +import { ParentCtrl, SetupConstraints, GameType, GameSetup, SetupCtrl } from 'gameSetup'; import LobbySocket from './socket'; import Filter from './filter'; @@ -268,7 +268,7 @@ export default class LobbyController implements ParentCtrl { hasPool = (id: string) => this.pools.some(p => p.id === id); - showSetupModal = async (gameType: GameType, opts?: ForceSetupOptions, friendUser?: string) => { + showSetupModal = async (gameType: GameType, opts?: SetupConstraints, friendUser?: string) => { if (!this.setupCtrl) this.setupCtrl = await lichess.loadEsm('gameSetup', { init: this }); this.leavePool(); this.setupCtrl.openModal(gameType, opts, friendUser); @@ -279,7 +279,7 @@ export default class LobbyController implements ParentCtrl { if (!['ai', 'friend', 'hook', 'local'].includes(locationHash)) return; let friendUser; - const forceOptions: ForceSetupOptions = {}; + const forceOptions: SetupConstraints = {}; const urlParams = new URLSearchParams(location.search); if (locationHash === 'hook') { if (urlParams.get('time') === 'realTime') { diff --git a/ui/lobby/src/view/table.ts b/ui/lobby/src/view/table.ts index f565c4200ea82..9112f60b2041d 100644 --- a/ui/lobby/src/view/table.ts +++ b/ui/lobby/src/view/table.ts @@ -22,6 +22,7 @@ export default function table(ctrl: LobbyController) { ['hook', 'createAGame', hookDisabled], ['friend', 'playWithAFriend', hasOngoingRealTimeGame], ['ai', 'playWithTheMachine', hasOngoingRealTimeGame], + ['local', 'Private Play', false], ].map(([gameType, transKey, disabled]: [GameType, string, boolean]) => h( `button.button.button-metal.config_${gameType}`, diff --git a/ui/localPlay/package.json b/ui/localPlay/package.json index 43ccffa9d866e..8f626475f8413 100644 --- a/ui/localPlay/package.json +++ b/ui/localPlay/package.json @@ -1,8 +1,6 @@ { "name": "local-play", - "version": "2.0.0", "private": true, - "description": "lichess.org local play", "author": "T-Bone Duplexus", "license": "AGPL-3.0-or-later", "dependencies": { @@ -12,6 +10,7 @@ "chessops": "^0.12.7", "common": "workspace:*", "game": "workspace:*", + "libot": "workspace:*", "nvui": "workspace:*", "puz": "workspace:*", "round": "workspace:*", @@ -29,10 +28,6 @@ "esm": { "src/main.ts": "localPlay" } - }, - "copy": { - "src": "node_modules/zerofish/dist/zerofishEngine.*", - "dest": "../../public/compiled" } } } diff --git a/ui/localPlay/src/bot.ts b/ui/localPlay/src/bot.ts deleted file mode 100644 index 9a8ca82aa186d..0000000000000 --- a/ui/localPlay/src/bot.ts +++ /dev/null @@ -1,54 +0,0 @@ -export interface Bot { - readonly name: string; - readonly description: string; - readonly image: string; - readonly net?: string; - readonly ratings: Map; - move: (fen: string) => Promise; -} - -export function netUrl(name: string) { - return `/assets/lifat/bots/weights/${name}`; -} - -export function imageUrl(name: string) { - return `/assets/lifat/bots/images/${name}`; -} - -export const bots = [ - { - name: 'Coral', - description: 'Coral is a simple bot that plays random moves.', - image: '/lifat/bots/images/coral.webp', - }, - { - name: 'Baby Howard', - description: 'Baby Howard is a bot that plays random moves.', - image: '/lifat/bots/images/baby-howard.webp', - }, - { - name: 'Baby Bot', - description: 'Baby Bot is a bot that plays random moves.', - image: '/lifat/bots/images/baby-robot.webp', - }, - { - name: 'Beatrice', - description: 'Beatrice is a bot that plays random moves.', - image: '/lifat/bots/images/beatrice.webp', - }, -]; - -/*function linesWithin(move: string, lines: PV[], bias = 0, threshold = 50) { - const zeroScore = lines.find(line => line.moves[0] === move)?.score ?? Number.NaN; - return lines.filter(fish => Math.abs(fish.score - bias - zeroScore) < threshold && fish.moves.length); -} - -function randomSprinkle(move: string, lines: PV[]) { - lines = linesWithin(move, lines, 0, 20); - if (!lines.length) return move; - return lines[Math.floor(Math.random() * lines.length)].moves[0] ?? move; -} - -function occurs(chance: number) { - return Math.random() < chance; -}*/ diff --git a/ui/localPlay/src/bots/coral.ts b/ui/localPlay/src/bots/coral.ts deleted file mode 100644 index 56b24e33a89a1..0000000000000 --- a/ui/localPlay/src/bots/coral.ts +++ /dev/null @@ -1,22 +0,0 @@ -import makeZerofish, { type Zerofish } from 'zerofish'; -import { Bot, netUrl } from '../bot'; - -export class CoralBot implements Bot { - name = 'Coral'; - description = 'Coral is a simple bot that plays random moves.'; - image = 'coral.webp'; - net = 'maia-1100.pb'; - ratings = new Map(); - - zf: Zerofish; - constructor() { - makeZerofish({ pbUrl: netUrl(this.net) }).then(zf => this.setZf(zf)); - } - setZf(zf: Zerofish) { - this.zf = zf; - this.zf; - } - async move(fen: string) { - return await this.zf.goZero(fen); - } -} diff --git a/ui/localPlay/src/ctrl.ts b/ui/localPlay/src/ctrl.ts index d470b6ec71b8c..e3155771047da 100644 --- a/ui/localPlay/src/ctrl.ts +++ b/ui/localPlay/src/ctrl.ts @@ -1,6 +1,6 @@ import { LocalPlayOpts } from './interfaces'; -import { Bot } from './bot'; -import { CoralBot } from './bots/coral'; +import { Coral } from 'libot/bots/coral'; +import { type Libot } from 'libot'; import { makeRounds } from './data'; import { makeFen /*, parseFen*/ } from 'chessops/fen'; import { makeSanAndPlay } from 'chessops/san'; @@ -8,13 +8,16 @@ import { Chess } from 'chessops'; import * as Chops from 'chessops'; export class Ctrl { - bot?: Bot = new CoralBot(); + bot?: Libot = new Coral(); chess = Chess.default(); tellRound: SocketSend; fiftyMovePly = 0; threefoldFens: Map = new Map(); - constructor(readonly opts: LocalPlayOpts, readonly redraw: () => void) { + constructor( + readonly opts: LocalPlayOpts, + readonly redraw: () => void, + ) { makeRounds(this).then(sender => (this.tellRound = sender)); } diff --git a/ui/localPlay/src/view.ts b/ui/localPlay/src/view.ts index d9dc0fd5e03d2..c46b618d3d547 100644 --- a/ui/localPlay/src/view.ts +++ b/ui/localPlay/src/view.ts @@ -1,7 +1,7 @@ import { h, VNode } from 'snabbdom'; //import * as licon from 'common/licon'; //import { bind } from 'common/snabbdom'; -import { bots } from './bot'; +import { localBots } from 'libot'; import { Ctrl } from './ctrl'; export default function (ctrl: Ctrl): VNode { @@ -11,8 +11,8 @@ export default function (ctrl: Ctrl): VNode { 'div#bot-content', h( 'div#bot-list', - bots.map(bot => botView(ctrl, bot)) - ) + Object.values(localBots).map(bot => botView(ctrl, bot)), + ), ), ]); } diff --git a/ui/localPlay/tsconfig.json b/ui/localPlay/tsconfig.json index 56ec36ef3aa73..4f86df60e8b39 100644 --- a/ui/localPlay/tsconfig.json +++ b/ui/localPlay/tsconfig.json @@ -11,6 +11,7 @@ { "path": "../chess/tsconfig.json" }, { "path": "../common/tsconfig.json" }, { "path": "../game/tsconfig.json" }, + { "path": "../libot/tsconfig.json" }, { "path": "../puz/tsconfig.json" }, { "path": "../nvui/tsconfig.json" }, { "path": "../round/tsconfig.json" }, From 08819285ea07010528d6cdcfb522e003e55e8059 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble <101470903+schlawg@users.noreply.github.com> Date: Thu, 24 Aug 2023 05:46:46 -0500 Subject: [PATCH 031/174] hookfilters gah --- ui/gameSetup/css/_game-setup.scss | 2 - ui/gameSetup/css/_local-setup.scss | 46 +++++++------------ ui/gameSetup/css/build/_game-setup.scss | 2 + ui/gameSetup/css/build/_local-setup.scss | 5 -- .../css/build/local-setup.ltr.dark.scss | 3 -- .../css/build/local-setup.ltr.light.scss | 3 -- .../css/build/local-setup.ltr.transp.scss | 3 -- .../css/build/local-setup.rtl.dark.scss | 3 -- .../css/build/local-setup.rtl.light.scss | 3 -- .../css/build/local-setup.rtl.transp.scss | 3 -- ui/gameSetup/src/view/components/botPicker.ts | 21 --------- ui/gameSetup/src/view/localContent.ts | 20 +++++++- ui/gameSetup/src/view/modal.ts | 4 +- ui/lobby/css/app/_app.scss | 9 ++-- .../{_hook-filter.scss => _hook-filters.scss} | 0 ui/lobby/css/build/_lobby.setup.scss | 6 --- ui/lobby/css/build/lobby.setup.ltr.dark.scss | 3 -- ui/lobby/css/build/lobby.setup.ltr.light.scss | 3 -- .../css/build/lobby.setup.ltr.transp.scss | 3 -- ui/lobby/css/build/lobby.setup.rtl.dark.scss | 3 -- ui/lobby/css/build/lobby.setup.rtl.light.scss | 3 -- .../css/build/lobby.setup.rtl.transp.scss | 3 -- ui/lobby/src/view/realTime/filter.ts | 1 - ui/lobby/src/view/table.ts | 4 +- 24 files changed, 47 insertions(+), 109 deletions(-) delete mode 100644 ui/gameSetup/css/build/_local-setup.scss delete mode 100644 ui/gameSetup/css/build/local-setup.ltr.dark.scss delete mode 100644 ui/gameSetup/css/build/local-setup.ltr.light.scss delete mode 100644 ui/gameSetup/css/build/local-setup.ltr.transp.scss delete mode 100644 ui/gameSetup/css/build/local-setup.rtl.dark.scss delete mode 100644 ui/gameSetup/css/build/local-setup.rtl.light.scss delete mode 100644 ui/gameSetup/css/build/local-setup.rtl.transp.scss delete mode 100644 ui/gameSetup/src/view/components/botPicker.ts rename ui/lobby/css/app/{_hook-filter.scss => _hook-filters.scss} (100%) delete mode 100644 ui/lobby/css/build/_lobby.setup.scss delete mode 100644 ui/lobby/css/build/lobby.setup.ltr.dark.scss delete mode 100644 ui/lobby/css/build/lobby.setup.ltr.light.scss delete mode 100644 ui/lobby/css/build/lobby.setup.ltr.transp.scss delete mode 100644 ui/lobby/css/build/lobby.setup.rtl.dark.scss delete mode 100644 ui/lobby/css/build/lobby.setup.rtl.light.scss delete mode 100644 ui/lobby/css/build/lobby.setup.rtl.transp.scss diff --git a/ui/gameSetup/css/_game-setup.scss b/ui/gameSetup/css/_game-setup.scss index 6920ef402a7b2..4893d3b2b7d67 100644 --- a/ui/gameSetup/css/_game-setup.scss +++ b/ui/gameSetup/css/_game-setup.scss @@ -1,5 +1,3 @@ -@import 'setup'; - .game-setup#modal-wrap { @extend %setup; width: 30em; diff --git a/ui/gameSetup/css/_local-setup.scss b/ui/gameSetup/css/_local-setup.scss index 77d22dc4b022f..f8a10abf4ce52 100644 --- a/ui/gameSetup/css/_local-setup.scss +++ b/ui/gameSetup/css/_local-setup.scss @@ -1,5 +1,3 @@ -@import 'setup'; - .local-setup#modal-wrap { @extend %setup; @@ -7,44 +5,34 @@ display: flex; flex-flow: column nowrap; overflow-y: auto; - } - .libot { - display: flex; - align-items: center; + .libot { + display: flex; + flex-flow: row nowrap; + align-items: center; - &:nth-child(even) { - background: $c-bg-zebra; - justify-content: space-between; - img.picture { - order: 3; + &:nth-child(even) { + background: $c-bg-zebra; + justify-content: space-between; } - } - &:hover { - background: mix($c-link, $c-bg-box, 15%); - } - - img.picture { - flex: 0 0 128px; + &:hover { + background: mix($c-link, $c-bg-box, 15%); + } - display: block; + img { + flex: 0 0 32px; + display: block; + } } - span { + + /*span { display: flex; flex-flow: row nowrap; align-items: center; * { margin-right: 6px; } - } - .overview { - margin: 20px 10px 10px 2.5vw; - display: flex; - flex: auto; - flex-flow: column; - justify-content: space-between; - padding-bottom: 15px; - } + }*/ } } diff --git a/ui/gameSetup/css/build/_game-setup.scss b/ui/gameSetup/css/build/_game-setup.scss index 340db309b8bd6..30cc4a568e54e 100644 --- a/ui/gameSetup/css/build/_game-setup.scss +++ b/ui/gameSetup/css/build/_game-setup.scss @@ -2,4 +2,6 @@ @import '../../../common/css/form/range'; @import '../../../common/css/form/radio'; @import '../../../common/css/component/modal'; +@import '../setup'; +@import '../local-setup'; @import '../game-setup'; diff --git a/ui/gameSetup/css/build/_local-setup.scss b/ui/gameSetup/css/build/_local-setup.scss deleted file mode 100644 index e32fa5c488df9..0000000000000 --- a/ui/gameSetup/css/build/_local-setup.scss +++ /dev/null @@ -1,5 +0,0 @@ -@import '../../../common/css/plugin'; -@import '../../../common/css/form/range'; -@import '../../../common/css/form/radio'; -@import '../../../common/css/component/modal'; -@import '../local-setup'; diff --git a/ui/gameSetup/css/build/local-setup.ltr.dark.scss b/ui/gameSetup/css/build/local-setup.ltr.dark.scss deleted file mode 100644 index 61100975ca58e..0000000000000 --- a/ui/gameSetup/css/build/local-setup.ltr.dark.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/ltr'; -@import '../../../common/css/theme/dark'; -@import 'local-setup'; diff --git a/ui/gameSetup/css/build/local-setup.ltr.light.scss b/ui/gameSetup/css/build/local-setup.ltr.light.scss deleted file mode 100644 index 7da4d3be64f21..0000000000000 --- a/ui/gameSetup/css/build/local-setup.ltr.light.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/ltr'; -@import '../../../common/css/theme/light'; -@import 'local-setup'; diff --git a/ui/gameSetup/css/build/local-setup.ltr.transp.scss b/ui/gameSetup/css/build/local-setup.ltr.transp.scss deleted file mode 100644 index 82a77006ac823..0000000000000 --- a/ui/gameSetup/css/build/local-setup.ltr.transp.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/ltr'; -@import '../../../common/css/theme/transp'; -@import 'local-setup'; diff --git a/ui/gameSetup/css/build/local-setup.rtl.dark.scss b/ui/gameSetup/css/build/local-setup.rtl.dark.scss deleted file mode 100644 index 0bbef2cd1dc0f..0000000000000 --- a/ui/gameSetup/css/build/local-setup.rtl.dark.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/rtl'; -@import '../../../common/css/theme/dark'; -@import 'local-setup'; diff --git a/ui/gameSetup/css/build/local-setup.rtl.light.scss b/ui/gameSetup/css/build/local-setup.rtl.light.scss deleted file mode 100644 index 8d0933f082417..0000000000000 --- a/ui/gameSetup/css/build/local-setup.rtl.light.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/rtl'; -@import '../../../common/css/theme/light'; -@import 'local-setup'; diff --git a/ui/gameSetup/css/build/local-setup.rtl.transp.scss b/ui/gameSetup/css/build/local-setup.rtl.transp.scss deleted file mode 100644 index 2fee23538b13e..0000000000000 --- a/ui/gameSetup/css/build/local-setup.rtl.transp.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/rtl'; -@import '../../../common/css/theme/transp'; -@import 'local-setup'; diff --git a/ui/gameSetup/src/view/components/botPicker.ts b/ui/gameSetup/src/view/components/botPicker.ts deleted file mode 100644 index f5e33e68e49c3..0000000000000 --- a/ui/gameSetup/src/view/components/botPicker.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { h, VNode } from 'snabbdom'; -import { SetupCtrl } from '../../ctrl'; -import { localBots, type BotInfo } from 'libot'; - -export function botPicker(ctrl: SetupCtrl) { - if (lichess.blindMode) return null; - return h( - 'div#bot-select', - {}, - Object.values(localBots).map(bot => botView(ctrl, bot)), - ); -} - -function botView(ctrl: SetupCtrl, bot: BotInfo): VNode { - ctrl; - return h('div.libot', [ - h('h1', bot.name), - h('p', bot.description), - h('img', { attrs: { src: bot.image } }), - ]); -} diff --git a/ui/gameSetup/src/view/localContent.ts b/ui/gameSetup/src/view/localContent.ts index e5bd21c8157fa..e30a6b63bd8ae 100644 --- a/ui/gameSetup/src/view/localContent.ts +++ b/ui/gameSetup/src/view/localContent.ts @@ -1,11 +1,11 @@ import { h } from 'snabbdom'; import { MaybeVNodes } from 'common/snabbdom'; import { SetupCtrl } from '../ctrl'; -import { botPicker } from './components/botPicker'; import { fenInput } from './components/fenInput'; import { timePickerAndSliders } from './components/timePickerAndSliders'; import { colorButtons } from './components/colorButtons'; import { ratingView } from './components/ratingView'; +import { localBots, type BotInfo } from 'libot'; export default function localContent(ctrl: SetupCtrl): MaybeVNodes { return [ @@ -19,3 +19,21 @@ export default function localContent(ctrl: SetupCtrl): MaybeVNodes { ratingView(ctrl), ]; } + +function botPicker(ctrl: SetupCtrl) { + if (lichess.blindMode) return null; + return h( + 'div#bot-select', + {}, + Object.values(localBots).map(bot => botView(ctrl, bot)), + ); +} + +function botView(ctrl: SetupCtrl, bot: BotInfo) { + ctrl; + return h('div.libot', [ + h('img', { attrs: { src: bot.image } }), + h('h3', bot.name), + h('p', bot.description), + ]); +} diff --git a/ui/gameSetup/src/view/modal.ts b/ui/gameSetup/src/view/modal.ts index 9f419d897eaf4..0e96e4240393f 100644 --- a/ui/gameSetup/src/view/modal.ts +++ b/ui/gameSetup/src/view/modal.ts @@ -17,8 +17,8 @@ export default function setupModal(ctrl: SetupCtrl): MaybeVNode { if (!ctrl.gameType) return null; const renderContent = gameTypeToRenderer[ctrl.gameType]; return snabModal({ - class: 'game-setup', - onInsert: () => lichess.loadCssPath('lobby.setup'), + class: ctrl.gameType === 'local' ? 'local-setup' : 'game-setup', + onInsert: () => lichess.loadCssPath('game-setup'), onClose: ctrl.closeModal, content: renderContent(ctrl), }); diff --git a/ui/lobby/css/app/_app.scss b/ui/lobby/css/app/_app.scss index db34b0a38eb79..a9b31cfb82b19 100644 --- a/ui/lobby/css/app/_app.scss +++ b/ui/lobby/css/app/_app.scss @@ -1,5 +1,6 @@ @import 'pool'; @import 'hook-chart'; +@import 'hook-filters'; @import 'hook-list'; .lobby__app { @@ -87,7 +88,7 @@ } } -.hook__filters { - // will be overridden by _hook-filter.scss once it's loaded - display: none; -} +//.hook__filters { WTF? +// will be overridden by _hook-filter.scss once it's loaded +//display: none; +//} diff --git a/ui/lobby/css/app/_hook-filter.scss b/ui/lobby/css/app/_hook-filters.scss similarity index 100% rename from ui/lobby/css/app/_hook-filter.scss rename to ui/lobby/css/app/_hook-filters.scss diff --git a/ui/lobby/css/build/_lobby.setup.scss b/ui/lobby/css/build/_lobby.setup.scss deleted file mode 100644 index e73971dabc2f4..0000000000000 --- a/ui/lobby/css/build/_lobby.setup.scss +++ /dev/null @@ -1,6 +0,0 @@ -@import '../../../common/css/plugin'; -@import '../../../common/css/form/range'; -@import '../../../common/css/form/radio'; -@import '../../../common/css/component/modal'; -@import '../../../gameSetup/css/game-setup'; -@import '../app/hook-filter'; diff --git a/ui/lobby/css/build/lobby.setup.ltr.dark.scss b/ui/lobby/css/build/lobby.setup.ltr.dark.scss deleted file mode 100644 index 7f384df494250..0000000000000 --- a/ui/lobby/css/build/lobby.setup.ltr.dark.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/ltr'; -@import '../../../common/css/theme/dark'; -@import 'lobby.setup'; diff --git a/ui/lobby/css/build/lobby.setup.ltr.light.scss b/ui/lobby/css/build/lobby.setup.ltr.light.scss deleted file mode 100644 index d14541ef77cc5..0000000000000 --- a/ui/lobby/css/build/lobby.setup.ltr.light.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/ltr'; -@import '../../../common/css/theme/light'; -@import 'lobby.setup'; diff --git a/ui/lobby/css/build/lobby.setup.ltr.transp.scss b/ui/lobby/css/build/lobby.setup.ltr.transp.scss deleted file mode 100644 index ca268b8287272..0000000000000 --- a/ui/lobby/css/build/lobby.setup.ltr.transp.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/ltr'; -@import '../../../common/css/theme/transp'; -@import 'lobby.setup'; diff --git a/ui/lobby/css/build/lobby.setup.rtl.dark.scss b/ui/lobby/css/build/lobby.setup.rtl.dark.scss deleted file mode 100644 index b105cd0b26f12..0000000000000 --- a/ui/lobby/css/build/lobby.setup.rtl.dark.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/rtl'; -@import '../../../common/css/theme/dark'; -@import 'lobby.setup'; diff --git a/ui/lobby/css/build/lobby.setup.rtl.light.scss b/ui/lobby/css/build/lobby.setup.rtl.light.scss deleted file mode 100644 index 80dfbfc665e5f..0000000000000 --- a/ui/lobby/css/build/lobby.setup.rtl.light.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/rtl'; -@import '../../../common/css/theme/light'; -@import 'lobby.setup'; diff --git a/ui/lobby/css/build/lobby.setup.rtl.transp.scss b/ui/lobby/css/build/lobby.setup.rtl.transp.scss deleted file mode 100644 index 74dd8a55368b6..0000000000000 --- a/ui/lobby/css/build/lobby.setup.rtl.transp.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/rtl'; -@import '../../../common/css/theme/transp'; -@import 'lobby.setup'; diff --git a/ui/lobby/src/view/realTime/filter.ts b/ui/lobby/src/view/realTime/filter.ts index de4d3d71fcc3c..a81abdd2dade9 100644 --- a/ui/lobby/src/view/realTime/filter.ts +++ b/ui/lobby/src/view/realTime/filter.ts @@ -87,7 +87,6 @@ export const render = (ctrl: LobbyController) => insert(vnode) { const el = vnode.elm as FilterNode; if (el.filterLoaded) return; - lichess.loadCssPath('lobby.setup'); xhr.text('/setup/filter').then(html => { el.innerHTML = html; el.filterLoaded = true; diff --git a/ui/lobby/src/view/table.ts b/ui/lobby/src/view/table.ts index 9112f60b2041d..843128c0c21e1 100644 --- a/ui/lobby/src/view/table.ts +++ b/ui/lobby/src/view/table.ts @@ -21,8 +21,8 @@ export default function table(ctrl: LobbyController) { [ ['hook', 'createAGame', hookDisabled], ['friend', 'playWithAFriend', hasOngoingRealTimeGame], - ['ai', 'playWithTheMachine', hasOngoingRealTimeGame], - ['local', 'Private Play', false], + //['ai', 'playWithTheMachine', hasOngoingRealTimeGame], + ['local', 'Private Play', hasOngoingRealTimeGame], ].map(([gameType, transKey, disabled]: [GameType, string, boolean]) => h( `button.button.button-metal.config_${gameType}`, From f56415ef628610570bfbc2432b8070740f784b3b Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Fri, 25 Aug 2023 09:21:59 -0500 Subject: [PATCH 032/174] grabbage --- ui/gameSetup/css/_game-setup.scss | 204 +++++++++++++++++++++++- ui/gameSetup/css/_local-setup.scss | 38 ----- ui/gameSetup/css/_setup.scss | 141 ---------------- ui/gameSetup/css/build/_game-setup.scss | 2 - ui/gameSetup/src/view/localContent.ts | 76 +++++++-- ui/lobby/src/view/table.ts | 2 +- 6 files changed, 262 insertions(+), 201 deletions(-) delete mode 100644 ui/gameSetup/css/_local-setup.scss delete mode 100644 ui/gameSetup/css/_setup.scss diff --git a/ui/gameSetup/css/_game-setup.scss b/ui/gameSetup/css/_game-setup.scss index 4893d3b2b7d67..0ea9f421b3f85 100644 --- a/ui/gameSetup/css/_game-setup.scss +++ b/ui/gameSetup/css/_game-setup.scss @@ -1,7 +1,150 @@ -.game-setup#modal-wrap { - @extend %setup; - width: 30em; +$c-setup: $c-secondary; +$c-slider: $c-setup; + +#modal-wrap { + &.game-setup { + width: 30em; + } + &.local-setup { + width: 60vw; + } text-align: center; + display: block; + + > div { + padding: 0; + max-height: 96vh; + } + + h2 { + margin: 1.5rem 0; + } + + .setup-content > div { + padding: 0.5em 1em; + } + + group.radio { + margin: 0 auto 1em auto; + width: 70%; + + .disabled { + opacity: 0.4; + cursor: default; + } + + input:checked + label { + background: $c-setup; + } + } + + .optional-config { + border-bottom: $border; + } + + .optional-config.disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .optional-config, + .ratings { + background: $c-bg-zebra; + border-top: $border; + } + + .mode-choice { + margin-top: 1em; + } + + .label-select { + @extend %flex-center; + + label { + flex: 0 0 33%; + text-align: right; + } + + select { + margin-#{$start-direction}: 0.8em; + font-weight: bold; + } + } + + .fen__form { + @extend %flex-center-nowrap; + } + + .fen__board { + display: block; + width: 50%; + margin: 0.5em auto 0 auto; + } + + #fen-input { + flex: 1 1 100%; + } + + .failure { + background: mix($c-bg-box, $c-bad, 80%); + box-shadow: 0 0 13px 6px mix($c-bg-box, $c-bad, 80%); + border-radius: 0.5em; + } + + .color-submits { + display: flex; + align-items: flex-end; + justify-content: center; + margin: 1em auto; + text-align: center; + + &__button { + margin: 0 0.5em; + width: 64px; + height: 64px; + padding: 7px; + + i { + display: block; + padding: 0; + width: 50px; + height: 50px; + background-size: 50px 50px; + } + + &.white i { + background-image: img-url('../piece/cburnett/wK.svg'); + } + + &.black i { + background-image: img-url('../piece/cburnett/bK.svg'); + } + + &.random { + width: 85px; + height: 85px; + padding: 10px; + + i { + background-image: img-url('wbK.svg'); + background-size: 65px 65px; + width: 65px; + height: 65px; + } + } + + &:disabled { + opacity: 0.3; + cursor: not-allowed; + } + } + + .spinner { + width: 85px; + height: 85px; + margin: 10px auto 20px auto; + } + } > div { padding: 0; @@ -51,4 +194,59 @@ margin-#{$end-direction}: 0.25em; } } + + #bot-select { + display: grid; + grid-template-areas: 'list' 'info'; + @include breakpoint($mq-xx-small) { + grid-template-columns: min-content 1fr; + grid-template-areas: 'list info'; + } + } + + #bot-list { + border: 1px solid $c-border; + grid-area: list; + display: grid; + width: 492px; + grid-template-columns: repeat(auto-fill, 160px); + grid-template-rows: repeat(auto-fill, 96px); + overflow-y: auto; + overflow-x: hidden; + height: 30vh; + } + div.libot { + width: 160px; + position: relative; + &:hover { + background: mix($c-link, $c-bg-box, 15%); + } + .selected { + background: mix($c-link, $c-bg-box, 30%); + } + img { + width: 96px; + } + div.label { + position: absolute; + white-space: nowrap; + left: 0; + right: 0; + bottom: 0; + font-size: 1.2em; + font-weight: bold; + text-align: center; + color: $c-font; + text-shadow: -2px 2px 2px black; + //background: transparent; + user-select: none; + } + } + + #bot-info { + grid-area: info; + display: flex; + flex-flow: column nowrap; + justify-content: space-between; + } } diff --git a/ui/gameSetup/css/_local-setup.scss b/ui/gameSetup/css/_local-setup.scss deleted file mode 100644 index f8a10abf4ce52..0000000000000 --- a/ui/gameSetup/css/_local-setup.scss +++ /dev/null @@ -1,38 +0,0 @@ -.local-setup#modal-wrap { - @extend %setup; - - #bot-select { - display: flex; - flex-flow: column nowrap; - overflow-y: auto; - - .libot { - display: flex; - flex-flow: row nowrap; - align-items: center; - - &:nth-child(even) { - background: $c-bg-zebra; - justify-content: space-between; - } - - &:hover { - background: mix($c-link, $c-bg-box, 15%); - } - - img { - flex: 0 0 32px; - display: block; - } - } - - /*span { - display: flex; - flex-flow: row nowrap; - align-items: center; - * { - margin-right: 6px; - } - }*/ - } -} diff --git a/ui/gameSetup/css/_setup.scss b/ui/gameSetup/css/_setup.scss deleted file mode 100644 index a41bd149810ba..0000000000000 --- a/ui/gameSetup/css/_setup.scss +++ /dev/null @@ -1,141 +0,0 @@ -$c-setup: $c-secondary; -$c-slider: $c-setup; - -%setup { - display: block; - - > div { - padding: 0; - max-height: 96vh; - } - - h2 { - margin: 1.5rem 0; - } - - .setup-content > div { - padding: 0.5em 1em; - } - - group.radio { - margin: 0 auto 1em auto; - width: 70%; - - .disabled { - opacity: 0.4; - cursor: default; - } - - input:checked + label { - background: $c-setup; - } - } - - .optional-config { - border-bottom: $border; - } - - .optional-config.disabled { - opacity: 0.5; - cursor: not-allowed; - } - - .optional-config, - .ratings { - background: $c-bg-zebra; - border-top: $border; - } - - .mode-choice { - margin-top: 1em; - } - - .label-select { - @extend %flex-center; - - label { - flex: 0 0 33%; - text-align: right; - } - - select { - margin-#{$start-direction}: 0.8em; - font-weight: bold; - } - } - - .fen__form { - @extend %flex-center-nowrap; - } - - .fen__board { - display: block; - width: 50%; - margin: 0.5em auto 0 auto; - } - - #fen-input { - flex: 1 1 100%; - } - - .failure { - background: mix($c-bg-box, $c-bad, 80%); - box-shadow: 0 0 13px 6px mix($c-bg-box, $c-bad, 80%); - border-radius: 0.5em; - } - - .color-submits { - display: flex; - align-items: flex-end; - justify-content: center; - margin: 1em auto; - text-align: center; - - &__button { - margin: 0 0.5em; - width: 64px; - height: 64px; - padding: 7px; - - i { - display: block; - padding: 0; - width: 50px; - height: 50px; - background-size: 50px 50px; - } - - &.white i { - background-image: img-url('../piece/cburnett/wK.svg'); - } - - &.black i { - background-image: img-url('../piece/cburnett/bK.svg'); - } - - &.random { - width: 85px; - height: 85px; - padding: 10px; - - i { - background-image: img-url('wbK.svg'); - background-size: 65px 65px; - width: 65px; - height: 65px; - } - } - - &:disabled { - opacity: 0.3; - cursor: not-allowed; - } - } - - .spinner { - width: 85px; - height: 85px; - margin: 10px auto 20px auto; - } - } -} diff --git a/ui/gameSetup/css/build/_game-setup.scss b/ui/gameSetup/css/build/_game-setup.scss index 30cc4a568e54e..340db309b8bd6 100644 --- a/ui/gameSetup/css/build/_game-setup.scss +++ b/ui/gameSetup/css/build/_game-setup.scss @@ -2,6 +2,4 @@ @import '../../../common/css/form/range'; @import '../../../common/css/form/radio'; @import '../../../common/css/component/modal'; -@import '../setup'; -@import '../local-setup'; @import '../game-setup'; diff --git a/ui/gameSetup/src/view/localContent.ts b/ui/gameSetup/src/view/localContent.ts index e30a6b63bd8ae..80c8ae152f225 100644 --- a/ui/gameSetup/src/view/localContent.ts +++ b/ui/gameSetup/src/view/localContent.ts @@ -1,5 +1,5 @@ import { h } from 'snabbdom'; -import { MaybeVNodes } from 'common/snabbdom'; +import { MaybeVNodes, onInsert } from 'common/snabbdom'; import { SetupCtrl } from '../ctrl'; import { fenInput } from './components/fenInput'; import { timePickerAndSliders } from './components/timePickerAndSliders'; @@ -7,33 +7,77 @@ import { colorButtons } from './components/colorButtons'; import { ratingView } from './components/ratingView'; import { localBots, type BotInfo } from 'libot'; +const botInfos = Object.values(localBots); export default function localContent(ctrl: SetupCtrl): MaybeVNodes { return [ h('h2', 'Local Play'), - h('div.setup-content', [ - botPicker(ctrl), - fenInput(ctrl), - timePickerAndSliders(ctrl, true), - colorButtons(ctrl), - ]), + h( + 'div.setup-content', + { + hook: onInsert(() => { + document.addEventListener('keydown', (e: KeyboardEvent) => { + console.log(e.key); + if (e.key === 'ArrowUp') select(ctrl, 'prev'); + else if (e.key === 'ArrowDown') select(ctrl, 'next'); + else return; + e.preventDefault(); + }); + }), + }, + [botPicker(ctrl), fenInput(ctrl), timePickerAndSliders(ctrl, true), colorButtons(ctrl)], + ), ratingView(ctrl), ]; } function botPicker(ctrl: SetupCtrl) { if (lichess.blindMode) return null; - return h( - 'div#bot-select', - {}, - Object.values(localBots).map(bot => botView(ctrl, bot)), - ); + return h('div#bot-select', [ + h( + 'div#bot-list', + botInfos.map(bot => botItem(ctrl, bot)), + ), + h('div#bot-info', botInfo(ctrl, botInfos[0])), + ]); } -function botView(ctrl: SetupCtrl, bot: BotInfo) { +function botInfo(ctrl: SetupCtrl, bot: BotInfo) { ctrl; - return h('div.libot', [ + return h('div', [ h('img', { attrs: { src: bot.image } }), - h('h3', bot.name), - h('p', bot.description), + h('div', [h('h2', bot.name), h('p', bot.description)]), ]); } + +function botItem(ctrl: SetupCtrl, bot: BotInfo) { + ctrl; + return h('div.libot', { hook: onInsert(el => el.addEventListener('click', () => select(ctrl, el))) }, [ + h('img', { attrs: { src: bot.image } }), + h('div.label', bot.name), + ]); +} + +function select(ctrl: SetupCtrl, el: Element | 'next' | 'prev') { + ctrl; + const $bots = $('.libot'); + const bots: EleLoose[] = Array.from($bots.get()); + const selectedIndex = bots.findIndex(b => b.classList.contains('selected')); + if (el === 'next') { + if (selectedIndex < bots.length - 1) { + bots[selectedIndex].classList.remove('selected'); + bots[selectedIndex + 1].classList.add('selected'); + } + } else if (el === 'prev') { + if (selectedIndex > 0) { + bots[selectedIndex].classList.remove('selected'); + bots[selectedIndex - 1].classList.add('selected'); + } + } else { + $('.libot').removeClass('selected'); + $(el).addClass('selected'); + } + const newIndex = bots.findIndex(b => b.classList.contains('selected')); + const bot = botInfos[newIndex]; + const info = document.querySelector('#bot-info'); + info?.firstElementChild?.replaceWith(botInfo(ctrl, bot).elm!); +} diff --git a/ui/lobby/src/view/table.ts b/ui/lobby/src/view/table.ts index 843128c0c21e1..3f5d91c3d6f9f 100644 --- a/ui/lobby/src/view/table.ts +++ b/ui/lobby/src/view/table.ts @@ -22,7 +22,7 @@ export default function table(ctrl: LobbyController) { ['hook', 'createAGame', hookDisabled], ['friend', 'playWithAFriend', hasOngoingRealTimeGame], //['ai', 'playWithTheMachine', hasOngoingRealTimeGame], - ['local', 'Private Play', hasOngoingRealTimeGame], + ['local', 'playWithTheMachine', hasOngoingRealTimeGame], ].map(([gameType, transKey, disabled]: [GameType, string, boolean]) => h( `button.button.button-metal.config_${gameType}`, From 1c8999a53dd0bc07c828d673e40cd668afe7d5cc Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Sat, 26 Aug 2023 16:37:29 -0500 Subject: [PATCH 033/174] gah --- ui/gameSetup/css/_game-setup.scss | 59 +-------------------------- ui/gameSetup/src/view/localContent.ts | 10 ++--- 2 files changed, 6 insertions(+), 63 deletions(-) diff --git a/ui/gameSetup/css/_game-setup.scss b/ui/gameSetup/css/_game-setup.scss index 0ea9f421b3f85..0aa14b9b8f28e 100644 --- a/ui/gameSetup/css/_game-setup.scss +++ b/ui/gameSetup/css/_game-setup.scss @@ -1,3 +1,4 @@ +@import 'bot-stuff'; $c-setup: $c-secondary; $c-slider: $c-setup; @@ -5,9 +6,6 @@ $c-slider: $c-setup; &.game-setup { width: 30em; } - &.local-setup { - width: 60vw; - } text-align: center; display: block; @@ -194,59 +192,4 @@ $c-slider: $c-setup; margin-#{$end-direction}: 0.25em; } } - - #bot-select { - display: grid; - grid-template-areas: 'list' 'info'; - @include breakpoint($mq-xx-small) { - grid-template-columns: min-content 1fr; - grid-template-areas: 'list info'; - } - } - - #bot-list { - border: 1px solid $c-border; - grid-area: list; - display: grid; - width: 492px; - grid-template-columns: repeat(auto-fill, 160px); - grid-template-rows: repeat(auto-fill, 96px); - overflow-y: auto; - overflow-x: hidden; - height: 30vh; - } - div.libot { - width: 160px; - position: relative; - &:hover { - background: mix($c-link, $c-bg-box, 15%); - } - .selected { - background: mix($c-link, $c-bg-box, 30%); - } - img { - width: 96px; - } - div.label { - position: absolute; - white-space: nowrap; - left: 0; - right: 0; - bottom: 0; - font-size: 1.2em; - font-weight: bold; - text-align: center; - color: $c-font; - text-shadow: -2px 2px 2px black; - //background: transparent; - user-select: none; - } - } - - #bot-info { - grid-area: info; - display: flex; - flex-flow: column nowrap; - justify-content: space-between; - } } diff --git a/ui/gameSetup/src/view/localContent.ts b/ui/gameSetup/src/view/localContent.ts index 80c8ae152f225..ef7303c136f0c 100644 --- a/ui/gameSetup/src/view/localContent.ts +++ b/ui/gameSetup/src/view/localContent.ts @@ -13,7 +13,7 @@ export default function localContent(ctrl: SetupCtrl): MaybeVNodes { h('h2', 'Local Play'), h( 'div.setup-content', - { + /*{ hook: onInsert(() => { document.addEventListener('keydown', (e: KeyboardEvent) => { console.log(e.key); @@ -23,18 +23,18 @@ export default function localContent(ctrl: SetupCtrl): MaybeVNodes { e.preventDefault(); }); }), - }, - [botPicker(ctrl), fenInput(ctrl), timePickerAndSliders(ctrl, true), colorButtons(ctrl)], + },*/ + [botSelector(ctrl), fenInput(ctrl), timePickerAndSliders(ctrl, true), colorButtons(ctrl)], ), ratingView(ctrl), ]; } -function botPicker(ctrl: SetupCtrl) { +function botSelector(ctrl: SetupCtrl) { if (lichess.blindMode) return null; return h('div#bot-select', [ h( - 'div#bot-list', + 'div#bot-carousel', botInfos.map(bot => botItem(ctrl, bot)), ), h('div#bot-info', botInfo(ctrl, botInfos[0])), From fec9b9dd7593adb93db2af65de7b5dd9555e433b Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Sun, 27 Aug 2023 11:08:56 -0500 Subject: [PATCH 034/174] build hack to disable delayed css caching --- ui/@build/src/esbuild.ts | 3 ++- ui/@build/src/main.ts | 9 ++++----- ui/site/src/component/assets.ts | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ui/@build/src/esbuild.ts b/ui/@build/src/esbuild.ts index a70edac9f9350..3a50d2abc307a 100644 --- a/ui/@build/src/esbuild.ts +++ b/ui/@build/src/esbuild.ts @@ -9,13 +9,14 @@ const typeBundles = new Map>(); export async function esbuild(): Promise { if (!env.esbuild) return; - const define = { + const define: { [_: string]: string } = { __info__: JSON.stringify({ date: new Date(new Date().toUTCString()).toISOString().split('.')[0] + '+00:00', commit: cps.execSync('git rev-parse -q HEAD', { encoding: 'utf-8' }).trim(), message: cps.execSync('git log -1 --pretty=%s', { encoding: 'utf-8' }).trim(), }), }; + if (env.dev) define.__dev__ = '{}'; for (const mod of buildModules) { preModule(mod); diff --git a/ui/@build/src/main.ts b/ui/@build/src/main.ts index 5fadc00897a38..5390a2f9c92b4 100644 --- a/ui/@build/src/main.ts +++ b/ui/@build/src/main.ts @@ -7,7 +7,7 @@ import { build, postBuild } from './build'; export function main() { const configPath = path.resolve(__dirname, '../build-config.json'); const config: BuildOpts = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, 'utf8')) : {}; - const oneDashArgs = ps.argv.filter(x => /^-([hpsw]+)$/.test(x))?.flatMap(x => x.slice(1).split('')); + const oneDashArgs = ps.argv.filter(x => /^-([hpdsw]+)$/.test(x))?.flatMap(x => x.slice(1).split('')); if (ps.argv.includes('--tsc') || ps.argv.includes('--sass') || ps.argv.includes('--esbuild')) { // cli args override json, including any of these flags sets those not present to false @@ -28,15 +28,13 @@ export function main() { env.watch = ps.argv.includes('--watch') || oneDashArgs.includes('w'); env.prod = ps.argv.includes('--prod') || oneDashArgs.includes('p'); env.split = ps.argv.includes('--split') || oneDashArgs.includes('s'); + env.dev = ps.argv.includes('--dev') || oneDashArgs.includes('d'); - if (env.prod && env.watch) { - env.error('You cannot watch prod builds! Think of the children'); - return; - } build(ps.argv.slice(2).filter(x => !x.startsWith('-'))); } export interface BuildOpts { + dev?: boolean; // dev bundle (super cache busting), default = false sass?: boolean; // compile scss, default = true esbuild?: boolean; // bundle with esbuild, default = true splitting?: boolean; // enable code splitting for esm modules, default = false @@ -115,6 +113,7 @@ class Env { watch = false; prod = false; split = false; + dev = false; exitCode = new Map<'sass' | 'tsc' | 'esbuild', number | false>(); startTime: number | undefined = Date.now(); diff --git a/ui/site/src/component/assets.ts b/ui/site/src/component/assets.ts index fc804891b1bc1..d0b7da95c9984 100644 --- a/ui/site/src/component/assets.ts +++ b/ui/site/src/component/assets.ts @@ -15,6 +15,7 @@ export const loadCss = (url: string, media?: 'dark' | 'light'): Promise => el.rel = 'stylesheet'; el.href = assetUrl(url); if (media) el.media = `(prefers-color-scheme: ${media})`; + if ((window as any).__dev__) url += '?_=' + Date.now(); loadedCss.set( url, new Promise(resolve => { @@ -35,8 +36,7 @@ export const loadCssPath = async (key: string): Promise => { ); if (theme === 'system') { if (supportsSystemTheme()) { - await load('dark', 'dark'); - await load('light', 'light'); + await Promise.all([load('dark', 'dark'), load('light', 'light')]); } else { await load('dark'); } From c63b0e650b6b43bad18d8ac5d1220e302c7ae41a Mon Sep 17 00:00:00 2001 From: Jonathan Gamble <101470903+schlawg@users.noreply.github.com> Date: Mon, 28 Aug 2023 09:09:44 -0500 Subject: [PATCH 035/174] gah --- ui/@build/src/esbuild.ts | 2 +- ui/@build/src/main.ts | 6 +- ui/@types/lichess/index.d.ts | 1 + ui/gameSetup/css/_game-setup.scss | 1 - ui/gameSetup/css/build/_game-setup.scss | 1 + ui/gameSetup/src/view/localContent.ts | 178 ++++++++++++++++-------- ui/gameSetup/tsconfig.json | 2 + ui/libot/src/main.ts | 114 ++++++++++++++- ui/site/src/component/assets.ts | 4 +- ui/site/src/site.lichess.globals.ts | 3 + 10 files changed, 245 insertions(+), 67 deletions(-) diff --git a/ui/@build/src/esbuild.ts b/ui/@build/src/esbuild.ts index 3a50d2abc307a..f9a610bb56a78 100644 --- a/ui/@build/src/esbuild.ts +++ b/ui/@build/src/esbuild.ts @@ -15,8 +15,8 @@ export async function esbuild(): Promise { commit: cps.execSync('git rev-parse -q HEAD', { encoding: 'utf-8' }).trim(), message: cps.execSync('git log -1 --pretty=%s', { encoding: 'utf-8' }).trim(), }), + __debug__: String(env.debug), }; - if (env.dev) define.__dev__ = '{}'; for (const mod of buildModules) { preModule(mod); diff --git a/ui/@build/src/main.ts b/ui/@build/src/main.ts index 5390a2f9c92b4..658253268603d 100644 --- a/ui/@build/src/main.ts +++ b/ui/@build/src/main.ts @@ -28,16 +28,14 @@ export function main() { env.watch = ps.argv.includes('--watch') || oneDashArgs.includes('w'); env.prod = ps.argv.includes('--prod') || oneDashArgs.includes('p'); env.split = ps.argv.includes('--split') || oneDashArgs.includes('s'); - env.dev = ps.argv.includes('--dev') || oneDashArgs.includes('d'); + env.debug = ps.argv.includes('--debug') || oneDashArgs.includes('d'); build(ps.argv.slice(2).filter(x => !x.startsWith('-'))); } export interface BuildOpts { - dev?: boolean; // dev bundle (super cache busting), default = false sass?: boolean; // compile scss, default = true esbuild?: boolean; // bundle with esbuild, default = true - splitting?: boolean; // enable code splitting for esm modules, default = false tsc?: boolean; // use tsc for type checking, default = true time?: boolean; // show time in log statements, default = true ctx?: boolean; // show context (tsc, rollup, etc), default = true @@ -113,7 +111,7 @@ class Env { watch = false; prod = false; split = false; - dev = false; + debug = false; exitCode = new Map<'sass' | 'tsc' | 'esbuild', number | false>(); startTime: number | undefined = Date.now(); diff --git a/ui/@types/lichess/index.d.ts b/ui/@types/lichess/index.d.ts index b77f53e150848..7219b75a12dac 100644 --- a/ui/@types/lichess/index.d.ts +++ b/ui/@types/lichess/index.d.ts @@ -4,6 +4,7 @@ interface Lichess { load: Promise; // DOMContentLoaded promise info: any; + debug: boolean; requestIdleCallback(f: () => void, timeout?: number): void; sri: string; storage: LichessStorageHelper; diff --git a/ui/gameSetup/css/_game-setup.scss b/ui/gameSetup/css/_game-setup.scss index 0aa14b9b8f28e..cce07bc7f480e 100644 --- a/ui/gameSetup/css/_game-setup.scss +++ b/ui/gameSetup/css/_game-setup.scss @@ -1,4 +1,3 @@ -@import 'bot-stuff'; $c-setup: $c-secondary; $c-slider: $c-setup; diff --git a/ui/gameSetup/css/build/_game-setup.scss b/ui/gameSetup/css/build/_game-setup.scss index 340db309b8bd6..3d8eb4c499fc0 100644 --- a/ui/gameSetup/css/build/_game-setup.scss +++ b/ui/gameSetup/css/build/_game-setup.scss @@ -3,3 +3,4 @@ @import '../../../common/css/form/radio'; @import '../../../common/css/component/modal'; @import '../game-setup'; +@import '../bot-stuff'; diff --git a/ui/gameSetup/src/view/localContent.ts b/ui/gameSetup/src/view/localContent.ts index ef7303c136f0c..9ef41936ed958 100644 --- a/ui/gameSetup/src/view/localContent.ts +++ b/ui/gameSetup/src/view/localContent.ts @@ -8,76 +8,140 @@ import { ratingView } from './components/ratingView'; import { localBots, type BotInfo } from 'libot'; const botInfos = Object.values(localBots); +let selector: HTMLDivElement; +const cards: HTMLDivElement[] = []; +let userMidX: number; +let userMidY: number; +let startAngle = 0, + startMag = 0, + handRotation: number = 0; +let selectedCard: HTMLDivElement | null = null; + export default function localContent(ctrl: SetupCtrl): MaybeVNodes { return [ h('h2', 'Local Play'), - h( - 'div.setup-content', - /*{ - hook: onInsert(() => { - document.addEventListener('keydown', (e: KeyboardEvent) => { - console.log(e.key); - if (e.key === 'ArrowUp') select(ctrl, 'prev'); - else if (e.key === 'ArrowDown') select(ctrl, 'next'); - else return; - e.preventDefault(); - }); + h('div.setup-content', [ + h('div#bot-selector', { + key: 'bot-selector', + hook: onInsert(el => { + selector = el as HTMLDivElement; + /*selector.addEventListener('click', () => { + console.log(selector.offsetWidth, selector.offsetHeight); + });*/ + botInfos.forEach(bot => createCard(bot)); + setTimeout(animate); }), - },*/ - [botSelector(ctrl), fenInput(ctrl), timePickerAndSliders(ctrl, true), colorButtons(ctrl)], - ), + }), + fenInput(ctrl), + timePickerAndSliders(ctrl, true), + colorButtons(ctrl), + ]), ratingView(ctrl), ]; } -function botSelector(ctrl: SetupCtrl) { - if (lichess.blindMode) return null; - return h('div#bot-select', [ - h( - 'div#bot-carousel', - botInfos.map(bot => botItem(ctrl, bot)), - ), - h('div#bot-info', botInfo(ctrl, botInfos[0])), - ]); +function createCard(bot: BotInfo) { + const card = document.createElement('div'); + card.classList.add('card'); + const img = document.createElement('img'); + img.src = bot.image; + card.appendChild(img); + card.addEventListener('pointerdown', startDrag); + card.addEventListener('pointermove', duringDrag); + card.addEventListener('pointerup', endDrag); + card.addEventListener('mouseenter', mouseEnter); + card.addEventListener('mouseleave', mouseLeave); + cards.push(card); + selector.appendChild(card); + return card; } -function botInfo(ctrl: SetupCtrl, bot: BotInfo) { - ctrl; - return h('div', [ - h('img', { attrs: { src: bot.image } }), - h('div', [h('h2', bot.name), h('p', bot.description)]), - ]); +function placeCards(index: number) { + const radius = selector.offsetWidth; + const containerHeight = selector.offsetHeight; + + userMidX = radius / 2; + userMidY = containerHeight + Math.sqrt(3) * userMidX; + + const beginAngle = -Math.PI / 8; + const visibleCards = cards.length; + const hovered = $as($('.pull')); + const hoveredIndex = cards.findIndex(x => x == hovered); + cards.forEach((card, cardIndex) => { + index; + const angleNudge = + !hovered || cardIndex == hoveredIndex + ? 0 + : cardIndex < hoveredIndex + ? 0 //(-Math.PI * 0.25) / visibleCards + : (Math.PI * 0.5) / visibleCards; + let angle = beginAngle + angleNudge + handRotation + ((Math.PI / 4) * (cardIndex + 0.5)) / visibleCards; + const mag = 15 + radius + ($(card).hasClass('pull') ? 40 : 0); + const x = userMidX + mag * Math.sin(angle) - 64; + const y = userMidY - mag * Math.cos(angle); + if (cardIndex === hoveredIndex) angle -= Math.PI / 8; + card.style.transform = `translate(${x}px, ${y}px) rotate(${angle}rad)`; + }); } -function botItem(ctrl: SetupCtrl, bot: BotInfo) { - ctrl; - return h('div.libot', { hook: onInsert(el => el.addEventListener('click', () => select(ctrl, el))) }, [ - h('img', { attrs: { src: bot.image } }), - h('div.label', bot.name), - ]); +function clientToOrigin(client: [number, number]): [number, number] { + const originX = client[0] - selector.offsetLeft - userMidX; + const originY = selector.offsetTop + userMidY - client[1]; + return [originX, originY]; } -function select(ctrl: SetupCtrl, el: Element | 'next' | 'prev') { - ctrl; - const $bots = $('.libot'); - const bots: EleLoose[] = Array.from($bots.get()); - const selectedIndex = bots.findIndex(b => b.classList.contains('selected')); - if (el === 'next') { - if (selectedIndex < bots.length - 1) { - bots[selectedIndex].classList.remove('selected'); - bots[selectedIndex + 1].classList.add('selected'); - } - } else if (el === 'prev') { - if (selectedIndex > 0) { - bots[selectedIndex].classList.remove('selected'); - bots[selectedIndex - 1].classList.add('selected'); - } - } else { - $('.libot').removeClass('selected'); - $(el).addClass('selected'); +function getAngle(client: [number, number]): number { + const translated = clientToOrigin(client); + return Math.atan2(translated[0], translated[1]); +} + +function getMag(client: [number, number]): number { + const userPt = clientToOrigin(client); + return Math.sqrt(userPt[0] * userPt[0] + userPt[1] * userPt[1]); +} + +function mouseEnter(e: MouseEvent) { + $(e.target as HTMLElement).addClass('pull'); +} + +function mouseLeave(e: MouseEvent) { + $(e.target as HTMLElement).removeClass('pull'); +} + +function startDrag(e: PointerEvent): void { + e.preventDefault(); + + startAngle = getAngle([e.clientX, e.clientY]); + startMag = getMag([e.clientX, e.clientY]); + + selectedCard = e.currentTarget as HTMLDivElement; + + selectedCard.setPointerCapture(e.pointerId); +} + +function duringDrag(e: PointerEvent): void { + e.preventDefault(); + if (!selectedCard) return; + + const newAngle = getAngle([e.clientX, e.clientY]); + + handRotation = newAngle - startAngle; + placeCards(0); +} + +function endDrag(e: PointerEvent): void { + if (selectedCard) { + selectedCard.releasePointerCapture(e.pointerId); } - const newIndex = bots.findIndex(b => b.classList.contains('selected')); - const bot = botInfos[newIndex]; - const info = document.querySelector('#bot-info'); - info?.firstElementChild?.replaceWith(botInfo(ctrl, bot).elm!); + selectedCard = null; +} + +function animate() { + requestAnimationFrame(animate); + placeCards(handRotation); } + +/*function botInfo(ctrl: SetupCtrl, bot: BotInfo) { + ctrl; + return h('div', [h('img', { attrs: { src: bot.image } }), h('div', [h('h2', bot.name), h('p', bot.description)])]); +}*/ diff --git a/ui/gameSetup/tsconfig.json b/ui/gameSetup/tsconfig.json index 811f17adcd8bb..8b9603d4210e4 100644 --- a/ui/gameSetup/tsconfig.json +++ b/ui/gameSetup/tsconfig.json @@ -5,6 +5,8 @@ "outDir": "dist", "esModuleInterop": true, "emitDeclarationOnly": true, + "noUnusedLocals": false, + "noUnusedParameters": false, "isolatedModules": true, "composite": true }, diff --git a/ui/libot/src/main.ts b/ui/libot/src/main.ts index 9d0cc7d886abd..9947b5fb241e1 100644 --- a/ui/libot/src/main.ts +++ b/ui/libot/src/main.ts @@ -18,8 +18,8 @@ export const localBots: { [key: string]: BotInfo } = { image: botImageUrl('baby-howard.webp'), }, babyBot: { - name: 'Baby Bot', - description: 'Baby Bot is a bot that plays random moves.', + name: 'Elsie Zero', + description: 'Elsie Zero is a bot that plays random moves.', image: botImageUrl('baby-robot.webp'), }, beatrice: { @@ -27,6 +27,116 @@ export const localBots: { [key: string]: BotInfo } = { description: 'Beatrice is a bot that plays random moves.', image: botImageUrl('beatrice.webp'), }, + benny: { + name: 'Benny', + description: '', + image: botImageUrl('benny.webp'), + }, + danny: { + name: 'Danny', + description: '', + image: botImageUrl('danny.webp'), + }, + dansby: { + name: 'Dansby', + description: '', + image: botImageUrl('dansby.webp'), + }, + gary: { + name: 'Gary', + description: '', + image: botImageUrl('gary.webp'), + }, + greta: { + name: 'Greta', + description: '', + image: botImageUrl('greta.webp'), + }, + grunt: { + name: 'Grunt', + description: '', + image: botImageUrl('grunt.webp'), + }, + helena: { + name: 'Helena', + description: '', + image: botImageUrl('helena.webp'), + }, + henry: { + name: 'Henry', + description: '', + image: botImageUrl('henry.webp'), + }, + larry: { + name: 'Larry', + description: '', + image: botImageUrl('larry.webp'), + }, + listress: { + name: 'Listress', + description: '', + image: botImageUrl('listress.webp'), + }, + louise: { + name: 'Louise', + description: '', + image: botImageUrl('louise.webp'), + }, + maia: { + name: 'Maia', + description: '', + image: botImageUrl('maia.webp'), + }, + marco: { + name: 'Marco', + description: '', + image: botImageUrl('marco.webp'), + }, + mitsoko: { + name: 'Mitsoko', + description: '', + image: botImageUrl('mitsoko.webp'), + }, + nacho: { + name: 'Nacho', + description: '', + image: botImageUrl('nacho.webp'), + }, + owen: { + name: 'Owen', + description: '', + image: botImageUrl('owen.webp'), + }, + shark: { + name: 'Shark', + description: '', + image: botImageUrl('shark.webp'), + }, + torso: { + name: 'Torso', + description: '', + image: botImageUrl('soldier-torso.webp'), + }, + ghost: { + name: 'Ghost', + description: '', + image: botImageUrl('specops-lady.webp'), + }, + terrence: { + name: 'Terrence', + description: '', + image: botImageUrl('terrence.webp'), + }, + agatha: { + name: 'Agatha', + description: '', + image: botImageUrl('witch1.webp'), + }, + sabine: { + name: 'Sabine', + description: '', + image: botImageUrl('witch2.webp'), + }, }; export function botNetUrl(weights: string) { diff --git a/ui/site/src/component/assets.ts b/ui/site/src/component/assets.ts index d0b7da95c9984..ee9a130b50e3c 100644 --- a/ui/site/src/component/assets.ts +++ b/ui/site/src/component/assets.ts @@ -13,9 +13,9 @@ export const loadCss = (url: string, media?: 'dark' | 'light'): Promise => if (!loadedCss.has(url)) { const el = document.createElement('link'); el.rel = 'stylesheet'; - el.href = assetUrl(url); + el.href = assetUrl(lichess.debug ? `${url}?_=${Date.now()}` : url); if (media) el.media = `(prefers-color-scheme: ${media})`; - if ((window as any).__dev__) url += '?_=' + Date.now(); + //console.log(url); loadedCss.set( url, new Promise(resolve => { diff --git a/ui/site/src/site.lichess.globals.ts b/ui/site/src/site.lichess.globals.ts index 3c9300e0a32a2..a4d623162b0e9 100644 --- a/ui/site/src/site.lichess.globals.ts +++ b/ui/site/src/site.lichess.globals.ts @@ -30,9 +30,12 @@ import { format as timeago, formatter as dateFormat } from './component/timeago' import watchers from './component/watchers'; import { Chessground } from 'chessground'; +declare const __debug__: boolean; + export default () => { window.$as = (cash: Cash) => cash[0] as T; const l = window.lichess; + l.debug = __debug__; l.StrongSocket = StrongSocket; l.mousetrap = new Mousetrap(document); l.requestIdleCallback = requestIdleCallback; From ac9eb12cd95c9806a70d9205e2325ec8d4ac2f19 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble <101470903+schlawg@users.noreply.github.com> Date: Mon, 28 Aug 2023 10:56:21 -0500 Subject: [PATCH 036/174] gah --- ui/gameSetup/css/_bot-stuff.scss | 86 ++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 ui/gameSetup/css/_bot-stuff.scss diff --git a/ui/gameSetup/css/_bot-stuff.scss b/ui/gameSetup/css/_bot-stuff.scss new file mode 100644 index 0000000000000..7f2e4c04a16b0 --- /dev/null +++ b/ui/gameSetup/css/_bot-stuff.scss @@ -0,0 +1,86 @@ +#modal-wrap.local-setup { + width: 96vw; + @include breakpoint($mq-x-small) { + width: 80vw; + } + @include breakpoint($mq-large) { + width: 60vw; + } + #bot-select { + display: grid; + grid-template-areas: 'list' 'info'; + @include breakpoint($mq-xx-small) { + grid-template-columns: min-content 1fr; + grid-template-areas: 'list info'; + } + } + + #bot-carousel { + .bot-grid { + border: 1px solid $c-border; + grid-area: list; + display: grid; + width: 492px; + grid-template-columns: repeat(auto-fill, 104px); + grid-template-rows: repeat(auto-fill, 96px); + overflow-y: auto; + overflow-x: hidden; + height: 30vh; + } + div.libot { + width: 104px; + position: relative; + &:hover { + background: mix($c-link, $c-bg-box, 15%); + } + .selected { + background: mix($c-link, $c-bg-box, 30%); + } + img { + width: 96px; + background: tranparent; + } + div.label { + position: absolute; + white-space: nowrap; + left: 0; + right: 0; + bottom: 0; + font-size: 1.2em; + font-weight: bold; + text-align: center; + color: $c-font; + text-shadow: -2px 2px 2px black; + //background: transparent; + user-select: none; + } + } + } + + #bot-info { + grid-area: info; + display: flex; + flex-flow: column nowrap; + justify-content: space-between; + } + + #bot-selector { + position: relative; + width: 100%; + height: 200px; + overflow: hidden; + } + + .card { + position: absolute; + width: 128px; + height: 128px; + border-radius: 6px; + background-color: white; + border: 1px solid black; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); + img { + width: 128px; + } + } +} From 2c143f9f9ec8236ca650b1cb8c46acb6feb746fa Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Mon, 28 Aug 2023 19:49:15 -0500 Subject: [PATCH 037/174] gah --- ui/gameSetup/css/_bot-stuff.scss | 25 +++++++++++++++++++++++-- ui/gameSetup/src/view/localContent.ts | 7 +++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/ui/gameSetup/css/_bot-stuff.scss b/ui/gameSetup/css/_bot-stuff.scss index 7f2e4c04a16b0..da3573e24955a 100644 --- a/ui/gameSetup/css/_bot-stuff.scss +++ b/ui/gameSetup/css/_bot-stuff.scss @@ -1,10 +1,19 @@ #modal-wrap.local-setup { + display: flex; width: 96vw; + height: min(96vh, 640px); + > div { + display: flex; + flex-grow: 1; + flex-flow: column nowrap; + } @include breakpoint($mq-x-small) { width: 80vw; + height: min(88vh, 640px); } @include breakpoint($mq-large) { width: 60vw; + height: min(80vh, 640px); } #bot-select { display: grid; @@ -64,17 +73,29 @@ justify-content: space-between; } + h2 { + flex: initial; + } + #bot-view { + @extend %flex-column; + min-height: 320px; + flex-grow: 1; + > div { + flex-shrink: 0; + } + } + #bot-selector { position: relative; width: 100%; - height: 200px; + flex-grow: 1; overflow: hidden; } .card { position: absolute; width: 128px; - height: 128px; + height: 192px; border-radius: 6px; background-color: white; border: 1px solid black; diff --git a/ui/gameSetup/src/view/localContent.ts b/ui/gameSetup/src/view/localContent.ts index 9ef41936ed958..8d40e6b80dfbf 100644 --- a/ui/gameSetup/src/view/localContent.ts +++ b/ui/gameSetup/src/view/localContent.ts @@ -19,8 +19,8 @@ let selectedCard: HTMLDivElement | null = null; export default function localContent(ctrl: SetupCtrl): MaybeVNodes { return [ - h('h2', 'Local Play'), - h('div.setup-content', [ + h('h2', 'Arcade Mode'), + h('div#bot-view', [ h('div#bot-selector', { key: 'bot-selector', hook: onInsert(el => { @@ -36,7 +36,6 @@ export default function localContent(ctrl: SetupCtrl): MaybeVNodes { timePickerAndSliders(ctrl, true), colorButtons(ctrl), ]), - ratingView(ctrl), ]; } @@ -111,7 +110,7 @@ function mouseLeave(e: MouseEvent) { function startDrag(e: PointerEvent): void { e.preventDefault(); - startAngle = getAngle([e.clientX, e.clientY]); + startAngle = getAngle([e.clientX, e.clientY]) - handRotation; startMag = getMag([e.clientX, e.clientY]); selectedCard = e.currentTarget as HTMLDivElement; From ee859d95f0616203ccfbf254c4e413ac11865733 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble <101470903+schlawg@users.noreply.github.com> Date: Mon, 28 Aug 2023 21:05:28 -0500 Subject: [PATCH 038/174] gah --- ui/gameSetup/css/_bot-stuff.scss | 13 +++++++++++++ ui/gameSetup/src/view/localContent.ts | 5 ++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/ui/gameSetup/css/_bot-stuff.scss b/ui/gameSetup/css/_bot-stuff.scss index da3573e24955a..83a0122882775 100644 --- a/ui/gameSetup/css/_bot-stuff.scss +++ b/ui/gameSetup/css/_bot-stuff.scss @@ -103,5 +103,18 @@ img { width: 128px; } + label { + font-weight: bold; + font-size: 1.3em; + text-align: center; + position: absolute; + top: -1.5em; + left: 0; + right: 0; + display: none; + } + &.pull label { + display: block; + } } } diff --git a/ui/gameSetup/src/view/localContent.ts b/ui/gameSetup/src/view/localContent.ts index 8d40e6b80dfbf..d52dfcb12947a 100644 --- a/ui/gameSetup/src/view/localContent.ts +++ b/ui/gameSetup/src/view/localContent.ts @@ -19,7 +19,7 @@ let selectedCard: HTMLDivElement | null = null; export default function localContent(ctrl: SetupCtrl): MaybeVNodes { return [ - h('h2', 'Arcade Mode'), + h('h2', 'Select Opponent'), h('div#bot-view', [ h('div#bot-selector', { key: 'bot-selector', @@ -44,6 +44,9 @@ function createCard(bot: BotInfo) { card.classList.add('card'); const img = document.createElement('img'); img.src = bot.image; + const label = document.createElement('label'); + label.innerText = bot.name; + card.appendChild(label); card.appendChild(img); card.addEventListener('pointerdown', startDrag); card.addEventListener('pointermove', duringDrag); From 22bda1804de710f9a2878c26b056076f5dd439b2 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Tue, 29 Aug 2023 20:25:59 -0500 Subject: [PATCH 039/174] gah --- ui/round/src/plugins/nvui.ts | 3 ++- ui/round/src/socket.ts | 3 +-- ui/round/src/sound.ts | 8 -------- 3 files changed, 3 insertions(+), 11 deletions(-) delete mode 100644 ui/round/src/sound.ts diff --git a/ui/round/src/plugins/nvui.ts b/ui/round/src/plugins/nvui.ts index 5ea235b647d54..d46c2b9693797 100644 --- a/ui/round/src/plugins/nvui.ts +++ b/ui/round/src/plugins/nvui.ts @@ -1,6 +1,7 @@ import { h, VNode } from 'snabbdom'; import RoundController from '../ctrl'; import { renderClock } from '../clock/clockView'; +import throttle from 'common/throttle'; import { renderTableWatch, renderTablePlay, renderTableEnd } from '../view/table'; import { makeConfig as makeCgConfig } from '../ground'; import renderCorresClock from '../corresClock/corresClockView'; @@ -33,8 +34,8 @@ import { import { renderSetting } from 'nvui/setting'; import { Notify } from 'nvui/notify'; import { commands } from 'nvui/command'; -import { throttled } from '../sound'; +const throttled = (sound: string) => throttle(100, () => lichess.sound.play(sound)); const selectSound = throttled('select'); const borderSound = throttled('outOfBound'); const errorSound = throttled('error'); diff --git a/ui/round/src/socket.ts b/ui/round/src/socket.ts index fb4bd8e486e66..9f1abec896323 100644 --- a/ui/round/src/socket.ts +++ b/ui/round/src/socket.ts @@ -2,7 +2,6 @@ import * as game from 'game'; import throttle from 'common/throttle'; import modal from 'common/modal'; import * as xhr from './xhr'; -import * as sound from './sound'; import RoundController from './ctrl'; import { defined } from 'common'; @@ -141,7 +140,7 @@ export function make(send: SocketSend, ctrl: RoundController): RoundSocket { !game.isPlayerTurn(ctrl.data) ) { ctrl.setRedirecting(); - sound.move(); + lichess.sound.move(); location.href = '/' + gameId; } }, diff --git a/ui/round/src/sound.ts b/ui/round/src/sound.ts deleted file mode 100644 index b50c2075ff3ac..0000000000000 --- a/ui/round/src/sound.ts +++ /dev/null @@ -1,8 +0,0 @@ -import throttle from 'common/throttle'; - -export const throttled = (sound: string) => throttle(100, () => lichess.sound.play(sound)); - -export const move = throttled('move'); -export const capture = throttled('capture'); -export const check = throttled('check'); -export const explode = throttled('explosion'); From b9c631fae8b9b70b0941b492c166c326ec0a3384 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble <101470903+schlawg@users.noreply.github.com> Date: Wed, 30 Aug 2023 21:49:56 -0500 Subject: [PATCH 040/174] . --- ui/gameSetup/css/_bot-stuff.scss | 6 +- ui/gameSetup/src/view/localContent.ts | 223 +++++++++++++------------- 2 files changed, 115 insertions(+), 114 deletions(-) diff --git a/ui/gameSetup/css/_bot-stuff.scss b/ui/gameSetup/css/_bot-stuff.scss index 83a0122882775..eebf1012be249 100644 --- a/ui/gameSetup/css/_bot-stuff.scss +++ b/ui/gameSetup/css/_bot-stuff.scss @@ -76,16 +76,16 @@ h2 { flex: initial; } - #bot-view { + /*#bot-view { @extend %flex-column; min-height: 320px; flex-grow: 1; > div { flex-shrink: 0; } - } + }*/ - #bot-selector { + #bot-view { position: relative; width: 100%; flex-grow: 1; diff --git a/ui/gameSetup/src/view/localContent.ts b/ui/gameSetup/src/view/localContent.ts index d52dfcb12947a..7bced0997da55 100644 --- a/ui/gameSetup/src/view/localContent.ts +++ b/ui/gameSetup/src/view/localContent.ts @@ -7,142 +7,143 @@ import { colorButtons } from './components/colorButtons'; import { ratingView } from './components/ratingView'; import { localBots, type BotInfo } from 'libot'; -const botInfos = Object.values(localBots); -let selector: HTMLDivElement; -const cards: HTMLDivElement[] = []; -let userMidX: number; -let userMidY: number; -let startAngle = 0, - startMag = 0, - handRotation: number = 0; -let selectedCard: HTMLDivElement | null = null; +let deck: BotDeck; export default function localContent(ctrl: SetupCtrl): MaybeVNodes { return [ h('h2', 'Select Opponent'), - h('div#bot-view', [ - h('div#bot-selector', { - key: 'bot-selector', - hook: onInsert(el => { - selector = el as HTMLDivElement; - /*selector.addEventListener('click', () => { - console.log(selector.offsetWidth, selector.offsetHeight); - });*/ - botInfos.forEach(bot => createCard(bot)); - setTimeout(animate); - }), + //h('div#bot-view', [ + h('div#bot-view', { + key: 'bot-view', + hook: onInsert(el => { + deck = new BotDeck(el as HTMLDivElement); }), - fenInput(ctrl), - timePickerAndSliders(ctrl, true), - colorButtons(ctrl), - ]), + }), + fenInput(ctrl), + timePickerAndSliders(ctrl, true), + colorButtons(ctrl), + //]), ]; } -function createCard(bot: BotInfo) { - const card = document.createElement('div'); - card.classList.add('card'); - const img = document.createElement('img'); - img.src = bot.image; - const label = document.createElement('label'); - label.innerText = bot.name; - card.appendChild(label); - card.appendChild(img); - card.addEventListener('pointerdown', startDrag); - card.addEventListener('pointermove', duringDrag); - card.addEventListener('pointerup', endDrag); - card.addEventListener('mouseenter', mouseEnter); - card.addEventListener('mouseleave', mouseLeave); - cards.push(card); - selector.appendChild(card); - return card; -} +class BotDeck { + constructor(readonly view: HTMLDivElement) { + this.botInfos.forEach(bot => this.createCard(bot)); + this.animate(); + } + botInfos = Object.values(localBots); + cards: HTMLDivElement[] = []; + userMidX: number; + userMidY: number; + startAngle = 0; + startMag = 0; + handRotation: number = 0; + selectedCard: HTMLDivElement | null = null; + + createCard(bot: BotInfo) { + const card = document.createElement('div'); + card.classList.add('card'); + const img = document.createElement('img'); + img.src = bot.image; + const label = document.createElement('label'); + label.innerText = bot.name; + card.appendChild(label); + card.appendChild(img); + card.addEventListener('pointerdown', e => this.startDrag(e)); + card.addEventListener('pointermove', e => this.duringDrag(e)); + card.addEventListener('pointerup', e => this.endDrag(e)); + card.addEventListener('mouseenter', e => this.mouseEnter(e)); + card.addEventListener('mouseleave', e => this.mouseLeave(e)); + this.cards.push(card); + this.view.appendChild(card); + return card; + } -function placeCards(index: number) { - const radius = selector.offsetWidth; - const containerHeight = selector.offsetHeight; - - userMidX = radius / 2; - userMidY = containerHeight + Math.sqrt(3) * userMidX; - - const beginAngle = -Math.PI / 8; - const visibleCards = cards.length; - const hovered = $as($('.pull')); - const hoveredIndex = cards.findIndex(x => x == hovered); - cards.forEach((card, cardIndex) => { - index; - const angleNudge = - !hovered || cardIndex == hoveredIndex - ? 0 - : cardIndex < hoveredIndex - ? 0 //(-Math.PI * 0.25) / visibleCards - : (Math.PI * 0.5) / visibleCards; - let angle = beginAngle + angleNudge + handRotation + ((Math.PI / 4) * (cardIndex + 0.5)) / visibleCards; - const mag = 15 + radius + ($(card).hasClass('pull') ? 40 : 0); - const x = userMidX + mag * Math.sin(angle) - 64; - const y = userMidY - mag * Math.cos(angle); - if (cardIndex === hoveredIndex) angle -= Math.PI / 8; - card.style.transform = `translate(${x}px, ${y}px) rotate(${angle}rad)`; - }); -} + placeCards() { + const radius = this.view.offsetWidth; + const containerHeight = this.view.offsetHeight; + + this.userMidX = radius / 2; + this.userMidY = containerHeight + Math.sqrt(3) * this.userMidX; + + const beginAngle = -Math.PI / 8; + const visibleCards = this.cards.length; + const hovered = $as($('.card.pull')); + const hoveredIndex = this.cards.findIndex(x => x == hovered); + this.cards.forEach((card, cardIndex) => { + const angleNudge = + !hovered || cardIndex == hoveredIndex + ? 0 + : cardIndex < hoveredIndex + ? 0 //(-Math.PI * 0.25) / visibleCards + : (Math.PI * 0.5) / visibleCards; + let angle = + beginAngle + angleNudge + this.handRotation + ((Math.PI / 4) * (cardIndex + 0.5)) / visibleCards; + const mag = 15 + radius + ($(card).hasClass('pull') ? 40 : 0); + const x = this.userMidX + mag * Math.sin(angle) - 64; + const y = this.userMidY - mag * Math.cos(angle); + if (cardIndex === hoveredIndex) angle -= Math.PI / 8; + card.style.transform = `translate(${x}px, ${y}px) rotate(${angle}rad)`; + }); + } -function clientToOrigin(client: [number, number]): [number, number] { - const originX = client[0] - selector.offsetLeft - userMidX; - const originY = selector.offsetTop + userMidY - client[1]; - return [originX, originY]; -} + clientToOrigin(client: [number, number]): [number, number] { + const originX = client[0] - this.view.offsetLeft - this.userMidX; + const originY = this.view.offsetTop + this.userMidY - client[1]; + return [originX, originY]; + } -function getAngle(client: [number, number]): number { - const translated = clientToOrigin(client); - return Math.atan2(translated[0], translated[1]); -} + getAngle(client: [number, number]): number { + const translated = this.clientToOrigin(client); + return Math.atan2(translated[0], translated[1]); + } -function getMag(client: [number, number]): number { - const userPt = clientToOrigin(client); - return Math.sqrt(userPt[0] * userPt[0] + userPt[1] * userPt[1]); -} + getMag(client: [number, number]): number { + const userPt = this.clientToOrigin(client); + return Math.sqrt(userPt[0] * userPt[0] + userPt[1] * userPt[1]); + } -function mouseEnter(e: MouseEvent) { - $(e.target as HTMLElement).addClass('pull'); -} + mouseEnter(e: MouseEvent) { + $(e.target as HTMLElement).addClass('pull'); + } -function mouseLeave(e: MouseEvent) { - $(e.target as HTMLElement).removeClass('pull'); -} + mouseLeave(e: MouseEvent) { + $(e.target as HTMLElement).removeClass('pull'); + } -function startDrag(e: PointerEvent): void { - e.preventDefault(); + startDrag(e: PointerEvent): void { + e.preventDefault(); - startAngle = getAngle([e.clientX, e.clientY]) - handRotation; - startMag = getMag([e.clientX, e.clientY]); + this.startAngle = this.getAngle([e.clientX, e.clientY]) - this.handRotation; + this.startMag = this.getMag([e.clientX, e.clientY]); - selectedCard = e.currentTarget as HTMLDivElement; + this.selectedCard = e.currentTarget as HTMLDivElement; - selectedCard.setPointerCapture(e.pointerId); -} + this.selectedCard.setPointerCapture(e.pointerId); + } -function duringDrag(e: PointerEvent): void { - e.preventDefault(); - if (!selectedCard) return; + duringDrag(e: PointerEvent): void { + e.preventDefault(); + if (!this.selectedCard) return; - const newAngle = getAngle([e.clientX, e.clientY]); + const newAngle = this.getAngle([e.clientX, e.clientY]); - handRotation = newAngle - startAngle; - placeCards(0); -} + this.handRotation = newAngle - this.startAngle; + this.placeCards(); + } -function endDrag(e: PointerEvent): void { - if (selectedCard) { - selectedCard.releasePointerCapture(e.pointerId); + endDrag(e: PointerEvent): void { + if (this.selectedCard) { + this.selectedCard.releasePointerCapture(e.pointerId); + } + this.selectedCard = null; } - selectedCard = null; -} -function animate() { - requestAnimationFrame(animate); - placeCards(handRotation); + animate() { + requestAnimationFrame(() => this.animate()); + this.placeCards(); + } } - /*function botInfo(ctrl: SetupCtrl, bot: BotInfo) { ctrl; return h('div', [h('img', { attrs: { src: bot.image } }), h('div', [h('h2', bot.name), h('p', bot.description)])]); From d5ccd1524cf973dbe9868cefdfcf1327f6470f9d Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Tue, 29 Aug 2023 12:21:50 -0500 Subject: [PATCH 041/174] maybe they wont notice yellow ones --- app/controllers/Main.scala | 4 +- app/views/analyse/jsI18n.scala | 1 + app/views/board/userAnalysisI18n.scala | 1 + app/views/site/help.scala | 33 +++++++------- conf/routes | 2 +- modules/i18n/src/main/I18nKeys.scala | 1 + pnpm-lock.yaml | 2 +- translation/source/site.xml | 1 + ui/analyse/css/_keyboard.scss | 10 +++++ ui/analyse/src/autoShape.ts | 17 ++++--- ui/analyse/src/ctrl.ts | 31 ++++++++++--- ui/analyse/src/keyboard.ts | 43 +++++++++--------- .../src/study/practice/studyPracticeCtrl.ts | 1 + ui/analyse/src/view/actionMenu.ts | 36 ++++++++++----- ui/common/css/abstract/_mixins.scss | 29 ++++++++++++ ui/common/css/component/_help.scss | 44 +++++++------------ ui/common/css/component/_modal.scss | 29 +----------- ui/common/src/dialog.ts | 40 +++++++++++++++++ 18 files changed, 200 insertions(+), 125 deletions(-) create mode 100644 ui/common/src/dialog.ts diff --git a/app/controllers/Main.scala b/app/controllers/Main.scala index 098c1b6519998..a4ebf0b90d433 100644 --- a/app/controllers/Main.scala +++ b/app/controllers/Main.scala @@ -114,8 +114,8 @@ final class Main( pageHit NotImplemented.page(html.site.message.temporarilyDisabled) - def analyseShiftKeyHelp = Open: - Ok.page(html.site.help.analyseShiftKey) + def analyseVariationArrowHelp = Open: + Ok.page(html.site.help.analyseVariationArrow) def keyboardMoveHelp = Open: Ok.page(html.site.help.keyboardMove) diff --git a/app/views/analyse/jsI18n.scala b/app/views/analyse/jsI18n.scala index 70c1cd3f1c9b5..36feda9bb6af5 100644 --- a/app/views/analyse/jsI18n.scala +++ b/app/views/analyse/jsI18n.scala @@ -63,6 +63,7 @@ object jsI18n: trans.computerAnalysis, trans.enable, trans.bestMoveArrow, + trans.showVariationArrows, trans.evaluationGauge, trans.infiniteAnalysis, trans.removesTheDepthLimit, diff --git a/app/views/board/userAnalysisI18n.scala b/app/views/board/userAnalysisI18n.scala index b9a2ec4d2ecc2..2593e95a4129f 100644 --- a/app/views/board/userAnalysisI18n.scala +++ b/app/views/board/userAnalysisI18n.scala @@ -130,6 +130,7 @@ object userAnalysisI18n: trans.computerAnalysis, trans.enable, trans.bestMoveArrow, + trans.showVariationArrows, trans.evaluationGauge, trans.infiniteAnalysis, trans.removesTheDepthLimit, diff --git a/app/views/site/help.scala b/app/views/site/help.scala index 4b73b2b5f68f8..e2fa9eb4c53bd 100644 --- a/app/views/site/help.scala +++ b/app/views/site/help.scala @@ -9,7 +9,7 @@ object help: private def header(text: Frag) = tr(th(colspan := 2)(p(text))) private def row(keys: Frag, desc: Frag) = tr(td(cls := "keys")(keys), td(cls := "desc")(desc)) - private val or = tag("or") + private val or = tag("or")("/") private val kbd = tag("kbd") private def voice(text: String) = strong(cls := "val-to-word", text) private def phonetic(text: String) = strong(cls := "val-to-word phonetic", text) @@ -66,8 +66,8 @@ object help: table( tbody( navigateMoves, - row(kbd("shift"), "Cycle selected move arrow"), - row(frag(kbd("shift"), kbd("←"), or, kbd("shift"), kbd("J")), "Rewind to mainline"), + row(frag(kbd("↑"), or, kbd("↓")), "Cycle selected variation"), + row(frag(kbd("shift"), kbd("←"), or, kbd("shift"), kbd("K")), "Rewind to mainline"), header(trans.analysisOptions()), flip, row(frag(kbd("shift"), kbd("I")), trans.inlineNotation()), @@ -106,20 +106,19 @@ object help: ) ) - def analyseShiftKey(using Lang) = - frag( - div(cls := "help-ephemeral")( - ul( - li("Purple arrow is mainline move"), - li("Pink arrows are variations"), - li("Blue arrow is eval best move") - ), - table( - tbody( - row(kbd("shift"), "cycle selected move arrow"), - row(kbd("→"), "play selected move"), - row(span(kbd("shift"), or, kbd("←")), "return to previous mainline move") - ) + def analyseVariationArrow(using Lang) = + div( + p("Variation arrows allow navigation without using the move list."), + p( + "The '", + strong("Show variation arrows"), + "' toggle in the hamburger menu turns them off." + ), + table( + tbody( + row(frag(kbd("↑"), or, kbd("↓")), "Cycle selected variation"), + row(kbd("→"), "play selected move"), + row(span(kbd("shift"), kbd("←")), "return to previous mainline move") ) ) ) diff --git a/conf/routes b/conf/routes index c40ad50b9a8f4..4e27589b7a94a 100644 --- a/conf/routes +++ b/conf/routes @@ -849,7 +849,7 @@ GET /variant/:key controllers.ContentPage.variant(key) # Help GET /help/contribute controllers.ContentPage.help GET /help/master controllers.ContentPage.master -GET /help/analyse/shift-key controllers.Main.analyseShiftKeyHelp +GET /help/analyse/variation-arrow controllers.Main.analyseVariationArrowHelp GET /help/keyboard-move controllers.Main.keyboardMoveHelp GET /help/voice/:module controllers.Main.voiceHelp(module) # DGT diff --git a/modules/i18n/src/main/I18nKeys.scala b/modules/i18n/src/main/I18nKeys.scala index f23d1739f8f66..c35933047e837 100644 --- a/modules/i18n/src/main/I18nKeys.scala +++ b/modules/i18n/src/main/I18nKeys.scala @@ -110,6 +110,7 @@ object I18nKeys: val `openStudy` = I18nKey("openStudy") val `enable` = I18nKey("enable") val `bestMoveArrow` = I18nKey("bestMoveArrow") + val `showVariationArrows` = I18nKey("showVariationArrows") val `evaluationGauge` = I18nKey("evaluationGauge") val `multipleLines` = I18nKey("multipleLines") val `cpus` = I18nKey("cpus") diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d6821bd4ff21..68c4f7b3a8c1a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' settings: autoInstallPeers: true diff --git a/translation/source/site.xml b/translation/source/site.xml index 481fda8a9fb03..3e5d9a1c09aee 100644 --- a/translation/source/site.xml +++ b/translation/source/site.xml @@ -115,6 +115,7 @@ Open study Enable Best move arrow + Show variation arrows Evaluation gauge Multiple lines CPUs diff --git a/ui/analyse/css/_keyboard.scss b/ui/analyse/css/_keyboard.scss index e5cf3f04ac610..d463b3f52cbf5 100644 --- a/ui/analyse/css/_keyboard.scss +++ b/ui/analyse/css/_keyboard.scss @@ -6,3 +6,13 @@ margin-#{$start-direction}: 1em; } } + +.variation-arrow-help { + @extend %help-dialog; + > div { + min-height: 1px; // safari + align-items: center; + text-align: center; + overflow-y: auto; + } +} diff --git a/ui/analyse/src/autoShape.ts b/ui/analyse/src/autoShape.ts index ae2470694b895..88952a54cd600 100644 --- a/ui/analyse/src/autoShape.ts +++ b/ui/analyse/src/autoShape.ts @@ -74,8 +74,11 @@ export function compute(ctrl: AnalyseCtrl): DrawShape[] { }); } ctrl.fork.hover(hovering?.uci); + if (hovering?.fen === nFen) shapes = shapes.concat(makeShapesFromUci(color, hovering.uci, 'paleBlue')); + if (ctrl.showAutoShapes() && ctrl.showComputer()) { - if (nEval.best) shapes = shapes.concat(makeShapesFromUci(rcolor, nEval.best, 'paleGreen')); + if (nEval.best && !ctrl.showVariationArrows()) + shapes = shapes.concat(makeShapesFromUci(rcolor, nEval.best, 'paleGreen')); if (!hovering && instance.multiPv()) { const nextBest = instance.enabled() && nCeval ? nCeval.pvs[0].moves[0] : ctrl.nextNodeBest(); if (nextBest) shapes = shapes.concat(makeShapesFromUci(color, nextBest, 'paleBlue', undefined)); @@ -116,26 +119,22 @@ export function compute(ctrl: AnalyseCtrl): DrawShape[] { }); } shapes = shapes.concat(annotationShapes(ctrl)); - - if (ctrl.showAutoShapes() && ctrl.node.children.length > 1) { + if (ctrl.showVariationArrows()) { ctrl.node.children.forEach((node, i) => { const existing = shapes.find(s => s.orig === node.uci!.slice(0, 2) && s.dest === node.uci!.slice(2, 4)); - const symbol = node.glyphs?.[0]?.symbol; if (existing) { - existing.brush = i === 0 ? 'purple' : existing.brush; if (i === ctrl.fork.selected()) { existing.modifiers ??= {}; existing.modifiers.hilite = true; } - if (symbol) existing.label = { text: symbol, fill: glyphColors[symbol] }; - } else + } else { shapes.push({ orig: node.uci!.slice(0, 2) as Key, dest: node.uci?.slice(2, 4) as Key, - brush: i === 0 ? 'purple' : 'pink', + brush: 'yellow', modifiers: { hilite: i === ctrl.fork.selected() }, - label: symbol ? { text: symbol, fill: glyphColors[symbol] } : undefined, }); + } }); } return shapes; diff --git a/ui/analyse/src/ctrl.ts b/ui/analyse/src/ctrl.ts index 19b4d75e70c4a..4decce87c52a6 100644 --- a/ui/analyse/src/ctrl.ts +++ b/ui/analyse/src/ctrl.ts @@ -7,6 +7,7 @@ import { plural } from './view/util'; import debounce from 'common/debounce'; import GamebookPlayCtrl from './study/gamebook/gamebookPlayCtrl'; import type makeStudyCtrl from './study/studyCtrl'; +import { isTouchDevice } from 'common/mobile'; import throttle from 'common/throttle'; import { AnalyseOpts, @@ -95,6 +96,7 @@ export default class AnalyseCtrl { flipped = false; showComments = true; // whether to display comments in the move tree showAutoShapes = storedBooleanProp('analyse.show-auto-shapes', true); + variationArrowsProp = storedBooleanProp('analyse.show-variation-arrows', true); showGauge = storedBooleanProp('analyse.show-gauge', true); showComputer = storedBooleanProp('analyse.show-computer', true); showMoveAnnotation = storedBooleanProp('analyse.show-move-annotation', true); @@ -609,10 +611,10 @@ export default class AnalyseCtrl { return treeOps.withMainlineChild(this.node, (n: Tree.Node) => n.eval?.best); } - setAutoShapes = (): void => { + setAutoShapes() { this.withCg(cg => cg.setAutoShapes(computeAutoShapes(this))); - if (this.node.children.length > 1) keyboard.maybeShowShiftKeyHelp(); - }; + keyboard.maybeShowVariationArrowHelp(this); + } private onNewCeval = (ev: Tree.ClientEval, path: Tree.Path, isThreat?: boolean): void => { this.tree.updateAt(path, (node: Tree.Node) => { @@ -772,15 +774,33 @@ export default class AnalyseCtrl { }; private resetAutoShapes() { - if (this.showAutoShapes() || this.showMoveAnnotation()) this.setAutoShapes(); + if (this.showAutoShapes() || this.variationArrowsProp() || this.showMoveAnnotation()) + this.setAutoShapes(); else this.chessground && this.chessground.setAutoShapes([]); } + showVariationArrows() { + const chap = this.study?.data.chapter; + return ( + !isTouchDevice() && + !chap?.practice && + !chap?.conceal && + !chap?.gamebook && + this.variationArrowsProp() && + this.node.children.length > 1 + ); + } + toggleAutoShapes = (v: boolean): void => { this.showAutoShapes(v); this.resetAutoShapes(); }; + toggleVariationArrows = (v: boolean): void => { + this.variationArrowsProp(v); + this.resetAutoShapes(); + }; + toggleGauge = () => { this.showGauge(!this.showGauge()); }; @@ -794,7 +814,8 @@ export default class AnalyseCtrl { if (!this.showComputer()) { this.tree.removeComputerVariations(); if (this.ceval.enabled()) this.toggleCeval(); - } else this.resetAutoShapes(); + } + this.resetAutoShapes(); } toggleComputer = () => { diff --git a/ui/analyse/src/keyboard.ts b/ui/analyse/src/keyboard.ts index ea2e1feb5eef8..9fbf216076578 100644 --- a/ui/analyse/src/keyboard.ts +++ b/ui/analyse/src/keyboard.ts @@ -1,19 +1,12 @@ import * as control from './control'; import * as xhr from 'common/xhr'; -import { isTouchDevice } from 'common/mobile'; import AnalyseCtrl from './ctrl'; import { h, VNode } from 'snabbdom'; import { snabModal } from 'common/modal'; +import { showDialog } from 'common/dialog'; import { spinnerVdom as spinner } from 'common/spinner'; export const bind = (ctrl: AnalyseCtrl) => { - document.addEventListener('keydown', (e: KeyboardEvent) => { - if (e.key !== 'Shift') return; - if ((e.location === 1 && ctrl.fork.prev()) || (e.location === 2 && ctrl.fork.next())) { - ctrl.setAutoShapes(); - ctrl.redraw(); - } - }); const kbd = window.lichess.mousetrap; kbd .bind(['left', 'k'], () => { @@ -29,11 +22,21 @@ export const bind = (ctrl: AnalyseCtrl) => { control.next(ctrl); ctrl.redraw(); }) - .bind(['up', '0', 'home'], () => { + .bind('up', () => { + if (ctrl.fork.prev()) ctrl.setAutoShapes(); + else control.first(ctrl); + ctrl.redraw(); + }) + .bind('down', () => { + if (ctrl.fork.next()) ctrl.setAutoShapes(); + else control.last(ctrl); + ctrl.redraw(); + }) + .bind(['0', 'home'], () => { control.first(ctrl); ctrl.redraw(); }) - .bind(['down', '$', 'end'], () => { + .bind(['$', 'end'], () => { control.last(ctrl); ctrl.redraw(); }) @@ -138,17 +141,11 @@ export function view(ctrl: AnalyseCtrl): VNode { }); } -export function maybeShowShiftKeyHelp() { - // we can probably delete this after a month or so - if (isTouchDevice() || !lichess.once('help.analyse.shift-key')) return; - Promise.all([lichess.loadCssPath('analyse.keyboard'), xhr.text('/help/analyse/shift-key')]).then( - ([, html]) => { - $('.cg-wrap').append($(html).attr('id', 'analyse-shift-key-tooltip')); - const cb = () => { - $(document).off('mousedown keydown wheel', cb); - $('#analyse-shift-key-tooltip').remove(); - }; - $(document).on('mousedown keydown wheel', cb); - }, - ); +export function maybeShowVariationArrowHelp(ctrl: AnalyseCtrl) { + if (ctrl.showVariationArrows() && lichess.once('help.analyse.variation-arrows-rtfm')) + showDialog({ + cls: 'variation-arrow-help', + htmlUrl: '/help/analyse/variation-arrow', + cssPath: 'analyse.keyboard', + }); } diff --git a/ui/analyse/src/study/practice/studyPracticeCtrl.ts b/ui/analyse/src/study/practice/studyPracticeCtrl.ts index 28d1cd80d1cc7..87c79c2cb30e3 100644 --- a/ui/analyse/src/study/practice/studyPracticeCtrl.ts +++ b/ui/analyse/src/study/practice/studyPracticeCtrl.ts @@ -23,6 +23,7 @@ export default function ( function onLoad() { root.showAutoShapes = readOnlyProp(true); + root.variationArrowsProp = readOnlyProp(false); root.showGauge = readOnlyProp(true); root.showComputer = readOnlyProp(true); goal(root.data.practiceGoal!); diff --git a/ui/analyse/src/view/actionMenu.ts b/ui/analyse/src/view/actionMenu.ts index 33378ed37e2ad..85d700bbc1cf9 100644 --- a/ui/analyse/src/view/actionMenu.ts +++ b/ui/analyse/src/view/actionMenu.ts @@ -1,6 +1,7 @@ import { isEmpty } from 'common'; import * as licon from 'common/licon'; import modal from 'common/modal'; +import { isTouchDevice } from 'common/mobile'; import { bind, dataIcon, MaybeVNodes } from 'common/snabbdom'; import { h, VNode } from 'snabbdom'; import { AutoplayDelay } from '../autoplay'; @@ -205,6 +206,7 @@ export function view(ctrl: AnalyseCtrl): VNode { }, ctrl, ), + ctrlToggle( { name: 'evaluationGauge', @@ -313,16 +315,30 @@ export function view(ctrl: AnalyseCtrl): VNode { }, ctrl, ), - ctrlToggle( - { - name: 'Annotations on board', - title: 'Display analysis symbols on the board', - id: 'move-annotation', - checked: ctrl.showMoveAnnotation(), - change: ctrl.toggleMoveAnnotation, - }, - ctrl, - ), + isTouchDevice() + ? null + : ctrlToggle( + { + name: 'showVariationArrows', + title: 'Variation navigation arrows', + id: 'variationArrows', + checked: ctrl.variationArrowsProp(), + change: ctrl.toggleVariationArrows, + }, + ctrl, + ), + ctrl.ongoing + ? null + : ctrlToggle( + { + name: 'Annotations on board', + title: 'Display analysis symbols on the board', + id: 'move-annotation', + checked: ctrl.showMoveAnnotation(), + change: ctrl.toggleMoveAnnotation, + }, + ctrl, + ), ]; return h('div.action-menu', [ diff --git a/ui/common/css/abstract/_mixins.scss b/ui/common/css/abstract/_mixins.scss index a7c3ed3e3c72c..caf1c9aaa618a 100644 --- a/ui/common/css/abstract/_mixins.scss +++ b/ui/common/css/abstract/_mixins.scss @@ -85,3 +85,32 @@ backdrop-filter: blur($size); -webkit-backdrop-filter: blur($size); } + +@mixin modal-close-x { + .close { + @extend %flex-around; + color: $c-font; + position: absolute; + font-size: 16px; + width: 32px; + height: 32px; + cursor: pointer; + top: 0; + #{$end-direction}: 0; + background: none; + + &:hover { + @extend %box-shadow; + + background: $c-bad; + color: #fff; + } + + @include breakpoint($mq-small) { + top: -12px; + #{$end-direction}: -12px; + background: $c-bg-popup; + border-radius: 50%; + } + } +} diff --git a/ui/common/css/component/_help.scss b/ui/common/css/component/_help.scss index 2b8e566abdb76..7e1a68b7c6a1a 100644 --- a/ui/common/css/component/_help.scss +++ b/ui/common/css/component/_help.scss @@ -1,4 +1,4 @@ -%help { +%help-modal { @extend %flex-column; th p { @@ -43,10 +43,6 @@ border-radius: 3px; box-shadow: inset 0 -1px 0 #bbb; } -} - -%help-modal { - @extend %help; table { width: 100%; @@ -66,41 +62,31 @@ } } -.help-ephemeral { - @extend %help, %box-radius, %popup-shadow; - position: absolute; - z-index: 100; +%help-dialog { + @extend %help-modal, %box-radius, %popup-shadow; + @include modal-close-x; + + position: relative; top: 50%; left: 50%; transform: translate(-50%, -50%); - align-items: center; + overflow: visible; background: $c-bg-popup; - padding: 1em 2em; - white-space: nowrap; - pointer-events: none; + padding: 2em 2em 1em; + border: none; + + .close { + outline: none; + border: none; + } kbd { font-family: roboto, sans-serif; } table { + margin: 1em 0; display: inline-block; width: auto; } - - ul { - margin-bottom: 1em; - display: inline-block; - width: auto; - font-size: larger; - li { - padding-left: 0.5em; - list-style: disc; - } - } - - &.fade-out { - transition: opacity 0.5s linear; - opacity: 0; - } } diff --git a/ui/common/css/component/_modal.scss b/ui/common/css/component/_modal.scss index 95ead1e7875c0..f0e67c74ab291 100644 --- a/ui/common/css/component/_modal.scss +++ b/ui/common/css/component/_modal.scss @@ -7,7 +7,7 @@ &-wrap { @extend %box-radius, %popup-shadow, %flex-column; - + @include modal-close-x; background: $c-bg-box; position: relative; text-align: center; @@ -19,32 +19,5 @@ overflow-y: auto; padding: 2rem; } - - .close { - @extend %flex-around; - color: $c-font; - position: absolute; - font-size: 16px; - width: 32px; - height: 32px; - cursor: pointer; - top: 0; - #{$end-direction}: 0; - background: none; - - &:hover { - @extend %box-shadow; - - background: $c-bad; - color: #fff; - } - - @include breakpoint($mq-small) { - top: -12px; - #{$end-direction}: -12px; - background: $c-bg-popup; - border-radius: 50%; - } - } } } diff --git a/ui/common/src/dialog.ts b/ui/common/src/dialog.ts new file mode 100644 index 0000000000000..09e1f381c6fad --- /dev/null +++ b/ui/common/src/dialog.ts @@ -0,0 +1,40 @@ +import * as xhr from './xhr'; +import * as licon from './licon'; + +export interface DialogOpts { + htmlUrl: string; + cls: string; + cssPath?: string; + attrs?: { [key: string]: string }; + container?: HTMLElement | Cash; +} + +export async function showDialog(opts: DialogOpts) { + const [html] = await Promise.all([ + xhr.text(opts.htmlUrl), + opts.cssPath ? lichess.loadCssPath(opts.cssPath) : Promise.resolve(), + ]); + + const dialog = document.createElement('dialog'); + dialog.classList.add(opts.cls); + dialog.appendChild($as($(html))); + + const close = () => { + dialog.remove(); + document.removeEventListener('click', clickOutside); + }; + const clickOutside = (e: UIEvent) => dialog.contains(e.target as Node) && close(); + + for (const [attr, val] of Object.entries(opts.attrs ?? {})) { + dialog.setAttribute(attr, val); + } + + $(dialog).prepend($(` - - ${trans('proceedToX', url.host)} - - - `, - ), - onInsert($wrap) { - $wrap.find('.cancel').on('click', modal.close); - }, - }), - ); + + + `, + }, + }).then(dlg => { + $('.cancel', dlg.view).on('click', dlg.close); + dlg.showModal(); + }); return false; }; diff --git a/ui/common/src/modal.ts b/ui/common/src/modal.ts deleted file mode 100644 index 053c7ba1bdc5c..0000000000000 --- a/ui/common/src/modal.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { h, VNode } from 'snabbdom'; -import { bind, MaybeVNodes, onInsert } from './snabbdom'; -import * as licon from './licon'; - -interface BaseModal { - class?: string; - onInsert?: ($wrap: Cash) => void; - onClose?(): void; - noClickAway?: boolean; -} - -interface Modal extends BaseModal { - content: Cash; -} - -interface SnabModal extends BaseModal { - content: MaybeVNodes; - onClose(): void; -} - -const overlayId = 'modal-overlay'; - -export default function modal(opts: Modal) { - modal.close(); - const $wrap = $( - ``, - ); - const $overlay = $(`
`); - if (!opts.noClickAway) $overlay.on('click', modal.close); - $('').appendTo($overlay); // guard against focus escaping to window chrome - $wrap.appendTo($overlay); - $('').appendTo($overlay); // guard against focus escaping to window chrome - opts.content.clone().removeClass('none').appendTo($wrap); - opts.onInsert && opts.onInsert($wrap); - modal.onClose = opts.onClose; - $wrap.find('.close,.cancel').each(function (this: HTMLElement) { - bindClose(this, modal.close); - }); - $('body').addClass('overlayed').prepend($overlay); - bindWrap($wrap); - return $wrap; -} - -modal.close = () => { - $('body').removeClass('overlayed'); - $(`#${overlayId}`).each(function (this: HTMLElement) { - if (modal.onClose) modal.onClose(); - $(this).remove(); - }); - delete modal.onClose; -}; - -modal.onClose = undefined as (() => void) | undefined; - -export function snabModal(opts: SnabModal): VNode { - const close = opts.onClose!; - return h( - `div#${overlayId}`, - opts.noClickAway - ? {} - : { - hook: bind('mousedown', (event: MouseEvent) => { - if ((event.target as HTMLElement).id == overlayId) close(); - }), - }, - [ - h( - 'div#modal-wrap.' + opts.class, - { - hook: onInsert(el => { - bindWrap($(el)); - opts.onInsert && opts.onInsert($(el)); - }), - }, - [ - h('span.close', { - attrs: { - 'data-icon': licon.X, - role: 'button', - 'aria-label': 'Close', - tabindex: '0', - }, - hook: onInsert(el => bindClose(el, close)), - }), - h('div', opts.content), - ], - ), - ], - ); -} - -const bindClose = (el: HTMLElement, close: () => void) => { - el.addEventListener('click', close); - el.addEventListener('keydown', e => (e.code === 'Enter' || e.code === 'Space' ? close() : true)); -}; - -const bindWrap = ($wrap: Cash) => { - $wrap.on('click', (e: Event) => e.stopPropagation()); - focusFirstChild($wrap); -}; - -const focusableSelectors = - 'button:not(:disabled), [href], input:not(:disabled):not([type="hidden"]), select:not(:disabled), textarea:not(:disabled), [tabindex="0"]'; - -export function trapFocus(event: FocusEvent) { - const wrap: HTMLElement | undefined = $('#modal-wrap')[0]; - if (!wrap) return; - const position = wrap.compareDocumentPosition(event.target as HTMLElement); - if (position & Node.DOCUMENT_POSITION_CONTAINED_BY) return; - const focusableChildren = $(wrap).find(focusableSelectors); - const index = position & Node.DOCUMENT_POSITION_FOLLOWING ? 0 : focusableChildren.length - 1; - focusableChildren.get(index)?.focus(); - event.preventDefault(); -} - -export const focusFirstChild = (parent: Cash) => { - const children = parent.find(focusableSelectors); - // prefer child 1 over child 0 because child 0 should be a close button - // use setTimeout to avoid race conditions with snabbdom - setTimeout(() => (children[1] ?? children[0])?.focus()); -}; diff --git a/ui/common/src/snabbdom.ts b/ui/common/src/snabbdom.ts index f92ccfa441d91..42cb6c6b4c1f2 100644 --- a/ui/common/src/snabbdom.ts +++ b/ui/common/src/snabbdom.ts @@ -1,4 +1,4 @@ -import { h, VNode, Hooks, Attrs } from 'snabbdom'; +import { h as snabH, VNode, VNodeData, VNodeChildElement, Hooks, Attrs } from 'snabbdom'; export type Redraw = () => void; export type MaybeVNode = VNode | string | null | undefined; @@ -37,3 +37,31 @@ export const dataIcon = (icon: string): Attrs => ({ }); export const iconTag = (icon: string) => h('i', { attrs: dataIcon(icon) }); + +type LooseVNode = VNodeChildElement | boolean; +type VNodeKids = LooseVNode | LooseVNode[]; + +function filterKids(children: VNodeKids): VNodeChildElement[] { + return ( + typeof children === 'boolean' + ? [] + : Array.isArray(children) + ? children.filter(x => typeof x !== 'boolean') + : [children] + ) as VNodeChildElement[]; +} + +/* obviate need for many ternary expressions in renders (blown up by prettier). Allows + h('div', [ kids && h('div', 'kid') ]) + h('div', [ noKids || h('div', 'kid') ]) + instead of + h('div', [ isKid ? h('div', 'kid') : null ]) + import this h rather than that h +*/ +export function h(sel: string, dataOrKids?: VNodeData | null | VNodeKids, kids?: VNodeKids): VNode { + if (kids) return snabH(sel, dataOrKids as VNodeData, filterKids(kids)); + if (!dataOrKids) return snabH(sel); + if (Array.isArray(dataOrKids) || (typeof dataOrKids === 'object' && 'sel' in dataOrKids)) + return snabH(sel, filterKids(dataOrKids as VNodeKids)); + else return snabH(sel, dataOrKids as VNodeData); +} diff --git a/ui/editor/src/view.ts b/ui/editor/src/view.ts index 730b478d22528..ea65d8a07ae37 100644 --- a/ui/editor/src/view.ts +++ b/ui/editor/src/view.ts @@ -5,7 +5,7 @@ import { dragNewPiece } from 'chessground/drag'; import { eventPosition, opposite } from 'chessground/util'; import { Rules } from 'chessops/types'; import { parseFen } from 'chessops/fen'; -import modal from 'common/modal'; +import { domDialog } from 'common/dialog'; import EditorCtrl from './ctrl'; import chessground from './chessground'; import { Selected, CastlingToggle, EditorState } from './interfaces'; @@ -275,9 +275,9 @@ function controls(ctrl: EditorCtrl, state: EditorState): VNode { on: { click: () => { if (state.playable) - modal({ - content: $('.continue-with'), - }); + domDialog({ + cash: $('.continue-with'), + }).then(d => d.showModal()); }, }, }, diff --git a/ui/gameSetup/src/ctrl.ts b/ui/gameSetup/src/ctrl.ts index 62836304fcdd8..4f7e504957154 100644 --- a/ui/gameSetup/src/ctrl.ts +++ b/ui/gameSetup/src/ctrl.ts @@ -198,13 +198,11 @@ export class SetupCtrl { this.friendUser = friendUser || ''; this.init(opts); this.root.redraw(); - //setTimeout(() => $as($('dialog.dialog')).showModal(), 5000); }; renderModal = () => renderSetup(this); closeModal = () => { - //if (document.activeElement instanceof HTMLElement) document.activeElement.blur(); this.gameType = null; this.root.redraw(); }; diff --git a/ui/gameSetup/src/view/modal.ts b/ui/gameSetup/src/view/modal.ts index 0951eb99b4f5d..ad0bef03ab590 100644 --- a/ui/gameSetup/src/view/modal.ts +++ b/ui/gameSetup/src/view/modal.ts @@ -20,6 +20,6 @@ export default function setupModal(ctrl: SetupCtrl): MaybeVNode { class: ctrl.gameType === 'local' ? 'game-setup.local-setup' : 'game-setup', cssPath: 'game-setup', onClose: ctrl.closeModal, - vnodes: renderContent(ctrl), + content: renderContent(ctrl), }); } diff --git a/ui/keyboardMove/css/_keyboardMove.help.scss b/ui/keyboardMove/css/_keyboardMove.help.scss deleted file mode 100644 index 1f1293c204cc0..0000000000000 --- a/ui/keyboardMove/css/_keyboardMove.help.scss +++ /dev/null @@ -1,13 +0,0 @@ -.keyboard-move-help { - @extend %help; - - td.tips li { - list-style: disc; - margin-#{$start-direction}: 2em; - margin-#{$end-direction}: 1em; - } - - a { - margin-#{$start-direction}: 0.25em; - } -} diff --git a/ui/keyboardMove/css/_keyboardMove.scss b/ui/keyboardMove/css/_keyboardMove.scss index 9517c898550c4..16109b305ed1e 100644 --- a/ui/keyboardMove/css/_keyboardMove.scss +++ b/ui/keyboardMove/css/_keyboardMove.scss @@ -21,3 +21,14 @@ color: $c-font-dim; } } + +dialog > .keyboard-move-help { + td.tips li { + list-style: disc; + margin-#{$start-direction}: 2em; + margin-#{$end-direction}: 1em; + } + a { + margin-#{$start-direction}: 0.25em; + } +} diff --git a/ui/keyboardMove/css/build/_keyboardMove.help.scss b/ui/keyboardMove/css/build/_keyboardMove.help.scss deleted file mode 100644 index e37505470581b..0000000000000 --- a/ui/keyboardMove/css/build/_keyboardMove.help.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/plugin'; -@import '../../../common/css/component/help'; -@import '../keyboardMove.help'; diff --git a/ui/keyboardMove/css/build/keyboardMove.help.ltr.dark.scss b/ui/keyboardMove/css/build/keyboardMove.help.ltr.dark.scss deleted file mode 100644 index 55de7d4ac6f6d..0000000000000 --- a/ui/keyboardMove/css/build/keyboardMove.help.ltr.dark.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/ltr'; -@import '../../../common/css/theme/dark'; -@import 'keyboardMove.help'; diff --git a/ui/keyboardMove/css/build/keyboardMove.help.ltr.light.scss b/ui/keyboardMove/css/build/keyboardMove.help.ltr.light.scss deleted file mode 100644 index 4be6201764856..0000000000000 --- a/ui/keyboardMove/css/build/keyboardMove.help.ltr.light.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/ltr'; -@import '../../../common/css/theme/light'; -@import 'keyboardMove.help'; diff --git a/ui/keyboardMove/css/build/keyboardMove.help.ltr.transp.scss b/ui/keyboardMove/css/build/keyboardMove.help.ltr.transp.scss deleted file mode 100644 index 593ddc0f7f6de..0000000000000 --- a/ui/keyboardMove/css/build/keyboardMove.help.ltr.transp.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/ltr'; -@import '../../../common/css/theme/transp'; -@import 'keyboardMove.help'; diff --git a/ui/keyboardMove/css/build/keyboardMove.help.rtl.dark.scss b/ui/keyboardMove/css/build/keyboardMove.help.rtl.dark.scss deleted file mode 100644 index 1f53d4ec12663..0000000000000 --- a/ui/keyboardMove/css/build/keyboardMove.help.rtl.dark.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/rtl'; -@import '../../../common/css/theme/dark'; -@import 'keyboardMove.help'; diff --git a/ui/keyboardMove/css/build/keyboardMove.help.rtl.light.scss b/ui/keyboardMove/css/build/keyboardMove.help.rtl.light.scss deleted file mode 100644 index ffbbd78fcf61c..0000000000000 --- a/ui/keyboardMove/css/build/keyboardMove.help.rtl.light.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/rtl'; -@import '../../../common/css/theme/light'; -@import 'keyboardMove.help'; diff --git a/ui/keyboardMove/css/build/keyboardMove.help.rtl.transp.scss b/ui/keyboardMove/css/build/keyboardMove.help.rtl.transp.scss deleted file mode 100644 index 4a558df57d3e1..0000000000000 --- a/ui/keyboardMove/css/build/keyboardMove.help.rtl.transp.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/rtl'; -@import '../../../common/css/theme/transp'; -@import 'keyboardMove.help'; diff --git a/ui/keyboardMove/src/main.ts b/ui/keyboardMove/src/main.ts index e67c91a2b3037..a49052d8c51c0 100644 --- a/ui/keyboardMove/src/main.ts +++ b/ui/keyboardMove/src/main.ts @@ -1,11 +1,9 @@ import * as cg from 'chessground/types'; -import * as xhr from 'common/xhr'; import { Api as CgApi } from 'chessground/api'; import { h } from 'snabbdom'; import { onInsert } from 'common/snabbdom'; import { promote } from 'chess/promotion'; -import { snabModal } from 'common/modal'; -import { spinnerVdom as spinner } from 'common/spinner'; +import { snabDialog } from 'common/dialog'; import { propWithEffect, Prop } from 'common'; import { load as loadKeyboardMove } from './plugins/keyboardMove'; import KeyboardChecker from './plugins/keyboardChecker'; @@ -164,17 +162,10 @@ export function render(ctrl: KeyboardMove) { ? h('em', 'Enter SAN (Nc3), ICCF (2133) or UCI (b1c3) moves, type ? to learn more') : h('strong', 'Press to focus'), ctrl.helpModalOpen() - ? snabModal({ - class: 'keyboard-move-help', - content: [h('div.scrollable', spinner())], + ? snabDialog({ + class: 'help.keyboard-move-help', + html: { url: '/help/keyboard-move' }, onClose: () => ctrl.helpModalOpen(false), - onInsert: async ($wrap: Cash) => { - const [, html] = await Promise.all([ - lichess.loadCssPath('keyboardMove.help'), - xhr.text(xhr.url('/help/keyboard-move', {})), - ]); - $wrap.find('.scrollable').html(html); - }, }) : null, ]); diff --git a/ui/puz/src/util.ts b/ui/puz/src/util.ts index 7dd51d16f9ec3..e7ea451dcf918 100644 --- a/ui/puz/src/util.ts +++ b/ui/puz/src/util.ts @@ -12,7 +12,7 @@ const loadSound = (file: string, volume?: number, delay?: number) => { }; export const sound = { - move: (take: boolean) => lichess.sound.play(take ? 'capture' : 'move'), + move: (take: boolean) => lichess.sound.move(take ? { san: 'x' } : {}, false), good: loadSound('lisp/PuzzleStormGood', 0.9, 1000), wrong: loadSound('lisp/Error', 1, 1000), end: loadSound('lisp/PuzzleStormEnd', 1, 5000), diff --git a/ui/puzzle/css/_keyboard.scss b/ui/puzzle/css/_keyboard.scss deleted file mode 100644 index 14694d88ed289..0000000000000 --- a/ui/puzzle/css/_keyboard.scss +++ /dev/null @@ -1,3 +0,0 @@ -.keyboard-help { - @extend %help; -} diff --git a/ui/puzzle/css/build/_puzzle.keyboard.scss b/ui/puzzle/css/build/_puzzle.keyboard.scss deleted file mode 100644 index 4939f1a00afe5..0000000000000 --- a/ui/puzzle/css/build/_puzzle.keyboard.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/plugin'; -@import '../../../common/css/component/help'; -@import '../keyboard'; diff --git a/ui/puzzle/css/build/puzzle.keyboard.ltr.dark.scss b/ui/puzzle/css/build/puzzle.keyboard.ltr.dark.scss deleted file mode 100644 index bf35ebfcf764b..0000000000000 --- a/ui/puzzle/css/build/puzzle.keyboard.ltr.dark.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/ltr'; -@import '../../../common/css/theme/dark'; -@import 'puzzle.keyboard'; diff --git a/ui/puzzle/css/build/puzzle.keyboard.ltr.light.scss b/ui/puzzle/css/build/puzzle.keyboard.ltr.light.scss deleted file mode 100644 index 7504ea63eaa5a..0000000000000 --- a/ui/puzzle/css/build/puzzle.keyboard.ltr.light.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/ltr'; -@import '../../../common/css/theme/light'; -@import 'puzzle.keyboard'; diff --git a/ui/puzzle/css/build/puzzle.keyboard.ltr.transp.scss b/ui/puzzle/css/build/puzzle.keyboard.ltr.transp.scss deleted file mode 100644 index ae702fdfbb04b..0000000000000 --- a/ui/puzzle/css/build/puzzle.keyboard.ltr.transp.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/ltr'; -@import '../../../common/css/theme/transp'; -@import 'puzzle.keyboard'; diff --git a/ui/puzzle/css/build/puzzle.keyboard.rtl.dark.scss b/ui/puzzle/css/build/puzzle.keyboard.rtl.dark.scss deleted file mode 100644 index 7c3f195fb286a..0000000000000 --- a/ui/puzzle/css/build/puzzle.keyboard.rtl.dark.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/rtl'; -@import '../../../common/css/theme/dark'; -@import 'puzzle.keyboard'; diff --git a/ui/puzzle/css/build/puzzle.keyboard.rtl.light.scss b/ui/puzzle/css/build/puzzle.keyboard.rtl.light.scss deleted file mode 100644 index 507fcba50ef16..0000000000000 --- a/ui/puzzle/css/build/puzzle.keyboard.rtl.light.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/rtl'; -@import '../../../common/css/theme/light'; -@import 'puzzle.keyboard'; diff --git a/ui/puzzle/css/build/puzzle.keyboard.rtl.transp.scss b/ui/puzzle/css/build/puzzle.keyboard.rtl.transp.scss deleted file mode 100644 index 352c3051a4d0a..0000000000000 --- a/ui/puzzle/css/build/puzzle.keyboard.rtl.transp.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/rtl'; -@import '../../../common/css/theme/transp'; -@import 'puzzle.keyboard'; diff --git a/ui/puzzle/src/keyboard.ts b/ui/puzzle/src/keyboard.ts index 7f164012cef09..d3ff8b330ab37 100644 --- a/ui/puzzle/src/keyboard.ts +++ b/ui/puzzle/src/keyboard.ts @@ -1,9 +1,6 @@ import * as control from './control'; -import * as xhr from 'common/xhr'; import { Controller, KeyboardController } from './interfaces'; -import { h, VNode } from 'snabbdom'; -import { snabModal } from 'common/modal'; -import { spinnerVdom as spinner } from 'common/spinner'; +import { snabDialog } from 'common/dialog'; export default (ctrl: KeyboardController) => lichess.mousetrap @@ -36,16 +33,9 @@ export default (ctrl: KeyboardController) => .bind('f', ctrl.flip) .bind('n', ctrl.nextPuzzle); -export const view = (ctrl: Controller): VNode => - snabModal({ - class: 'keyboard-help', - onInsert: async ($wrap: Cash) => { - const [, html] = await Promise.all([ - lichess.loadCssPath('puzzle.keyboard'), - xhr.text(xhr.url('/training/help', {})), - ]); - $wrap.find('.scrollable').html(html); - }, +export const view = (ctrl: Controller) => + snabDialog({ + class: 'help', + html: { url: '/training/help' }, onClose: () => ctrl.keyboardHelp(false), - content: [h('div.scrollable', spinner())], }); diff --git a/ui/round/css/_keyboard.scss b/ui/round/css/_keyboard.scss deleted file mode 100644 index 14694d88ed289..0000000000000 --- a/ui/round/css/_keyboard.scss +++ /dev/null @@ -1,3 +0,0 @@ -.keyboard-help { - @extend %help; -} diff --git a/ui/round/css/build/_round.keyboard.scss b/ui/round/css/build/_round.keyboard.scss deleted file mode 100644 index 4939f1a00afe5..0000000000000 --- a/ui/round/css/build/_round.keyboard.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/plugin'; -@import '../../../common/css/component/help'; -@import '../keyboard'; diff --git a/ui/round/css/build/round.keyboard.ltr.dark.scss b/ui/round/css/build/round.keyboard.ltr.dark.scss deleted file mode 100644 index 75ce08cc2c1d4..0000000000000 --- a/ui/round/css/build/round.keyboard.ltr.dark.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/ltr'; -@import '../../../common/css/theme/dark'; -@import 'round.keyboard'; diff --git a/ui/round/css/build/round.keyboard.ltr.light.scss b/ui/round/css/build/round.keyboard.ltr.light.scss deleted file mode 100644 index 94fe74fceea9d..0000000000000 --- a/ui/round/css/build/round.keyboard.ltr.light.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/ltr'; -@import '../../../common/css/theme/light'; -@import 'round.keyboard'; diff --git a/ui/round/css/build/round.keyboard.ltr.transp.scss b/ui/round/css/build/round.keyboard.ltr.transp.scss deleted file mode 100644 index 847ab3f953b81..0000000000000 --- a/ui/round/css/build/round.keyboard.ltr.transp.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/ltr'; -@import '../../../common/css/theme/transp'; -@import 'round.keyboard'; diff --git a/ui/round/css/build/round.keyboard.rtl.dark.scss b/ui/round/css/build/round.keyboard.rtl.dark.scss deleted file mode 100644 index 6fa5bc187bfa9..0000000000000 --- a/ui/round/css/build/round.keyboard.rtl.dark.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/rtl'; -@import '../../../common/css/theme/dark'; -@import 'round.keyboard'; diff --git a/ui/round/css/build/round.keyboard.rtl.light.scss b/ui/round/css/build/round.keyboard.rtl.light.scss deleted file mode 100644 index c787ad33e2b7c..0000000000000 --- a/ui/round/css/build/round.keyboard.rtl.light.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/rtl'; -@import '../../../common/css/theme/light'; -@import 'round.keyboard'; diff --git a/ui/round/css/build/round.keyboard.rtl.transp.scss b/ui/round/css/build/round.keyboard.rtl.transp.scss deleted file mode 100644 index 082103463548b..0000000000000 --- a/ui/round/css/build/round.keyboard.rtl.transp.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../common/css/dir/rtl'; -@import '../../../common/css/theme/transp'; -@import 'round.keyboard'; diff --git a/ui/round/src/keyboard.ts b/ui/round/src/keyboard.ts index 54b4e4734509c..a732dcf2dd097 100644 --- a/ui/round/src/keyboard.ts +++ b/ui/round/src/keyboard.ts @@ -1,8 +1,6 @@ import RoundController from './ctrl'; -import { h, VNode } from 'snabbdom'; -import * as xhr from 'common/xhr'; -import { snabModal } from 'common/modal'; -import { spinnerVdom as spinner } from 'common/spinner'; +import { VNode } from 'snabbdom'; +import { snabDialog } from 'common/dialog'; export const prev = (ctrl: RoundController) => ctrl.userJump(ctrl.ply - 1); @@ -34,18 +32,11 @@ export const init = (ctrl: RoundController) => }); export const view = (ctrl: RoundController): VNode => - snabModal({ - class: 'keyboard-help', - onInsert: async ($wrap: Cash) => { - const [, html] = await Promise.all([ - lichess.loadCssPath('round.keyboard'), - xhr.text(xhr.url('/round/help', {})), - ]); - $wrap.find('.scrollable').html(html); - }, + snabDialog({ + class: 'help', + html: { url: '/round/help' }, onClose() { ctrl.keyboardHelp = false; ctrl.redraw(); }, - content: [h('div.scrollable', spinner())], }); diff --git a/ui/round/src/socket.ts b/ui/round/src/socket.ts index eda43ca0e087d..cb74ba6f05d21 100644 --- a/ui/round/src/socket.ts +++ b/ui/round/src/socket.ts @@ -1,6 +1,6 @@ import * as game from 'game'; import throttle from 'common/throttle'; -import modal from 'common/modal'; +import { domDialog } from 'common/dialog'; import * as xhr from './xhr'; import RoundController from './ctrl'; import { defined } from 'common'; @@ -145,12 +145,11 @@ export function make(send: SocketSend, ctrl: RoundController): RoundSocket { } }, simulEnd(simul: game.Simul) { - lichess.loadCssPath('modal'); - modal({ - content: $( - ``, - ), - }); + domDialog({ + html: { + text: ``, + }, + }).then(d => d.showModal()); }, }; diff --git a/ui/simul/src/view/created.ts b/ui/simul/src/view/created.ts index 9b46b79238f33..e50cfa751493c 100644 --- a/ui/simul/src/view/created.ts +++ b/ui/simul/src/view/created.ts @@ -5,7 +5,7 @@ import SimulCtrl from '../ctrl'; import { Applicant } from '../interfaces'; import xhr from '../xhr'; import * as util from './util'; -import modal from 'common/modal'; +import { domDialog } from 'common/dialog'; export default function (showText: (ctrl: SimulCtrl) => MaybeVNode) { return (ctrl: SimulCtrl) => { @@ -51,17 +51,11 @@ export default function (showText: (ctrl: SimulCtrl) => MaybeVNode) { ? bind('click', () => { if (ctrl.data.variants.length === 1) xhr.join(ctrl.data.id, ctrl.data.variants[0].key); - else { - modal({ - content: $('.simul .continue-with'), - onInsert($wrap) { - $wrap.find('button').on('click', function (this: HTMLElement) { - modal.close(); - xhr.join(ctrl.data.id, $(this).data('variant')); - }); - }, - }); - } + else + domDialog({ + cash: $('.simul .continue-with'), + onClose: dlg => xhr.join(ctrl.data.id, $(dlg.view).data('variant')), + }).then(d => d.showModal()); }) : {}, }, diff --git a/ui/site/src/component/sound.ts b/ui/site/src/component/sound.ts index a5489f29c83bf..cbf58ac750147 100644 --- a/ui/site/src/component/sound.ts +++ b/ui/site/src/component/sound.ts @@ -54,6 +54,7 @@ export default new (class implements SoundI { } async play(name: Name, volume = 1): Promise { + //console.trace('play', name); if (!this.enabled()) return; const sound = await this.load(name); if (sound && (await this.resumeContext())) await sound.play(this.getVolume() * volume); @@ -66,6 +67,7 @@ export default new (class implements SoundI { }); async move(node?: { uci?: Uci; san?: string }, music?: boolean) { + console.trace('move', node?.san, node?.uci); if (music !== false && this.theme === 'music') { this.music ??= await lichess.loadEsm('soundMove'); this.music(node, music); @@ -144,7 +146,7 @@ export default new (class implements SoundI { if (isIOS()) this.ctx?.resume(); this.theme = s; this.publish(); - this.move(); + //this.move(); }; set = () => this.theme; diff --git a/ui/site/src/forum.ts b/ui/site/src/forum.ts index c48f4df7b6618..3ef0b43b00ebc 100644 --- a/ui/site/src/forum.ts +++ b/ui/site/src/forum.ts @@ -1,23 +1,20 @@ import * as xhr from 'common/xhr'; -import modal from 'common/modal'; +import { domDialog } from 'common/dialog'; lichess.load.then(() => { $('.forum') .on('click', 'a.delete', function (this: HTMLAnchorElement) { const link = this; - modal({ - content: $('.forum-delete-modal'), - onInsert($wrap) { - $wrap - .find('form') - .attr('action', link.href) - .on('submit', function (this: HTMLFormElement, e: Event) { - e.preventDefault(); - xhr.formToXhr(this); - modal.close(); - $(link).closest('.forum-post').hide(); - }); - }, + domDialog({ + cash: $('.forum-delete-modal'), + attrs: { view: { action: link.href } }, + }).then(dlg => { + $('form', dlg.view).on('submit', () => { + //e.preventDefault(); + xhr.text(link.href, { method: 'post' }); + $(link).closest('.forum-post').hide(); + }); + dlg.showModal(); }); return false; }) diff --git a/ui/site/src/publicChats.ts b/ui/site/src/publicChats.ts index 3a50abe21a58c..2ba7d39d78a1d 100644 --- a/ui/site/src/publicChats.ts +++ b/ui/site/src/publicChats.ts @@ -1,5 +1,5 @@ import { text, form } from 'common/xhr'; -import modal from 'common/modal'; +import { domDialog } from 'common/dialog'; lichess.load.then(() => { let autoRefreshEnabled = true; @@ -40,27 +40,26 @@ lichess.load.then(() => { $('#communication').on('click', '.line:not(.lichess)', function (this: HTMLDivElement) { const $l = $(this); - modal({ - content: $('.timeout-modal'), - onInsert($wrap) { - $wrap.find('.username').text($l.find('.user-link').text()); - $wrap.find('.text').text($l.text().split(' ').slice(1).join(' ')); - $wrap.on('click', '.button', function (this: HTMLButtonElement) { - const roomId = $l.parents('.game').data('room'); - const chan = $l.parents('.game').data('chan'); - text('/mod/public-chat/timeout', { - method: 'post', - body: form({ - roomId, - chan, - userId: $wrap.find('.username').text().toLowerCase(), - reason: this.value, - text: $wrap.find('.text').text(), - }), - }).then(_ => setTimeout(reloadNow, 1000)); - modal.close(); - }); - }, + domDialog({ cash: $('.timeout-modal') }).then(dlg => { + const $wrap = $(dlg.view); + $('.username', dlg.view).text($l.find('.user-link').text()); + $('.text', dlg.view).text($l.text().split(' ').slice(1).join(' ')); + $('.button', dlg.view).on('click', function (this: HTMLButtonElement) { + const roomId = $l.parents('.game').data('room'); + const chan = $l.parents('.game').data('chan'); + text('/mod/public-chat/timeout', { + method: 'post', + body: form({ + roomId, + chan, + userId: $wrap.find('.username').text().toLowerCase(), + reason: this.value, + text: $wrap.find('.text').text(), + }), + }).then(_ => setTimeout(reloadNow, 1000)); + dlg.close(); + }); + dlg.showModal(); }); }); }; diff --git a/ui/site/src/site.ts b/ui/site/src/site.ts index c7c8306913457..49f4b919c1d76 100644 --- a/ui/site/src/site.ts +++ b/ui/site/src/site.ts @@ -18,7 +18,6 @@ import { reload } from './component/reload'; import { requestIdleCallback } from './component/functions'; import { userComplete } from './component/assets'; import { siteTrans } from './component/trans'; -import { trapFocus } from 'common/modal'; import { isIOS } from 'common/mobile'; window.$as = (cashOrHtml: Cash | string) => @@ -118,17 +117,6 @@ lichess.load.then(() => { return false; }); - $('body').on('focusin', trapFocus); - - lichess.mousetrap.bind('esc', () => { - const $oc = $('#modal-wrap .close'); - if ($oc.length) $oc.trigger('click'); - else { - const $input = $(':focus'); - if ($input.length) $input.trigger('blur'); - } - }); - /* Edge randomly fails to rasterize SVG on page load * A different SVG must be loaded so a new image can be rasterized */ if (navigator.userAgent.includes('Edge/')) diff --git a/ui/storm/src/ctrl.ts b/ui/storm/src/ctrl.ts index ac7aa108b0ee1..19cea350fe54a 100644 --- a/ui/storm/src/ctrl.ts +++ b/ui/storm/src/ctrl.ts @@ -144,7 +144,7 @@ export default class StormCtrl implements PuzCtrl { if (this.run.clock.flag()) this.end(); else if (!this.incPuzzle()) this.end(); } - lichess.sound.move({ san: makeSan(pos, move), uci }); + lichess.sound.move({ san: makeSan(pos, move), uci }, true); this.redraw(); this.redrawQuick(); this.redrawSlow(); diff --git a/ui/tournament/src/view/battle.ts b/ui/tournament/src/view/battle.ts index da6ff2e966bf9..6c9e6c746c3fa 100644 --- a/ui/tournament/src/view/battle.ts +++ b/ui/tournament/src/view/battle.ts @@ -3,7 +3,7 @@ import { bind, MaybeVNode } from 'common/snabbdom'; import { playerName } from './util'; import { h, VNode } from 'snabbdom'; import { TeamBattle, RankedTeam } from '../interfaces'; -import { snabModal } from 'common/modal'; +import { snabDialog } from 'common/dialog'; export function joinWithTeamSelector(ctrl: TournamentController) { const tb = ctrl.data.teamBattle!; @@ -11,13 +11,14 @@ export function joinWithTeamSelector(ctrl: TournamentController) { ctrl.joinWithTeamSelector = false; ctrl.redraw(); }; - return snabModal({ + return snabDialog({ class: 'team-battle__choice', - onInsert($el) { - $el.on('click', '.team-picker__team', e => { + onInsert(dlg) { + $('.team-picker__team', dlg.view).on('click', e => { ctrl.join(e.target.dataset['id']); - onClose(); + dlg.close(); }); + dlg.showModal(); }, onClose, content: [ diff --git a/ui/voice/css/_voice.scss b/ui/voice/css/_voice.scss index 4f5b8169fe8de..1cb348acca95a 100644 --- a/ui/voice/css/_voice.scss +++ b/ui/voice/css/_voice.scss @@ -154,12 +154,13 @@ button#microphone-button { .active { @extend %active-primary; } +} - &__device { - flex-flow: row nowrap; - select { - width: 0; - flex: 1; - } +.voice-choices__device { + flex-flow: row nowrap; + + select { + width: 0; + flex: 1; } } diff --git a/ui/voice/css/_voiceMove.help.scss b/ui/voice/css/_voiceMove.help.scss index 765508c9ff415..48f69c4490e5c 100644 --- a/ui/voice/css/_voiceMove.help.scss +++ b/ui/voice/css/_voiceMove.help.scss @@ -1,6 +1,4 @@ -.voice-move-help { - @extend %help; - +dialog > .voice-move-help { td.tips li { list-style: disc; font-size: 1.1em; @@ -10,30 +8,35 @@ a { margin-#{$start-direction}: 0.25em; } - &#modal-wrap { - max-width: 1000px; - &.bigger { - width: 100%; - } - } + table { align-self: start; } + + voice { + display: block; + font-weight: bold; + color: $c-font; + } + .commands { - td:first-child { - padding-#{$start-direction}: 2em; - } + width: 100%; display: flex; flex-direction: row; @media (max-width: 600px) { flex-direction: column; } + td:first-child { + padding-#{$start-direction}: 2em; + } } - #all-phrases-button { + + .all-phrases-button { margin-top: 1em; padding: 0.6em 1.2em; } - #big-table { + + .big-table { td { padding: 0 1em; white-space: nowrap; @@ -49,8 +52,3 @@ } } } -voice { - display: block; - font-weight: bold; - color: $c-font; -} diff --git a/ui/voice/src/view.ts b/ui/voice/src/view.ts index 45f47c47156df..5b6a0680a1262 100644 --- a/ui/voice/src/view.ts +++ b/ui/voice/src/view.ts @@ -1,9 +1,9 @@ import { h } from 'snabbdom'; import * as licon from 'common/licon'; import { onInsert, bind } from 'common/snabbdom'; -import { snabModal } from 'common/modal'; -import { spinnerVdom as spinner } from 'common/spinner'; +import { snabDialog, type Dialog } from 'common/dialog'; import * as xhr from 'common/xhr'; +import { isIOS } from 'common/mobile'; import { onClickAway } from 'common'; import { Entry, VoiceCtrl } from './interfaces'; import { supportedLangs } from './main'; @@ -148,8 +148,8 @@ function voiceDisable() { } function renderHelpModal(ctrl: VoiceCtrl) { - const showMoveList = (el: Cash) => { - let html = ''; + const showMoveList = (dlg: Dialog) => { + let html = '
'; const all = ctrl .module() @@ -165,32 +165,34 @@ function renderHelpModal(ctrl: VoiceCtrl) { html += ''; } html += '
'; - el.find('.scrollable').html(html); + dlg.view.innerHTML = html; + if (!dlg.open) dlg.showModal(); }; - return snabModal({ - class: `voice-move-help`, - content: [h('div.scrollable', spinner())], + return snabDialog({ + class: 'help.voice-move-help', + html: { url: `/help/voice/${ctrl.moduleId}` }, + cssPath: 'voiceMove.help', + attrs: { + view: { + style: isIOS() ? `padding-bottom: ${window.screen.availHeight - window.innerHeight}px` : '', + }, + }, onClose: () => ctrl.showHelp(false), - onInsert: async el => { - const [, grammar, html] = await Promise.all([ - lichess.loadCssPath('voiceMove.help'), - ctrl.moduleId !== 'coords' - ? xhr.jsonSimple(lichess.assetUrl(`compiled/grammar/${ctrl.moduleId}-${ctrl.lang()}.json`)) - : Promise.resolve({ entries: [] }), - xhr.text(xhr.url(`/help/voice/${ctrl.moduleId}`, {})), - ]); - + onInsert: async dlg => { if (ctrl.showHelp() === 'list') { - showMoveList(el); + showMoveList(dlg); return; } - // using lexicon instead of crowdin translations for moves/commands - el.find('.scrollable').html(html); + const grammar = await (ctrl.moduleId !== 'coords' + ? xhr.jsonSimple(lichess.assetUrl(`compiled/grammar/${ctrl.moduleId}-${ctrl.lang()}.json`)) + : Promise.resolve({ entries: [] })); + + // TODO - fix using lexicon instead of crowdin translations for moves/commands const valToWord = (val: string, phonetic: boolean) => grammar.entries.find( (e: Entry) => (e.val ?? e.tok) === val && (!phonetic || e.tags?.includes('phonetic')), )?.in; - $('.val-to-word', el).each(function (this: HTMLElement) { + $('.val-to-word', dlg.view).each(function (this: HTMLElement) { const tryPhonetic = (val: string) => (this.classList.contains('phonetic') && valToWord(val, true)) || valToWord(val, false); this.innerText = this.innerText @@ -198,7 +200,8 @@ function renderHelpModal(ctrl: VoiceCtrl) { .map(v => tryPhonetic(v)) .join(' '); }); - el.find('#all-phrases-button').on('click', () => showMoveList(el)); + $('.all-phrases-button', dlg.view).on('click', () => showMoveList(dlg)); + dlg.showModal(); }, }); } From 0909d4bf83bc5dc2819949a1e8160acf502e5747 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Tue, 5 Sep 2023 06:09:12 -0500 Subject: [PATCH 050/174] . --- public/javascripts/study/tour-chapter.js | 16 +-- ui/analyse/css/_keyboard.scss | 4 +- ui/analyse/css/study/_modal.scss | 2 +- ui/analyse/src/explorer/explorerConfig.ts | 2 +- ui/analyse/src/keyboard.ts | 3 +- ui/analyse/src/serverSideUnderboard.ts | 3 +- ui/analyse/src/study/chapterEditForm.ts | 2 +- ui/analyse/src/study/chapterNewForm.ts | 2 +- ui/analyse/src/study/inviteForm.ts | 2 +- ui/analyse/src/study/studyForm.ts | 2 +- ui/analyse/src/study/studySearch.ts | 2 +- ui/analyse/src/study/topics.ts | 2 +- ui/analyse/src/view/actionMenu.ts | 2 +- ui/cli/src/main.ts | 1 + ui/common/css/component/_dialog.scss | 43 ++++--- ui/common/css/component/_help.scss | 2 +- ui/common/src/dialog.ts | 144 +++++++++++----------- ui/editor/src/view.ts | 5 +- ui/gameSetup/css/_bot-stuff.scss | 6 +- ui/gameSetup/css/_game-setup.scss | 13 +- ui/gameSetup/src/view/modal.ts | 2 +- ui/keyboardMove/css/_keyboardMove.scss | 2 +- ui/round/src/socket.ts | 3 +- ui/simul/src/view/created.ts | 3 +- ui/site/src/publicChats.ts | 5 +- ui/tournament/src/view/battle.ts | 2 +- ui/voice/css/_voiceMove.help.scss | 2 +- ui/voice/src/view.ts | 13 +- 28 files changed, 146 insertions(+), 144 deletions(-) diff --git a/public/javascripts/study/tour-chapter.js b/public/javascripts/study/tour-chapter.js index 1ae89d4e2a854..9852080b68554 100644 --- a/public/javascripts/study/tour-chapter.js +++ b/public/javascripts/study/tour-chapter.js @@ -39,18 +39,18 @@ lichess.studyTourChapter = function (study) { 'A study can have several chapters.
' + 'Each chapter has a distinct move tree,
' + 'and can be created in various ways.', - attachTo: '.modal-wrap label[for=chapter-name] left', + attachTo: '.dialog-content label[for=chapter-name] left', }, { title: 'From initial position', text: 'Just a board setup for a new game.
' + 'Suited to explore openings.', - attachTo: '.modal-wrap .tabs-horiz .init top', + attachTo: '.dialog-content .tabs-horiz .init top', when: onTab('init'), }, { title: 'Custom position', text: 'Setup the board your way.
' + 'Suited to explore endgames.', - attachTo: '.modal-wrap .tabs-horiz .edit bottom', + attachTo: '.dialog-content .tabs-horiz .edit bottom', when: onTab('edit'), }, { @@ -59,7 +59,7 @@ lichess.studyTourChapter = function (study) { 'Paste a lichess game URL
' + '(like lichess.org/7fHIU0XI)
' + 'to load the game moves in the chapter.', - attachTo: '.modal-wrap .tabs-horiz .game top', + attachTo: '.dialog-content .tabs-horiz .game top', when: onTab('game'), }, { @@ -68,19 +68,19 @@ lichess.studyTourChapter = function (study) { 'Paste a position in FEN format
' + '4k3/4rb2/8/7p/8/5Q2/1PP5/1K6 w
' + 'to start the chapter from a position.', - attachTo: '.modal-wrap .tabs-horiz .fen top', + attachTo: '.dialog-content .tabs-horiz .fen top', when: onTab('fen'), }, { title: 'From a PGN game', text: 'Paste a game in PGN format.
' + 'to load moves, comments and variations in the chapter.', - attachTo: '.modal-wrap .tabs-horiz .pgn top', + attachTo: '.dialog-content .tabs-horiz .pgn top', when: onTab('pgn'), }, { title: 'Studies support variants', text: 'Yes, you can study crazyhouse,
' + 'and all lichess variants!', - attachTo: '.modal-wrap label[for=chapter-variant] left', + attachTo: '.dialog-content label[for=chapter-variant] left', when: onTab('init'), }, { @@ -92,7 +92,7 @@ lichess.studyTourChapter = function (study) { action: tour.next, }, ], - attachTo: '.modal-wrap .help bottom', + attachTo: '.dialog-content .help bottom', }, ].forEach(function (s) { tour.addStep(s.title, s); diff --git a/ui/analyse/css/_keyboard.scss b/ui/analyse/css/_keyboard.scss index cb91f920cde3f..a5ba202364d71 100644 --- a/ui/analyse/css/_keyboard.scss +++ b/ui/analyse/css/_keyboard.scss @@ -1,10 +1,10 @@ -dialog > .keyboard-help { +dialog .keyboard-help { td.mouse li { margin-#{$start-direction}: 1em; list-style: disc inside; } } -dialog > div.variation-help { +dialog div.variation-help { padding: 2em; } diff --git a/ui/analyse/css/study/_modal.scss b/ui/analyse/css/study/_modal.scss index f3cf393abeb17..cf992d91f368e 100644 --- a/ui/analyse/css/study/_modal.scss +++ b/ui/analyse/css/study/_modal.scss @@ -1,4 +1,4 @@ -dialog > div.modal-wrap { +dialog div.dialog-content { min-width: 80vw; @include breakpoint($mq-x-small) { diff --git a/ui/analyse/src/explorer/explorerConfig.ts b/ui/analyse/src/explorer/explorerConfig.ts index 771d5be47c973..fb8ecbb8f57b5 100644 --- a/ui/analyse/src/explorer/explorerConfig.ts +++ b/ui/analyse/src/explorer/explorerConfig.ts @@ -333,7 +333,7 @@ const playerModal = (ctrl: ExplorerConfigCtrl) => { ctrl.data.playerName.open(false); ctrl.root.redraw(); }, - content: [ + vnodes: [ h('h2', 'Personal opening explorer'), h('div.input-wrapper', [ h('input', { diff --git a/ui/analyse/src/keyboard.ts b/ui/analyse/src/keyboard.ts index 15b7325485982..277729ce20177 100644 --- a/ui/analyse/src/keyboard.ts +++ b/ui/analyse/src/keyboard.ts @@ -138,5 +138,6 @@ export function maybeShowVariationArrowHelp(ctrl: AnalyseCtrl) { domDialog({ class: 'help.variation-help', html: { url: '/help/analyse/variation-arrow' }, - }).then(d => d.showModal()); + show: 'modal', + }); } diff --git a/ui/analyse/src/serverSideUnderboard.ts b/ui/analyse/src/serverSideUnderboard.ts index 5d189cfc712e0..7a65e77f4ce4c 100644 --- a/ui/analyse/src/serverSideUnderboard.ts +++ b/ui/analyse/src/serverSideUnderboard.ts @@ -144,6 +144,7 @@ export default function (element: HTMLElement, ctrl: AnalyseCtrl) { const url = `${baseUrl()}/embed/game/${data.game.id}?theme=auto&bg=auto${location.hash}`; const iframe = ``; domDialog({ + show: 'modal', html: { text: '
' + @@ -156,6 +157,6 @@ export default function (element: HTMLElement, ctrl: AnalyseCtrl) { '

' + `Read more about embedding games
`, }, - }).then(d => d.showModal()); + }); }); } diff --git a/ui/analyse/src/study/chapterEditForm.ts b/ui/analyse/src/study/chapterEditForm.ts index 07e38389d8bd5..4f9620e641c74 100644 --- a/ui/analyse/src/study/chapterEditForm.ts +++ b/ui/analyse/src/study/chapterEditForm.ts @@ -94,7 +94,7 @@ export function view(ctrl: StudyChapterEditFormCtrl): VNode | undefined { ctrl.current(null); ctrl.redraw(); }, - content: [ + vnodes: [ h('h2', noarg('editChapter')), h( 'form.form3', diff --git a/ui/analyse/src/study/chapterNewForm.ts b/ui/analyse/src/study/chapterNewForm.ts index 49573e3d7b5c3..be37823bd21bd 100644 --- a/ui/analyse/src/study/chapterNewForm.ts +++ b/ui/analyse/src/study/chapterNewForm.ts @@ -154,7 +154,7 @@ export function view(ctrl: StudyChapterNewFormCtrl): VNode { ctrl.redraw(); }, noClickAway: true, - content: [ + vnodes: [ activeTab === 'edit' ? null : h('h2', [ diff --git a/ui/analyse/src/study/inviteForm.ts b/ui/analyse/src/study/inviteForm.ts index 2acf9641b30e2..50a8015a63ba7 100644 --- a/ui/analyse/src/study/inviteForm.ts +++ b/ui/analyse/src/study/inviteForm.ts @@ -65,7 +65,7 @@ export function view(ctrl: ReturnType): VNode { ctrl.open(false); ctrl.redraw(); }, - content: [ + vnodes: [ h('h2', ctrl.trans.noarg('inviteToTheStudy')), h( 'p.info', diff --git a/ui/analyse/src/study/studyForm.ts b/ui/analyse/src/study/studyForm.ts index 67cd7afa8e0bc..624c46c6fdcf7 100644 --- a/ui/analyse/src/study/studyForm.ts +++ b/ui/analyse/src/study/studyForm.ts @@ -122,7 +122,7 @@ export function view(ctrl: StudyFormCtrl): VNode { ctrl.open(false); ctrl.redraw(); }, - content: [ + vnodes: [ h('h2', ctrl.trans.noarg(ctrl.relay ? 'editRoundStudy' : isNew ? 'createStudy' : 'editStudy')), h( 'form.form3', diff --git a/ui/analyse/src/study/studySearch.ts b/ui/analyse/src/study/studySearch.ts index 916d13405878e..95cfe032065bc 100644 --- a/ui/analyse/src/study/studySearch.ts +++ b/ui/analyse/src/study/studySearch.ts @@ -54,7 +54,7 @@ export function view(ctrl: SearchCtrl) { onClose() { ctrl.open(false); }, - content: [ + vnodes: [ h('input', { attrs: { autofocus: 1, placeholder: `Search in ${ctrl.studyName}`, value: ctrl.query() }, hook: onInsert((el: HTMLInputElement) => { diff --git a/ui/analyse/src/study/topics.ts b/ui/analyse/src/study/topics.ts index 6f8c3744065de..425adb564c37f 100644 --- a/ui/analyse/src/study/topics.ts +++ b/ui/analyse/src/study/topics.ts @@ -49,7 +49,7 @@ export const formView = (ctrl: TopicsCtrl, userId?: string): VNode => ctrl.open(false); ctrl.redraw(); }, - content: [ + vnodes: [ h('h2', ctrl.trans.noarg('topics')), h( 'form', diff --git a/ui/analyse/src/view/actionMenu.ts b/ui/analyse/src/view/actionMenu.ts index aad27ecf251bd..4826fe145881a 100644 --- a/ui/analyse/src/view/actionMenu.ts +++ b/ui/analyse/src/view/actionMenu.ts @@ -163,7 +163,7 @@ export function view(ctrl: AnalyseCtrl): VNode { 'a.button.button-empty', { hook: bind('click', () => - domDialog({ cash: $('.continue-with.g_' + d.game.id) }).then(d => d.showModal()), + domDialog({ show: 'modal', cash: $('.continue-with.g_' + d.game.id) }), ), attrs: dataIcon(licon.Swords), }, diff --git a/ui/cli/src/main.ts b/ui/cli/src/main.ts index cea73099c4e95..94b2951e8bc33 100644 --- a/ui/cli/src/main.ts +++ b/ui/cli/src/main.ts @@ -57,6 +57,7 @@ function help() { domDialog({ cssPath: 'clinput.help', class: 'clinput-help', + show: 'modal', html: { text: '

Commands

' + diff --git a/ui/common/css/component/_dialog.scss b/ui/common/css/component/_dialog.scss index 59043044fcf24..d7ae412dded54 100644 --- a/ui/common/css/component/_dialog.scss +++ b/ui/common/css/component/_dialog.scss @@ -8,18 +8,15 @@ dialog { z-index: z('modal'); padding: 0; border: none; - overflow: visible; - background: $c-bg-popup; + background: $c-bg-high; &::backdrop { background: $c-page-mask; } - > :last-child { - @extend %flex-column; - max-height: 96vh; - padding: 2em; - overflow: hidden; + > .scrollable { + max-height: calc(var(--vh, 1vh) * 100); // ios safari vh fix + overflow-x: clip; overflow-y: auto; } @@ -33,22 +30,20 @@ dialog { @extend %flex-around; position: absolute; - top: 2px; - #{$end-direction}: 2px; + top: 4px; + #{$end-direction}: 4px; width: 32px; height: 32px; - z-index: z('modal') + 1; - background: $c-bg-popup; + background: transparent; border: none; color: $c-font; font-size: 16px; cursor: pointer; - &:hover { - @extend %box-shadow; - background: $c-bad; - color: #fff; + focus { + z-index: z('modal') + 1; + background: $c-bg-high; } &:not(:focus) { outline: none; @@ -56,13 +51,29 @@ dialog { } } -@media (min-width: 720px) and (min-height: 720px) { +.dialog-content { + text-align: center; + padding: 2em; +} + +@media (hover: hover) { + dialog .close-button:hover { + box-shadow: $box-shadow; + background: $c-bad; + color: #fff; + } +} + +@media (min-height: 640px) { dialog { margin-top: 12px; + overflow: visible; } dialog button.close-button { top: -12px; #{$end-direction}: -12px; + z-index: z('modal') + 1; border-radius: 50%; + background: $c-bg-high; } } diff --git a/ui/common/css/component/_help.scss b/ui/common/css/component/_help.scss index bbc273689652c..7df39d7e7abac 100644 --- a/ui/common/css/component/_help.scss +++ b/ui/common/css/component/_help.scss @@ -1,4 +1,4 @@ -dialog > .help { +dialog .help { align-items: center; text-align: center; padding: 0.8em 0; diff --git a/ui/common/src/dialog.ts b/ui/common/src/dialog.ts index 61a281849813b..ac9e37c72e415 100644 --- a/ui/common/src/dialog.ts +++ b/ui/common/src/dialog.ts @@ -1,15 +1,12 @@ import { VNode, Attrs } from 'snabbdom'; import { onInsert, h, MaybeVNodes } from './snabbdom'; import { spinnerVdom } from './spinner'; -import { isTouchDevice } from './mobile'; import * as xhr from './xhr'; import * as licon from './licon'; export interface Dialog { readonly open: boolean; - readonly returnValue: string | undefined; readonly view: HTMLElement; - showModal(): void; show(): void; close(): void; @@ -27,12 +24,13 @@ interface DialogOpts { } export interface SnabDialogOpts extends DialogOpts { - content?: MaybeVNodes; + vnodes?: MaybeVNodes; onInsert?: (dialog: Dialog) => void; // prevents showModal, caller must do so manually } export interface DomDialogOpts extends DialogOpts { parent?: HTMLElement | Cash; // TODO - for positioning, need to fix css to be useful + show?: 'modal' | boolean; // auto-show and remove from dom when closed, no reshow } export function snabDialog(o: SnabDialogOpts): VNode { @@ -56,20 +54,23 @@ export function snabDialog(o: SnabDialogOpts): VNode { }), ), h( - 'div.modal-wrap' + (o.class ? `.${o.class}` : ''), - { - attrs: o.attrs?.view, - hook: onInsert(async view => { - const [html] = await ass; - if (html) view.innerHTML = html; - - const dlg = makeDialog(dialog, view, o); - - if (o.onInsert) o.onInsert(dlg); - else dlg.showModal(); - }), - }, - o.content ?? spinnerVdom(), + 'div.scrollable', + h( + 'div.dialog-content' + (o.class ? `.${o.class}` : ''), + { + attrs: o.attrs?.view, + hook: onInsert(async view => { + const [html] = await ass; + if (html && !o.vnodes) view.innerHTML = html; + + const wrapper = new DialogWrapper(dialog, view, o); + + if (o.onInsert) o.onInsert(wrapper); + else wrapper.showModal(); + }), + }, + o.vnodes ?? spinnerVdom(), + ), ), ], ); @@ -81,71 +82,76 @@ export async function domDialog(o: DomDialogOpts): Promise { const dialog = document.createElement('dialog'); for (const [k, v] of Object.entries(o.attrs?.dialog ?? {})) dialog.setAttribute(k, String(v)); - const view = document.createElement('div'); - view.classList.add('modal-wrap', ...(o.class ?? '').split('.')); + const scrollable = $as('
'); + const view = $as('
'); + view.classList.add(...(o.class ?? '').split('.')); for (const [k, v] of Object.entries(o.attrs?.view ?? {})) view.setAttribute(k, String(v)); - if (html) view.innerHTML = html; + scrollable.appendChild(view); if (!o.noCloseButton) { - const anchor = $as('
'); - const btn = anchor.appendChild( - $as(`
`, ); - console.log(anchor); + anchor.querySelector('button')?.addEventListener('click', () => dialog.close()); dialog.appendChild(anchor); - btn.addEventListener('click', () => dialog.close()); + } + dialog.appendChild(scrollable); + if (!o.parent) document.body.appendChild(dialog); + else { + $(o.parent).append(dialog); + dialog.style.position = 'absolute'; } - dialog.appendChild(view); - if (o.parent) $(o.parent).append(dialog); - else document.body.appendChild(dialog); + const wrapper = new DialogWrapper(dialog, view, o); + if (o.show && o.show === 'modal') wrapper.showModal(); + else if (o.show) wrapper.show(); - return makeDialog(dialog, view, o); + return wrapper; } -function makeDialog(dialog: HTMLDialogElement, view: HTMLElement, o: DialogOpts): Dialog { - let modalListeners: { [event: string]: (e: Event) => void } = { - keydown: onModalKeydown, - touchmove: (e: TouchEvent) => isTouchDevice() && e.preventDefault(), +class DialogWrapper implements Dialog { + constructor( + readonly dialog: HTMLDialogElement, + readonly view: HTMLElement, + readonly o: DialogOpts, + ) { + dialog.addEventListener('close', () => this.onClose()); + if ('show' in o && o.show) dialog.addEventListener('close', dialog.remove); + if (!o.noClickAway) dialog.addEventListener('click', () => dialog.close()); + view.addEventListener('click', e => e.stopPropagation()); + this.onResize(); // safari vh + } + get open() { + return this.dialog.open; + } + + show = () => this.dialog.show(); + close = () => this.dialog.close(); + + onResize = () => this.view.style.setProperty('--vh', `${window.innerHeight * 0.01}px`); + onClose = () => { + window.removeEventListener('resize', this.onResize); + this.o.onClose?.(this); }; - function show(modal: boolean) { - const focii = Array.from($(focusQuery, view)) as HTMLElement[]; - if (focii.length) (focii.length > 1 ? focii[1] : focii[0]).focus(); - if (modal) { - Object.entries(modalListeners).forEach(([e, l]) => dialog.addEventListener(e, l)); - modalListeners = {}; // only add them once, dialog may be reshown - } - dialog.returnValue = ''; - view.scrollTop = 0; - - if (modal) dialog.showModal(); - else dialog.show(); - } + showModal = () => { + const focii = Array.from($(focusQuery, this.view)) as HTMLElement[]; + if (focii.length > 1) focii[1].focus(); // skip close button + else if (focii.length) focii[0].focus(); + window.addEventListener('resize', this.onResize); + + this.addModalListeners?.(); + this.view.scrollTop = 0; + this.dialog.showModal(); + }; - return new (class implements Dialog { - constructor(readonly view: HTMLElement) { - dialog.addEventListener('close', () => o.onClose?.(this)); - if (!o.noClickAway) dialog.addEventListener('click', () => dialog.close()); - view.addEventListener('click', e => e.stopPropagation()); - } - show = () => show(false); - showModal = () => show(true); - close = () => dialog.close(); - get open() { - return dialog.open; - } - get returnValue() { - return dialog.returnValue; - } - get html() { - return view.innerHTML; - } - set html(html: string) { - view.innerHTML = html; - } - })(view); + addModalListeners? = () => { + this.dialog.addEventListener('keydown', onModalKeydown); + this.dialog.addEventListener('touchmove', (e: TouchEvent) => e.stopPropagation()); + this.addModalListeners = undefined; // only do this once + }; } function assets(o: DialogOpts) { diff --git a/ui/editor/src/view.ts b/ui/editor/src/view.ts index ea65d8a07ae37..16cb98a0e16c4 100644 --- a/ui/editor/src/view.ts +++ b/ui/editor/src/view.ts @@ -274,10 +274,7 @@ function controls(ctrl: EditorCtrl, state: EditorState): VNode { }, on: { click: () => { - if (state.playable) - domDialog({ - cash: $('.continue-with'), - }).then(d => d.showModal()); + if (state.playable) domDialog({ cash: $('.continue-with'), show: 'modal' }); }, }, }, diff --git a/ui/gameSetup/css/_bot-stuff.scss b/ui/gameSetup/css/_bot-stuff.scss index d6259714c492f..503e4baf0562f 100644 --- a/ui/gameSetup/css/_bot-stuff.scss +++ b/ui/gameSetup/css/_bot-stuff.scss @@ -1,15 +1,11 @@ -.local-setup { - @extend %flex-column; +dialog .local-setup { width: 96vw; - //height: min(96vh, 640px); @include breakpoint($mq-x-small) { width: 80vw; - //height: min(88vh, 640px); } @include breakpoint($mq-large) { width: 60vw; - //height: min(80vh, 640px); } h2 { diff --git a/ui/gameSetup/css/_game-setup.scss b/ui/gameSetup/css/_game-setup.scss index 614c12a7a3c44..597a4569226e1 100644 --- a/ui/gameSetup/css/_game-setup.scss +++ b/ui/gameSetup/css/_game-setup.scss @@ -1,16 +1,12 @@ $c-setup: $c-secondary; $c-slider: $c-setup; -.game-setup { +dialog div.game-setup { width: 30em; + padding: 0; text-align: center; - > div { - padding: 0; - max-height: 96vh; - } - h2 { margin: 1.5rem 0; } @@ -142,11 +138,6 @@ $c-slider: $c-setup; } } - > div { - padding: 0; - max-height: 96vh; - } - .label-select { &.variant { margin-bottom: 1em; diff --git a/ui/gameSetup/src/view/modal.ts b/ui/gameSetup/src/view/modal.ts index ad0bef03ab590..0951eb99b4f5d 100644 --- a/ui/gameSetup/src/view/modal.ts +++ b/ui/gameSetup/src/view/modal.ts @@ -20,6 +20,6 @@ export default function setupModal(ctrl: SetupCtrl): MaybeVNode { class: ctrl.gameType === 'local' ? 'game-setup.local-setup' : 'game-setup', cssPath: 'game-setup', onClose: ctrl.closeModal, - content: renderContent(ctrl), + vnodes: renderContent(ctrl), }); } diff --git a/ui/keyboardMove/css/_keyboardMove.scss b/ui/keyboardMove/css/_keyboardMove.scss index 16109b305ed1e..266d79d9535ce 100644 --- a/ui/keyboardMove/css/_keyboardMove.scss +++ b/ui/keyboardMove/css/_keyboardMove.scss @@ -22,7 +22,7 @@ } } -dialog > .keyboard-move-help { +dialog .keyboard-move-help { td.tips li { list-style: disc; margin-#{$start-direction}: 2em; diff --git a/ui/round/src/socket.ts b/ui/round/src/socket.ts index cb74ba6f05d21..10bb9934bae66 100644 --- a/ui/round/src/socket.ts +++ b/ui/round/src/socket.ts @@ -146,10 +146,11 @@ export function make(send: SocketSend, ctrl: RoundController): RoundSocket { }, simulEnd(simul: game.Simul) { domDialog({ + show: 'modal', html: { text: ``, }, - }).then(d => d.showModal()); + }); }, }; diff --git a/ui/simul/src/view/created.ts b/ui/simul/src/view/created.ts index e50cfa751493c..bee5ece02bcd0 100644 --- a/ui/simul/src/view/created.ts +++ b/ui/simul/src/view/created.ts @@ -53,9 +53,10 @@ export default function (showText: (ctrl: SimulCtrl) => MaybeVNode) { xhr.join(ctrl.data.id, ctrl.data.variants[0].key); else domDialog({ + show: 'modal', cash: $('.simul .continue-with'), onClose: dlg => xhr.join(ctrl.data.id, $(dlg.view).data('variant')), - }).then(d => d.showModal()); + }); }) : {}, }, diff --git a/ui/site/src/publicChats.ts b/ui/site/src/publicChats.ts index 2ba7d39d78a1d..2d49cd5d11721 100644 --- a/ui/site/src/publicChats.ts +++ b/ui/site/src/publicChats.ts @@ -41,7 +41,6 @@ lichess.load.then(() => { $('#communication').on('click', '.line:not(.lichess)', function (this: HTMLDivElement) { const $l = $(this); domDialog({ cash: $('.timeout-modal') }).then(dlg => { - const $wrap = $(dlg.view); $('.username', dlg.view).text($l.find('.user-link').text()); $('.text', dlg.view).text($l.text().split(' ').slice(1).join(' ')); $('.button', dlg.view).on('click', function (this: HTMLButtonElement) { @@ -52,9 +51,9 @@ lichess.load.then(() => { body: form({ roomId, chan, - userId: $wrap.find('.username').text().toLowerCase(), + userId: $('.username', dlg.view).text().toLowerCase(), reason: this.value, - text: $wrap.find('.text').text(), + text: $('.text', dlg.view).text(), }), }).then(_ => setTimeout(reloadNow, 1000)); dlg.close(); diff --git a/ui/tournament/src/view/battle.ts b/ui/tournament/src/view/battle.ts index 6c9e6c746c3fa..9437149ed145e 100644 --- a/ui/tournament/src/view/battle.ts +++ b/ui/tournament/src/view/battle.ts @@ -21,7 +21,7 @@ export function joinWithTeamSelector(ctrl: TournamentController) { dlg.showModal(); }, onClose, - content: [ + vnodes: [ h('div.team-picker', [ h('h2', 'Pick your team'), h('br'), diff --git a/ui/voice/css/_voiceMove.help.scss b/ui/voice/css/_voiceMove.help.scss index 48f69c4490e5c..bbde82191d52f 100644 --- a/ui/voice/css/_voiceMove.help.scss +++ b/ui/voice/css/_voiceMove.help.scss @@ -1,4 +1,4 @@ -dialog > .voice-move-help { +dialog .voice-move-help { td.tips li { list-style: disc; font-size: 1.1em; diff --git a/ui/voice/src/view.ts b/ui/voice/src/view.ts index 5b6a0680a1262..103632415b4fe 100644 --- a/ui/voice/src/view.ts +++ b/ui/voice/src/view.ts @@ -168,24 +168,21 @@ function renderHelpModal(ctrl: VoiceCtrl) { dlg.view.innerHTML = html; if (!dlg.open) dlg.showModal(); }; + isIOS; return snabDialog({ class: 'help.voice-move-help', html: { url: `/help/voice/${ctrl.moduleId}` }, cssPath: 'voiceMove.help', - attrs: { - view: { - style: isIOS() ? `padding-bottom: ${window.screen.availHeight - window.innerHeight}px` : '', - }, - }, onClose: () => ctrl.showHelp(false), onInsert: async dlg => { if (ctrl.showHelp() === 'list') { showMoveList(dlg); return; } - const grammar = await (ctrl.moduleId !== 'coords' - ? xhr.jsonSimple(lichess.assetUrl(`compiled/grammar/${ctrl.moduleId}-${ctrl.lang()}.json`)) - : Promise.resolve({ entries: [] })); + const grammar = + ctrl.moduleId === 'coords' + ? [] + : await xhr.jsonSimple(lichess.assetUrl(`compiled/grammar/${ctrl.moduleId}-${ctrl.lang()}.json`)); // TODO - fix using lexicon instead of crowdin translations for moves/commands const valToWord = (val: string, phonetic: boolean) => From 52ae825dc51c87bbf7841b1e64f5391fac929cca Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Tue, 5 Sep 2023 20:03:11 -0500 Subject: [PATCH 051/174] . --- .gitignore | 9 +- app/templating/AssetHelper.scala | 4 +- pnpm-lock.yaml | 9 +- public/javascripts/chessground.min.js | 1 + ui/analyse/src/study/topics.ts | 2 +- ui/ceval/src/ctrl.ts | 8 +- ui/chart/src/common.ts | 2 +- ui/common/css/component/_dialog.scss | 63 ++++++------ ui/common/package.json | 7 ++ ui/common/src/dialog.ts | 135 +++++++++++++++----------- ui/common/src/linkPopup.ts | 2 + ui/site/package.json | 10 +- ui/site/src/component/assets.ts | 6 +- ui/site/src/component/mic.ts | 12 +-- ui/site/src/gameSearch.ts | 2 - ui/voice/src/view.ts | 3 +- 16 files changed, 148 insertions(+), 127 deletions(-) create mode 100644 public/javascripts/chessground.min.js diff --git a/.gitignore b/.gitignore index e362640fff62b..a3c6df2d0431d 100644 --- a/.gitignore +++ b/.gitignore @@ -14,14 +14,7 @@ project/metals.sbt project/project project/target public/compiled -public/javascripts/chessground.min.js -public/vendor/highcharts-4.2.5 -public/vendor/hopscotch -public/vendor/tagify -public/vendor/stockfish.wasm -public/vendor/stockfish-nnue.wasm -public/vendor/stockfish-mv.wasm -public/vendor/stockfish.js +public/npm public/lifat public/css/ target diff --git a/app/templating/AssetHelper.scala b/app/templating/AssetHelper.scala index 6bcda0cdc1d27..1077461ccfbc3 100644 --- a/app/templating/AssetHelper.scala +++ b/app/templating/AssetHelper.scala @@ -84,8 +84,8 @@ if (window.matchMedia('(prefers-color-scheme: dark)').media === 'not all') def captchaTag = jsModule("captcha") def cashTag = iifeModule("javascripts/vendor/cash.min.js") def fingerprintTag = iifeModule("javascripts/fipr.js") - def highchartsLatestTag = iifeModule("vendor/highcharts-4.2.5/highcharts.js") - def highchartsMoreTag = iifeModule("vendor/highcharts-4.2.5/highcharts-more.js") + def highchartsLatestTag = iifeModule("npm/highcharts-4.2.5/highcharts.js") + def highchartsMoreTag = iifeModule("npm/highcharts-4.2.5/highcharts-more.js") def chessgroundTag = script(tpe := "module", src := assetUrl("javascripts/chessground.min.js")) def prismicJs(using PageContext): Frag = diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 406eabea88624..31af830fbfc1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.1' +lockfileVersion: '6.0' settings: autoInstallPeers: true @@ -214,6 +214,9 @@ importers: ui/common: dependencies: + dialog-polyfill: + specifier: 0.5.6 + version: 0.5.6 lichess-pgn-viewer: specifier: ^2.0.0 version: 2.0.0 @@ -2967,6 +2970,10 @@ packages: engines: {node: '>=8'} dev: false + /dialog-polyfill@0.5.6: + resolution: {integrity: sha512-ZbVDJI9uvxPAKze6z146rmfUZjBqNEwcnFTVamQzXH+svluiV7swmVIGr7miwADgfgt1G2JQIytypM9fbyhX4w==} + dev: false + /diff-sequences@29.3.1: resolution: {integrity: sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} diff --git a/public/javascripts/chessground.min.js b/public/javascripts/chessground.min.js new file mode 100644 index 0000000000000..8099a1eb8a3fb --- /dev/null +++ b/public/javascripts/chessground.min.js @@ -0,0 +1 @@ +var Ve=["white","black"],z=["a","b","c","d","e","f","g","h"],j=["1","2","3","4","5","6","7","8"];var Le=[...j].reverse(),re=Array.prototype.concat(...z.map(e=>j.map(o=>e+o))),S=e=>re[8*e[0]+e[1]],g=e=>[e.charCodeAt(0)-97,e.charCodeAt(1)-49];var te=re.map(g);function Ge(e){let o,r=()=>(o===void 0&&(o=e()),o);return r.clear=()=>{o=void 0},r}var Fe=()=>{let e;return{start(){e=performance.now()},cancel(){e=void 0},stop(){if(!e)return 0;let o=performance.now()-e;return e=void 0,o}}},ne=e=>e==="white"?"black":"white",V=(e,o)=>{let r=e[0]-o[0],t=e[1]-o[1];return r*r+t*t},U=(e,o)=>e.role===o.role&&e.color===o.color,B=e=>(o,r)=>[(r?o[0]:7-o[0])*e.width/8,(r?7-o[1]:o[1])*e.height/8],x=(e,o)=>{e.style.transform=`translate(${o[0]}px,${o[1]}px)`},be=(e,o,r=1)=>{e.style.transform=`translate(${o[0]}px,${o[1]}px) scale(${r})`},Z=(e,o)=>{e.style.visibility=o?"visible":"hidden"},T=e=>{if(e.clientX||e.clientX===0)return[e.clientX,e.clientY];if(e.targetTouches?.[0])return[e.targetTouches[0].clientX,e.targetTouches[0].clientY]},ie=e=>e.buttons===2||e.button===2,K=(e,o)=>{let r=document.createElement(e);return o&&(r.className=o),r};function ae(e,o,r){let t=g(e);return o||(t[0]=7-t[0],t[1]=7-t[1]),[r.left+r.width*t[0]/8+r.width/16,r.top+r.height*(7-t[1])/8+r.height/16]}var L=(e,o)=>Math.abs(e-o),Go=e=>(o,r,t,n)=>L(o,t)<2&&(e==="white"?n===r+1||r<=1&&n===r+2&&o===t:n===r-1||r>=6&&n===r-2&&o===t),he=(e,o,r,t)=>{let n=L(e,r),i=L(o,t);return n===1&&i===2||n===2&&i===1},We=(e,o,r,t)=>L(e,r)===L(o,t),ze=(e,o,r,t)=>e===r||o===t,ve=(e,o,r,t)=>We(e,o,r,t)||ze(e,o,r,t),Fo=(e,o,r)=>(t,n,i,a)=>L(t,i)<2&&L(n,a)<2||r&&n===a&&n===(e==="white"?0:7)&&(t===4&&(i===2&&o.includes(0)||i===6&&o.includes(7))||o.includes(i));function Wo(e,o){let r=o==="white"?"1":"8",t=[];for(let[n,i]of e)n[1]===r&&i.color===o&&i.role==="rook"&&t.push(g(n)[0]);return t}function ye(e,o,r){let t=e.get(o);if(!t)return[];let n=g(o),i=t.role,a=i==="pawn"?Go(t.color):i==="knight"?he:i==="bishop"?We:i==="rook"?ze:i==="queen"?ve:Fo(t.color,Wo(e,t.color),r);return te.filter(c=>(n[0]!==c[0]||n[1]!==c[1])&&a(n[0],n[1],c[0],c[1])).map(S)}function P(e,...o){e&&setTimeout(()=>e(...o),1)}function $e(e){e.orientation=ne(e.orientation),e.animation.current=e.draggable.current=e.selected=void 0}function Ie(e,o){for(let[r,t]of o)t?e.pieces.set(r,t):e.pieces.delete(r)}function je(e,o){if(e.check=void 0,o===!0&&(o=e.turnColor),o)for(let[r,t]of e.pieces)t.role==="king"&&t.color===o&&(e.check=r)}function zo(e,o,r,t){k(e),e.premovable.current=[o,r],P(e.premovable.events.set,o,r,t)}function D(e){e.premovable.current&&(e.premovable.current=void 0,P(e.premovable.events.unset))}function $o(e,o,r){D(e),e.predroppable.current={role:o,key:r},P(e.predroppable.events.set,o,r)}function k(e){let o=e.predroppable;o.current&&(o.current=void 0,P(o.events.unset))}function Io(e,o,r){if(!e.autoCastle)return!1;let t=e.pieces.get(o);if(!t||t.role!=="king")return!1;let n=g(o),i=g(r);if(n[1]!==0&&n[1]!==7||n[1]!==i[1])return!1;n[0]===4&&!e.pieces.has(r)&&(i[0]===6?r=S([7,i[1]]):i[0]===2&&(r=S([0,i[1]])));let a=e.pieces.get(r);return!a||a.color!==t.color||a.role!=="rook"?!1:(e.pieces.delete(o),e.pieces.delete(r),n[0]o!==r&&Ze(e,o)&&(e.movable.free||!!e.movable.dests?.get(o)?.includes(r));function jo(e,o,r){let t=e.pieces.get(o);return!!t&&(o===r||!e.pieces.has(r))&&(e.movable.color==="both"||e.movable.color===t.color&&e.turnColor===t.color)}function Me(e,o){let r=e.pieces.get(o);return!!r&&e.premovable.enabled&&e.movable.color===r.color&&e.turnColor!==r.color}function Uo(e,o,r){let t=e.premovable.customDests?.get(o)??ye(e.pieces,o,e.premovable.castle);return o!==r&&Me(e,o)&&t.includes(r)}function Zo(e,o,r){let t=e.pieces.get(o),n=e.pieces.get(r);return!!t&&(!n||n.color!==e.movable.color)&&e.predroppable.enabled&&(t.role!=="pawn"||r[1]!=="1"&&r[1]!=="8")&&e.movable.color===t.color&&e.turnColor!==t.color}function Qe(e,o){let r=e.pieces.get(o);return!!r&&e.draggable.enabled&&(e.movable.color==="both"||e.movable.color===r.color&&(e.turnColor===r.color||e.premovable.enabled))}function Xe(e){let o=e.premovable.current;if(!o)return!1;let r=o[0],t=o[1],n=!1;if(le(e,r,t)){let i=Ue(e,r,t);if(i){let a={premove:!0};i!==!0&&(a.captured=i),P(e.movable.events.after,r,t,a),n=!0}}return D(e),n}function Ye(e,o){let r=e.predroppable.current,t=!1;if(!r)return!1;if(o(r)){let n={role:r.role,color:e.movable.color};ce(e,n,r.key)&&(P(e.movable.events.afterNewPiece,r.role,r.key,{premove:!1,predrop:!0}),t=!0)}return k(e),t}function Y(e){D(e),k(e),w(e)}function xe(e){e.movable.color=e.movable.dests=e.animation.current=void 0,Y(e)}function E(e,o,r){let t=Math.floor(8*(e[0]-r.left)/r.width);o||(t=7-t);let n=7-Math.floor(8*(e[1]-r.top)/r.height);return o||(n=7-n),t>=0&&t<8&&n>=0&&n<8?S([t,n]):void 0}function Je(e,o,r,t){let n=g(e),i=te.filter(s=>ve(n[0],n[1],s[0],s[1])||he(n[0],n[1],s[0],s[1])),c=i.map(s=>ae(S(s),r,t)).map(s=>V(o,s)),[,l]=c.reduce((s,f,d)=>s[0]e.orientation==="white";var Ce="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR",Qo={p:"pawn",r:"rook",n:"knight",b:"bishop",q:"queen",k:"king"},Xo={pawn:"p",rook:"r",knight:"n",bishop:"b",queen:"q",king:"k"};function de(e){e==="start"&&(e=Ce);let o=new Map,r=7,t=0;for(let n of e)switch(n){case" ":case"[":return o;case"/":if(--r,r<0)return o;t=0;break;case"~":{let i=o.get(S([t-1,r]));i&&(i.promoted=!0);break}default:{let i=n.charCodeAt(0);if(i<57)t+=i-48;else{let a=n.toLowerCase();o.set(S([t,r]),{role:Qo[a],color:n===a?"black":"white"}),++t}}}return o}function _e(e){return Le.map(o=>z.map(r=>{let t=e.get(r+o);if(t){let n=Xo[t.role];return t.color==="white"&&(n=n.toUpperCase()),t.promoted&&(n+="~"),n}else return"1"}).join("")).join("/").replace(/1{2,}/g,o=>o.length.toString())}function De(e,o){o.animation&&(ke(e.animation,o.animation),(e.animation.duration||0)<70&&(e.animation.enabled=!1))}function ue(e,o){if(o.movable?.dests&&(e.movable.dests=void 0),o.drawable?.autoShapes&&(e.drawable.autoShapes=[]),ke(e,o),o.fen&&(e.pieces=de(o.fen),e.drawable.shapes=o.drawable?.shapes||[]),"check"in o&&je(e,o.check||!1),"lastMove"in o&&!o.lastMove?e.lastMove=void 0:o.lastMove&&(e.lastMove=o.lastMove),e.selected&&Pe(e,e.selected),De(e,o),!e.movable.rookCastle&&e.movable.dests){let r=e.movable.color==="white"?"1":"8",t="e"+r,n=e.movable.dests.get(t),i=e.pieces.get(t);if(!n||!i||i.role!=="king")return;e.movable.dests.set(t,n.filter(a=>!(a==="a"+r&&n.includes("c"+r))&&!(a==="h"+r&&n.includes("g"+r))))}}function ke(e,o){for(let r in o)Object.prototype.hasOwnProperty.call(o,r)&&(Object.prototype.hasOwnProperty.call(e,r)&&eo(e[r])&&eo(o[r])?ke(e[r],o[r]):e[r]=o[r])}function eo(e){if(typeof e!="object"||e===null)return!1;let o=Object.getPrototypeOf(e);return o===Object.prototype||o===null}var R=(e,o)=>o.animation.enabled?er(e,o):q(e,o);function q(e,o){let r=e(o);return o.dom.redraw(),r}var Ee=(e,o)=>({key:e,pos:g(e),piece:o}),Jo=(e,o)=>o.sort((r,t)=>V(e.pos,r.pos)-V(e.pos,t.pos))[0];function _o(e,o){let r=new Map,t=[],n=new Map,i=[],a=[],c=new Map,l,s,f;for(let[d,p]of e)c.set(d,Ee(d,p));for(let d of re)l=o.pieces.get(d),s=c.get(d),l?s?U(l,s.piece)||(i.push(s),a.push(Ee(d,l))):a.push(Ee(d,l)):s&&i.push(s);for(let d of a)s=Jo(d,i.filter(p=>U(d.piece,p.piece))),s&&(f=[s.pos[0]-d.pos[0],s.pos[1]-d.pos[1]],r.set(d.key,f.concat(f)),t.push(s.key));for(let d of i)t.includes(d.key)||n.set(d.key,d.piece);return{anims:r,fadings:n}}function oo(e,o){let r=e.animation.current;if(r===void 0){e.dom.destroyed||e.dom.redrawNow();return}let t=1-(o-r.start)*r.frequency;if(t<=0)e.animation.current=void 0,e.dom.redrawNow();else{let n=or(t);for(let i of r.plan.anims.values())i[2]=i[0]*n,i[3]=i[1]*n;e.dom.redrawNow(!0),requestAnimationFrame((i=performance.now())=>oo(e,i))}}function er(e,o){let r=new Map(o.pieces),t=e(o),n=_o(r,o);if(n.anims.size||n.fadings.size){let i=o.animation.current&&o.animation.current.start;o.animation.current={start:performance.now(),frequency:1/o.animation.duration,plan:n},i||oo(o,performance.now())}else o.dom.redraw();return t}var or=e=>e<.5?4*e*e*e:(e-1)*(2*e-2)*(2*e-2)+1;var rr=["green","red","blue","yellow"];function ro(e,o){if(o.touches&&o.touches.length>1)return;o.stopPropagation(),o.preventDefault(),o.ctrlKey?w(e):Y(e);let r=T(o),t=E(r,b(e),e.dom.bounds());t&&(e.drawable.current={orig:t,pos:r,brush:tr(o),snapToValidMove:e.drawable.defaultSnapToValidMove},to(e))}function to(e){requestAnimationFrame(()=>{let o=e.drawable.current;if(o){let r=E(o.pos,b(e),e.dom.bounds());r||(o.snapToValidMove=!1);let t=o.snapToValidMove?Je(o.orig,o.pos,b(e),e.dom.bounds()):r;t!==o.mouseSq&&(o.mouseSq=t,o.dest=t!==o.orig?t:void 0,e.dom.redrawNow()),to(e)}})}function no(e,o){e.drawable.current&&(e.drawable.current.pos=T(o))}function io(e){let o=e.drawable.current;o&&(o.mouseSq&&nr(e.drawable,o),Ae(e))}function Ae(e){e.drawable.current&&(e.drawable.current=void 0,e.dom.redraw())}function ao(e){e.drawable.shapes.length&&(e.drawable.shapes=[],e.dom.redraw(),co(e.drawable))}function tr(e){let o=(e.shiftKey||e.ctrlKey)&&ie(e),r=e.altKey||e.metaKey||e.getModifierState?.("AltGraph");return rr[(o?1:0)+(r?2:0)]}function nr(e,o){let r=n=>n.orig===o.orig&&n.dest===o.dest,t=e.shapes.find(r);t&&(e.shapes=e.shapes.filter(n=>!r(n))),(!t||t.brush!==o.brush)&&e.shapes.push({orig:o.orig,dest:o.dest,brush:o.brush}),co(e)}function co(e){e.onChange&&e.onChange(e.shapes)}function so(e,o){if(!(e.trustAllEvents||o.isTrusted)||o.button!==void 0&&o.button!==0||o.touches&&o.touches.length>1)return;let r=e.dom.bounds(),t=T(o),n=E(t,b(e),r);if(!n)return;let i=e.pieces.get(n),a=e.selected;if(!a&&e.drawable.enabled&&(e.drawable.eraseOnClick||!i||i.color!==e.turnColor)&&ao(e),o.cancelable!==!1&&(!o.touches||e.blockTouchScroll||i||a||ar(e,t)))o.preventDefault();else if(o.touches)return;let c=!!e.premovable.current,l=!!e.predroppable.current;e.stats.ctrlKey=o.ctrlKey,e.selected&&le(e,e.selected,n)?R(d=>X(d,n),e):X(e,n);let s=e.selected===n,f=go(e,n);if(i&&f&&s&&Qe(e,n)){e.draggable.current={orig:n,piece:i,origPos:t,pos:t,started:e.draggable.autoDistance&&e.stats.dragged,element:f,previouslySelected:a,originTarget:o.target,keyHasChanged:!1},f.cgDragging=!0,f.classList.add("dragging");let d=e.dom.elements.ghost;d&&(d.className=`ghost ${i.color} ${i.role}`,x(d,B(r)(g(n),b(e))),Z(d,!0)),Ne(e)}else c&&D(e),l&&k(e);e.dom.redraw()}function ar(e,o){let r=b(e),t=e.dom.bounds(),n=Math.pow(t.width/8,2);for(let i of e.pieces.keys()){let a=ae(i,r,t);if(V(a,o)<=n)return!0}return!1}function lo(e,o,r,t){let n="a0";e.pieces.set(n,o),e.dom.redraw();let i=T(r);e.draggable.current={orig:n,piece:o,origPos:i,pos:i,started:!0,element:()=>go(e,n),originTarget:r.target,newPiece:!0,force:!!t,keyHasChanged:!1},Ne(e)}function Ne(e){requestAnimationFrame(()=>{let o=e.draggable.current;if(!o)return;e.animation.current?.plan.anims.has(o.orig)&&(e.animation.current=void 0);let r=e.pieces.get(o.orig);if(!r||!U(r,o.piece))G(e);else if(!o.started&&V(o.pos,o.origPos)>=Math.pow(e.draggable.distance,2)&&(o.started=!0),o.started){if(typeof o.element=="function"){let n=o.element();if(!n)return;n.cgDragging=!0,n.classList.add("dragging"),o.element=n}let t=e.dom.bounds();x(o.element,[o.pos[0]-t.left-t.width/16,o.pos[1]-t.top-t.height/16]),o.keyHasChanged||=o.orig!==E(o.pos,b(e),t)}Ne(e)})}function uo(e,o){e.draggable.current&&(!o.touches||o.touches.length<2)&&(e.draggable.current.pos=T(o))}function po(e,o){let r=e.draggable.current;if(!r)return;if(o.type==="touchend"&&o.cancelable!==!1&&o.preventDefault(),o.type==="touchend"&&r.originTarget!==o.target&&!r.newPiece){e.draggable.current=void 0;return}D(e),k(e);let t=T(o)||r.pos,n=E(t,b(e),e.dom.bounds());n&&r.started&&r.orig!==n?r.newPiece?se(e,r.orig,n,r.force):(e.stats.ctrlKey=o.ctrlKey,we(e,r.orig,n)&&(e.stats.dragged=!0)):r.newPiece?e.pieces.delete(r.orig):e.draggable.deleteOnDropOff&&!n&&(e.pieces.delete(r.orig),P(e.events.change)),(r.orig===r.previouslySelected||r.keyHasChanged)&&(r.orig===n||!n)?w(e):e.selectable.enabled||w(e),fo(e),e.draggable.current=void 0,e.dom.redraw()}function G(e){let o=e.draggable.current;o&&(o.newPiece&&e.pieces.delete(o.orig),e.draggable.current=void 0,w(e),fo(e),e.dom.redraw())}function fo(e){let o=e.dom.elements;o.ghost&&Z(o.ghost,!1)}function go(e,o){let r=e.dom.elements.board.firstChild;for(;r;){if(r.cgKey===o&&r.tagName==="PIECE")return r;r=r.nextSibling}}function bo(e,o){e.exploding={stage:1,keys:o},e.dom.redraw(),setTimeout(()=>{mo(e,2),setTimeout(()=>mo(e,void 0),120)},120)}function mo(e,o){e.exploding&&(o?e.exploding.stage=o:e.exploding=void 0,e.dom.redraw())}function ho(e,o){function r(){$e(e),o()}return{set(t){t.orientation&&t.orientation!==e.orientation&&r(),De(e,t),(t.fen?R:q)(n=>ue(n,t),e)},state:e,getFen:()=>_e(e.pieces),toggleOrientation:r,setPieces(t){R(n=>Ie(n,t),e)},selectSquare(t,n){t?R(i=>X(i,t,n),e):e.selected&&(w(e),e.dom.redraw())},move(t,n){R(i=>Se(i,t,n),e)},newPiece(t,n){R(i=>ce(i,t,n),e)},playPremove(){if(e.premovable.current){if(R(Xe,e))return!0;e.dom.redraw()}return!1},playPredrop(t){if(e.predroppable.current){let n=Ye(e,t);return e.dom.redraw(),n}return!1},cancelPremove(){q(D,e)},cancelPredrop(){q(k,e)},cancelMove(){q(t=>{Y(t),G(t)},e)},stop(){q(t=>{xe(t),G(t)},e)},explode(t){bo(e,t)},setAutoShapes(t){q(n=>n.drawable.autoShapes=t,e)},setShapes(t){q(n=>n.drawable.shapes=t,e)},getKeyAtDomPos(t){return E(t,b(e),e.dom.bounds())},redrawAll:o,dragNewPiece(t,n,i){lo(e,t,n,i)},destroy(){xe(e),e.dom.unbind&&e.dom.unbind(),e.dom.destroyed=!0}}}function vo(){return{pieces:de(Ce),orientation:"white",turnColor:"white",coordinates:!0,ranksPosition:"right",autoCastle:!0,viewOnly:!1,disableContextMenu:!1,addPieceZIndex:!1,blockTouchScroll:!1,pieceKey:!1,trustAllEvents:!1,highlight:{lastMove:!0,check:!0},animation:{enabled:!0,duration:200},movable:{free:!0,color:"both",showDests:!0,events:{},rookCastle:!0},premovable:{enabled:!0,showDests:!0,castle:!0,events:{}},predroppable:{enabled:!1,events:{}},draggable:{enabled:!0,distance:3,autoDistance:!0,showGhost:!0,deleteOnDropOff:!1},dropmode:{active:!1},selectable:{enabled:!0},stats:{dragged:!("ontouchstart"in window)},events:{},drawable:{enabled:!0,visible:!0,defaultSnapToValidMove:!0,eraseOnClick:!0,shapes:[],autoShapes:[],brushes:{green:{key:"g",color:"#15781B",opacity:1,lineWidth:10},red:{key:"r",color:"#882020",opacity:1,lineWidth:10},blue:{key:"b",color:"#003088",opacity:1,lineWidth:10},yellow:{key:"y",color:"#e68f00",opacity:1,lineWidth:10},paleBlue:{key:"pb",color:"#003088",opacity:.4,lineWidth:15},paleGreen:{key:"pg",color:"#15781B",opacity:.4,lineWidth:15},paleRed:{key:"pr",color:"#882020",opacity:.4,lineWidth:15},paleGrey:{key:"pgr",color:"#4a4a4a",opacity:.35,lineWidth:15},purple:{key:"purp",color:"#68217a",opacity:.65,lineWidth:10},pink:{key:"pink",color:"#ee2080",opacity:.5,lineWidth:10},hilite:{key:"hilite",color:"#fff",opacity:1,lineWidth:1}},prevSvgHash:""},hold:Fe()}}function wo(){let e=h("defs"),o=y(h("filter"),{id:"cg-filter-blur"});return o.appendChild(y(h("feGaussianBlur"),{stdDeviation:"0.022"})),e.appendChild(o),e}function Po(e,o,r){let t=e.drawable,n=t.current,i=n&&n.mouseSq?n:void 0,a=new Map,c=e.dom.bounds(),l=t.autoShapes.filter(p=>!p.piece);for(let p of t.shapes.concat(l).concat(i?[i]:[])){if(!p.dest)continue;let M=a.get(p.dest)??new Set,v=fe(pe(g(p.orig),e.orientation),c),u=fe(pe(g(p.dest),e.orientation),c);M.add(Te(v,u)),a.set(p.dest,M)}let s=t.shapes.concat(l).map(p=>({shape:p,current:!1,hash:yo(p,He(p.dest,a),!1,c)}));i&&s.push({shape:i,current:!0,hash:yo(i,He(i.dest,a),!0,c)});let f=s.map(p=>p.hash).join(";");if(f===e.drawable.prevSvgHash)return;e.drawable.prevSvgHash=f;let d=o.querySelector("defs");sr(t,s,d),lr(s,o.querySelector("g"),r.querySelector("g"),p=>pr(e,p,t.brushes,a,c))}function sr(e,o,r){let t=new Map,n;for(let c of o.filter(l=>l.shape.dest&&l.shape.brush))n=Mo(e.brushes[c.shape.brush],c.shape.modifiers),c.shape.modifiers?.hilite&&t.set("hilite",e.brushes.hilite),t.set(n.key,n);let i=new Set,a=r.firstElementChild;for(;a;)i.add(a.getAttribute("cgKey")),a=a.nextElementSibling;for(let[c,l]of t.entries())i.has(c)||r.appendChild(mr(l))}function lr(e,o,r,t){let n=new Map;for(let i of e)n.set(i.hash,!1);for(let i of[o,r]){let a=[],c=i.firstElementChild,l;for(;c;)l=c.getAttribute("cgHash"),n.has(l)?n.set(l,!0):a.push(c),c=c.nextElementSibling;for(let s of a)i.removeChild(s)}for(let i of e.filter(a=>!n.get(a.hash)))for(let a of t(i))a.isCustom?r.appendChild(a.el):o.appendChild(a.el)}function yo({orig:e,dest:o,brush:r,piece:t,modifiers:n,customSvg:i,label:a},c,l,s){return[s.width,s.height,l,e,o,r,c&&"-",t&&dr(t),n&&ur(n),i&&`custom-${So(i.html)},${i.center?.[0]??"o"}`,a&&`label-${So(a.text)}`].filter(f=>f).join(",")}function dr(e){return[e.color,e.role,e.scale].filter(o=>o).join(",")}function ur(e){return[e.lineWidth,e.hilite&&"*"].filter(o=>o).join(",")}function So(e){let o=0;for(let r=0;r>>0;return o.toString()}function pr(e,{shape:o,current:r,hash:t},n,i,a){let c=fe(pe(g(o.orig),e.orientation),a),l=o.dest?fe(pe(g(o.dest),e.orientation),a):c,s=o.brush&&Mo(n[o.brush],o.modifiers),f=i.get(o.dest),d=[];if(s){let p=y(h("g"),{cgHash:t});d.push({el:p}),c[0]!==l[0]||c[1]!==l[1]?p.appendChild(gr(o,s,c,l,r,He(o.dest,i))):p.appendChild(fr(n[o.brush],c,r,a))}if(o.label){let p=o.label;p.fill??=o.brush&&n[o.brush].color;let M=o.brush?void 0:"tr";d.push({el:br(p,t,c,l,f,M),isCustom:!0})}if(o.customSvg){let p=o.customSvg.center??"orig",[M,v]=p==="label"?Ko(c,l,f).map(F=>F-.5):p==="dest"?l:c,u=y(h("g"),{transform:`translate(${M},${v})`,cgHash:t});u.innerHTML=`${o.customSvg.html}`,d.push({el:u,isCustom:!0})}return d}function fr(e,o,r,t){let n=hr(),i=(t.width+t.height)/(4*Math.max(t.width,t.height));return y(h("circle"),{stroke:e.color,"stroke-width":n[r?0:1],fill:"none",opacity:xo(e,r),cx:o[0],cy:o[1],r:i-n[1]/2})}function gr(e,o,r,t,n,i){function a(s){let f=yr(i&&!n),d=t[0]-r[0],p=t[1]-r[1],M=Math.atan2(p,d),v=Math.cos(M)*f,u=Math.sin(M)*f;return y(h("line"),{stroke:s?"white":o.color,"stroke-width":vr(o,n)+(s?.04:0),"stroke-linecap":"round","marker-end":`url(#arrowhead-${s?"hilite":o.key})`,opacity:e.modifiers?.hilite?1:xo(o,n),x1:r[0],y1:r[1],x2:t[0]-v,y2:t[1]-u})}if(!e.modifiers?.hilite)return a(!1);let c=h("g"),l=y(h("g"),{filter:"url(#cg-filter-blur)"});return l.appendChild(Sr(r,t)),l.appendChild(a(!0)),c.appendChild(l),c.appendChild(a(!1)),c}function mr(e){let o=y(h("marker"),{id:"arrowhead-"+e.key,orient:"auto",overflow:"visible",markerWidth:4,markerHeight:4,refX:e.key==="hilite"?1.86:2.05,refY:2});return o.appendChild(y(h("path"),{d:"M0,0 V4 L3,2 Z",fill:e.color})),o.setAttribute("cgKey",e.key),o}function br(e,o,r,t,n,i){let c=.4*.75**e.text.length,l=Ko(r,t,n),s=i==="tr"?.4:0,f=y(h("g"),{transform:`translate(${l[0]+s},${l[1]-s})`,cgHash:o});f.appendChild(y(h("circle"),{r:.4/2,"fill-opacity":i?1:.8,"stroke-opacity":i?1:.7,"stroke-width":.03,fill:e.fill??"#666",stroke:"white"}));let d=y(h("text"),{"font-size":c,"font-family":"Noto Sans","text-anchor":"middle",fill:"white",y:.13*.75**e.text.length});return d.innerHTML=e.text,f.appendChild(d),f}function pe(e,o){return o==="white"?e:[7-e[0],7-e[1]]}function He(e,o){return(e&&o.has(e)&&o.get(e).size>1)===!0}function h(e){return document.createElementNS("http://www.w3.org/2000/svg",e)}function y(e,o){for(let r in o)Object.prototype.hasOwnProperty.call(o,r)&&e.setAttribute(r,o[r]);return e}function Mo(e,o){return o?{color:e.color,opacity:Math.round(e.opacity*10)/10,lineWidth:Math.round(o.lineWidth||e.lineWidth),key:[e.key,o.lineWidth].filter(r=>r).join("")}:e}function hr(){return[3/64,4/64]}function vr(e,o){return(e.lineWidth||10)*(o?.85:1)/64}function xo(e,o){return(e.opacity||1)*(o?.9:1)}function yr(e){return(e?20:10)/64}function fe(e,o){let r=Math.min(1,o.width/o.height),t=Math.min(1,o.height/o.width);return[(e[0]-3.5)*r,(3.5-e[1])*t]}function Sr(e,o){let r={from:[Math.floor(Math.min(e[0],o[0])),Math.floor(Math.min(e[1],o[1]))],to:[Math.ceil(Math.max(e[0],o[0])),Math.ceil(Math.max(e[1],o[1]))]};return y(h("rect"),{x:r.from[0],y:r.from[1],width:r.to[0]-r.from[0],height:r.to[1]-r.from[1],fill:"none",stroke:"none"})}function Te(e,o,r=!0){let t=Math.atan2(o[1]-e[1],o[0]-e[0])+Math.PI;return r?(Math.round(t*8/Math.PI)+16)%16:t}function wr(e,o){return Math.sqrt([e[0]-o[0],e[1]-o[1]].reduce((r,t)=>r+t*t,0))}function Ko(e,o,r){let t=wr(e,o),n=Te(e,o,!1);if(r&&(t-=33/64,r.size>1)){t-=10/64;let i=Te(e,o);(r.has((i+1)%16)||r.has((i+15)%16))&&i&1&&(t-=.4)}return[e[0]-Math.cos(n)*t,e[1]-Math.sin(n)*t].map(i=>i+.5)}function Do(e,o){e.innerHTML="",e.classList.add("cg-wrap");for(let l of Ve)e.classList.toggle("orientation-"+l,o.orientation===l);e.classList.toggle("manipulable",!o.viewOnly);let r=K("cg-container");e.appendChild(r);let t=K("cg-board");r.appendChild(t);let n,i,a;if(o.drawable.visible&&(n=y(h("svg"),{class:"cg-shapes",viewBox:"-4 -4 8 8",preserveAspectRatio:"xMidYMid slice"}),n.appendChild(wo()),n.appendChild(h("g")),i=y(h("svg"),{class:"cg-custom-svgs",viewBox:"-3.5 -3.5 8 8",preserveAspectRatio:"xMidYMid slice"}),i.appendChild(h("g")),a=K("cg-auto-pieces"),r.appendChild(n),r.appendChild(i),r.appendChild(a)),o.coordinates){let l=o.orientation==="black"?" black":"",s=o.ranksPosition==="left"?" left":"";r.appendChild(Co(j,"ranks"+l+s)),r.appendChild(Co(z,"files"+l))}let c;return o.draggable.enabled&&o.draggable.showGhost&&(c=K("piece","ghost"),Z(c,!1),r.appendChild(c)),{board:t,container:r,wrap:e,ghost:c,svg:n,customSvg:i,autoPieces:a}}function Co(e,o){let r=K("coords",o),t;for(let n of e)t=K("coord"),t.textContent=n,r.appendChild(t);return r}function ko(e,o){if(!e.dropmode.active)return;D(e),k(e);let r=e.dropmode.piece;if(r){e.pieces.set("a0",r);let t=T(o),n=t&&E(t,b(e),e.dom.bounds());n&&se(e,"a0",n)}e.dom.redraw()}function Ao(e,o){let r=e.dom.elements.board;if("ResizeObserver"in window&&new ResizeObserver(o).observe(e.dom.elements.wrap),(e.disableContextMenu||e.drawable.enabled)&&r.addEventListener("contextmenu",n=>n.preventDefault()),e.viewOnly)return;let t=Mr(e);r.addEventListener("touchstart",t,{passive:!1}),r.addEventListener("mousedown",t,{passive:!1})}function No(e,o){let r=[];if("ResizeObserver"in window||r.push(J(document.body,"chessground.resize",o)),!e.viewOnly){let t=Eo(e,uo,no),n=Eo(e,po,io);for(let a of["touchmove","mousemove"])r.push(J(document,a,t));for(let a of["touchend","mouseup"])r.push(J(document,a,n));let i=()=>e.dom.bounds.clear();r.push(J(document,"scroll",i,{capture:!0,passive:!0})),r.push(J(window,"resize",i,{passive:!0}))}return()=>r.forEach(t=>t())}function J(e,o,r,t){return e.addEventListener(o,r,t),()=>e.removeEventListener(o,r,t)}var Mr=e=>o=>{e.draggable.current?G(e):e.drawable.current?Ae(e):o.shiftKey||ie(o)?e.drawable.enabled&&ro(e,o):e.viewOnly||(e.dropmode.active?ko(e,o):so(e,o))},Eo=(e,o,r)=>t=>{e.drawable.current?e.drawable.enabled&&r(e,t):e.viewOnly||o(e,t)};function To(e){let o=b(e),r=B(e.dom.bounds()),t=e.dom.elements.board,n=e.pieces,i=e.animation.current,a=i?i.plan.anims:new Map,c=i?i.plan.fadings:new Map,l=e.draggable.current,s=Kr(e),f=new Set,d=new Set,p=new Map,M=new Map,v,u,F,W,C,$,ge,A,me,ee;for(u=t.firstChild;u;){if(v=u.cgKey,qo(u))if(F=n.get(v),C=a.get(v),$=c.get(v),W=u.cgPiece,u.cgDragging&&(!l||l.orig!==v)&&(u.classList.remove("dragging"),x(u,r(g(v),o)),u.cgDragging=!1),!$&&u.cgFading&&(u.cgFading=!1,u.classList.remove("fading")),F){if(C&&u.cgAnimating&&W===_(F)){let m=g(v);m[0]+=C[2],m[1]+=C[3],u.classList.add("anim"),x(u,r(m,o))}else u.cgAnimating&&(u.cgAnimating=!1,u.classList.remove("anim"),x(u,r(g(v),o)),e.addPieceZIndex&&(u.style.zIndex=Re(g(v),o)));W===_(F)&&(!$||!u.cgFading)?f.add(v):$&&W===_($)?(u.classList.add("fading"),u.cgFading=!0):qe(p,W,u)}else qe(p,W,u);else if(Oo(u)){let m=u.className;s.get(v)===m?d.add(v):qe(M,m,u)}u=u.nextSibling}for(let[m,I]of s)if(!d.has(m)){me=M.get(I),ee=me&&me.pop();let N=r(g(m),o);if(ee)ee.cgKey=m,x(ee,N);else{let H=K("square",I);H.cgKey=m,x(H,N),t.insertBefore(H,t.firstChild)}}for(let[m,I]of n)if(C=a.get(m),!f.has(m))if(ge=p.get(_(I)),A=ge&&ge.pop(),A){A.cgKey=m,A.cgFading&&(A.classList.remove("fading"),A.cgFading=!1);let N=g(m);e.addPieceZIndex&&(A.style.zIndex=Re(N,o)),C&&(A.cgAnimating=!0,A.classList.add("anim"),N[0]+=C[2],N[1]+=C[3]),x(A,r(N,o))}else{let N=_(I),H=K("piece",N),oe=g(m);H.cgPiece=N,H.cgKey=m,C&&(H.cgAnimating=!0,oe[0]+=C[2],oe[1]+=C[3]),x(H,r(oe,o)),e.addPieceZIndex&&(H.style.zIndex=Re(oe,o)),t.appendChild(H)}for(let m of p.values())Ho(e,m);for(let m of M.values())Ho(e,m)}function Ro(e){let o=b(e),r=B(e.dom.bounds()),t=e.dom.elements.board.firstChild;for(;t;)(qo(t)&&!t.cgAnimating||Oo(t))&&x(t,r(g(t.cgKey),o)),t=t.nextSibling}function Oe(e){let o=e.dom.elements.wrap.getBoundingClientRect(),r=e.dom.elements.container,t=o.height/o.width,n=Math.floor(o.width*window.devicePixelRatio/8)*8/window.devicePixelRatio,i=n*t;r.style.width=n+"px",r.style.height=i+"px",e.dom.bounds.clear(),e.addDimensionsCssVarsTo?.style.setProperty("--cg-width",n+"px"),e.addDimensionsCssVarsTo?.style.setProperty("--cg-height",i+"px")}var qo=e=>e.tagName==="PIECE",Oo=e=>e.tagName==="SQUARE";function Ho(e,o){for(let r of o)e.dom.elements.board.removeChild(r)}function Re(e,o){let t=e[1];return`${o?3+7-t:3+t}`}var _=e=>`${e.color} ${e.role}`;function Kr(e){let o=new Map;if(e.lastMove&&e.highlight.lastMove)for(let n of e.lastMove)O(o,n,"last-move");if(e.check&&e.highlight.check&&O(o,e.check,"check"),e.selected&&(O(o,e.selected,"selected"),e.movable.showDests)){let n=e.movable.dests?.get(e.selected);if(n)for(let a of n)O(o,a,"move-dest"+(e.pieces.has(a)?" oc":""));let i=e.premovable.customDests?.get(e.selected)??e.premovable.dests;if(i)for(let a of i)O(o,a,"premove-dest"+(e.pieces.has(a)?" oc":""))}let r=e.premovable.current;if(r)for(let n of r)O(o,n,"current-premove");else e.predroppable.current&&O(o,e.predroppable.current.key,"current-premove");let t=e.exploding;if(t)for(let n of t.keys)O(o,n,"exploding"+t.stage);return e.highlight.custom&&e.highlight.custom.forEach((n,i)=>{O(o,i,n)}),o}function O(e,o,r){let t=e.get(o);t?e.set(o,`${t} ${r}`):e.set(o,r)}function qe(e,o,r){let t=e.get(o);t?t.push(r):e.set(o,[r])}function Vo(e,o,r){let t=new Map,n=[];for(let c of e)t.set(c.hash,!1);let i=o.firstElementChild,a;for(;i;)a=i.getAttribute("cgHash"),t.has(a)?t.set(a,!0):n.push(i),i=i.nextElementSibling;for(let c of n)o.removeChild(c);for(let c of e)t.get(c.hash)||o.appendChild(r(c))}function Bo(e,o){let t=e.drawable.autoShapes.filter(n=>n.piece).map(n=>({shape:n,hash:Dr(n),current:!1}));Vo(t,o,n=>Cr(e,n,e.dom.bounds()))}function Lo(e){let o=b(e),r=B(e.dom.bounds()),t=e.dom.elements.autoPieces?.firstChild;for(;t;)be(t,r(g(t.cgKey),o),t.cgScale),t=t.nextSibling}function Cr(e,{shape:o,hash:r},t){let n=o.orig,i=o.piece?.role,a=o.piece?.color,c=o.piece?.scale,l=K("piece",`${i} ${a}`);return l.setAttribute("cgHash",r),l.cgKey=n,l.cgScale=c,be(l,B(t)(g(n),b(e)),c),l}var Dr=e=>[e.orig,e.piece?.role,e.piece?.color,e.piece?.scale].join(",");function Dt({el:e,config:o}){return Er(e,o)}function Er(e,o){let r=vo();ue(r,o||{});function t(){let n="dom"in r?r.dom.unbind:void 0,i=Do(e,r),a=Ge(()=>i.board.getBoundingClientRect()),c=f=>{To(s),i.autoPieces&&Bo(s,i.autoPieces),!f&&i.svg&&Po(s,i.svg,i.customSvg)},l=()=>{Oe(s),Ro(s),i.autoPieces&&Lo(s)},s=r;return s.dom={elements:i,bounds:a,redraw:Ar(c),redrawNow:c,unbind:n},s.drawable.prevSvgHash="",Oe(s),c(!1),Ao(s,l),n||(s.dom.unbind=No(s,l)),s.events.insert&&s.events.insert(i),s}return ho(t(),t)}function Ar(e){let o=!1;return()=>{o||(o=!0,requestAnimationFrame(()=>{e(),o=!1}))}}export{Er as Chessground,Dt as initModule}; diff --git a/ui/analyse/src/study/topics.ts b/ui/analyse/src/study/topics.ts index 425adb564c37f..fbf8cd93fe7bd 100644 --- a/ui/analyse/src/study/topics.ts +++ b/ui/analyse/src/study/topics.ts @@ -84,7 +84,7 @@ export const formView = (ctrl: TopicsCtrl, userId?: string): VNode => function setupTagify(elm: HTMLInputElement | HTMLTextAreaElement, userId?: string) { lichess.loadCssPath('tagify'); - lichess.loadIife('vendor/tagify/tagify.min.js').then(() => { + lichess.loadIife('npm/tagify/tagify.min.js').then(() => { const tagi = (tagify = new (window.Tagify as typeof Tagify)(elm, { pattern: /.{2,}/, maxTags: 30, diff --git a/ui/ceval/src/ctrl.ts b/ui/ceval/src/ctrl.ts index df21426504008..06fbcaa2495a8 100644 --- a/ui/ceval/src/ctrl.ts +++ b/ui/ceval/src/ctrl.ts @@ -177,7 +177,7 @@ export default class CevalCtrl { else if (this.technology == 'nnue') this.worker = new ThreadedWasmWorker( { - baseUrl: 'vendor/stockfish-nnue.wasm/', + baseUrl: 'npm/stockfish-nnue.wasm/', module: 'Stockfish', downloadProgress: throttle(200, mb => { this.downloadProgress(mb); @@ -192,7 +192,7 @@ export default class CevalCtrl { else if (this.technology == 'hce') this.worker = new ThreadedWasmWorker( { - baseUrl: this.officialStockfish ? 'vendor/stockfish.wasm/' : 'vendor/stockfish-mv.wasm/', + baseUrl: this.officialStockfish ? 'npm/stockfish.wasm/' : 'npm/stockfish-mv.wasm/', module: this.officialStockfish ? 'Stockfish' : 'StockfishMv', version: 'a022fa', wasmMemory: sharedWasmMemory(1024, this.platform.maxWasmPages(1088)), @@ -204,8 +204,8 @@ export default class CevalCtrl { { url: this.technology == 'wasm' - ? 'vendor/stockfish.js/stockfish.wasm.js' - : 'vendor/stockfish.js/stockfish.js', + ? 'npm/stockfish.js/stockfish.wasm.js' + : 'npm/stockfish.js/stockfish.js', }, this.opts.redraw, ); diff --git a/ui/chart/src/common.ts b/ui/chart/src/common.ts index 80b25740d7b57..2f1ee5e4c8111 100644 --- a/ui/chart/src/common.ts +++ b/ui/chart/src/common.ts @@ -20,7 +20,7 @@ export function selectPly(this: PlyChart, ply: number, onMainline: boolean) { export async function loadHighcharts(tpe: string) { if (highchartsPromise) return highchartsPromise; const file = tpe === 'highstock' ? 'highstock.js' : 'highcharts.js'; - highchartsPromise = lichess.loadIife('vendor/highcharts-4.2.5/' + file, { + highchartsPromise = lichess.loadIife('npm/highcharts-4.2.5/' + file, { noVersion: true, }); await highchartsPromise; diff --git a/ui/common/css/component/_dialog.scss b/ui/common/css/component/_dialog.scss index d7ae412dded54..030e4807ec80d 100644 --- a/ui/common/css/component/_dialog.scss +++ b/ui/common/css/component/_dialog.scss @@ -14,8 +14,8 @@ dialog { background: $c-page-mask; } - > .scrollable { - max-height: calc(var(--vh, 1vh) * 100); // ios safari vh fix + > div.scrollable { + max-height: calc(100 * var(--vh) - 16px); overflow-x: clip; overflow-y: auto; } @@ -32,48 +32,45 @@ dialog { position: absolute; top: 4px; #{$end-direction}: 4px; - width: 32px; - height: 32px; - background: transparent; - border: none; + width: 40px; // bigger for phones + height: 40px; + z-index: z('modal') + 1; + background: $c-bg-high; color: $c-font; - font-size: 16px; + border-radius: 6px; + border: 1px solid $c-border; + font-size: 20px; + text-align: center; cursor: pointer; - focus { - z-index: z('modal') + 1; - background: $c-bg-high; - } &:not(:focus) { outline: none; } } -} -.dialog-content { - text-align: center; - padding: 2em; -} + &:not(.touch-scroll) { + margin-top: 16px; + overflow: visible; -@media (hover: hover) { - dialog .close-button:hover { - box-shadow: $box-shadow; - background: $c-bad; - color: #fff; + button.close-button { + transform: translate($transform-direction * 18px, -18px); + width: 32px; + height: 32px; + font-size: 16px; + border-radius: 50%; + border: none; + &:hover { + box-shadow: $box-shadow; + background: $c-bad; + color: #fff; + } + } } } -@media (min-height: 640px) { - dialog { - margin-top: 12px; - overflow: visible; - } - dialog button.close-button { - top: -12px; - #{$end-direction}: -12px; - z-index: z('modal') + 1; - border-radius: 50%; - background: $c-bg-high; - } +// top level to reduce specificity, allow easy overrides +.dialog-content { + text-align: center; + padding: 2em; } diff --git a/ui/common/package.json b/ui/common/package.json index 5ec0590bf0f26..c9bcbc384677d 100644 --- a/ui/common/package.json +++ b/ui/common/package.json @@ -24,6 +24,7 @@ "author": "Thibault Duplessis", "license": "AGPL-3.0-or-later", "dependencies": { + "dialog-polyfill": "0.5.6", "lichess-pgn-viewer": "^2.0.0", "snabbdom": "^3.5.1", "tablesort": "^5.3.0" @@ -32,5 +33,11 @@ "compile": "tsc", "dev": "tsc", "prod": "tsc" + }, + "lichess": { + "copy": { + "src": "node_modules/dialog-polyfill/dist/dialog-polyfill.esm.js", + "dest": "../../public/npm" + } } } diff --git a/ui/common/src/dialog.ts b/ui/common/src/dialog.ts index ac9e37c72e415..afc7103411693 100644 --- a/ui/common/src/dialog.ts +++ b/ui/common/src/dialog.ts @@ -1,9 +1,19 @@ import { VNode, Attrs } from 'snabbdom'; import { onInsert, h, MaybeVNodes } from './snabbdom'; import { spinnerVdom } from './spinner'; +import { isTouchDevice, isIOS } from './mobile'; import * as xhr from './xhr'; import * as licon from './licon'; +let dialogPolyfill: { registerDialog: (dialog: HTMLDialogElement) => void }; + +lichess.load.then(() => { + window.addEventListener('resize', onResize); + if (isIOS({ below: 15.4 })) { + import(lichess.assetUrl('npm/dialog-polyfill.esm.js')).then(m => (dialogPolyfill = m.default)); + } +}); + export interface Dialog { readonly open: boolean; readonly view: HTMLElement; @@ -23,22 +33,54 @@ interface DialogOpts { noClickAway?: boolean; } +export interface DomDialogOpts extends DialogOpts { + parent?: Element; + show?: 'modal' | boolean; // if not falsy, auto-show & remove from dom when closed +} + export interface SnabDialogOpts extends DialogOpts { vnodes?: MaybeVNodes; onInsert?: (dialog: Dialog) => void; // prevents showModal, caller must do so manually } -export interface DomDialogOpts extends DialogOpts { - parent?: HTMLElement | Cash; // TODO - for positioning, need to fix css to be useful - show?: 'modal' | boolean; // auto-show and remove from dom when closed, no reshow +export async function domDialog(o: DomDialogOpts): Promise { + const [html] = await assets(o.html, o.cssPath, o.cash); + + const dialog = document.createElement('dialog'); + if (isTouchDevice()) dialog.classList.add('touch-scroll'); + if (o.parent) dialog.style.position = 'absolute'; + for (const [k, v] of Object.entries(o.attrs?.dialog ?? {})) dialog.setAttribute(k, String(v)); + + if (!o.noCloseButton) { + const anchor = $as('
'); + anchor.innerHTML = `
`, - ); - anchor.querySelector('button')?.addEventListener('click', () => dialog.close()); - dialog.appendChild(anchor); - } - dialog.appendChild(scrollable); - if (!o.parent) document.body.appendChild(dialog); - else { - $(o.parent).append(dialog); - dialog.style.position = 'absolute'; - } - - const wrapper = new DialogWrapper(dialog, view, o); - if (o.show && o.show === 'modal') wrapper.showModal(); - else if (o.show) wrapper.show(); - - return wrapper; -} - class DialogWrapper implements Dialog { + restoreFocus?: HTMLElement; + constructor( readonly dialog: HTMLDialogElement, readonly view: HTMLElement, readonly o: DialogOpts, ) { - dialog.addEventListener('close', () => this.onClose()); - if ('show' in o && o.show) dialog.addEventListener('close', dialog.remove); - if (!o.noClickAway) dialog.addEventListener('click', () => dialog.close()); + if (dialogPolyfill) dialogPolyfill.registerDialog(dialog); // ios < 15.4 + + view.parentElement?.style.setProperty('--vh', `${window.innerHeight * 0.01}px`); // ios safari view.addEventListener('click', e => e.stopPropagation()); - this.onResize(); // safari vh + + dialog.addEventListener('close', this.onClose); + + if (!o.noClickAway) setTimeout(() => document.addEventListener('click', this.close), 0); } + get open() { return this.dialog.open; } @@ -130,34 +140,36 @@ class DialogWrapper implements Dialog { show = () => this.dialog.show(); close = () => this.dialog.close(); - onResize = () => this.view.style.setProperty('--vh', `${window.innerHeight * 0.01}px`); onClose = () => { - window.removeEventListener('resize', this.onResize); this.o.onClose?.(this); + if ('show' in this.o && this.o.show === 'modal') this.dialog.remove(); + this.restoreFocus?.focus(); + this.restoreFocus = undefined; }; showModal = () => { - const focii = Array.from($(focusQuery, this.view)) as HTMLElement[]; - if (focii.length > 1) focii[1].focus(); // skip close button - else if (focii.length) focii[0].focus(); - window.addEventListener('resize', this.onResize); + this.restoreFocus = document.activeElement as HTMLElement; + $(focusQuery, this.view)[1]?.focus(); this.addModalListeners?.(); this.view.scrollTop = 0; + this.dialog.showModal(); }; addModalListeners? = () => { this.dialog.addEventListener('keydown', onModalKeydown); - this.dialog.addEventListener('touchmove', (e: TouchEvent) => e.stopPropagation()); - this.addModalListeners = undefined; // only do this once + if (isTouchDevice()) this.dialog.addEventListener('touchmove', (e: TouchEvent) => e.stopPropagation()); + this.addModalListeners = undefined; // only do this once per HTMLDialogElement }; } -function assets(o: DialogOpts) { +function assets(html?: { url?: string; text?: string }, cssPath?: string, cash?: Cash) { return Promise.all([ - o.html?.url ? xhr.text(o.html.url) : Promise.resolve(o.cash?.html() ?? o.html?.text), - o.cssPath ? lichess.loadCssPath(o.cssPath) : Promise.resolve(), + html?.url + ? xhr.text(html.url) + : Promise.resolve(cash ? $as($(cash).clone().removeClass('none')).outerHTML : html?.text), + cssPath ? lichess.loadCssPath(cssPath) : Promise.resolve(), ]); } @@ -175,6 +187,11 @@ function onModalKeydown(e: KeyboardEvent) { e.stopPropagation(); } +function onResize() { + const vh = window.innerHeight * 0.01; + $('dialog > div.scrollable').css('--vh', `${vh}px`); +} + const focusQuery = ['button', 'input', 'select', 'textarea'] .map(sel => `${sel}:not(:disabled)`) .concat(['[href]', '[tabindex="0"]']) diff --git a/ui/common/src/linkPopup.ts b/ui/common/src/linkPopup.ts index dc504e3d7b2c8..bd3e85b08b625 100644 --- a/ui/common/src/linkPopup.ts +++ b/ui/common/src/linkPopup.ts @@ -8,6 +8,7 @@ export const makeLinkPopups = (dom: HTMLElement | Cash, trans: Trans, selector = export const onClick = (a: HTMLLinkElement, trans: Trans): boolean => { const url = new URL(a.href); if (isPassList(url)) return true; + domDialog({ cssPath: 'linkPopup', html: { @@ -28,6 +29,7 @@ export const onClick = (a: HTMLLinkElement, trans: Trans): boolean => { }, }).then(dlg => { $('.cancel', dlg.view).on('click', dlg.close); + $('a', dlg.view).on('click', () => setTimeout(dlg.close, 1000)); dlg.showModal(); }); return false; diff --git a/ui/site/package.json b/ui/site/package.json index 76f2089852211..cd3dc30b82561 100644 --- a/ui/site/package.json +++ b/ui/site/package.json @@ -77,23 +77,23 @@ "copy": [ { "src": "node_modules/hopscotch/dist/**", - "dest": "../../public/vendor/hopscotch/dist" + "dest": "../../public/npm/hopscotch/dist" }, { "src": "node_modules/highcharts/*.js", - "dest": "../../public/vendor/highcharts-4.2.5" + "dest": "../../public/npm/highcharts-4.2.5" }, { "src": "node_modules/@yaireo/tagify/dist/tagify.min.js", - "dest": "../../public/vendor/tagify" + "dest": "../../public/npm/tagify" }, { "src": "node_modules/stockfish*/*.{js,wasm}", - "dest": "../../public/vendor" + "dest": "../../public/npm" }, { "src": "../../node_modules/chessground/dist/chessground.min.js", - "dest": "../../public/javascripts" + "dest": "../../public/npm" } ] } diff --git a/ui/site/src/component/assets.ts b/ui/site/src/component/assets.ts index b12afc515b5cb..608abfd0e9b66 100644 --- a/ui/site/src/component/assets.ts +++ b/ui/site/src/component/assets.ts @@ -65,12 +65,12 @@ export const userComplete = async (opts: UserCompleteOpts): Promise { - loadCss('vendor/hopscotch/dist/css/hopscotch.min.css'); - return loadIife('vendor/hopscotch/dist/js/hopscotch.min.js', { + loadCss('npm/hopscotch/dist/css/hopscotch.min.css'); + return loadIife('npm/hopscotch/dist/js/hopscotch.min.js', { noVersion: true, }); }; export const embedChessground = () => { - return import(assetUrl('javascripts/chessground.min.js', { noVersion: true })); + return import(assetUrl('npm/chessground.min.js', { noVersion: true })); }; diff --git a/ui/site/src/component/mic.ts b/ui/site/src/component/mic.ts index 3da8f4bb6f59d..e0fd92a1c5ff9 100644 --- a/ui/site/src/component/mic.ts +++ b/ui/site/src/component/mic.ts @@ -183,11 +183,11 @@ export const mic = new (class implements Voice.Microphone { this.interrupt = true; } - private get micTrack(): MediaStreamTrack | undefined { + get micTrack(): MediaStreamTrack | undefined { return this.mediaStream?.getAudioTracks()[0]; } - private initKaldi(recId: string, rec: RecNode) { + initKaldi(recId: string, rec: RecNode) { if (rec.node) return; rec.node = this.vosk?.initRecognizer({ recId: recId, @@ -198,7 +198,7 @@ export const mic = new (class implements Voice.Microphone { }); } - private async initModel(): Promise { + async initModel(): Promise { if (this.vosk?.isLoaded(this.lang)) { await this.initAudio(); return; @@ -216,7 +216,7 @@ export const mic = new (class implements Voice.Microphone { await audioAsync; } - private async initAudio(): Promise { + async initAudio(): Promise { if (this.audioCtx?.state === 'suspended') await this.audioCtx.resume(); if (this.audioCtx?.state === 'running') return; else if (this.audioCtx) throw `Error ${this.audioCtx.state}`; @@ -236,7 +236,7 @@ export const mic = new (class implements Voice.Microphone { this.recs.ctx = { vosk: this.vosk, source: this.micSource, ctx: this.audioCtx }; } - private broadcast(text: string, msgType: Voice.MsgType = 'status', forMs = 0) { + broadcast(text: string, msgType: Voice.MsgType = 'status', forMs = 0) { this.ctrl?.call(this, text, msgType); if (msgType === 'status' || msgType === 'full') window.clearTimeout(this.broadcastTimeout); this.voskStatus = text; @@ -247,7 +247,7 @@ export const mic = new (class implements Voice.Microphone { this.broadcastTimeout = forMs > 0 ? window.setTimeout(() => this.broadcast(''), forMs) : undefined; } - private async downloadModel(emscriptenPath: string): Promise { + async downloadModel(emscriptenPath: string): Promise { const voskStore = await objectStorage({ db: '/vosk', store: 'FILE_DATA', diff --git a/ui/site/src/gameSearch.ts b/ui/site/src/gameSearch.ts index 968763dbc8102..b3b6feafb0784 100644 --- a/ui/site/src/gameSearch.ts +++ b/ui/site/src/gameSearch.ts @@ -1,5 +1,3 @@ -export {}; // for tsc isolatedModules - lichess.load.then(() => { const form = document.querySelector('.search__form') as HTMLFormElement, $form = $(form), diff --git a/ui/voice/src/view.ts b/ui/voice/src/view.ts index 103632415b4fe..9633ef8f912cd 100644 --- a/ui/voice/src/view.ts +++ b/ui/voice/src/view.ts @@ -3,7 +3,6 @@ import * as licon from 'common/licon'; import { onInsert, bind } from 'common/snabbdom'; import { snabDialog, type Dialog } from 'common/dialog'; import * as xhr from 'common/xhr'; -import { isIOS } from 'common/mobile'; import { onClickAway } from 'common'; import { Entry, VoiceCtrl } from './interfaces'; import { supportedLangs } from './main'; @@ -168,7 +167,7 @@ function renderHelpModal(ctrl: VoiceCtrl) { dlg.view.innerHTML = html; if (!dlg.open) dlg.showModal(); }; - isIOS; + return snabDialog({ class: 'help.voice-move-help', html: { url: `/help/voice/${ctrl.moduleId}` }, From 58040b0a3902a3323766a32e94e00c7e491d2876 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Tue, 5 Sep 2023 20:04:02 -0500 Subject: [PATCH 052/174] . --- public/javascripts/chessground.min.js | 1 - 1 file changed, 1 deletion(-) delete mode 100644 public/javascripts/chessground.min.js diff --git a/public/javascripts/chessground.min.js b/public/javascripts/chessground.min.js deleted file mode 100644 index 8099a1eb8a3fb..0000000000000 --- a/public/javascripts/chessground.min.js +++ /dev/null @@ -1 +0,0 @@ -var Ve=["white","black"],z=["a","b","c","d","e","f","g","h"],j=["1","2","3","4","5","6","7","8"];var Le=[...j].reverse(),re=Array.prototype.concat(...z.map(e=>j.map(o=>e+o))),S=e=>re[8*e[0]+e[1]],g=e=>[e.charCodeAt(0)-97,e.charCodeAt(1)-49];var te=re.map(g);function Ge(e){let o,r=()=>(o===void 0&&(o=e()),o);return r.clear=()=>{o=void 0},r}var Fe=()=>{let e;return{start(){e=performance.now()},cancel(){e=void 0},stop(){if(!e)return 0;let o=performance.now()-e;return e=void 0,o}}},ne=e=>e==="white"?"black":"white",V=(e,o)=>{let r=e[0]-o[0],t=e[1]-o[1];return r*r+t*t},U=(e,o)=>e.role===o.role&&e.color===o.color,B=e=>(o,r)=>[(r?o[0]:7-o[0])*e.width/8,(r?7-o[1]:o[1])*e.height/8],x=(e,o)=>{e.style.transform=`translate(${o[0]}px,${o[1]}px)`},be=(e,o,r=1)=>{e.style.transform=`translate(${o[0]}px,${o[1]}px) scale(${r})`},Z=(e,o)=>{e.style.visibility=o?"visible":"hidden"},T=e=>{if(e.clientX||e.clientX===0)return[e.clientX,e.clientY];if(e.targetTouches?.[0])return[e.targetTouches[0].clientX,e.targetTouches[0].clientY]},ie=e=>e.buttons===2||e.button===2,K=(e,o)=>{let r=document.createElement(e);return o&&(r.className=o),r};function ae(e,o,r){let t=g(e);return o||(t[0]=7-t[0],t[1]=7-t[1]),[r.left+r.width*t[0]/8+r.width/16,r.top+r.height*(7-t[1])/8+r.height/16]}var L=(e,o)=>Math.abs(e-o),Go=e=>(o,r,t,n)=>L(o,t)<2&&(e==="white"?n===r+1||r<=1&&n===r+2&&o===t:n===r-1||r>=6&&n===r-2&&o===t),he=(e,o,r,t)=>{let n=L(e,r),i=L(o,t);return n===1&&i===2||n===2&&i===1},We=(e,o,r,t)=>L(e,r)===L(o,t),ze=(e,o,r,t)=>e===r||o===t,ve=(e,o,r,t)=>We(e,o,r,t)||ze(e,o,r,t),Fo=(e,o,r)=>(t,n,i,a)=>L(t,i)<2&&L(n,a)<2||r&&n===a&&n===(e==="white"?0:7)&&(t===4&&(i===2&&o.includes(0)||i===6&&o.includes(7))||o.includes(i));function Wo(e,o){let r=o==="white"?"1":"8",t=[];for(let[n,i]of e)n[1]===r&&i.color===o&&i.role==="rook"&&t.push(g(n)[0]);return t}function ye(e,o,r){let t=e.get(o);if(!t)return[];let n=g(o),i=t.role,a=i==="pawn"?Go(t.color):i==="knight"?he:i==="bishop"?We:i==="rook"?ze:i==="queen"?ve:Fo(t.color,Wo(e,t.color),r);return te.filter(c=>(n[0]!==c[0]||n[1]!==c[1])&&a(n[0],n[1],c[0],c[1])).map(S)}function P(e,...o){e&&setTimeout(()=>e(...o),1)}function $e(e){e.orientation=ne(e.orientation),e.animation.current=e.draggable.current=e.selected=void 0}function Ie(e,o){for(let[r,t]of o)t?e.pieces.set(r,t):e.pieces.delete(r)}function je(e,o){if(e.check=void 0,o===!0&&(o=e.turnColor),o)for(let[r,t]of e.pieces)t.role==="king"&&t.color===o&&(e.check=r)}function zo(e,o,r,t){k(e),e.premovable.current=[o,r],P(e.premovable.events.set,o,r,t)}function D(e){e.premovable.current&&(e.premovable.current=void 0,P(e.premovable.events.unset))}function $o(e,o,r){D(e),e.predroppable.current={role:o,key:r},P(e.predroppable.events.set,o,r)}function k(e){let o=e.predroppable;o.current&&(o.current=void 0,P(o.events.unset))}function Io(e,o,r){if(!e.autoCastle)return!1;let t=e.pieces.get(o);if(!t||t.role!=="king")return!1;let n=g(o),i=g(r);if(n[1]!==0&&n[1]!==7||n[1]!==i[1])return!1;n[0]===4&&!e.pieces.has(r)&&(i[0]===6?r=S([7,i[1]]):i[0]===2&&(r=S([0,i[1]])));let a=e.pieces.get(r);return!a||a.color!==t.color||a.role!=="rook"?!1:(e.pieces.delete(o),e.pieces.delete(r),n[0]o!==r&&Ze(e,o)&&(e.movable.free||!!e.movable.dests?.get(o)?.includes(r));function jo(e,o,r){let t=e.pieces.get(o);return!!t&&(o===r||!e.pieces.has(r))&&(e.movable.color==="both"||e.movable.color===t.color&&e.turnColor===t.color)}function Me(e,o){let r=e.pieces.get(o);return!!r&&e.premovable.enabled&&e.movable.color===r.color&&e.turnColor!==r.color}function Uo(e,o,r){let t=e.premovable.customDests?.get(o)??ye(e.pieces,o,e.premovable.castle);return o!==r&&Me(e,o)&&t.includes(r)}function Zo(e,o,r){let t=e.pieces.get(o),n=e.pieces.get(r);return!!t&&(!n||n.color!==e.movable.color)&&e.predroppable.enabled&&(t.role!=="pawn"||r[1]!=="1"&&r[1]!=="8")&&e.movable.color===t.color&&e.turnColor!==t.color}function Qe(e,o){let r=e.pieces.get(o);return!!r&&e.draggable.enabled&&(e.movable.color==="both"||e.movable.color===r.color&&(e.turnColor===r.color||e.premovable.enabled))}function Xe(e){let o=e.premovable.current;if(!o)return!1;let r=o[0],t=o[1],n=!1;if(le(e,r,t)){let i=Ue(e,r,t);if(i){let a={premove:!0};i!==!0&&(a.captured=i),P(e.movable.events.after,r,t,a),n=!0}}return D(e),n}function Ye(e,o){let r=e.predroppable.current,t=!1;if(!r)return!1;if(o(r)){let n={role:r.role,color:e.movable.color};ce(e,n,r.key)&&(P(e.movable.events.afterNewPiece,r.role,r.key,{premove:!1,predrop:!0}),t=!0)}return k(e),t}function Y(e){D(e),k(e),w(e)}function xe(e){e.movable.color=e.movable.dests=e.animation.current=void 0,Y(e)}function E(e,o,r){let t=Math.floor(8*(e[0]-r.left)/r.width);o||(t=7-t);let n=7-Math.floor(8*(e[1]-r.top)/r.height);return o||(n=7-n),t>=0&&t<8&&n>=0&&n<8?S([t,n]):void 0}function Je(e,o,r,t){let n=g(e),i=te.filter(s=>ve(n[0],n[1],s[0],s[1])||he(n[0],n[1],s[0],s[1])),c=i.map(s=>ae(S(s),r,t)).map(s=>V(o,s)),[,l]=c.reduce((s,f,d)=>s[0]e.orientation==="white";var Ce="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR",Qo={p:"pawn",r:"rook",n:"knight",b:"bishop",q:"queen",k:"king"},Xo={pawn:"p",rook:"r",knight:"n",bishop:"b",queen:"q",king:"k"};function de(e){e==="start"&&(e=Ce);let o=new Map,r=7,t=0;for(let n of e)switch(n){case" ":case"[":return o;case"/":if(--r,r<0)return o;t=0;break;case"~":{let i=o.get(S([t-1,r]));i&&(i.promoted=!0);break}default:{let i=n.charCodeAt(0);if(i<57)t+=i-48;else{let a=n.toLowerCase();o.set(S([t,r]),{role:Qo[a],color:n===a?"black":"white"}),++t}}}return o}function _e(e){return Le.map(o=>z.map(r=>{let t=e.get(r+o);if(t){let n=Xo[t.role];return t.color==="white"&&(n=n.toUpperCase()),t.promoted&&(n+="~"),n}else return"1"}).join("")).join("/").replace(/1{2,}/g,o=>o.length.toString())}function De(e,o){o.animation&&(ke(e.animation,o.animation),(e.animation.duration||0)<70&&(e.animation.enabled=!1))}function ue(e,o){if(o.movable?.dests&&(e.movable.dests=void 0),o.drawable?.autoShapes&&(e.drawable.autoShapes=[]),ke(e,o),o.fen&&(e.pieces=de(o.fen),e.drawable.shapes=o.drawable?.shapes||[]),"check"in o&&je(e,o.check||!1),"lastMove"in o&&!o.lastMove?e.lastMove=void 0:o.lastMove&&(e.lastMove=o.lastMove),e.selected&&Pe(e,e.selected),De(e,o),!e.movable.rookCastle&&e.movable.dests){let r=e.movable.color==="white"?"1":"8",t="e"+r,n=e.movable.dests.get(t),i=e.pieces.get(t);if(!n||!i||i.role!=="king")return;e.movable.dests.set(t,n.filter(a=>!(a==="a"+r&&n.includes("c"+r))&&!(a==="h"+r&&n.includes("g"+r))))}}function ke(e,o){for(let r in o)Object.prototype.hasOwnProperty.call(o,r)&&(Object.prototype.hasOwnProperty.call(e,r)&&eo(e[r])&&eo(o[r])?ke(e[r],o[r]):e[r]=o[r])}function eo(e){if(typeof e!="object"||e===null)return!1;let o=Object.getPrototypeOf(e);return o===Object.prototype||o===null}var R=(e,o)=>o.animation.enabled?er(e,o):q(e,o);function q(e,o){let r=e(o);return o.dom.redraw(),r}var Ee=(e,o)=>({key:e,pos:g(e),piece:o}),Jo=(e,o)=>o.sort((r,t)=>V(e.pos,r.pos)-V(e.pos,t.pos))[0];function _o(e,o){let r=new Map,t=[],n=new Map,i=[],a=[],c=new Map,l,s,f;for(let[d,p]of e)c.set(d,Ee(d,p));for(let d of re)l=o.pieces.get(d),s=c.get(d),l?s?U(l,s.piece)||(i.push(s),a.push(Ee(d,l))):a.push(Ee(d,l)):s&&i.push(s);for(let d of a)s=Jo(d,i.filter(p=>U(d.piece,p.piece))),s&&(f=[s.pos[0]-d.pos[0],s.pos[1]-d.pos[1]],r.set(d.key,f.concat(f)),t.push(s.key));for(let d of i)t.includes(d.key)||n.set(d.key,d.piece);return{anims:r,fadings:n}}function oo(e,o){let r=e.animation.current;if(r===void 0){e.dom.destroyed||e.dom.redrawNow();return}let t=1-(o-r.start)*r.frequency;if(t<=0)e.animation.current=void 0,e.dom.redrawNow();else{let n=or(t);for(let i of r.plan.anims.values())i[2]=i[0]*n,i[3]=i[1]*n;e.dom.redrawNow(!0),requestAnimationFrame((i=performance.now())=>oo(e,i))}}function er(e,o){let r=new Map(o.pieces),t=e(o),n=_o(r,o);if(n.anims.size||n.fadings.size){let i=o.animation.current&&o.animation.current.start;o.animation.current={start:performance.now(),frequency:1/o.animation.duration,plan:n},i||oo(o,performance.now())}else o.dom.redraw();return t}var or=e=>e<.5?4*e*e*e:(e-1)*(2*e-2)*(2*e-2)+1;var rr=["green","red","blue","yellow"];function ro(e,o){if(o.touches&&o.touches.length>1)return;o.stopPropagation(),o.preventDefault(),o.ctrlKey?w(e):Y(e);let r=T(o),t=E(r,b(e),e.dom.bounds());t&&(e.drawable.current={orig:t,pos:r,brush:tr(o),snapToValidMove:e.drawable.defaultSnapToValidMove},to(e))}function to(e){requestAnimationFrame(()=>{let o=e.drawable.current;if(o){let r=E(o.pos,b(e),e.dom.bounds());r||(o.snapToValidMove=!1);let t=o.snapToValidMove?Je(o.orig,o.pos,b(e),e.dom.bounds()):r;t!==o.mouseSq&&(o.mouseSq=t,o.dest=t!==o.orig?t:void 0,e.dom.redrawNow()),to(e)}})}function no(e,o){e.drawable.current&&(e.drawable.current.pos=T(o))}function io(e){let o=e.drawable.current;o&&(o.mouseSq&&nr(e.drawable,o),Ae(e))}function Ae(e){e.drawable.current&&(e.drawable.current=void 0,e.dom.redraw())}function ao(e){e.drawable.shapes.length&&(e.drawable.shapes=[],e.dom.redraw(),co(e.drawable))}function tr(e){let o=(e.shiftKey||e.ctrlKey)&&ie(e),r=e.altKey||e.metaKey||e.getModifierState?.("AltGraph");return rr[(o?1:0)+(r?2:0)]}function nr(e,o){let r=n=>n.orig===o.orig&&n.dest===o.dest,t=e.shapes.find(r);t&&(e.shapes=e.shapes.filter(n=>!r(n))),(!t||t.brush!==o.brush)&&e.shapes.push({orig:o.orig,dest:o.dest,brush:o.brush}),co(e)}function co(e){e.onChange&&e.onChange(e.shapes)}function so(e,o){if(!(e.trustAllEvents||o.isTrusted)||o.button!==void 0&&o.button!==0||o.touches&&o.touches.length>1)return;let r=e.dom.bounds(),t=T(o),n=E(t,b(e),r);if(!n)return;let i=e.pieces.get(n),a=e.selected;if(!a&&e.drawable.enabled&&(e.drawable.eraseOnClick||!i||i.color!==e.turnColor)&&ao(e),o.cancelable!==!1&&(!o.touches||e.blockTouchScroll||i||a||ar(e,t)))o.preventDefault();else if(o.touches)return;let c=!!e.premovable.current,l=!!e.predroppable.current;e.stats.ctrlKey=o.ctrlKey,e.selected&&le(e,e.selected,n)?R(d=>X(d,n),e):X(e,n);let s=e.selected===n,f=go(e,n);if(i&&f&&s&&Qe(e,n)){e.draggable.current={orig:n,piece:i,origPos:t,pos:t,started:e.draggable.autoDistance&&e.stats.dragged,element:f,previouslySelected:a,originTarget:o.target,keyHasChanged:!1},f.cgDragging=!0,f.classList.add("dragging");let d=e.dom.elements.ghost;d&&(d.className=`ghost ${i.color} ${i.role}`,x(d,B(r)(g(n),b(e))),Z(d,!0)),Ne(e)}else c&&D(e),l&&k(e);e.dom.redraw()}function ar(e,o){let r=b(e),t=e.dom.bounds(),n=Math.pow(t.width/8,2);for(let i of e.pieces.keys()){let a=ae(i,r,t);if(V(a,o)<=n)return!0}return!1}function lo(e,o,r,t){let n="a0";e.pieces.set(n,o),e.dom.redraw();let i=T(r);e.draggable.current={orig:n,piece:o,origPos:i,pos:i,started:!0,element:()=>go(e,n),originTarget:r.target,newPiece:!0,force:!!t,keyHasChanged:!1},Ne(e)}function Ne(e){requestAnimationFrame(()=>{let o=e.draggable.current;if(!o)return;e.animation.current?.plan.anims.has(o.orig)&&(e.animation.current=void 0);let r=e.pieces.get(o.orig);if(!r||!U(r,o.piece))G(e);else if(!o.started&&V(o.pos,o.origPos)>=Math.pow(e.draggable.distance,2)&&(o.started=!0),o.started){if(typeof o.element=="function"){let n=o.element();if(!n)return;n.cgDragging=!0,n.classList.add("dragging"),o.element=n}let t=e.dom.bounds();x(o.element,[o.pos[0]-t.left-t.width/16,o.pos[1]-t.top-t.height/16]),o.keyHasChanged||=o.orig!==E(o.pos,b(e),t)}Ne(e)})}function uo(e,o){e.draggable.current&&(!o.touches||o.touches.length<2)&&(e.draggable.current.pos=T(o))}function po(e,o){let r=e.draggable.current;if(!r)return;if(o.type==="touchend"&&o.cancelable!==!1&&o.preventDefault(),o.type==="touchend"&&r.originTarget!==o.target&&!r.newPiece){e.draggable.current=void 0;return}D(e),k(e);let t=T(o)||r.pos,n=E(t,b(e),e.dom.bounds());n&&r.started&&r.orig!==n?r.newPiece?se(e,r.orig,n,r.force):(e.stats.ctrlKey=o.ctrlKey,we(e,r.orig,n)&&(e.stats.dragged=!0)):r.newPiece?e.pieces.delete(r.orig):e.draggable.deleteOnDropOff&&!n&&(e.pieces.delete(r.orig),P(e.events.change)),(r.orig===r.previouslySelected||r.keyHasChanged)&&(r.orig===n||!n)?w(e):e.selectable.enabled||w(e),fo(e),e.draggable.current=void 0,e.dom.redraw()}function G(e){let o=e.draggable.current;o&&(o.newPiece&&e.pieces.delete(o.orig),e.draggable.current=void 0,w(e),fo(e),e.dom.redraw())}function fo(e){let o=e.dom.elements;o.ghost&&Z(o.ghost,!1)}function go(e,o){let r=e.dom.elements.board.firstChild;for(;r;){if(r.cgKey===o&&r.tagName==="PIECE")return r;r=r.nextSibling}}function bo(e,o){e.exploding={stage:1,keys:o},e.dom.redraw(),setTimeout(()=>{mo(e,2),setTimeout(()=>mo(e,void 0),120)},120)}function mo(e,o){e.exploding&&(o?e.exploding.stage=o:e.exploding=void 0,e.dom.redraw())}function ho(e,o){function r(){$e(e),o()}return{set(t){t.orientation&&t.orientation!==e.orientation&&r(),De(e,t),(t.fen?R:q)(n=>ue(n,t),e)},state:e,getFen:()=>_e(e.pieces),toggleOrientation:r,setPieces(t){R(n=>Ie(n,t),e)},selectSquare(t,n){t?R(i=>X(i,t,n),e):e.selected&&(w(e),e.dom.redraw())},move(t,n){R(i=>Se(i,t,n),e)},newPiece(t,n){R(i=>ce(i,t,n),e)},playPremove(){if(e.premovable.current){if(R(Xe,e))return!0;e.dom.redraw()}return!1},playPredrop(t){if(e.predroppable.current){let n=Ye(e,t);return e.dom.redraw(),n}return!1},cancelPremove(){q(D,e)},cancelPredrop(){q(k,e)},cancelMove(){q(t=>{Y(t),G(t)},e)},stop(){q(t=>{xe(t),G(t)},e)},explode(t){bo(e,t)},setAutoShapes(t){q(n=>n.drawable.autoShapes=t,e)},setShapes(t){q(n=>n.drawable.shapes=t,e)},getKeyAtDomPos(t){return E(t,b(e),e.dom.bounds())},redrawAll:o,dragNewPiece(t,n,i){lo(e,t,n,i)},destroy(){xe(e),e.dom.unbind&&e.dom.unbind(),e.dom.destroyed=!0}}}function vo(){return{pieces:de(Ce),orientation:"white",turnColor:"white",coordinates:!0,ranksPosition:"right",autoCastle:!0,viewOnly:!1,disableContextMenu:!1,addPieceZIndex:!1,blockTouchScroll:!1,pieceKey:!1,trustAllEvents:!1,highlight:{lastMove:!0,check:!0},animation:{enabled:!0,duration:200},movable:{free:!0,color:"both",showDests:!0,events:{},rookCastle:!0},premovable:{enabled:!0,showDests:!0,castle:!0,events:{}},predroppable:{enabled:!1,events:{}},draggable:{enabled:!0,distance:3,autoDistance:!0,showGhost:!0,deleteOnDropOff:!1},dropmode:{active:!1},selectable:{enabled:!0},stats:{dragged:!("ontouchstart"in window)},events:{},drawable:{enabled:!0,visible:!0,defaultSnapToValidMove:!0,eraseOnClick:!0,shapes:[],autoShapes:[],brushes:{green:{key:"g",color:"#15781B",opacity:1,lineWidth:10},red:{key:"r",color:"#882020",opacity:1,lineWidth:10},blue:{key:"b",color:"#003088",opacity:1,lineWidth:10},yellow:{key:"y",color:"#e68f00",opacity:1,lineWidth:10},paleBlue:{key:"pb",color:"#003088",opacity:.4,lineWidth:15},paleGreen:{key:"pg",color:"#15781B",opacity:.4,lineWidth:15},paleRed:{key:"pr",color:"#882020",opacity:.4,lineWidth:15},paleGrey:{key:"pgr",color:"#4a4a4a",opacity:.35,lineWidth:15},purple:{key:"purp",color:"#68217a",opacity:.65,lineWidth:10},pink:{key:"pink",color:"#ee2080",opacity:.5,lineWidth:10},hilite:{key:"hilite",color:"#fff",opacity:1,lineWidth:1}},prevSvgHash:""},hold:Fe()}}function wo(){let e=h("defs"),o=y(h("filter"),{id:"cg-filter-blur"});return o.appendChild(y(h("feGaussianBlur"),{stdDeviation:"0.022"})),e.appendChild(o),e}function Po(e,o,r){let t=e.drawable,n=t.current,i=n&&n.mouseSq?n:void 0,a=new Map,c=e.dom.bounds(),l=t.autoShapes.filter(p=>!p.piece);for(let p of t.shapes.concat(l).concat(i?[i]:[])){if(!p.dest)continue;let M=a.get(p.dest)??new Set,v=fe(pe(g(p.orig),e.orientation),c),u=fe(pe(g(p.dest),e.orientation),c);M.add(Te(v,u)),a.set(p.dest,M)}let s=t.shapes.concat(l).map(p=>({shape:p,current:!1,hash:yo(p,He(p.dest,a),!1,c)}));i&&s.push({shape:i,current:!0,hash:yo(i,He(i.dest,a),!0,c)});let f=s.map(p=>p.hash).join(";");if(f===e.drawable.prevSvgHash)return;e.drawable.prevSvgHash=f;let d=o.querySelector("defs");sr(t,s,d),lr(s,o.querySelector("g"),r.querySelector("g"),p=>pr(e,p,t.brushes,a,c))}function sr(e,o,r){let t=new Map,n;for(let c of o.filter(l=>l.shape.dest&&l.shape.brush))n=Mo(e.brushes[c.shape.brush],c.shape.modifiers),c.shape.modifiers?.hilite&&t.set("hilite",e.brushes.hilite),t.set(n.key,n);let i=new Set,a=r.firstElementChild;for(;a;)i.add(a.getAttribute("cgKey")),a=a.nextElementSibling;for(let[c,l]of t.entries())i.has(c)||r.appendChild(mr(l))}function lr(e,o,r,t){let n=new Map;for(let i of e)n.set(i.hash,!1);for(let i of[o,r]){let a=[],c=i.firstElementChild,l;for(;c;)l=c.getAttribute("cgHash"),n.has(l)?n.set(l,!0):a.push(c),c=c.nextElementSibling;for(let s of a)i.removeChild(s)}for(let i of e.filter(a=>!n.get(a.hash)))for(let a of t(i))a.isCustom?r.appendChild(a.el):o.appendChild(a.el)}function yo({orig:e,dest:o,brush:r,piece:t,modifiers:n,customSvg:i,label:a},c,l,s){return[s.width,s.height,l,e,o,r,c&&"-",t&&dr(t),n&&ur(n),i&&`custom-${So(i.html)},${i.center?.[0]??"o"}`,a&&`label-${So(a.text)}`].filter(f=>f).join(",")}function dr(e){return[e.color,e.role,e.scale].filter(o=>o).join(",")}function ur(e){return[e.lineWidth,e.hilite&&"*"].filter(o=>o).join(",")}function So(e){let o=0;for(let r=0;r>>0;return o.toString()}function pr(e,{shape:o,current:r,hash:t},n,i,a){let c=fe(pe(g(o.orig),e.orientation),a),l=o.dest?fe(pe(g(o.dest),e.orientation),a):c,s=o.brush&&Mo(n[o.brush],o.modifiers),f=i.get(o.dest),d=[];if(s){let p=y(h("g"),{cgHash:t});d.push({el:p}),c[0]!==l[0]||c[1]!==l[1]?p.appendChild(gr(o,s,c,l,r,He(o.dest,i))):p.appendChild(fr(n[o.brush],c,r,a))}if(o.label){let p=o.label;p.fill??=o.brush&&n[o.brush].color;let M=o.brush?void 0:"tr";d.push({el:br(p,t,c,l,f,M),isCustom:!0})}if(o.customSvg){let p=o.customSvg.center??"orig",[M,v]=p==="label"?Ko(c,l,f).map(F=>F-.5):p==="dest"?l:c,u=y(h("g"),{transform:`translate(${M},${v})`,cgHash:t});u.innerHTML=`${o.customSvg.html}`,d.push({el:u,isCustom:!0})}return d}function fr(e,o,r,t){let n=hr(),i=(t.width+t.height)/(4*Math.max(t.width,t.height));return y(h("circle"),{stroke:e.color,"stroke-width":n[r?0:1],fill:"none",opacity:xo(e,r),cx:o[0],cy:o[1],r:i-n[1]/2})}function gr(e,o,r,t,n,i){function a(s){let f=yr(i&&!n),d=t[0]-r[0],p=t[1]-r[1],M=Math.atan2(p,d),v=Math.cos(M)*f,u=Math.sin(M)*f;return y(h("line"),{stroke:s?"white":o.color,"stroke-width":vr(o,n)+(s?.04:0),"stroke-linecap":"round","marker-end":`url(#arrowhead-${s?"hilite":o.key})`,opacity:e.modifiers?.hilite?1:xo(o,n),x1:r[0],y1:r[1],x2:t[0]-v,y2:t[1]-u})}if(!e.modifiers?.hilite)return a(!1);let c=h("g"),l=y(h("g"),{filter:"url(#cg-filter-blur)"});return l.appendChild(Sr(r,t)),l.appendChild(a(!0)),c.appendChild(l),c.appendChild(a(!1)),c}function mr(e){let o=y(h("marker"),{id:"arrowhead-"+e.key,orient:"auto",overflow:"visible",markerWidth:4,markerHeight:4,refX:e.key==="hilite"?1.86:2.05,refY:2});return o.appendChild(y(h("path"),{d:"M0,0 V4 L3,2 Z",fill:e.color})),o.setAttribute("cgKey",e.key),o}function br(e,o,r,t,n,i){let c=.4*.75**e.text.length,l=Ko(r,t,n),s=i==="tr"?.4:0,f=y(h("g"),{transform:`translate(${l[0]+s},${l[1]-s})`,cgHash:o});f.appendChild(y(h("circle"),{r:.4/2,"fill-opacity":i?1:.8,"stroke-opacity":i?1:.7,"stroke-width":.03,fill:e.fill??"#666",stroke:"white"}));let d=y(h("text"),{"font-size":c,"font-family":"Noto Sans","text-anchor":"middle",fill:"white",y:.13*.75**e.text.length});return d.innerHTML=e.text,f.appendChild(d),f}function pe(e,o){return o==="white"?e:[7-e[0],7-e[1]]}function He(e,o){return(e&&o.has(e)&&o.get(e).size>1)===!0}function h(e){return document.createElementNS("http://www.w3.org/2000/svg",e)}function y(e,o){for(let r in o)Object.prototype.hasOwnProperty.call(o,r)&&e.setAttribute(r,o[r]);return e}function Mo(e,o){return o?{color:e.color,opacity:Math.round(e.opacity*10)/10,lineWidth:Math.round(o.lineWidth||e.lineWidth),key:[e.key,o.lineWidth].filter(r=>r).join("")}:e}function hr(){return[3/64,4/64]}function vr(e,o){return(e.lineWidth||10)*(o?.85:1)/64}function xo(e,o){return(e.opacity||1)*(o?.9:1)}function yr(e){return(e?20:10)/64}function fe(e,o){let r=Math.min(1,o.width/o.height),t=Math.min(1,o.height/o.width);return[(e[0]-3.5)*r,(3.5-e[1])*t]}function Sr(e,o){let r={from:[Math.floor(Math.min(e[0],o[0])),Math.floor(Math.min(e[1],o[1]))],to:[Math.ceil(Math.max(e[0],o[0])),Math.ceil(Math.max(e[1],o[1]))]};return y(h("rect"),{x:r.from[0],y:r.from[1],width:r.to[0]-r.from[0],height:r.to[1]-r.from[1],fill:"none",stroke:"none"})}function Te(e,o,r=!0){let t=Math.atan2(o[1]-e[1],o[0]-e[0])+Math.PI;return r?(Math.round(t*8/Math.PI)+16)%16:t}function wr(e,o){return Math.sqrt([e[0]-o[0],e[1]-o[1]].reduce((r,t)=>r+t*t,0))}function Ko(e,o,r){let t=wr(e,o),n=Te(e,o,!1);if(r&&(t-=33/64,r.size>1)){t-=10/64;let i=Te(e,o);(r.has((i+1)%16)||r.has((i+15)%16))&&i&1&&(t-=.4)}return[e[0]-Math.cos(n)*t,e[1]-Math.sin(n)*t].map(i=>i+.5)}function Do(e,o){e.innerHTML="",e.classList.add("cg-wrap");for(let l of Ve)e.classList.toggle("orientation-"+l,o.orientation===l);e.classList.toggle("manipulable",!o.viewOnly);let r=K("cg-container");e.appendChild(r);let t=K("cg-board");r.appendChild(t);let n,i,a;if(o.drawable.visible&&(n=y(h("svg"),{class:"cg-shapes",viewBox:"-4 -4 8 8",preserveAspectRatio:"xMidYMid slice"}),n.appendChild(wo()),n.appendChild(h("g")),i=y(h("svg"),{class:"cg-custom-svgs",viewBox:"-3.5 -3.5 8 8",preserveAspectRatio:"xMidYMid slice"}),i.appendChild(h("g")),a=K("cg-auto-pieces"),r.appendChild(n),r.appendChild(i),r.appendChild(a)),o.coordinates){let l=o.orientation==="black"?" black":"",s=o.ranksPosition==="left"?" left":"";r.appendChild(Co(j,"ranks"+l+s)),r.appendChild(Co(z,"files"+l))}let c;return o.draggable.enabled&&o.draggable.showGhost&&(c=K("piece","ghost"),Z(c,!1),r.appendChild(c)),{board:t,container:r,wrap:e,ghost:c,svg:n,customSvg:i,autoPieces:a}}function Co(e,o){let r=K("coords",o),t;for(let n of e)t=K("coord"),t.textContent=n,r.appendChild(t);return r}function ko(e,o){if(!e.dropmode.active)return;D(e),k(e);let r=e.dropmode.piece;if(r){e.pieces.set("a0",r);let t=T(o),n=t&&E(t,b(e),e.dom.bounds());n&&se(e,"a0",n)}e.dom.redraw()}function Ao(e,o){let r=e.dom.elements.board;if("ResizeObserver"in window&&new ResizeObserver(o).observe(e.dom.elements.wrap),(e.disableContextMenu||e.drawable.enabled)&&r.addEventListener("contextmenu",n=>n.preventDefault()),e.viewOnly)return;let t=Mr(e);r.addEventListener("touchstart",t,{passive:!1}),r.addEventListener("mousedown",t,{passive:!1})}function No(e,o){let r=[];if("ResizeObserver"in window||r.push(J(document.body,"chessground.resize",o)),!e.viewOnly){let t=Eo(e,uo,no),n=Eo(e,po,io);for(let a of["touchmove","mousemove"])r.push(J(document,a,t));for(let a of["touchend","mouseup"])r.push(J(document,a,n));let i=()=>e.dom.bounds.clear();r.push(J(document,"scroll",i,{capture:!0,passive:!0})),r.push(J(window,"resize",i,{passive:!0}))}return()=>r.forEach(t=>t())}function J(e,o,r,t){return e.addEventListener(o,r,t),()=>e.removeEventListener(o,r,t)}var Mr=e=>o=>{e.draggable.current?G(e):e.drawable.current?Ae(e):o.shiftKey||ie(o)?e.drawable.enabled&&ro(e,o):e.viewOnly||(e.dropmode.active?ko(e,o):so(e,o))},Eo=(e,o,r)=>t=>{e.drawable.current?e.drawable.enabled&&r(e,t):e.viewOnly||o(e,t)};function To(e){let o=b(e),r=B(e.dom.bounds()),t=e.dom.elements.board,n=e.pieces,i=e.animation.current,a=i?i.plan.anims:new Map,c=i?i.plan.fadings:new Map,l=e.draggable.current,s=Kr(e),f=new Set,d=new Set,p=new Map,M=new Map,v,u,F,W,C,$,ge,A,me,ee;for(u=t.firstChild;u;){if(v=u.cgKey,qo(u))if(F=n.get(v),C=a.get(v),$=c.get(v),W=u.cgPiece,u.cgDragging&&(!l||l.orig!==v)&&(u.classList.remove("dragging"),x(u,r(g(v),o)),u.cgDragging=!1),!$&&u.cgFading&&(u.cgFading=!1,u.classList.remove("fading")),F){if(C&&u.cgAnimating&&W===_(F)){let m=g(v);m[0]+=C[2],m[1]+=C[3],u.classList.add("anim"),x(u,r(m,o))}else u.cgAnimating&&(u.cgAnimating=!1,u.classList.remove("anim"),x(u,r(g(v),o)),e.addPieceZIndex&&(u.style.zIndex=Re(g(v),o)));W===_(F)&&(!$||!u.cgFading)?f.add(v):$&&W===_($)?(u.classList.add("fading"),u.cgFading=!0):qe(p,W,u)}else qe(p,W,u);else if(Oo(u)){let m=u.className;s.get(v)===m?d.add(v):qe(M,m,u)}u=u.nextSibling}for(let[m,I]of s)if(!d.has(m)){me=M.get(I),ee=me&&me.pop();let N=r(g(m),o);if(ee)ee.cgKey=m,x(ee,N);else{let H=K("square",I);H.cgKey=m,x(H,N),t.insertBefore(H,t.firstChild)}}for(let[m,I]of n)if(C=a.get(m),!f.has(m))if(ge=p.get(_(I)),A=ge&&ge.pop(),A){A.cgKey=m,A.cgFading&&(A.classList.remove("fading"),A.cgFading=!1);let N=g(m);e.addPieceZIndex&&(A.style.zIndex=Re(N,o)),C&&(A.cgAnimating=!0,A.classList.add("anim"),N[0]+=C[2],N[1]+=C[3]),x(A,r(N,o))}else{let N=_(I),H=K("piece",N),oe=g(m);H.cgPiece=N,H.cgKey=m,C&&(H.cgAnimating=!0,oe[0]+=C[2],oe[1]+=C[3]),x(H,r(oe,o)),e.addPieceZIndex&&(H.style.zIndex=Re(oe,o)),t.appendChild(H)}for(let m of p.values())Ho(e,m);for(let m of M.values())Ho(e,m)}function Ro(e){let o=b(e),r=B(e.dom.bounds()),t=e.dom.elements.board.firstChild;for(;t;)(qo(t)&&!t.cgAnimating||Oo(t))&&x(t,r(g(t.cgKey),o)),t=t.nextSibling}function Oe(e){let o=e.dom.elements.wrap.getBoundingClientRect(),r=e.dom.elements.container,t=o.height/o.width,n=Math.floor(o.width*window.devicePixelRatio/8)*8/window.devicePixelRatio,i=n*t;r.style.width=n+"px",r.style.height=i+"px",e.dom.bounds.clear(),e.addDimensionsCssVarsTo?.style.setProperty("--cg-width",n+"px"),e.addDimensionsCssVarsTo?.style.setProperty("--cg-height",i+"px")}var qo=e=>e.tagName==="PIECE",Oo=e=>e.tagName==="SQUARE";function Ho(e,o){for(let r of o)e.dom.elements.board.removeChild(r)}function Re(e,o){let t=e[1];return`${o?3+7-t:3+t}`}var _=e=>`${e.color} ${e.role}`;function Kr(e){let o=new Map;if(e.lastMove&&e.highlight.lastMove)for(let n of e.lastMove)O(o,n,"last-move");if(e.check&&e.highlight.check&&O(o,e.check,"check"),e.selected&&(O(o,e.selected,"selected"),e.movable.showDests)){let n=e.movable.dests?.get(e.selected);if(n)for(let a of n)O(o,a,"move-dest"+(e.pieces.has(a)?" oc":""));let i=e.premovable.customDests?.get(e.selected)??e.premovable.dests;if(i)for(let a of i)O(o,a,"premove-dest"+(e.pieces.has(a)?" oc":""))}let r=e.premovable.current;if(r)for(let n of r)O(o,n,"current-premove");else e.predroppable.current&&O(o,e.predroppable.current.key,"current-premove");let t=e.exploding;if(t)for(let n of t.keys)O(o,n,"exploding"+t.stage);return e.highlight.custom&&e.highlight.custom.forEach((n,i)=>{O(o,i,n)}),o}function O(e,o,r){let t=e.get(o);t?e.set(o,`${t} ${r}`):e.set(o,r)}function qe(e,o,r){let t=e.get(o);t?t.push(r):e.set(o,[r])}function Vo(e,o,r){let t=new Map,n=[];for(let c of e)t.set(c.hash,!1);let i=o.firstElementChild,a;for(;i;)a=i.getAttribute("cgHash"),t.has(a)?t.set(a,!0):n.push(i),i=i.nextElementSibling;for(let c of n)o.removeChild(c);for(let c of e)t.get(c.hash)||o.appendChild(r(c))}function Bo(e,o){let t=e.drawable.autoShapes.filter(n=>n.piece).map(n=>({shape:n,hash:Dr(n),current:!1}));Vo(t,o,n=>Cr(e,n,e.dom.bounds()))}function Lo(e){let o=b(e),r=B(e.dom.bounds()),t=e.dom.elements.autoPieces?.firstChild;for(;t;)be(t,r(g(t.cgKey),o),t.cgScale),t=t.nextSibling}function Cr(e,{shape:o,hash:r},t){let n=o.orig,i=o.piece?.role,a=o.piece?.color,c=o.piece?.scale,l=K("piece",`${i} ${a}`);return l.setAttribute("cgHash",r),l.cgKey=n,l.cgScale=c,be(l,B(t)(g(n),b(e)),c),l}var Dr=e=>[e.orig,e.piece?.role,e.piece?.color,e.piece?.scale].join(",");function Dt({el:e,config:o}){return Er(e,o)}function Er(e,o){let r=vo();ue(r,o||{});function t(){let n="dom"in r?r.dom.unbind:void 0,i=Do(e,r),a=Ge(()=>i.board.getBoundingClientRect()),c=f=>{To(s),i.autoPieces&&Bo(s,i.autoPieces),!f&&i.svg&&Po(s,i.svg,i.customSvg)},l=()=>{Oe(s),Ro(s),i.autoPieces&&Lo(s)},s=r;return s.dom={elements:i,bounds:a,redraw:Ar(c),redrawNow:c,unbind:n},s.drawable.prevSvgHash="",Oe(s),c(!1),Ao(s,l),n||(s.dom.unbind=No(s,l)),s.events.insert&&s.events.insert(i),s}return ho(t(),t)}function Ar(e){let o=!1;return()=>{o||(o=!0,requestAnimationFrame(()=>{e(),o=!1}))}}export{Er as Chessground,Dt as initModule}; From c95481bb4d7bc6e2a8842d2a5ae092509a54d24f Mon Sep 17 00:00:00 2001 From: Jonathan Gamble <101470903+schlawg@users.noreply.github.com> Date: Tue, 5 Sep 2023 22:18:03 -0500 Subject: [PATCH 053/174] . --- pnpm-lock.yaml | 2 +- public/vendor/shepherd | 2 +- ui/analyse/css/study/_editor.scss | 5 +++ ui/common/css/component/_continue-with.scss | 4 +- ui/common/src/dialog.ts | 41 +++++++++++---------- ui/localPlay/i18n.json | 2 +- ui/localPlay/src/botVsBot/bvbCtrl.ts | 7 +++- ui/localPlay/src/botVsBot/bvbView.ts | 4 +- ui/round/tsconfig.json | 4 +- ui/site/src/component/powertip.ts | 20 ++++++---- 10 files changed, 54 insertions(+), 37 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31af830fbfc1f..153476172e571 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' settings: autoInstallPeers: true diff --git a/public/vendor/shepherd b/public/vendor/shepherd index e3ed0fcbf5c31..30ecedb915987 160000 --- a/public/vendor/shepherd +++ b/public/vendor/shepherd @@ -1 +1 @@ -Subproject commit e3ed0fcbf5c31137ece409d3ad6fea4e264ca1cc +Subproject commit 30ecedb9159874e452edf4e9b1e17b5de885732a diff --git a/ui/analyse/css/study/_editor.scss b/ui/analyse/css/study/_editor.scss index c99702790b137..67072901bbb7e 100644 --- a/ui/analyse/css/study/_editor.scss +++ b/ui/analyse/css/study/_editor.scss @@ -15,6 +15,11 @@ grid-template-columns: 280px 2vmin 210px; grid-template-rows: min-content auto min-content; grid-template-areas: '. . e-tools' 'spare-top . e-tools' 'e-board . e-tools' 'spare-bottom . e-tools' '. . e-tools'; + @media (max-width: 576px) { + grid-template-columns: 100%; + grid-template-rows: auto min-content; + grid-template-areas: 'e-board' 'e-tools'; + } user-select: none; .main-board { diff --git a/ui/common/css/component/_continue-with.scss b/ui/common/css/component/_continue-with.scss index 35331ed25daeb..cbe682caccab3 100644 --- a/ui/common/css/component/_continue-with.scss +++ b/ui/common/css/component/_continue-with.scss @@ -1,6 +1,8 @@ .continue-with { @extend %flex-column; - + @media (hover: none) { + padding: 2em; + } > *:not(:first-child) { margin-top: 1em; } diff --git a/ui/common/src/dialog.ts b/ui/common/src/dialog.ts index afc7103411693..0725a05bb60b8 100644 --- a/ui/common/src/dialog.ts +++ b/ui/common/src/dialog.ts @@ -1,6 +1,5 @@ import { VNode, Attrs } from 'snabbdom'; import { onInsert, h, MaybeVNodes } from './snabbdom'; -import { spinnerVdom } from './spinner'; import { isTouchDevice, isIOS } from './mobile'; import * as xhr from './xhr'; import * as licon from './licon'; @@ -35,12 +34,12 @@ interface DialogOpts { export interface DomDialogOpts extends DialogOpts { parent?: Element; - show?: 'modal' | boolean; // if not falsy, auto-show & remove from dom when closed + show?: 'modal' | boolean; } export interface SnabDialogOpts extends DialogOpts { vnodes?: MaybeVNodes; - onInsert?: (dialog: Dialog) => void; // prevents showModal, caller must do so manually + onInsert?: (dialog: Dialog) => void; } export async function domDialog(o: DomDialogOpts): Promise { @@ -108,7 +107,7 @@ export function snabDialog(o: SnabDialogOpts): VNode { else wrapper.showModal(); }), }, - o.vnodes ?? spinnerVdom(), + o.vnodes, ), ), ], @@ -116,21 +115,22 @@ export function snabDialog(o: SnabDialogOpts): VNode { } class DialogWrapper implements Dialog { - restoreFocus?: HTMLElement; - constructor( readonly dialog: HTMLDialogElement, readonly view: HTMLElement, readonly o: DialogOpts, ) { - if (dialogPolyfill) dialogPolyfill.registerDialog(dialog); // ios < 15.4 - + if (dialogPolyfill) { + console.log('registered one'); + dialogPolyfill.registerDialog(dialog); // ios < 15.4 + } view.parentElement?.style.setProperty('--vh', `${window.innerHeight * 0.01}px`); // ios safari view.addEventListener('click', e => e.stopPropagation()); dialog.addEventListener('close', this.onClose); + dialog.querySelector('button.close-button')?.addEventListener('click', this.close); - if (!o.noClickAway) setTimeout(() => document.addEventListener('click', this.close), 0); + if (!o.noClickAway) setTimeout(() => dialog.addEventListener('click', this.close), 0); } get open() { @@ -138,14 +138,8 @@ class DialogWrapper implements Dialog { } show = () => this.dialog.show(); - close = () => this.dialog.close(); - onClose = () => { - this.o.onClose?.(this); - if ('show' in this.o && this.o.show === 'modal') this.dialog.remove(); - this.restoreFocus?.focus(); - this.restoreFocus = undefined; - }; + restoreFocus?: HTMLElement; showModal = () => { this.restoreFocus = document.activeElement as HTMLElement; @@ -157,10 +151,19 @@ class DialogWrapper implements Dialog { this.dialog.showModal(); }; + close = () => this.dialog.close(); + + onClose = () => { + this.o.onClose?.(this); + if ('show' in this.o && this.o.show === 'modal') this.dialog.remove(); + this.restoreFocus?.focus(); + this.restoreFocus = undefined; + }; + addModalListeners? = () => { this.dialog.addEventListener('keydown', onModalKeydown); - if (isTouchDevice()) this.dialog.addEventListener('touchmove', (e: TouchEvent) => e.stopPropagation()); - this.addModalListeners = undefined; // only do this once per HTMLDialogElement + //if (isTouchDevice()) this.dialog.addEventListener('touchmove', (e: TouchEvent) => e.stopPropagation()); + this.addModalListeners = undefined; // only once per HTMLDialogElement }; } @@ -188,7 +191,7 @@ function onModalKeydown(e: KeyboardEvent) { } function onResize() { - const vh = window.innerHeight * 0.01; + const vh = window.innerHeight * 0.01; // ios safari $('dialog > div.scrollable').css('--vh', `${vh}px`); } diff --git a/ui/localPlay/i18n.json b/ui/localPlay/i18n.json index 4dcf70a2be4dc..1c32c0df93799 100644 --- a/ui/localPlay/i18n.json +++ b/ui/localPlay/i18n.json @@ -65,4 +65,4 @@ "nbDays": "%s days", "nbHours:one": "%s hour", "nbHours": "%s hours" -} \ No newline at end of file +} diff --git a/ui/localPlay/src/botVsBot/bvbCtrl.ts b/ui/localPlay/src/botVsBot/bvbCtrl.ts index 4f0af552a811a..ca5c5d579d741 100644 --- a/ui/localPlay/src/botVsBot/bvbCtrl.ts +++ b/ui/localPlay/src/botVsBot/bvbCtrl.ts @@ -24,11 +24,14 @@ export class BvbCtrl { fiftyMovePly = 0; threefoldFens: Map = new Map(); - constructor(readonly opts: BvbOpts, readonly redraw: () => void) { + constructor( + readonly opts: BvbOpts, + readonly redraw: () => void, + ) { this.promotion = new PromotionCtrl( f => f(this.cg), () => this.cg.set(this.cgOpts()), - this.redraw + this.redraw, ); Promise.all([makeZerofish(), makeZerofish()]).then(([wz, bz]) => { this.zf ??= { white: wz, black: bz }; diff --git a/ui/localPlay/src/botVsBot/bvbView.ts b/ui/localPlay/src/botVsBot/bvbView.ts index c30ddf3adec6c..06f22335e10de 100644 --- a/ui/localPlay/src/botVsBot/bvbView.ts +++ b/ui/localPlay/src/botVsBot/bvbView.ts @@ -36,10 +36,10 @@ function controls(ctrl: BvbCtrl) { 'button#go.button.disabled', { hook: onInsert(el => - el.addEventListener('click', () => ctrl.go(parseInt($('#num-games').val() as string) || 1)) + el.addEventListener('click', () => ctrl.go(parseInt($('#num-games').val() as string) || 1)), ), }, - 'GO' + 'GO', ), h('input#num-games', { attrs: { type: 'number', min: '1', max: '1000', value: '1' }, diff --git a/ui/round/tsconfig.json b/ui/round/tsconfig.json index c3c2f07e2b15b..89dc5b54ceb5d 100644 --- a/ui/round/tsconfig.json +++ b/ui/round/tsconfig.json @@ -1,11 +1,11 @@ { "extends": "../tsconfig.base.json", - "compilerOptions": { + "compilerOptions": { "outDir": "dist", "rootDir": "src", "composite": true, "declaration": true, - "emitDeclarationOnly": true, + "emitDeclarationOnly": true }, "isolatedModules": true, "references": [ diff --git a/ui/site/src/component/powertip.ts b/ui/site/src/component/powertip.ts index 80a8c28c5c52c..5ba9580565e05 100644 --- a/ui/site/src/component/powertip.ts +++ b/ui/site/src/component/powertip.ts @@ -32,7 +32,7 @@ const userPowertip = (el: HTMLElement, pos?: PowerTip.Placement) => uptA('/@/' + u + '/tv', licon.AnalogTv) + uptA('/inbox/new?user=' + u, licon.BubbleSpeech) + uptA('/?user=' + u + '#friend', licon.Swords) + - '
' + '
', ); }), placement: @@ -58,7 +58,7 @@ function powerTipWith(el: HTMLElement, ev: Event, f: (el: HTMLElement) => void) function onIdleForAll(par: HTMLElement, sel: string, f: (el: HTMLElement) => void) { requestIdleCallback( () => Array.prototype.forEach.call(par.querySelectorAll(sel), (el: HTMLElement) => f(el)), // do not codegolf to `f` - 800 + 800, ); } @@ -248,7 +248,11 @@ class DisplayController { hoverTimer?: number; el: WithTooltip; - constructor(readonly element: Cash, readonly options: Options, readonly tipController: TooltipController) { + constructor( + readonly element: Cash, + readonly options: Options, + readonly tipController: TooltipController, + ) { this.el = $as(element); this.scoped = session.scoped[options.popupId!]; // expose the methods @@ -326,7 +330,7 @@ function placementCalculator() { placement: PowerTip.Placement, tipWidth: number, tipHeight: number, - offset: number + offset: number, ) { const placementBase = placement.split('-')[0], // ignore 'alt' for corners coords = cssCoordinates(), @@ -541,7 +545,7 @@ class TooltipController { const collisions = getViewportCollisions( this.placeTooltip(element, pos), this.tipElement.outerWidth() || this.options.defaultSize[0], - this.tipElement.outerHeight() || this.options.defaultSize[1] + this.tipElement.outerHeight() || this.options.defaultSize[1], ); // break if there were no collisions @@ -580,7 +584,7 @@ class TooltipController { placement, tipWidth, tipHeight, - this.options.offset! + this.options.offset!, ); // place the tooltip @@ -657,7 +661,7 @@ function initTracking() { session.windowWidth = $window.width(); session.windowHeight = $window.height(); }, - { passive: true } + { passive: true }, ); window.addEventListener( @@ -674,7 +678,7 @@ function initTracking() { session.scrollTop = y; } }, - { passive: true } + { passive: true }, ); } } From ff1e6fa7fd3ca8d3e474bab71cb0b034fe4bdb27 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Thu, 7 Sep 2023 05:33:05 -0500 Subject: [PATCH 054/174] . --- ui/common/src/dialog.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/ui/common/src/dialog.ts b/ui/common/src/dialog.ts index c3a724c11bfc4..fb02d95dbd71b 100644 --- a/ui/common/src/dialog.ts +++ b/ui/common/src/dialog.ts @@ -119,6 +119,8 @@ export function snabDialog(o: SnabDialogOpts): VNode { } class DialogWrapper implements Dialog { + restoreFocus?: HTMLElement; + constructor( readonly dialog: HTMLDialogElement, readonly view: HTMLElement, @@ -179,15 +181,6 @@ class DialogWrapper implements Dialog { this.dialog.showModal(); }; - close = () => this.dialog.close(); - - onClose = () => { - this.o.onClose?.(this); - if ('show' in this.o && this.o.show === 'modal') this.dialog.remove(); - this.restoreFocus?.focus(); - this.restoreFocus = undefined; - }; - addModalListeners? = () => { this.dialog.addEventListener('keydown', onModalKeydown); this.addModalListeners = undefined; // only do this once per HTMLDialogElement From c449414fbcc3ff3b86c75a8ebeecad60c07122de Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Thu, 7 Sep 2023 09:54:52 -0500 Subject: [PATCH 055/174] . --- ui/analyse/src/study/studyShare.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/analyse/src/study/studyShare.ts b/ui/analyse/src/study/studyShare.ts index 53b8bdf5cde52..d298e1c27ad16 100644 --- a/ui/analyse/src/study/studyShare.ts +++ b/ui/analyse/src/study/studyShare.ts @@ -22,6 +22,7 @@ export interface StudyShareCtrl { shareable(): boolean; redraw: () => void; trans: Trans; + gamebook: boolean; } function fromPly(ctrl: StudyShareCtrl): VNode { @@ -75,6 +76,7 @@ export function ctrl( shareable: () => data.features.shareable, redraw, trans, + gamebook: data.chapter.gamebook, }; } @@ -257,7 +259,9 @@ export function view(ctrl: StudyShareCtrl): VNode { readonly: true, disabled: isPrivate, value: !isPrivate - ? `` : ctrl.trans.noarg('onlyPublicStudiesCanBeEmbedded'), From e5edcab52ed2914a0aa6d37f8c588a01b4ac83fe Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Thu, 7 Sep 2023 19:49:47 -0500 Subject: [PATCH 056/174] gah --- pnpm-lock.yaml | 26 +++++++++++++++++++++----- ui/libot/package.json | 2 +- ui/libot/src/bots/babyBot.ts | 20 -------------------- ui/libot/src/bots/babyHoward.ts | 8 ++++---- ui/libot/src/bots/beatrice.ts | 8 ++++---- ui/libot/src/bots/coral.ts | 9 ++++----- ui/libot/src/main.ts | 8 +++++--- ui/localPlay/package.json | 2 +- ui/localPlay/src/botVsBot/bvbCtrl.ts | 2 +- ui/localPlay/src/ctrl.ts | 6 +++--- 10 files changed, 44 insertions(+), 47 deletions(-) delete mode 100644 ui/libot/src/bots/babyBot.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11a6a08d33977..626fd18cc2aa1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.1' +lockfileVersion: '6.0' settings: autoInstallPeers: true @@ -356,8 +356,8 @@ importers: specifier: workspace:* version: link:../tree zerofish: - specifier: ^0.0.11 - version: 0.0.11 + specifier: ^0.0.13 + version: 0.0.13 ui/lobby: dependencies: @@ -419,8 +419,8 @@ importers: specifier: workspace:* version: link:../tree zerofish: - specifier: ^0.0.11 - version: 0.0.11 + specifier: ^0.0.13 + version: 0.0.13 ui/mod: dependencies: @@ -4781,6 +4781,12 @@ packages: hasBin: true dev: false + /prettier@3.0.1: + resolution: {integrity: sha512-fcOWSnnpCrovBsmFZIGIy9UqK2FaI7Hqax+DIO0A9UxeVoY4iweyaFjS5TavZN97Hfehph0nhsZnjlVKzEQSrQ==} + engines: {node: '>=14'} + hasBin: true + dev: false + /prettier@3.0.2: resolution: {integrity: sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==} engines: {node: '>=14'} @@ -5594,6 +5600,16 @@ packages: typescript: 5.1.6 dev: false + /zerofish@0.0.13: + resolution: {integrity: sha512-+13F0FJB1+YS+pkhH+hKV2lP1GKjuQrEo1teljf8WMJi2V4GD3Stfmj3GlVWzQYX5lGRG0F5m9g59nttPLOaEQ==} + dependencies: + '@types/emscripten': 1.39.7 + '@types/node': 20.5.0 + '@types/web': 0.0.113 + prettier: 3.0.1 + typescript: 5.1.6 + dev: false + /zxcvbn@4.4.2: resolution: {integrity: sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ==} dev: false diff --git a/ui/libot/package.json b/ui/libot/package.json index d750bb10cd0a6..b2ae344914507 100644 --- a/ui/libot/package.json +++ b/ui/libot/package.json @@ -21,7 +21,7 @@ "chessops": "^0.12.7", "common": "workspace:*", "tree": "workspace:*", - "zerofish": "^0.0.11" + "zerofish": "^0.0.13" }, "scripts": { "compile": "tsc", diff --git a/ui/libot/src/bots/babyBot.ts b/ui/libot/src/bots/babyBot.ts deleted file mode 100644 index ec3d5a1e42bbe..0000000000000 --- a/ui/libot/src/bots/babyBot.ts +++ /dev/null @@ -1,20 +0,0 @@ -import makeZerofish, { type Zerofish } from 'zerofish'; -import { type Libot, botNetUrl, localBots } from '../main'; - -export class BabyBot implements Libot { - name = localBots.babyBot.name; - description = localBots.babyBot.description; - image = localBots.babyBot.image; - net = botNetUrl('maia-1100.pb'); - ratings = new Map(); - - zf: Zerofish; - constructor(opts?: any) { - opts; - makeZerofish({ pbUrl: this.net }).then(zf => (this.zf = zf)); - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} diff --git a/ui/libot/src/bots/babyHoward.ts b/ui/libot/src/bots/babyHoward.ts index 59a0402f20c01..ea14b7ca5e933 100644 --- a/ui/libot/src/bots/babyHoward.ts +++ b/ui/libot/src/bots/babyHoward.ts @@ -1,17 +1,17 @@ -import makeZerofish, { type Zerofish } from 'zerofish'; +import { type Zerofish } from 'zerofish'; import { type Libot, botNetUrl, localBots } from '../main'; export class BabyHoward implements Libot { name = localBots.babyHoward.name; description = localBots.babyHoward.description; image = localBots.babyHoward.image; - net = botNetUrl('maia-1100.pb'); + net = 'maia-1100'; ratings = new Map(); zf: Zerofish; - constructor(opts?: any) { + constructor(zf: Zerofish, opts?: any) { opts; - makeZerofish({ pbUrl: this.net }).then(zf => (this.zf = zf)); + this.zf = zf; } async move(fen: string) { diff --git a/ui/libot/src/bots/beatrice.ts b/ui/libot/src/bots/beatrice.ts index 77122481c1b0b..ee13205b08daf 100644 --- a/ui/libot/src/bots/beatrice.ts +++ b/ui/libot/src/bots/beatrice.ts @@ -1,17 +1,17 @@ -import makeZerofish, { type Zerofish } from 'zerofish'; +import { type Zerofish } from 'zerofish'; import { Libot, botNetUrl, localBots } from '../main'; export class Beatrice implements Libot { name = localBots.beatrice.name; description = localBots.beatrice.description; image = localBots.beatrice.image; - net = botNetUrl('maia-1100.pb'); + net = 'maia-1100'; ratings = new Map(); zf: Zerofish; - constructor(opts?: any) { + constructor(zf: Zerofish, opts?: any) { opts; - makeZerofish({ pbUrl: this.net }).then(zf => (this.zf = zf)); + this.zf = zf; } async move(fen: string) { diff --git a/ui/libot/src/bots/coral.ts b/ui/libot/src/bots/coral.ts index 248e0c93299d9..e574bc67fcea1 100644 --- a/ui/libot/src/bots/coral.ts +++ b/ui/libot/src/bots/coral.ts @@ -1,17 +1,16 @@ -import makeZerofish, { type Zerofish } from 'zerofish'; +import { type Zerofish } from 'zerofish'; import { Libot, botNetUrl, localBots } from '../main'; export class Coral implements Libot { name = localBots.coral.name; description = localBots.coral.description; image = localBots.coral.image; - net = botNetUrl('maia-1100.pb'); + net = 'maia-1100'; ratings = new Map(); zf: Zerofish; - - constructor(opts?: any) { + constructor(zf: Zerofish, opts?: any) { opts; - makeZerofish({ pbUrl: this.net }).then(zf => (this.zf = zf)); + this.zf = zf; } async move(fen: string) { diff --git a/ui/libot/src/main.ts b/ui/libot/src/main.ts index 9947b5fb241e1..ff658518af852 100644 --- a/ui/libot/src/main.ts +++ b/ui/libot/src/main.ts @@ -1,5 +1,7 @@ export * from './interfaces'; +export * from './ctrl'; + export interface BotInfo { readonly name: string; readonly description: string; @@ -17,7 +19,7 @@ export const localBots: { [key: string]: BotInfo } = { description: 'Baby Howard is a bot that plays random moves.', image: botImageUrl('baby-howard.webp'), }, - babyBot: { + elsieZero: { name: 'Elsie Zero', description: 'Elsie Zero is a bot that plays random moves.', image: botImageUrl('baby-robot.webp'), @@ -139,8 +141,8 @@ export const localBots: { [key: string]: BotInfo } = { }, }; -export function botNetUrl(weights: string) { - return lichess.assetUrl(`lifat/bots/weights/${weights}`, { noVersion: true }); +export function botNetUrl(net: string) { + return lichess.assetUrl(`lifat/bots/weights/${net}.pb`, { noVersion: true }); } export function botImageUrl(image: string) { diff --git a/ui/localPlay/package.json b/ui/localPlay/package.json index 8f626475f8413..223dc1bbd95de 100644 --- a/ui/localPlay/package.json +++ b/ui/localPlay/package.json @@ -16,7 +16,7 @@ "round": "workspace:*", "snabbdom": "^3.5.1", "tree": "workspace:*", - "zerofish": "^0.0.11" + "zerofish": "^0.0.13" }, "scripts": { "compile": "tsc", diff --git a/ui/localPlay/src/botVsBot/bvbCtrl.ts b/ui/localPlay/src/botVsBot/bvbCtrl.ts index ca5c5d579d741..1c6704d52d8c9 100644 --- a/ui/localPlay/src/botVsBot/bvbCtrl.ts +++ b/ui/localPlay/src/botVsBot/bvbCtrl.ts @@ -207,7 +207,7 @@ export class BvbCtrl { const reader = new FileReader(); const weights = e.dataTransfer.files.item(0) as File; reader.onload = e => { - this.zf[color]!.setZeroWeights(new Uint8Array(e.target!.result as ArrayBuffer)); + this.zf[color]!.setNet(weights.name, new Uint8Array(e.target!.result as ArrayBuffer)); $(`#${color} p`).first().text(weights.name); this.players[color] = 'zero'; if (this.players[Chops.opposite(color)] !== 'human') $('#go').removeClass('disabled'); diff --git a/ui/localPlay/src/ctrl.ts b/ui/localPlay/src/ctrl.ts index e3155771047da..b8fb2b865ec19 100644 --- a/ui/localPlay/src/ctrl.ts +++ b/ui/localPlay/src/ctrl.ts @@ -1,6 +1,6 @@ import { LocalPlayOpts } from './interfaces'; import { Coral } from 'libot/bots/coral'; -import { type Libot } from 'libot'; +import { Ctrl as LibotCtrl } from 'libot'; import { makeRounds } from './data'; import { makeFen /*, parseFen*/ } from 'chessops/fen'; import { makeSanAndPlay } from 'chessops/san'; @@ -8,7 +8,7 @@ import { Chess } from 'chessops'; import * as Chops from 'chessops'; export class Ctrl { - bot?: Libot = new Coral(); + //bot?: Libot = new Coral(); chess = Chess.default(); tellRound: SocketSend; fiftyMovePly = 0; @@ -71,7 +71,7 @@ export class Ctrl { } async botMove() { - this.move(await this.bot!.move(this.fen)); + //this.move(await this.bot!.move(this.fen)); } fifty(move?: Chops.Move) { From 30529d57b6dc2385494d3d5683a2eb481988fba5 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Thu, 7 Sep 2023 19:50:28 -0500 Subject: [PATCH 057/174] gah --- ui/libot/src/bots/elsieZero.ts | 20 ++++++++++++++ ui/libot/src/ctrl.ts | 49 ++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 ui/libot/src/bots/elsieZero.ts create mode 100644 ui/libot/src/ctrl.ts diff --git a/ui/libot/src/bots/elsieZero.ts b/ui/libot/src/bots/elsieZero.ts new file mode 100644 index 0000000000000..f014c26d176cd --- /dev/null +++ b/ui/libot/src/bots/elsieZero.ts @@ -0,0 +1,20 @@ +import { type Zerofish } from 'zerofish'; +import { type Libot, botNetUrl, localBots } from '../main'; + +export class ElsieZero implements Libot { + name = localBots.elsieZero.name; + description = localBots.elsieZero.description; + image = localBots.elsieZero.image; + net = 'maia-1100'; + ratings = new Map(); + + zf: Zerofish; + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} diff --git a/ui/libot/src/ctrl.ts b/ui/libot/src/ctrl.ts new file mode 100644 index 0000000000000..6583628a354be --- /dev/null +++ b/ui/libot/src/ctrl.ts @@ -0,0 +1,49 @@ +import makeZerofish, { type Zerofish } from 'zerofish'; +import { Libot } from './interfaces'; +import { Coral } from './bots/coral'; +import { BabyHoward } from './bots/babyHoward'; +import { ElsieZero } from './bots/elsieZero'; +import { Beatrice } from './bots/beatrice'; + +export interface Ctrl { + zf: Zerofish; + setBot(name: string): Promise; + move(fen: string): Promise; +} + +export async function makeCtrl(): Promise { + const zf = await makeZerofish(); + const nets = new Map(); + const bots: { [k: string]: Libot } = { + coral: new Coral(zf), + babyHoward: new BabyHoward(zf), + elsieZero: new ElsieZero(zf), + beatrice: new Beatrice(zf), + }; + + return { + zf, + setBot(name: string) { + const net = bots[name]?.net; + if (!net) throw new Error(`unknown bot ${name} or no net`); + if (zf.netName !== bots[name].net) { + if (!nets.has(bots[name].net)) { + nets.set(bots[name].net, fetchNet(bots[name].net)); + } + return nets.get(bots[name].net).then(buf => { + zf.setNet(buf); + zf.netName = bots[name].net; + }); + } + }, + move(fen: string) { + return zf.goZero(fen); + }, + }; +} + +function fetchNet(netName: string): Promise { + return fetch(`/lifat/bots/weights/${netName}.pb`) + .then(res => res.arrayBuffer()) + .then(buf => new Uint8Array(buf)); +} From d19dde9981f6a492b500a6a3447becb695b636c0 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Thu, 2 Nov 2023 08:15:36 -0500 Subject: [PATCH 058/174] fishes --- ui/ceval/package.json | 1 + ui/ceval/src/engines/engines.ts | 84 +++++++++++++++---- ui/ceval/src/engines/externalEngine.ts | 7 +- ui/ceval/src/engines/simpleEngine.ts | 9 +- ui/ceval/src/engines/stockfishWebEngine.ts | 26 ++++-- ui/ceval/src/engines/threadedEngine.ts | 52 ++++++++---- ui/ceval/src/main.ts | 4 +- ui/ceval/src/types.ts | 1 + ui/common/src/objectStorage.ts | 8 +- ui/gameSetup/src/ctrl.ts | 2 +- ui/localPlay/css/_bot-vs-bot.scss | 95 ++++++---------------- ui/localPlay/css/build/_bot-vs-bot.scss | 1 + ui/localPlay/package.json | 9 +- ui/localPlay/src/botVsBot/bvbCtrl.ts | 2 + ui/localPlay/src/botVsBot/bvbMain.ts | 15 ++-- ui/localPlay/tsconfig.json | 1 + 16 files changed, 190 insertions(+), 127 deletions(-) diff --git a/ui/ceval/package.json b/ui/ceval/package.json index d2efedf97cd40..53e639db96921 100644 --- a/ui/ceval/package.json +++ b/ui/ceval/package.json @@ -23,6 +23,7 @@ "stockfish.js": "^10.0.2", "stockfish.wasm": "^0.10.0", "stockfish-mv.wasm": "^0.6.1", + "stockfish-nnue.wasm": "1.0.0-1946a675.smolnet", "lila-stockfish-web": "^0.0.1" }, "scripts": { diff --git a/ui/ceval/src/engines/engines.ts b/ui/ceval/src/engines/engines.ts index 5f0cf9833ff6c..e29638255f929 100644 --- a/ui/ceval/src/engines/engines.ts +++ b/ui/ceval/src/engines/engines.ts @@ -1,5 +1,6 @@ import { BrowserEngineInfo, ExternalEngineInfo, EngineInfo, CevalEngine } from '../types'; import CevalCtrl from '../ctrl'; +import { LegacyBot } from './legacyBot'; import { SimpleEngine } from './simpleEngine'; import { StockfishWebEngine } from './stockfishWebEngine'; import { ThreadedEngine } from './threadedEngine'; @@ -19,17 +20,17 @@ export class Engines { private selected: StoredProp; active?: EngineInfo; - constructor(readonly ctrl: CevalCtrl) { + constructor(readonly ctrl?: CevalCtrl) { this.localEngineMap = this.makeEngineMap(); this.localEngines = [...this.localEngineMap.values()].map(e => e.info); - this.externalEngines = this.ctrl.opts.externalEngines?.map(e => ({ tech: 'EXTERNAL', ...e })) ?? []; + this.externalEngines = this.ctrl?.opts.externalEngines?.map(e => ({ tech: 'EXTERNAL', ...e })) ?? []; this.selected = storedStringProp('ceval.engine', this.localEngines[0].id); if (this.selected() === 'lichess') this.selected(this.localEngines[0].id); // delete this 2024-01-01 - this.active = this.engineFor({ id: this.selected(), variant: this.ctrl.opts.variant.key }); + this.active = this.info({ id: this.selected(), variant: this.ctrl?.opts.variant.key ?? 'standard' }); } get external() { @@ -37,7 +38,6 @@ export class Engines { } supporting(variant: VariantKey): EngineInfo[] { - console.log(variant, this.externalEngines); return [ ...this.localEngines.filter(e => e.variants?.includes(variant)), ...this.externalEngines.filter(e => externalEngineSupports(e, variant)), @@ -45,11 +45,11 @@ export class Engines { } select(id: string) { - this.active = this.engineFor({ id })!; + this.active = this.info({ id })!; this.selected(id); } - engineFor(selector?: { id?: string; variant?: VariantKey }): EngineInfo | undefined { + info(selector?: { id?: string; variant?: VariantKey }): EngineInfo | undefined { const id = selector?.id ?? this.selected(); const variant = selector?.variant ?? 'standard'; return ( @@ -61,19 +61,28 @@ export class Engines { } make(selector?: { id?: string; variant?: VariantKey }): CevalEngine { - const e = (this.active = this.engineFor(selector)); + const e = (this.active = this.info(selector)); if (!e) throw Error(`Engine not found ${selector?.id ?? selector?.variant ?? this.selected()}}`); - return e.tech !== 'EXTERNAL' - ? this.localEngineMap.get(e.id)!.make(e as BrowserEngineInfo) - : new ExternalEngine(e as ExternalEngineInfo, this.ctrl.opts.redraw); + return ( + e.tech !== 'EXTERNAL' + ? this.localEngineMap.get(e.id)!.make(e as BrowserEngineInfo) + : new ExternalEngine(e as ExternalEngineInfo, this.ctrl!.opts.redraw) + ) as CevalEngine; + } + + makeBot(id: string): LegacyBot { + const e = this.info({ id }); + if (!e) throw Error(`Engine not found ${id}`); + e.isBot = true; + return this.localEngineMap.get(e.id)!.make(e as BrowserEngineInfo) as LegacyBot; } makeEngineMap() { - const redraw = this.ctrl.opts.redraw; + const redraw = this.ctrl?.opts.redraw ?? (() => {}); const progress = (download?: { bytes: number; total: number }) => { - if (this.ctrl.enabled()) this.ctrl.download = download; - this.ctrl.opts.redraw(); + if (this.ctrl?.enabled()) this.ctrl.download = download; + redraw(); }; return new Map( @@ -93,6 +102,21 @@ export class Engines { }, make: (e: BrowserEngineInfo) => new StockfishWebEngine(e, progress), }, + { + info: { + id: '__sf16nnue12', + name: 'Stockfish 16 NNUE · 12MB', + short: 'SF 16 · 12MB', + tech: 'NNUE', + requires: 'simd', + minMem: 1536, + assets: { + root: 'npm/lila-stockfish-web', + js: 'linrock-nnue-12.js', + }, + }, + make: (e: BrowserEngineInfo) => new StockfishWebEngine(e, progress), + }, { info: { id: '__sf16nnue40', @@ -108,6 +132,21 @@ export class Engines { }, make: (e: BrowserEngineInfo) => new StockfishWebEngine(e, progress), }, + { + info: { + id: '__sf16nnue60', + name: 'Stockfish 16 NNUE · 60MB', + short: 'SF 16 · 60MB', + tech: 'NNUE', + requires: 'simd', + minMem: 2048, + assets: { + root: 'npm/lila-stockfish-web', + js: 'sf-nnue-60.js', + }, + }, + make: (e: BrowserEngineInfo) => new StockfishWebEngine(e, progress), + }, { info: { id: '__sf16hce', @@ -189,6 +228,23 @@ export class Engines { }, make: (e: BrowserEngineInfo) => new ThreadedEngine(e, redraw), }, + { + info: { + id: '__sf14nnue', + name: 'Stockfish 14 NNUE', + short: 'SF 14', + version: 'b6939d', + class: 'NNUE', + requires: 'simd', + minMem: 2048, + assets: { + root: 'npm/stockfish-nnue.wasm', + js: 'stockfish.js', + wasm: 'stockfish.wasm', + }, + }, + make: (e: BrowserEngineInfo) => new ThreadedEngine(e, redraw, progress), + }, { info: { id: '__sfwasm', @@ -254,5 +310,5 @@ const withDefaults = (engine: BrowserEngineInfo): BrowserEngineInfo => ({ type WithMake = { info: BrowserEngineInfo; - make: (e: BrowserEngineInfo) => CevalEngine; + make: (e: BrowserEngineInfo) => CevalEngine | LegacyBot; }; diff --git a/ui/ceval/src/engines/externalEngine.ts b/ui/ceval/src/engines/externalEngine.ts index de66d574d1745..f5aaa09fdf13c 100644 --- a/ui/ceval/src/engines/externalEngine.ts +++ b/ui/ceval/src/engines/externalEngine.ts @@ -1,4 +1,5 @@ import { Redraw, Work, ExternalEngineInfo, CevalEngine, CevalState } from '../types'; +import { LegacyBot } from './legacyBot'; import { randomToken } from 'common/random'; import { readNdJson } from 'common/ndjson'; @@ -14,7 +15,7 @@ interface ExternalEngineOutput { }[]; } -export class ExternalEngine implements CevalEngine { +export class ExternalEngine extends LegacyBot implements CevalEngine { private state = CevalState.Initial; private sessionId = randomToken(); private req: AbortController | undefined; @@ -22,7 +23,9 @@ export class ExternalEngine implements CevalEngine { constructor( private opts: ExternalEngineInfo, private redraw: Redraw, - ) {} + ) { + super(opts); + } getState() { return this.state; diff --git a/ui/ceval/src/engines/simpleEngine.ts b/ui/ceval/src/engines/simpleEngine.ts index 495c593ccd85d..1c867e894653f 100644 --- a/ui/ceval/src/engines/simpleEngine.ts +++ b/ui/ceval/src/engines/simpleEngine.ts @@ -1,16 +1,19 @@ import { Protocol } from '../protocol'; +import { LegacyBot } from './legacyBot'; import { Redraw, Work, CevalState, CevalEngine, BrowserEngineInfo } from '../types'; -export class SimpleEngine implements CevalEngine { +export class SimpleEngine extends LegacyBot implements CevalEngine { private failed = false; - private protocol = new Protocol(); + private protocol: Protocol; private worker: Worker | undefined; url: string; constructor( - info: BrowserEngineInfo, + readonly info: BrowserEngineInfo, private redraw: Redraw, ) { + super(info); + if (!info.isBot) this.protocol = new Protocol(); this.url = `${info.assets.root}/${info.assets.js}`; } diff --git a/ui/ceval/src/engines/stockfishWebEngine.ts b/ui/ceval/src/engines/stockfishWebEngine.ts index be328de2f9c16..0cc25cbb1ebe9 100644 --- a/ui/ceval/src/engines/stockfishWebEngine.ts +++ b/ui/ceval/src/engines/stockfishWebEngine.ts @@ -2,27 +2,40 @@ import { Work, CevalEngine, CevalState, BrowserEngineInfo } from '../types'; import { Protocol } from '../protocol'; import { objectStorage } from 'common/objectStorage'; import { sharedWasmMemory } from '../util'; +import { LegacyBot } from './legacyBot'; import type StockfishWeb from 'lila-stockfish-web'; -export class StockfishWebEngine implements CevalEngine { +export class StockfishWebEngine extends LegacyBot implements CevalEngine { failed = false; protocol: Protocol; - module: StockfishWeb; + sfweb: StockfishWeb; wasmMemory: WebAssembly.Memory; - + loaded = () => {}; + isLoaded = new Promise(resolve => { + this.loaded = resolve; + }); constructor( readonly info: BrowserEngineInfo, readonly nnue?: (download?: { bytes: number; total: number }) => void, readonly variantMap?: (v: string) => string, ) { + super(info); this.protocol = new Protocol(variantMap); + this.wasmMemory = sharedWasmMemory(info.minMem!); this.boot().catch(e => { console.error(e); this.failed = true; }); - this.wasmMemory = sharedWasmMemory(info.minMem!); } - + get module() { + return { + postMessage: (x: string) => this.sfweb.postMessage(x), + listen: (x: (y: string) => void) => (this.sfweb.listen = x), + }; + } + load(): Promise { + return this.isLoaded; + } async boot() { const [version, root, js] = [this.info.assets.version, this.info.assets.root, this.info.assets.js]; const makeModule = await import(lichess.assetUrl(`${root}/${js}`, { version })); @@ -74,7 +87,8 @@ export class StockfishWebEngine implements CevalEngine { } module.listen = (data: string) => this.protocol.received(data); this.protocol.connected(cmd => module.postMessage(cmd)); - this.module = module; + this.sfweb = module; + this.loaded(); } getState() { diff --git a/ui/ceval/src/engines/threadedEngine.ts b/ui/ceval/src/engines/threadedEngine.ts index 2e57ccd2d6687..5c46dfeaac806 100644 --- a/ui/ceval/src/engines/threadedEngine.ts +++ b/ui/ceval/src/engines/threadedEngine.ts @@ -2,6 +2,7 @@ import { Protocol } from '../protocol'; import { Redraw, Work, CevalEngine, CevalState, BrowserEngineInfo } from '../types'; import { sharedWasmMemory } from '../util'; import { Cache } from '../cache'; +import { LegacyBot } from './legacyBot'; interface WasmModule { (opts: { @@ -13,6 +14,7 @@ interface WasmModule { interface Stockfish { addMessageListener(cb: (msg: string) => void): void; + removeMessageListener(cb: (msg: string) => void): void; postMessage(msg: string): void; } @@ -23,16 +25,35 @@ declare global { } } -export class ThreadedEngine implements CevalEngine { +export class ThreadedEngine extends LegacyBot implements CevalEngine { failed: boolean; protocol: Protocol; - + loaded = () => {}; + isLoaded = new Promise(resolve => { + this.loaded = resolve; + }); + moduleProxy: { postMessage: (msg: string) => void; listen: (cb: (msg: string) => void) => void }; constructor( readonly info: BrowserEngineInfo, readonly redraw: Redraw, readonly progress?: (download?: { bytes: number; total: number }) => void, readonly variantMap?: (v: string) => string, - ) {} + ) { + super(info); + if (!this.info.isBot) this.protocol = new Protocol(this.variantMap); + this.boot().catch(err => { + console.error(err); + this.failed = true; + this.redraw(); + }); + } + + get module() { + return this.moduleProxy; + } + load(): Promise { + return this.isLoaded; + } getState() { return !this.protocol @@ -95,21 +116,24 @@ export class ThreadedEngine implements CevalEngine { lichess.assetUrl(`${root}/${path}`, { version, sameDomain: path.endsWith('.worker.js') }), wasmMemory: sharedWasmMemory(this.info.minMem!), }); - - sf.addMessageListener(data => this.protocol.received(data)); - this.protocol.connected(msg => sf.postMessage(msg)); + if (!this.info.isBot) { + sf.addMessageListener(data => this.protocol.received(data)); + this.protocol.connected(msg => sf.postMessage(msg)); + } else { + let oldListener: (msg: string) => void; + this.moduleProxy = { + postMessage: (msg: string) => sf.postMessage(msg), + listen: (cb: (msg: string) => void) => { + if (oldListener) sf.removeMessageListener(oldListener); + sf.addMessageListener((oldListener = cb)); + }, + }; + } + this.loaded(); return sf; } async start(work: Work) { - if (!this.protocol) { - this.protocol = new Protocol(this.variantMap); - this.boot().catch(err => { - console.error(err); - this.failed = true; - this.redraw(); - }); - } this.protocol.compute(work); } diff --git a/ui/ceval/src/main.ts b/ui/ceval/src/main.ts index 68b66ecd074b4..079632b9bd788 100644 --- a/ui/ceval/src/main.ts +++ b/ui/ceval/src/main.ts @@ -2,9 +2,11 @@ import CevalCtrl from './ctrl'; import * as view from './view/main'; import * as winningChances from './winningChances'; -export type { NodeEvals, Eval, EvalMeta, CevalOpts, ExternalEngineInfo } from './types'; +export type { NodeEvals, Eval, EvalMeta, CevalOpts, ExternalEngineInfo, BrowserEngineInfo } from './types'; export { isEvalBetter, renderEval, sanIrreversible } from './util'; export { CevalCtrl, view, winningChances }; +export { Engines } from './engines/engines'; +export { LegacyBot } from './engines/legacyBot'; // stop when another tab starts. Listen only once here, // as the ctrl can be instantiated several times. diff --git a/ui/ceval/src/types.ts b/ui/ceval/src/types.ts index 000f2fa8b3d95..ec69437db1fff 100644 --- a/ui/ceval/src/types.ts +++ b/ui/ceval/src/types.ts @@ -34,6 +34,7 @@ export interface EngineInfo { maxThreads?: number; maxHash?: number; requires?: Feature; + isBot?: boolean; } export interface ExternalEngineInfo extends EngineInfo { diff --git a/ui/common/src/objectStorage.ts b/ui/common/src/objectStorage.ts index 3fd592f46f836..b5ce1a2074fae 100644 --- a/ui/common/src/objectStorage.ts +++ b/ui/common/src/objectStorage.ts @@ -12,9 +12,10 @@ export interface DbInfo { } export interface ObjectStorage { + list(): Promise; get(key: string): Promise; put(key: string, value: V): Promise; // returns key - count(key: string): Promise; + count(key?: string): Promise; remove(key: string): Promise; clear(): Promise; // remove all txn(mode: IDBTransactionMode): IDBTransaction; // do anything else @@ -36,13 +37,16 @@ export async function objectStorage(dbInfo: DbInfo): Promise } return { + list() { + return actionPromise(() => objectStore('readonly').getAllKeys()); + }, get(key: string) { return actionPromise(() => objectStore('readonly').get(key)); }, put(key: string, value: V) { return actionPromise(() => objectStore('readwrite').put(value, key)); }, - count(key: string) { + count(key?: string) { return actionPromise(() => objectStore('readonly').count(key)); }, remove(key: string) { diff --git a/ui/gameSetup/src/ctrl.ts b/ui/gameSetup/src/ctrl.ts index c3d8af05f316d..852d311c95b43 100644 --- a/ui/gameSetup/src/ctrl.ts +++ b/ui/gameSetup/src/ctrl.ts @@ -292,11 +292,11 @@ export class SetupCtrl { this.closeModal(); return; } - this.loading = true; this.root.redraw(); let urlPath = `/setup/${this.gameType}`; + console.log(urlPath); if (this.gameType === 'hook') urlPath += '/' + lichess.sri; const urlParams = { user: this.friendUser || undefined }; let response; diff --git a/ui/localPlay/css/_bot-vs-bot.scss b/ui/localPlay/css/_bot-vs-bot.scss index 6872bb765a33c..711a4bf3f5119 100644 --- a/ui/localPlay/css/_bot-vs-bot.scss +++ b/ui/localPlay/css/_bot-vs-bot.scss @@ -13,91 +13,36 @@ $mq-col2: $mq-col2-uniboard; grid-template-areas: 'board side'; } - .about__link { - margin-top: 2vh; - text-align: center; - font-size: 0.8em; - } - #moves { - flex: 2 1 0; + .puz-side { display: flex; - flex-direction: column; - justify-content: space-between; - - // 0 size forces vertical scrollbar - overflow-y: auto; - overflow-x: hidden; - - // else a scrollbar appears sometimes - border-top: $border; - position: relative; - - /* required so line::before scrolls along the moves! */ - .result, - .status { - background: $c-bg-zebra; - text-align: center; - } - - .result { - border-top: $border; - font-weight: bold; - font-size: 1.2em; - padding: 5px 0 3px 0; - } - - .status { - font-size: 1em; - font-style: italic; - padding-bottom: 7px; - } - - button.next { - border: 0; - background: $c-bg-box; - color: $c-link; - padding: 0.5em; - width: 100%; - - @include transition; - - &:hover { - color: $c-link-hover; - } - - &::before { - margin-#{$end-direction}: 0.3em; - } - - &.highlighted { - background: mix($c-primary, $c-bg-box, 80%); - color: $c-primary-over; - - &:hover { - background: $c-primary; - } - } + flex-flow: column; + justify-content: stretch; + /*#white { + background-color: white; + color: #333; } - } - - .puz-side { + #black { + background-color: black; + color: white; + }*/ .puz-bot { @extend %flex-column; align-items: center; width: 300px; - height: 100px; - border: 2px dashed #888; } .totals { font-size: xx-large; text-align: center; } - - .puz-bot.hilite { - background-color: #888; + .spacer { + flex: 1 1 auto; + } + #controls { + @extend %flex-column; + row-gap: 1em; } #pgn { - height: 400px; + flex: 1 1 auto; } #num-games { width: 120px; @@ -109,6 +54,12 @@ $mq-col2: $mq-col2-uniboard; margin: 0 15px; } } + #results { + flex: auto; + } + #clear { + flex: auto; + } #go { flex: auto; } diff --git a/ui/localPlay/css/build/_bot-vs-bot.scss b/ui/localPlay/css/build/_bot-vs-bot.scss index 9fadc86b3c0e5..1104b186b1e2b 100644 --- a/ui/localPlay/css/build/_bot-vs-bot.scss +++ b/ui/localPlay/css/build/_bot-vs-bot.scss @@ -8,3 +8,4 @@ @import '../../../chess/css/promotion'; @import '../bot-vs-bot'; +@import '../settings'; diff --git a/ui/localPlay/package.json b/ui/localPlay/package.json index 4dd9fd13c0e69..ef940323bb78f 100644 --- a/ui/localPlay/package.json +++ b/ui/localPlay/package.json @@ -6,6 +6,7 @@ "dependencies": { "@types/cash": "workspace:*", "@types/lichess": "workspace:*", + "ceval": "workspace:*", "chess": "workspace:*", "chessops": "^0.12.7", "common": "workspace:*", @@ -29,12 +30,6 @@ "esm": { "src/main.ts": "localPlay" } - }, - "copy": [ - { - "src": "node_modules/lila-stockfish-web/*.{js,wasm}", - "dest": "../../public/npm" - } - ] + } } } diff --git a/ui/localPlay/src/botVsBot/bvbCtrl.ts b/ui/localPlay/src/botVsBot/bvbCtrl.ts index 1c6704d52d8c9..67f463956e4aa 100644 --- a/ui/localPlay/src/botVsBot/bvbCtrl.ts +++ b/ui/localPlay/src/botVsBot/bvbCtrl.ts @@ -7,6 +7,7 @@ import makeZerofish, { Zerofish, PV } from 'zerofish'; import { Api as CgApi } from 'chessground/api'; import { Config as CgConfig } from 'chessground/config'; import { Key } from 'chessground/types'; +import { Engines } from 'ceval'; type Player = 'human' | 'zero' | 'fish'; @@ -24,6 +25,7 @@ export class BvbCtrl { fiftyMovePly = 0; threefoldFens: Map = new Map(); + engines = new Engines(); constructor( readonly opts: BvbOpts, readonly redraw: () => void, diff --git a/ui/localPlay/src/botVsBot/bvbMain.ts b/ui/localPlay/src/botVsBot/bvbMain.ts index c3facea122f20..32d1835b59ecd 100644 --- a/ui/localPlay/src/botVsBot/bvbMain.ts +++ b/ui/localPlay/src/botVsBot/bvbMain.ts @@ -1,20 +1,25 @@ import { attributesModule, classModule, init } from 'snabbdom'; -import { BvbCtrl } from './bvbCtrl'; -import bvbView from './bvbView'; +//import { BvbCtrl } from './bvbCtrl'; +import { BvbStockfishWebCtrl } from './bvbStockfishWebCtrl'; +//import bvbView from './bvbView'; +import bvbStockfishWebView from './bvbStockfishWebView'; import { BvbOpts } from './bvbInterfaces'; const patch = init([classModule, attributesModule]); export default async function (opts: BvbOpts) { - const ctrl = new BvbCtrl(opts, redraw); + //const ctrl = new BvbCtrl(opts, redraw); - const blueprint = bvbView(ctrl); + //const blueprint = bvbView(ctrl); + const ctrl = new BvbStockfishWebCtrl(opts, redraw); + const blueprint = bvbStockfishWebView(ctrl); const element = document.querySelector('main') as HTMLElement; element.innerHTML = ''; let vnode = patch(element, blueprint); function redraw() { - vnode = patch(vnode, bvbView(ctrl)); + //vnode = patch(vnode, bvbView(ctrl)); + vnode = patch(vnode, bvbStockfishWebView(ctrl)); } redraw(); } diff --git a/ui/localPlay/tsconfig.json b/ui/localPlay/tsconfig.json index 4f86df60e8b39..a896900b44279 100644 --- a/ui/localPlay/tsconfig.json +++ b/ui/localPlay/tsconfig.json @@ -8,6 +8,7 @@ "allowUnreachableCode": true }, "references": [ + { "path": "../ceval/tsconfig.json" }, { "path": "../chess/tsconfig.json" }, { "path": "../common/tsconfig.json" }, { "path": "../game/tsconfig.json" }, From 1e0ba9c28c409c9834441464df69681f274da513 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Fri, 3 Nov 2023 11:10:15 -0500 Subject: [PATCH 059/174] gah --- ui/ceval/package.json | 2 +- ui/ceval/src/engines/engines.ts | 2 +- ui/localPlay/css/_bot-vs-bot.scss | 43 ++++++++++++++++++++----------- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/ui/ceval/package.json b/ui/ceval/package.json index 53e639db96921..4a17005f82de7 100644 --- a/ui/ceval/package.json +++ b/ui/ceval/package.json @@ -24,7 +24,7 @@ "stockfish.wasm": "^0.10.0", "stockfish-mv.wasm": "^0.6.1", "stockfish-nnue.wasm": "1.0.0-1946a675.smolnet", - "lila-stockfish-web": "^0.0.1" + "lila-stockfish-web": "link:/home/gamblej/ws/lichess/lila-stockfish-web" }, "scripts": { "compile": "tsc", diff --git a/ui/ceval/src/engines/engines.ts b/ui/ceval/src/engines/engines.ts index e29638255f929..806e2f1397e5b 100644 --- a/ui/ceval/src/engines/engines.ts +++ b/ui/ceval/src/engines/engines.ts @@ -284,7 +284,7 @@ export class Engines { } function maxHashMB() { - if (navigator.deviceMemory) return pow2floor(navigator.deviceMemory * 128); // chrome/edge/opera + if (navigator.deviceMemory) return Math.min(1024, pow2floor(navigator.deviceMemory * 128)); else if (isAndroid()) return 64; // budget androids are easy to crash @ 128 else if (isIPad()) return 64; // iPadOS safari pretends to be desktop but acts more like iphone else if (isIOS()) return 32; diff --git a/ui/localPlay/css/_bot-vs-bot.scss b/ui/localPlay/css/_bot-vs-bot.scss index 711a4bf3f5119..c832e25be4829 100644 --- a/ui/localPlay/css/_bot-vs-bot.scss +++ b/ui/localPlay/css/_bot-vs-bot.scss @@ -17,26 +17,39 @@ $mq-col2: $mq-col2-uniboard; display: flex; flex-flow: column; justify-content: stretch; - /*#white { - background-color: white; - color: #333; - } - #black { - background-color: black; - color: white; - }*/ .puz-bot { - @extend %flex-column; - align-items: center; - width: 300px; - } - .totals { - font-size: xx-large; - text-align: center; + @extend %flex-between; + padding: 8px; + border: 1px solid $c-primary; + align-content: center; + width: 360px; + font-size: 1em; + &.white { + background-color: white; + color: #333; + } + &.black { + background-color: black; + color: white; + } + .select-engine { + width: 60%; + } + .totals { + font-size: large; + text-align: center; + } } .spacer { flex: 1 1 auto; } + .starting-fen { + &::placeholder { + font-family: monospace; + color: #888; + } + width: 100%; + } #controls { @extend %flex-column; row-gap: 1em; From 44397f721313c07f9951f8080026bbcedeee74f8 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Fri, 3 Nov 2023 11:13:51 -0500 Subject: [PATCH 060/174] gah --- bin/results.py | 73 +++++ ui/ceval/src/engines/legacyBot.ts | 38 +++ ui/localPlay/css/_settings.scss | 58 ++++ .../botVsBot/bvbStockfishWebChessground.ts | 65 +++++ .../src/botVsBot/bvbStockfishWebCtrl.ts | 270 ++++++++++++++++++ .../src/botVsBot/bvbStockfishWebView.ts | 261 +++++++++++++++++ 6 files changed, 765 insertions(+) create mode 100755 bin/results.py create mode 100644 ui/ceval/src/engines/legacyBot.ts create mode 100644 ui/localPlay/css/_settings.scss create mode 100644 ui/localPlay/src/botVsBot/bvbStockfishWebChessground.ts create mode 100644 ui/localPlay/src/botVsBot/bvbStockfishWebCtrl.ts create mode 100644 ui/localPlay/src/botVsBot/bvbStockfishWebView.ts diff --git a/bin/results.py b/bin/results.py new file mode 100755 index 0000000000000..831a473767446 --- /dev/null +++ b/bin/results.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 + +import json +import sys + +# type Info = { +# name: string; +# id: string; +# movetime: number; +# threads: number; +# hash: number; +# }; + +# type GameResult = { +# result: string; +# reason: string; +# white: Info; +# black: Info; +# moves: number; +# //moves: string[]; +# }; + +def main(): + draws = {} + byEngine = {} + results = [] + errors = [] + template = {'W': 0, 'D': 0, 'L': 0} + + for x in range(1, len(sys.argv)): + + with open(sys.argv[x], 'r') as f: + results.extend(json.load(f)) + + for result in results: + outcome = result['result'] + reason = result['reason'] + if outcome == 'draw': + if reason.startswith('Stockfish'): + errors.append(reason) + else: + byEngine.setdefault(get_name_compat(result, 'white'), template.copy())['D'] += 1 + byEngine.setdefault(get_name_compat(result, 'black'), template.copy())['D'] += 1 + draws[reason] = draws.get(reason, 0) + 1 + elif outcome == 'error': + errors.append(reason) + else: + try: + winner = result[outcome]['name'] + loser = result['black' if outcome == 'white' else 'white']['name'] + except TypeError as e: + winner = result[outcome] + loser = result['black' if outcome == 'white' else 'white'] + byEngine.setdefault(loser, template.copy())['L'] += 1 + byEngine.setdefault(winner, template.copy())['W'] += 1 + + print('Total games:', len(results)) + print('Draws by reason:') + for reason, count in draws.items(): + print(f' {reason}: {count}') + print(' error: ', len(errors)) + print('Records by engine (W/D/L):') + for engine, record in byEngine.items(): + print(f' {engine}: {record["W"]}/{record["D"]}/{record["L"]}') + +def get_name_compat(result, color): + try: + return result[color]['name'] + except TypeError as e: + return result[color] + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/ui/ceval/src/engines/legacyBot.ts b/ui/ceval/src/engines/legacyBot.ts new file mode 100644 index 0000000000000..1cf9a31934dbd --- /dev/null +++ b/ui/ceval/src/engines/legacyBot.ts @@ -0,0 +1,38 @@ +import { EngineInfo } from '../types'; + +export class LegacyBot { + constructor(readonly info: EngineInfo) {} + movetime: number = 100; + get module(): + | { postMessage: (msg: string) => void; listen: (cb: (msg: string) => void) => void } + | undefined { + return undefined; + } + load(): Promise { + return Promise.resolve(); + } + reset(opts: { movetime?: number; threads?: number; hash?: number } = {}) { + if (opts.threads) this.module!.postMessage(`setoption name Threads value ${opts.threads}`); + if (opts.hash) this.module!.postMessage(`setoption name Hash value ${opts.hash}MB`); + if (opts.movetime) this.movetime = opts.movetime; + //this.module?.postMessage('setoption name UCI_AnalyseMode value true'); + this.module?.postMessage('ucinewgame'); + } + getMove(fenOrMoves: string | string[]): Promise { + return new Promise(resolve => { + this.module!.listen((line: string) => { + const tokens = line.split(' '); + if (tokens[0] === 'bestmove') resolve(tokens[1]); + }); + if (Array.isArray(fenOrMoves)) { + this.module!.postMessage(`position startpos moves${fenOrMoves.map(x => ' ' + x).join('')}`); + } else { + this.module!.postMessage(`position fen ${fenOrMoves}`); + } + this.module!.postMessage(`go movetime ${this.movetime}`); + }); + } + stop() { + this.module?.postMessage('stop'); + } +} diff --git a/ui/localPlay/css/_settings.scss b/ui/localPlay/css/_settings.scss new file mode 100644 index 0000000000000..fd5168e4b5cde --- /dev/null +++ b/ui/localPlay/css/_settings.scss @@ -0,0 +1,58 @@ +.bot-settings { + @extend %flex-column, %dropdown-shadow, %box-radius-bottom; + //border-top: 2px solid $c-primary; + z-index: z(mz-menu); + width: 100%; + background: $c-bg-high; + gap: 1.5em; + padding: 2em 1.5em; + + > hr { + margin: 5px; + } + + .setting { + @extend %flex-center-nowrap; + + * { + white-space: nowrap; + } + + label { + flex: 4 1 auto; + cursor: pointer; + } + + .switch + label { + margin-#{$start-direction}: 1ch; + } + + input[type='range'] { + cursor: pointer; + flex: 1 4 auto; + padding: 0; + height: 1.6em; + width: 100%; + margin: 0 1ch; + } + + select { + flex: 1 4 auto; + min-width: 0; + margin-#{$start-direction}: 1ch; + padding: 4px; + overflow: hidden; + text-overflow: ellipsis; + } + + .select-engine { + padding: 0.5em; + } + + .range_value { + direction: ltr; + flex: 0 0 15%; + text-align: left; + } + } +} diff --git a/ui/localPlay/src/botVsBot/bvbStockfishWebChessground.ts b/ui/localPlay/src/botVsBot/bvbStockfishWebChessground.ts new file mode 100644 index 0000000000000..d4741b3fb6edd --- /dev/null +++ b/ui/localPlay/src/botVsBot/bvbStockfishWebChessground.ts @@ -0,0 +1,65 @@ +import resizeHandle from 'common/resize'; +import { Config as CgConfig } from 'chessground/config'; +import * as Prefs from 'common/prefs'; +import { BvbStockfishWebCtrl } from './bvbStockfishWebCtrl'; + +const pref = { + coords: Prefs.Coords.Hidden, + is3d: false, + destination: false, + rookCastle: false, + moveEvent: 0, + highlight: false, + animation: 0, +}; + +export function makeBvbConfig(ctrl: BvbStockfishWebCtrl): CgConfig { + const opts = ctrl.cgOpts(); + return { + fen: opts.fen, + orientation: opts.orientation, + turnColor: opts.turnColor, + check: opts.check, + lastMove: opts.lastMove, + coordinates: true, //pref.coords !== Prefs.Coords.Hidden, + addPieceZIndex: pref.is3d, + addDimensionsCssVarsTo: document.body, + movable: { + free: false, + color: opts.movable!.color, + dests: opts.movable!.dests, + showDests: true, //pref.destination, + rookCastle: pref.rookCastle, + events: { + after: ctrl.cgUserMove, + }, + }, + draggable: { + enabled: pref.moveEvent > 0, + showGhost: pref.highlight, + }, + selectable: { + enabled: pref.moveEvent !== 1, + }, + events: { + insert(elements) { + resizeHandle(elements, Prefs.ShowResizeHandle.OnlyAtStart, 0, p => p == 0); + }, + }, + premovable: { + enabled: false, + }, + drawable: { + enabled: true, + defaultSnapToValidMove: lichess.storage.boolean('arrow.snap').getOrDefault(true), + }, + highlight: { + lastMove: pref.highlight, + check: pref.highlight, + }, + animation: { + duration: pref.animation, + }, + disableContextMenu: true, + }; +} diff --git a/ui/localPlay/src/botVsBot/bvbStockfishWebCtrl.ts b/ui/localPlay/src/botVsBot/bvbStockfishWebCtrl.ts new file mode 100644 index 0000000000000..234729a8afcac --- /dev/null +++ b/ui/localPlay/src/botVsBot/bvbStockfishWebCtrl.ts @@ -0,0 +1,270 @@ +import { BvbOpts } from './bvbInterfaces'; +import { makeFen } from 'chessops/fen'; +import { Chess, Role } from 'chessops'; +import * as Chops from 'chessops'; +import { Api as CgApi } from 'chessground/api'; +import { Config as CgConfig } from 'chessground/config'; +import { Key } from 'chessground/types'; +import { Engines, LegacyBot } from 'ceval'; +import { storedStringProp, storedIntProp } from 'common/storage'; +import { ObjectStorage, objectStorage } from 'common/objectStorage'; + +export interface MatchParams { + startingFen: string; + movetime: number; + threads: number; + hash: number; + nfold: number; +} + +type GameResult = { + result: string; + reason: string; + white: string; + black: string; + movetime: number; + threads: number; + hash: number; + nfold?: number; + startingFen: string; + moves: number | string[]; +}; + +export class BvbStockfishWebCtrl { + cg: CgApi; + path = ''; + chess = Chess.default(); + bots: { white: LegacyBot; black: LegacyBot }; + totals: { gamesLeft: number; white: number; black: number; draw: number; error: number }; + ids: { white: string; black: string }; + params: MatchParams; + engines = new Engines(); + fen = ''; + key = 0; + flipped = false; + fiftyMovePly = 0; + threefoldFens: Map = new Map(); + moves: string[] = []; + results: ObjectStorage; + + constructor( + readonly opts: BvbOpts, + readonly redraw: () => void, + ) { + this.fetchParams(); + objectStorage({ store: 'bvb' }).then(async x => { + this.results = x; + this.key = await this.results.count(); + }); + + this.bots = { + white: this.engines.makeBot(this.ids.white), + black: this.engines.makeBot(this.ids.black), + }; + } + + fetchParams() { + if (!this.ids) + this.ids = { + white: storedStringProp('one', '__sf16nnue7')(), + black: storedStringProp('two', '__sf16nnue12')(), + }; + this.params = { + startingFen: storedStringProp('fen', '')(), + nfold: storedIntProp('nfold', 12)(), + movetime: storedIntProp('movetime', 100)(), + threads: storedIntProp('threads', 4)(), + hash: storedIntProp('hash', 16)(), + }; + } + + storeParams() { + storedIntProp('movetime', 100)(this.params.movetime); + storedIntProp('threads', 4)(this.params.threads); + storedIntProp('hash', 16)(this.params.hash); + storedStringProp('startingfen', '')(this.params.startingFen); + storedStringProp('one', '__sf16nnue7')(this.ids.white); + storedStringProp('two', '__sf16nnue12')(this.ids.black); + storedIntProp('nfold', 12)(this.params.nfold); + this.totals = { gamesLeft: this.totals?.gamesLeft ?? 1, white: 0, black: 0, draw: 0, error: 0 }; + } + + swapSides() { + this.bots = { white: this.bots.black, black: this.bots.white }; + this.ids = { white: this.ids.black, black: this.ids.white }; + this.totals = { + white: this.totals.black, + black: this.totals.white, + draw: this.totals.draw, + gamesLeft: this.totals.gamesLeft, + error: this.totals.error, + }; + } + + async go(numGames?: number) { + this.moves = []; + if (this.bots.white?.info.id !== this.ids.white) { + this.bots.white = this.engines.makeBot(this.ids.white); + } + if (this.bots.black?.info.id !== this.ids.black) { + this.bots.black = this.engines.makeBot(this.ids.black); + } + const [nextKey] = await Promise.all([ + this.results.count(), + this.bots.white.load(), + this.bots.black.load(), + ]); + this.key = nextKey; + this.totals ??= { gamesLeft: 1, white: 0, black: 0, draw: 0, error: 0 }; + if (numGames) this.totals.gamesLeft = numGames; + this.fiftyMovePly = 0; + this.threefoldFens.clear(); + if (this.params.startingFen) + this.chess = Chess.fromSetup(Chops.fen.parseFen(this.params.startingFen).unwrap()).unwrap(); + else this.chess.reset(); + this.fen = makeFen(this.chess.toSetup()); + this.cg.set({ fen: this.fen }); + this.bots.white?.reset(this.params); + this.bots.black?.reset(this.params); + this.getBotMove(); + $('#go').addClass('disabled'); + this.redraw(); + } + + checkGameOver(userEnd?: 'whiteResign' | 'blackResign' | 'mutualDraw'): { + end: boolean; + result?: string; + reason?: string; + } { + let result = 'draw', + reason = userEnd ?? 'checkmate'; + if (this.chess.isCheckmate()) result = Chops.opposite(this.chess.turn); + else if (this.chess.isInsufficientMaterial()) reason = 'insufficient'; + else if (this.chess.isStalemate()) reason = 'stalemate'; + else if (this.fifty()) reason = 'fifty'; + else if (this.threefold()) reason = 'threefold'; + else if (userEnd) { + if (userEnd !== 'mutualDraw') reason = 'resign'; + if (userEnd === 'whiteResign') result = 'black'; + else if (userEnd === 'blackResign') result = 'white'; + } else return { end: false }; + return { end: true, result, reason }; + } + + async doGameOver(result: string, reason: string) { + this.results.put(`game ${this.key++}`, { + white: this.bots.white.info.name, + black: this.bots.black.info.name, + ...this.params, + result, + reason, + moves: this.moves, + }); + console.log( + `${this.bots.white.info.name} (white) vs ${this.bots.black.info.name} (black) - ${result} ${reason} - ${ + this.params.startingFen + } ${this.moves.join(' ')}`, + ); + + this.totals[result as 'white' | 'black' | 'draw'] += 1; + + if (--this.totals.gamesLeft < 1) { + $('#go').removeClass('disabled'); + this.redraw(); + return; + } + setTimeout(() => { + this.swapSides(); + this.go(); + }); + } + + resultsText(color: Color) { + return this.totals + ? `${this.totals[color]}W / ${this.totals.draw + this.totals.error}D / ${ + this.totals[Chops.opposite(color)] + }L` + : '0W / 0D / 0L'; + } + + move(uci: Uci) { + const move = Chops.parseUci(uci); + this.moves.push(uci); + if (!move || !this.chess.isLegal(move)) { + this.doGameOver( + 'error', + `${this.bots[this.chess.turn].info.name} made illegal move ${uci} at ${makeFen( + this.chess.toSetup(), + )}`, + ); + return; + } + /*let castle = false; + if (this.chess.board.get(Chops.parseSquare(uci.slice(0, 2))!)?.role === 'king') { + const destPiece = this.chess.board.get(Chops.parseSquare(uci.slice(2, 4))!); + if (destPiece && destPiece.role === 'rook' && destPiece.color === this.chess.turn) { + castle = true; + } + }*/ + this.chess.play(move); + this.fen = makeFen(this.chess.toSetup()); + this.fifty(move); + this.threefold('update'); + this.updateCgBoard(uci); + const { end, result, reason } = this.checkGameOver(); + if (end) this.doGameOver(result!, reason!); + else this.getBotMove(/*castle*/); + } + + cgUserMove = (orig: Key, dest: Key) => { + this.move(orig + dest); + }; + + async getBotMove(byFen = true) { + const bot = this.bots[this.chess.turn]!; + this.move(await bot.getMove(byFen ? this.fen : this.moves)); + } + + updateCgBoard(uci: Uci) { + const { from, to, role } = splitUci(uci); + this.cg.move(from, to); + if (role) this.cg.setPieces(new Map([[to, { color: this.chess.turn, role, promoted: true }]])); + this.cg.set(this.cgOpts(true)); + } + + cgOpts(withFen = true): CgConfig { + return { + fen: withFen ? this.fen : undefined, + orientation: this.flipped ? 'black' : 'white', + turnColor: this.chess.turn, + check: this.chess.isCheck() ? this.chess.turn : false, + movable: { + color: this.chess.turn, + dests: new Map(), + }, + }; + } + + fifty(move?: Chops.Move) { + if (move) + if ( + !('from' in move) || + this.chess.board.getRole(move.from) === 'pawn' || + this.chess.board.get(move.to) + ) + this.fiftyMovePly = 0; + else this.fiftyMovePly++; + return this.fiftyMovePly >= 100; + } + + threefold(update: 'update' | false = false) { + const boardFen = this.fen.split('-')[0]; + let fenCount = this.threefoldFens.get(boardFen) ?? 0; + if (update) this.threefoldFens.set(boardFen, ++fenCount); + return this.params.nfold !== 0 ? fenCount >= this.params.nfold : false; + } +} + +function splitUci(uci: Uci): { from: Key; to: Key; role?: Role } { + return { from: uci.slice(0, 2) as Key, to: uci.slice(2, 4) as Key, role: Chops.charToRole(uci.slice(4)) }; +} diff --git a/ui/localPlay/src/botVsBot/bvbStockfishWebView.ts b/ui/localPlay/src/botVsBot/bvbStockfishWebView.ts new file mode 100644 index 0000000000000..8a1c45af5cd8e --- /dev/null +++ b/ui/localPlay/src/botVsBot/bvbStockfishWebView.ts @@ -0,0 +1,261 @@ +import { Chessground } from 'chessground'; +import { h, VNode } from 'snabbdom'; +import { makeBvbConfig as makeCgConfig } from './bvbStockfishWebChessground'; +import * as Chops from 'chessops'; +import { onInsert, bind } from 'common/snabbdom'; +import { rangeConfig } from 'common/controls'; +import { hasFeature } from 'common/device'; +import { BvbStockfishWebCtrl } from './bvbStockfishWebCtrl'; + +const searchTicks: [number, string][] = [ + [100, '100ms'], + [200, '200ms'], + [400, '400ms'], + [600, '600ms'], + [800, '800ms'], + [1000, '1s'], + [3000, '3s'], + [5000, '5s'], + [10000, '10s'], +]; + +export default function render(ctrl: BvbStockfishWebCtrl): VNode { + return h('div#local-play', [ + h('div.puz-board.main-board', [chessground(ctrl)]), + h('div.puz-side', [ + results(ctrl), + h('hr'), + h('div', bot(ctrl, 'black')), + h('div.spacer'), + startingFen(ctrl), + renderSettings(ctrl), + h('hr'), + controls(ctrl), + h('div.spacer'), + h('div', [bot(ctrl, 'white')]), + ]), + ]); +} +function startingFen(ctrl: BvbStockfishWebCtrl): VNode { + return h('input.starting-fen', { + attrs: { placeholder: Chops.fen.INITIAL_BOARD_FEN, spellcheck: 'false' }, + hook: bind('input', e => { + const el = e.target as HTMLInputElement; + if (!el.value) { + console.log('wiping', ctrl.params.startingFen); + ctrl.params.startingFen = ''; + ctrl.storeParams(); + return; + } + const text = Chops.fen.parseFen(el.value); + if (text.isOk) { + el.style.backgroundColor = ''; + ctrl.params.startingFen = el.value; + ctrl.storeParams(); + ctrl.redraw(); + } else el.style.backgroundColor = 'red'; + }), + props: { + value: ctrl.params.startingFen, + }, + }); +} +function chessground(ctrl: BvbStockfishWebCtrl): VNode { + return h('div.cg-wrap', { + hook: { + insert: vnode => (ctrl.cg = Chessground(vnode.elm as HTMLElement, makeCgConfig(ctrl))), + }, + }); +} + +function bot(ctrl: BvbStockfishWebCtrl, color: Color): VNode { + return h(`div.${color}.puz-bot`, [engineSelection(ctrl, color), h('span.totals', ctrl.resultsText(color))]); +} + +function controls(ctrl: BvbStockfishWebCtrl) { + return h('span', [ + h('input#num-games', { + attrs: { type: 'number', min: '1', max: '1000', value: '1' }, + }), + h( + 'button#go.button', + { + hook: onInsert(el => + el.addEventListener('click', () => ctrl.go(parseInt($('#num-games').val() as string) || 1)), + ), + }, + 'GO', + ), + ]); +} + +function results(ctrl: BvbStockfishWebCtrl) { + return h('span', [ + h( + 'button#results.button-link', + { + hook: onInsert(el => el.addEventListener('click', () => downloadResults(ctrl))), + }, + 'Download results', + ), + h( + 'button#clear.button-link', + { + hook: onInsert(el => el.addEventListener('click', () => clearResults(ctrl))), + }, + 'Clear results', + ), + ]); +} +async function downloadResults(ctrl: BvbStockfishWebCtrl) { + const results = []; + for (const key of await ctrl.results.list()) { + const result = await ctrl.results.get(key); + results.push(result); + console.log(result); + } + + const blob = new Blob([JSON.stringify(results)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'results.json'; + a.click(); + URL.revokeObjectURL(url); +} + +function clearResults(ctrl: BvbStockfishWebCtrl) { + if (!confirm('Clear all results?')) return; + ctrl.results.clear(); + ctrl.totals.white = ctrl.totals.black = ctrl.totals.draw = 0; + ctrl.redraw(); +} +const formatHashSize = (v: number): string => (v < 1000 ? v + 'MB' : Math.round(v / 1024) + 'GB'); + +function renderSettings(ctrl: BvbStockfishWebCtrl): VNode | null { + const noarg = (text: string) => text, + engCtrl = ctrl.engines; + + function searchTick() { + return Math.max( + 0, + searchTicks.findIndex(([tickMs]) => tickMs >= ctrl.params.movetime), + ); + } + + return h('div.bot-settings', [ + (id => { + return h('div.setting', [ + h('label', 'Move time'), + h('input#' + id, { + attrs: { type: 'range', min: 0, max: searchTicks.length - 1, step: 1 }, + hook: rangeConfig(searchTick, n => { + ctrl.params.movetime = searchTicks[n][0]; + ctrl.storeParams(); + ctrl.redraw(); + }), + }), + h('div.range_value', searchTicks[searchTick()][1]), + ]); + })('engine-search-ms'), + (id => { + return h('div.setting', [ + h('label', 'N-fold draw'), + h('input#' + id, { + attrs: { type: 'range', min: 0, max: 12, step: 3 }, + hook: rangeConfig( + () => ctrl.params.nfold, + x => { + ctrl.params.nfold = x; + ctrl.storeParams(); + ctrl.redraw(); + }, + ), + }), + h('div.range_value', ctrl.params.nfold === 0 ? 'no draw' : `${ctrl.params.nfold} moves`), + ]); + })('draw-after'), + hasFeature('sharedMem') + ? (id => { + return h('div.setting', [ + h('label', { attrs: { for: id } }, noarg('Threads')), + h('input#' + id, { + attrs: { + type: 'range', + min: 1, + max: navigator.hardwareConcurrency, + step: 1, + }, + hook: rangeConfig( + () => Math.min(ctrl.params.threads, navigator.hardwareConcurrency), + x => { + ctrl.params.threads = x; + ctrl.storeParams(); + ctrl.redraw(); + }, + ), + }), + h('div.range_value', `${ctrl.params.threads} / ${navigator.hardwareConcurrency}`), + ]); + })('analyse-threads') + : null, + (id => + h('div.setting', [ + h('label', { attrs: { for: id } }, noarg('Memory')), + h('input#' + id, { + attrs: { + type: 'range', + min: 4, + max: Math.floor(Math.log2(engCtrl.active?.maxHash ?? 4)), + step: 1, + }, + hook: rangeConfig( + () => { + return Math.floor(Math.log2(ctrl.params.hash)); + }, + v => { + ctrl.params.hash = Math.pow(2, v); + ctrl.storeParams(); + ctrl.redraw(); + }, + ), + }), + h('div.range_value', formatHashSize(ctrl.params.hash)), + ]))('analyse-memory'), + ]); +} + +function engineSelection(ctrl: BvbStockfishWebCtrl, color: Color): VNode { + const engines = ctrl.engines.supporting('standard'); + return h( + 'select.select-engine', + { + hook: bind('change', e => { + if (ctrl.ids[color] === (e.target as HTMLSelectElement).value) return; + ctrl.totals = { gamesLeft: ctrl.totals?.gamesLeft ?? 1, white: 0, black: 0, draw: 0, error: 0 }; + ctrl.ids[color] = (e.target as HTMLSelectElement).value; + ctrl.storeParams(); + ctrl.redraw(); + }), + props: { + value: ctrl.ids[color], + }, + }, + [ + ...engines.map(engine => { + console.log(color, ctrl.ids[color], engine.id, engine.name); + + return h( + 'option', + { + attrs: { + value: engine.id, + selected: ctrl.ids[color] === engine.id, + }, + }, + engine.name, + ); + }), + ], + ); +} From c88edb52ad0ee1eabeef2e044d941f509d76cfc5 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Mon, 6 Nov 2023 13:40:27 -0600 Subject: [PATCH 061/174] stockfish web testing --- bin/results.py | 118 +++++++++++------- public/batches.tgz | Bin 0 -> 123 bytes ui/ceval/src/engines/engines.ts | 2 +- ui/editor/src/ctrl.ts | 2 + ui/localPlay/src/botVsBot/bvbChessground.ts | 6 +- ui/localPlay/src/botVsBot/bvbCtrl.ts | 4 +- ui/localPlay/src/botVsBot/bvbInterfaces.ts | 5 + ui/localPlay/src/botVsBot/bvbMain.ts | 16 ++- .../botVsBot/bvbStockfishWebChessground.ts | 65 ---------- .../bvbStockfishWebCtrl.ts | 18 +-- .../bvbStockfishWebView.ts | 2 +- 11 files changed, 98 insertions(+), 140 deletions(-) create mode 100644 public/batches.tgz delete mode 100644 ui/localPlay/src/botVsBot/bvbStockfishWebChessground.ts rename ui/localPlay/src/botVsBot/{ => stockfishWebTest}/bvbStockfishWebCtrl.ts (94%) rename ui/localPlay/src/botVsBot/{ => stockfishWebTest}/bvbStockfishWebView.ts (98%) diff --git a/bin/results.py b/bin/results.py index 831a473767446..ebe098274db75 100755 --- a/bin/results.py +++ b/bin/results.py @@ -2,72 +2,98 @@ import json import sys +import os -# type Info = { -# name: string; -# id: string; +# type GameResult = { +# result: 'white'|'black'|'draw'|'error'; +# reason: string; +# white: string|{name: string}; +# black: string|{name: string}; # movetime: number; # threads: number; # hash: number; -# }; - -# type GameResult = { -# result: string; -# reason: string; -# white: Info; -# black: Info; -# moves: number; -# //moves: string[]; +# nfold?: number; +# startingFen?: string; +# moves: number | uci[]; # }; def main(): + width = 16 # crosstable width (minus 1 padding) + engines = ['11hce','16hce','14nn12','16nn07','16nn12','16nn40','16nn60'] # preferred sort order + template = {'W': 0, 'D': 0, 'L': 0} draws = {} - byEngine = {} + xtable = {} results = [] errors = [] - template = {'W': 0, 'D': 0, 'L': 0} for x in range(1, len(sys.argv)): - - with open(sys.argv[x], 'r') as f: - results.extend(json.load(f)) + try: + with open(sys.argv[x], 'r') as f: + results.extend(json.load(f)) + print('Loaded ' + sys.argv[x]) + except IOError as e: + print(str(e) + ' while loading ' + sys.argv[x]) - for result in results: - outcome = result['result'] - reason = result['reason'] - if outcome == 'draw': - if reason.startswith('Stockfish'): - errors.append(reason) - else: - byEngine.setdefault(get_name_compat(result, 'white'), template.copy())['D'] += 1 - byEngine.setdefault(get_name_compat(result, 'black'), template.copy())['D'] += 1 - draws[reason] = draws.get(reason, 0) + 1 - elif outcome == 'error': - errors.append(reason) + for game in results: + outcome = game['result'] + reason = game['reason'] + w = short(game['white']) + b = short(game['black']) + if outcome == 'error' or outcome == 'draw' and reason.startswith('Stockfish'): + errors.append(reason) # tolerate errors in some earlier batches + elif outcome == 'draw': + xtable.setdefault(w, {}).setdefault(b, template.copy())['D'] += 1 + xtable.setdefault(b, {}).setdefault(w, template.copy())['D'] += 1 + draws[reason] = draws.get(reason, 0) + 1 else: - try: - winner = result[outcome]['name'] - loser = result['black' if outcome == 'white' else 'white']['name'] - except TypeError as e: - winner = result[outcome] - loser = result['black' if outcome == 'white' else 'white'] - byEngine.setdefault(loser, template.copy())['L'] += 1 - byEngine.setdefault(winner, template.copy())['W'] += 1 + winner = w if outcome == 'white' else b + loser = b if outcome == 'white' else w + xtable.setdefault(winner, {}).setdefault(loser, template.copy())['W'] += 1 + xtable.setdefault(loser, {}).setdefault(winner, template.copy())['L'] += 1 - print('Total games:', len(results)) + print('Total games:', len(results) - len(errors)) print('Draws by reason:') for reason, count in draws.items(): print(f' {reason}: {count}') - print(' error: ', len(errors)) - print('Records by engine (W/D/L):') - for engine, record in byEngine.items(): - print(f' {engine}: {record["W"]}/{record["D"]}/{record["L"]}') + print('Crosstable:\n| ' + 'row vs col'.ljust(width), end='| ') + for e in engines: + print(f"W/D/L vs {e}".ljust(width), end='| ') + print() + for _ in range(len(engines)+1): + print('|'.ljust(width+2,'-'), end='') + print('|') + for e in engines: + print('| ' + e.ljust(width), end='| ') + for vsE in engines: + try: + print(wdl(xtable[e][vsE]).ljust(width), end='| ') + except KeyError: + print(' -'.ljust(width), end='| ') + print() -def get_name_compat(result, color): +def wdl(tally): + return f"{str(tally['W']).ljust(4)}/ {str(tally['D']).ljust(4)}/ {str(tally['L']).ljust(4)}" + +def short(engine): # don't want the full engine name try: - return result[color]['name'] + engine = engine['name'][10:] except TypeError as e: - return result[color] - + engine = engine[10:] # compensate for a breaking change in the dataset format + if engine == '11': + return '11hce' + elif engine == '16 HCE': + return '16hce' + elif engine == '14 NNUE': + return '14nn12' + elif engine.endswith('7MB'): + return '16nn07' + elif engine.endswith('12MB'): + return '16nn12' + elif engine.endswith('40MB'): + return '16nn40' + elif engine.endswith('60MB'): + return '16nn60' + return 'unknown' + if __name__ == '__main__': main() \ No newline at end of file diff --git a/public/batches.tgz b/public/batches.tgz new file mode 100644 index 0000000000000000000000000000000000000000..055690e474ed6cb0b7fd62f0eccb74d042a16d30 GIT binary patch literal 123 zcmb2|=3oE==C|jp`I;O=ST96!X&apR5Pd#@<5-4r^5a)W549W%NG$a$o?M>)@mS2c z>6w;Oe~R`6t^c*{Z;H&Ws@d_1+hV;|zD$mJZ^&CKyz1A6ezSSrC;L}%74Pfc{_ 0, - showGhost: pref.highlight, - }, - selectable: { - enabled: pref.moveEvent !== 1, - }, - events: { - insert(elements) { - resizeHandle(elements, Prefs.ShowResizeHandle.OnlyAtStart, 0, p => p == 0); - }, - }, - premovable: { - enabled: false, - }, - drawable: { - enabled: true, - defaultSnapToValidMove: lichess.storage.boolean('arrow.snap').getOrDefault(true), - }, - highlight: { - lastMove: pref.highlight, - check: pref.highlight, - }, - animation: { - duration: pref.animation, - }, - disableContextMenu: true, - }; -} diff --git a/ui/localPlay/src/botVsBot/bvbStockfishWebCtrl.ts b/ui/localPlay/src/botVsBot/stockfishWebTest/bvbStockfishWebCtrl.ts similarity index 94% rename from ui/localPlay/src/botVsBot/bvbStockfishWebCtrl.ts rename to ui/localPlay/src/botVsBot/stockfishWebTest/bvbStockfishWebCtrl.ts index 234729a8afcac..b78de9eef840b 100644 --- a/ui/localPlay/src/botVsBot/bvbStockfishWebCtrl.ts +++ b/ui/localPlay/src/botVsBot/stockfishWebTest/bvbStockfishWebCtrl.ts @@ -1,4 +1,4 @@ -import { BvbOpts } from './bvbInterfaces'; +import { BvbOpts, CgHost } from '../bvbInterfaces'; import { makeFen } from 'chessops/fen'; import { Chess, Role } from 'chessops'; import * as Chops from 'chessops'; @@ -30,7 +30,7 @@ type GameResult = { moves: number | string[]; }; -export class BvbStockfishWebCtrl { +export class BvbStockfishWebCtrl implements CgHost { cg: CgApi; path = ''; chess = Chess.default(); @@ -56,7 +56,7 @@ export class BvbStockfishWebCtrl { this.results = x; this.key = await this.results.count(); }); - + this.totals ??= { gamesLeft: 1, white: 0, black: 0, draw: 0, error: 0 }; this.bots = { white: this.engines.makeBot(this.ids.white), black: this.engines.makeBot(this.ids.black), @@ -115,7 +115,6 @@ export class BvbStockfishWebCtrl { this.bots.black.load(), ]); this.key = nextKey; - this.totals ??= { gamesLeft: 1, white: 0, black: 0, draw: 0, error: 0 }; if (numGames) this.totals.gamesLeft = numGames; this.fiftyMovePly = 0; this.threefoldFens.clear(); @@ -152,7 +151,7 @@ export class BvbStockfishWebCtrl { } async doGameOver(result: string, reason: string) { - this.results.put(`game ${this.key++}`, { + this.results.put(`game ${String(this.key++).padStart(4, '0')}`, { white: this.bots.white.info.name, black: this.bots.black.info.name, ...this.params, @@ -199,13 +198,6 @@ export class BvbStockfishWebCtrl { ); return; } - /*let castle = false; - if (this.chess.board.get(Chops.parseSquare(uci.slice(0, 2))!)?.role === 'king') { - const destPiece = this.chess.board.get(Chops.parseSquare(uci.slice(2, 4))!); - if (destPiece && destPiece.role === 'rook' && destPiece.color === this.chess.turn) { - castle = true; - } - }*/ this.chess.play(move); this.fen = makeFen(this.chess.toSetup()); this.fifty(move); @@ -213,7 +205,7 @@ export class BvbStockfishWebCtrl { this.updateCgBoard(uci); const { end, result, reason } = this.checkGameOver(); if (end) this.doGameOver(result!, reason!); - else this.getBotMove(/*castle*/); + else this.getBotMove(); } cgUserMove = (orig: Key, dest: Key) => { diff --git a/ui/localPlay/src/botVsBot/bvbStockfishWebView.ts b/ui/localPlay/src/botVsBot/stockfishWebTest/bvbStockfishWebView.ts similarity index 98% rename from ui/localPlay/src/botVsBot/bvbStockfishWebView.ts rename to ui/localPlay/src/botVsBot/stockfishWebTest/bvbStockfishWebView.ts index 8a1c45af5cd8e..5906297a08b43 100644 --- a/ui/localPlay/src/botVsBot/bvbStockfishWebView.ts +++ b/ui/localPlay/src/botVsBot/stockfishWebTest/bvbStockfishWebView.ts @@ -1,6 +1,6 @@ import { Chessground } from 'chessground'; import { h, VNode } from 'snabbdom'; -import { makeBvbConfig as makeCgConfig } from './bvbStockfishWebChessground'; +import { makeBvbConfig as makeCgConfig } from '../bvbChessground'; import * as Chops from 'chessops'; import { onInsert, bind } from 'common/snabbdom'; import { rangeConfig } from 'common/controls'; From cbe0d4929b27a366c2f01d869b55c5be38f96749 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Sat, 11 Nov 2023 06:58:34 -0600 Subject: [PATCH 062/174] gah --- ui/@build/src/build.ts | 1 + ui/@build/src/esbuild.ts | 3 +- ui/@build/src/parse.ts | 2 +- ui/analyse/package.json | 2 +- ui/gameSetup/src/view/localContent.ts | 17 +-- ui/libot/package.json | 26 ++--- ui/libot/src/bots/babyHoward.ts | 15 ++- ui/libot/src/bots/beatrice.ts | 13 ++- ui/libot/src/bots/coral.ts | 14 ++- ui/libot/src/bots/elsieZero.ts | 15 ++- ui/libot/src/ctrl.ts | 46 ++++---- ui/libot/src/interfaces.ts | 4 +- ui/libot/src/main.ts | 160 +++----------------------- ui/localPlay/css/_vs-bot.scss | 49 +++++--- ui/localPlay/package.json | 3 +- ui/localPlay/src/botVsBot/bvbCtrl.ts | 20 +++- ui/localPlay/src/ctrl.ts | 15 ++- ui/localPlay/src/data.ts | 27 +---- ui/localPlay/src/interfaces.ts | 3 + ui/localPlay/src/main.ts | 28 +++++ ui/localPlay/src/view.ts | 11 +- ui/round/package.json | 3 +- ui/round/src/ctrl.ts | 20 ++-- ui/round/src/view/user.ts | 3 +- 24 files changed, 205 insertions(+), 295 deletions(-) diff --git a/ui/@build/src/build.ts b/ui/@build/src/build.ts index 8114baa3792a2..9a9ab31344398 100644 --- a/ui/@build/src/build.ts +++ b/ui/@build/src/build.ts @@ -32,6 +32,7 @@ export async function build(mods: string[]) { await fs.promises.mkdir(env.cssDir, { recursive: true }); sass(); + for (const mod of buildModules) preModule(mod); await tsc(); await copies(); await esbuild(); diff --git a/ui/@build/src/esbuild.ts b/ui/@build/src/esbuild.ts index f9a610bb56a78..3a4d95f2f066a 100644 --- a/ui/@build/src/esbuild.ts +++ b/ui/@build/src/esbuild.ts @@ -1,7 +1,7 @@ import * as cps from 'node:child_process'; import * as path from 'node:path'; import * as es from 'esbuild'; -import { preModule, buildModules } from './build'; +import { buildModules } from './build'; import { env, errorMark, colors as c } from './main'; const typeBundles = new Map>(); @@ -19,7 +19,6 @@ export async function esbuild(): Promise { }; for (const mod of buildModules) { - preModule(mod); for (const tpe in mod.bundles) { if (!typeBundles.has(tpe)) typeBundles.set(tpe, new Map()); for (const r of mod.bundles[tpe]) typeBundles.get(tpe)?.set(r.output, path.join(mod.root, r.input)); diff --git a/ui/@build/src/parse.ts b/ui/@build/src/parse.ts index 84cc84a97d024..61f83ff16e6fc 100644 --- a/ui/@build/src/parse.ts +++ b/ui/@build/src/parse.ts @@ -91,7 +91,7 @@ function tokenizeArgs(argstr: string): string[] { // go through package json scripts and get what we need from 'compile', 'dev', and 'deps' // if some other script is necessary, add it to buildScriptKeys function parseScripts(module: LichessModule, pkgScripts: any) { - const buildScriptKeys = ['deps', 'compile', 'dev', 'post'].concat(env.prod ? ['prod'] : []); + const buildScriptKeys = ['pre', 'deps', 'compile', 'dev', 'post'].concat(env.prod ? ['prod'] : []); for (const script in pkgScripts) { if (!buildScriptKeys.includes(script)) continue; diff --git a/ui/analyse/package.json b/ui/analyse/package.json index 38dfdc6693ee4..ed73968474310 100644 --- a/ui/analyse/package.json +++ b/ui/analyse/package.json @@ -23,7 +23,7 @@ "chart": "workspace:*", "chat": "workspace:*", "chess": "workspace:*", - "chessops": "^0.12.7", + "chessops": "^0.12.8", "common": "workspace:*", "debounce-promise": "^3.1.2", "game": "workspace:*", diff --git a/ui/gameSetup/src/view/localContent.ts b/ui/gameSetup/src/view/localContent.ts index 7bced0997da55..2b7dfbf524607 100644 --- a/ui/gameSetup/src/view/localContent.ts +++ b/ui/gameSetup/src/view/localContent.ts @@ -4,8 +4,8 @@ import { SetupCtrl } from '../ctrl'; import { fenInput } from './components/fenInput'; import { timePickerAndSliders } from './components/timePickerAndSliders'; import { colorButtons } from './components/colorButtons'; +import { type Libot } from 'libot'; import { ratingView } from './components/ratingView'; -import { localBots, type BotInfo } from 'libot'; let deck: BotDeck; @@ -16,7 +16,7 @@ export default function localContent(ctrl: SetupCtrl): MaybeVNodes { h('div#bot-view', { key: 'bot-view', hook: onInsert(el => { - deck = new BotDeck(el as HTMLDivElement); + deck = new BotDeck(el as HTMLDivElement, ctrl); }), }), fenInput(ctrl), @@ -27,11 +27,14 @@ export default function localContent(ctrl: SetupCtrl): MaybeVNodes { } class BotDeck { - constructor(readonly view: HTMLDivElement) { - this.botInfos.forEach(bot => this.createCard(bot)); + constructor( + readonly view: HTMLDivElement, + readonly ctrl: SetupCtrl, + ) { + this.bots.forEach(bot => this.createCard(bot)); this.animate(); } - botInfos = Object.values(localBots); + bots = Object.values(this.ctrl.libot.bots); cards: HTMLDivElement[] = []; userMidX: number; userMidY: number; @@ -40,11 +43,11 @@ class BotDeck { handRotation: number = 0; selectedCard: HTMLDivElement | null = null; - createCard(bot: BotInfo) { + createCard(bot: Libot) { const card = document.createElement('div'); card.classList.add('card'); const img = document.createElement('img'); - img.src = bot.image; + img.src = bot.imageUrl; const label = document.createElement('label'); label.innerText = bot.name; card.appendChild(label); diff --git a/ui/libot/package.json b/ui/libot/package.json index b2ae344914507..3d90888e8baf6 100644 --- a/ui/libot/package.json +++ b/ui/libot/package.json @@ -4,34 +4,26 @@ "author": "T-Bone Duplexus", "license": "AGPL-3.0-or-later", "types": "dist/main.d.ts", - "typings": "main", - "exports": { - ".": "./dist/main.js", - "./*": "./dist/*.js" - }, - "typesVersions": { - "*": { - "*": [ - "dist/*" - ] - } - }, + "typings": "dist/main.d.ts", "dependencies": { "chess": "workspace:*", "chessops": "^0.12.7", "common": "workspace:*", "tree": "workspace:*", - "zerofish": "^0.0.13" + "zerofish": "^0.0.14" }, "scripts": { - "compile": "tsc", - "dev": "tsc", - "prod": "tsc" + "pre": "node @build/index.mjs" }, "lichess": { + "modules": { + "esm": { + "src/main.ts": "libot" + } + }, "copy": { "src": "node_modules/zerofish/dist/zerofishEngine.*", - "dest": "../../public/compiled" + "dest": "../../public/npm" } } } diff --git a/ui/libot/src/bots/babyHoward.ts b/ui/libot/src/bots/babyHoward.ts index ea14b7ca5e933..df99bb3bb78dd 100644 --- a/ui/libot/src/bots/babyHoward.ts +++ b/ui/libot/src/bots/babyHoward.ts @@ -1,14 +1,15 @@ import { type Zerofish } from 'zerofish'; -import { type Libot, botNetUrl, localBots } from '../main'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; export class BabyHoward implements Libot { - name = localBots.babyHoward.name; - description = localBots.babyHoward.description; - image = localBots.babyHoward.image; - net = 'maia-1100'; + name = 'Baby Howard'; + description = 'Baby Howard is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/baby-howard.webp', { noVersion: true }); + netName = 'maia-1100'; ratings = new Map(); - zf: Zerofish; + constructor(zf: Zerofish, opts?: any) { opts; this.zf = zf; @@ -18,3 +19,5 @@ export class BabyHoward implements Libot { return await this.zf.goZero(fen); } } + +registry.babyHoward = BabyHoward; diff --git a/ui/libot/src/bots/beatrice.ts b/ui/libot/src/bots/beatrice.ts index ee13205b08daf..1fe2ca1b86721 100644 --- a/ui/libot/src/bots/beatrice.ts +++ b/ui/libot/src/bots/beatrice.ts @@ -1,11 +1,12 @@ import { type Zerofish } from 'zerofish'; -import { Libot, botNetUrl, localBots } from '../main'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; export class Beatrice implements Libot { - name = localBots.beatrice.name; - description = localBots.beatrice.description; - image = localBots.beatrice.image; - net = 'maia-1100'; + name = 'Beatrice'; + description = 'Beatrice is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/beatrice.webp', { noVersion: true }); + netName = 'maia-1100'; ratings = new Map(); zf: Zerofish; @@ -18,3 +19,5 @@ export class Beatrice implements Libot { return await this.zf.goZero(fen); } } + +registry.beatrice = Beatrice; diff --git a/ui/libot/src/bots/coral.ts b/ui/libot/src/bots/coral.ts index e574bc67fcea1..45494de6afc0f 100644 --- a/ui/libot/src/bots/coral.ts +++ b/ui/libot/src/bots/coral.ts @@ -1,13 +1,15 @@ import { type Zerofish } from 'zerofish'; -import { Libot, botNetUrl, localBots } from '../main'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; export class Coral implements Libot { - name = localBots.coral.name; - description = localBots.coral.description; - image = localBots.coral.image; - net = 'maia-1100'; + name = 'Coral'; + description = 'Coral is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/coral.webp', { noVersion: true }); + netName = 'maia-1100'; ratings = new Map(); zf: Zerofish; + constructor(zf: Zerofish, opts?: any) { opts; this.zf = zf; @@ -17,3 +19,5 @@ export class Coral implements Libot { return await this.zf.goZero(fen); } } + +registry.coral = Coral; diff --git a/ui/libot/src/bots/elsieZero.ts b/ui/libot/src/bots/elsieZero.ts index f014c26d176cd..abceba590359b 100644 --- a/ui/libot/src/bots/elsieZero.ts +++ b/ui/libot/src/bots/elsieZero.ts @@ -1,14 +1,15 @@ import { type Zerofish } from 'zerofish'; -import { type Libot, botNetUrl, localBots } from '../main'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; export class ElsieZero implements Libot { - name = localBots.elsieZero.name; - description = localBots.elsieZero.description; - image = localBots.elsieZero.image; - net = 'maia-1100'; + name = 'Elsie Zero'; + description = 'Elsie Zero is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/baby-robot.webp', { noVersion: true }); + netName = 'maia-1100'; ratings = new Map(); - zf: Zerofish; + constructor(zf: Zerofish, opts?: any) { opts; this.zf = zf; @@ -18,3 +19,5 @@ export class ElsieZero implements Libot { return await this.zf.goZero(fen); } } + +registry.elsieZero = ElsieZero; diff --git a/ui/libot/src/ctrl.ts b/ui/libot/src/ctrl.ts index 1ab278477963a..ec4cbf762919b 100644 --- a/ui/libot/src/ctrl.ts +++ b/ui/libot/src/ctrl.ts @@ -1,48 +1,42 @@ -import makeZerofish, { type Zerofish } from 'zerofish'; +import { type Zerofish } from 'zerofish'; import { Libot } from './interfaces'; -import { Coral } from './bots/coral'; -import { BabyHoward } from './bots/babyHoward'; -import { ElsieZero } from './bots/elsieZero'; -import { Beatrice } from './bots/beatrice'; export interface Ctrl { zf: Zerofish; + bots: { [id: string]: Libot }; setBot(name: string): Promise; move(fen: string): Promise; } -export async function makeCtrl(): Promise { - const zf = await makeZerofish(); - const nets = new Map(); - const bots: { [k: string]: Libot } = { - coral: new Coral(zf), - babyHoward: new BabyHoward(zf), - elsieZero: new ElsieZero(zf), - beatrice: new Beatrice(zf), - }; +type Constructor = new (...args: any[]) => T; +export const registry: { [k: string]: Constructor } = {}; + +export async function makeCtrl(libots: { [id: string]: Libot }, zf: Zerofish): Promise { + const nets = new Map(); + let bot: Libot; return { zf, - async setBot(name: string) { - const net = bots[name]?.net; - if (!net) throw new Error(`unknown bot ${name} or no net`); - if (zf.netName !== bots[name].net) { - if (!nets.has(bots[name].net!)) { - nets.set(bots[name].net!, await fetchNet(bots[name].net!)); + bots: libots, + async setBot(id: string) { + bot = libots[id]; + if (!bot.netName) throw new Error(`unknown bot ${id} or no net`); + if (zf.netName !== bot.netName) { + if (!nets.has(bot.netName)) { + nets.set(bot.netName, await fetchNet(bot.netName)); } - const net = nets.get(bots[name].net!); - zf.setNet(name, net!); - zf.netName = bots[name].net; + zf.setNet(id, nets.get(bot.netName)!); + zf.netName = bot.netName; } }, move(fen: string) { - return zf.goZero(fen); + return bot.move(fen); }, }; } -function fetchNet(netName: string): Promise { - return fetch(`/lifat/bots/weights/${netName}.pb`) +async function fetchNet(netName: string): Promise { + return fetch(lichess.assetUrl(`lifat/bots/weights/${netName}.pb`, { noVersion: true })) .then(res => res.arrayBuffer()) .then(buf => new Uint8Array(buf)); } diff --git a/ui/libot/src/interfaces.ts b/ui/libot/src/interfaces.ts index 71fe62254b7ec..ba14be798d2fc 100644 --- a/ui/libot/src/interfaces.ts +++ b/ui/libot/src/interfaces.ts @@ -1,8 +1,8 @@ export interface Libot { readonly name: string; readonly description: string; - readonly image: string; - readonly net?: string; + readonly imageUrl: string; + readonly netName?: string; readonly ratings: Map; move: (fen: string) => Promise; diff --git a/ui/libot/src/main.ts b/ui/libot/src/main.ts index ff658518af852..5e430f89848e9 100644 --- a/ui/libot/src/main.ts +++ b/ui/libot/src/main.ts @@ -1,150 +1,16 @@ +import { Libot } from './interfaces'; +import makeZerofish from 'zerofish'; +import { makeCtrl, registry } from './ctrl'; +export { type Ctrl } from './ctrl'; export * from './interfaces'; +export * from './index.gen'; -export * from './ctrl'; - -export interface BotInfo { - readonly name: string; - readonly description: string; - readonly image: string; -} - -export const localBots: { [key: string]: BotInfo } = { - coral: { - name: 'Coral', - description: 'Coral is a simple bot that plays random moves.', - image: botImageUrl('coral.webp'), - }, - babyHoward: { - name: 'Baby Howard', - description: 'Baby Howard is a bot that plays random moves.', - image: botImageUrl('baby-howard.webp'), - }, - elsieZero: { - name: 'Elsie Zero', - description: 'Elsie Zero is a bot that plays random moves.', - image: botImageUrl('baby-robot.webp'), - }, - beatrice: { - name: 'Beatrice', - description: 'Beatrice is a bot that plays random moves.', - image: botImageUrl('beatrice.webp'), - }, - benny: { - name: 'Benny', - description: '', - image: botImageUrl('benny.webp'), - }, - danny: { - name: 'Danny', - description: '', - image: botImageUrl('danny.webp'), - }, - dansby: { - name: 'Dansby', - description: '', - image: botImageUrl('dansby.webp'), - }, - gary: { - name: 'Gary', - description: '', - image: botImageUrl('gary.webp'), - }, - greta: { - name: 'Greta', - description: '', - image: botImageUrl('greta.webp'), - }, - grunt: { - name: 'Grunt', - description: '', - image: botImageUrl('grunt.webp'), - }, - helena: { - name: 'Helena', - description: '', - image: botImageUrl('helena.webp'), - }, - henry: { - name: 'Henry', - description: '', - image: botImageUrl('henry.webp'), - }, - larry: { - name: 'Larry', - description: '', - image: botImageUrl('larry.webp'), - }, - listress: { - name: 'Listress', - description: '', - image: botImageUrl('listress.webp'), - }, - louise: { - name: 'Louise', - description: '', - image: botImageUrl('louise.webp'), - }, - maia: { - name: 'Maia', - description: '', - image: botImageUrl('maia.webp'), - }, - marco: { - name: 'Marco', - description: '', - image: botImageUrl('marco.webp'), - }, - mitsoko: { - name: 'Mitsoko', - description: '', - image: botImageUrl('mitsoko.webp'), - }, - nacho: { - name: 'Nacho', - description: '', - image: botImageUrl('nacho.webp'), - }, - owen: { - name: 'Owen', - description: '', - image: botImageUrl('owen.webp'), - }, - shark: { - name: 'Shark', - description: '', - image: botImageUrl('shark.webp'), - }, - torso: { - name: 'Torso', - description: '', - image: botImageUrl('soldier-torso.webp'), - }, - ghost: { - name: 'Ghost', - description: '', - image: botImageUrl('specops-lady.webp'), - }, - terrence: { - name: 'Terrence', - description: '', - image: botImageUrl('terrence.webp'), - }, - agatha: { - name: 'Agatha', - description: '', - image: botImageUrl('witch1.webp'), - }, - sabine: { - name: 'Sabine', - description: '', - image: botImageUrl('witch2.webp'), - }, -}; - -export function botNetUrl(net: string) { - return lichess.assetUrl(`lifat/bots/weights/${net}.pb`, { noVersion: true }); -} - -export function botImageUrl(image: string) { - return lichess.assetUrl(`lifat/bots/images/${image}`, { noVersion: true }); +export async function initModule(stubs = false) { + const libots: { [id: string]: Libot } = {}; + const zf = !stubs ? await makeZerofish() : undefined; + for (const name in registry) { + libots[name] = new registry[name](zf); + } + if (zf) return makeCtrl(libots, zf); + else return libots; } diff --git a/ui/localPlay/css/_vs-bot.scss b/ui/localPlay/css/_vs-bot.scss index e656339ba0dc7..c11c55c4e33bd 100644 --- a/ui/localPlay/css/_vs-bot.scss +++ b/ui/localPlay/css/_vs-bot.scss @@ -1,48 +1,63 @@ #bot-view { display: flex; flex-flow: column nowrap; + overflow: hidden; + max-height: var(--cg-height); #bot-content { flex: 1 1 auto; - overflow: hidden; + overflow: auto; } } .fancy-bot { display: flex; align-items: center; + img { + width: 64px; + flex: 0 0 64px; + } +} +#bot-content .fancy-bot { + &:nth-child(odd) { + background: $c-bg-zebra2; + } &:nth-child(even) { background: $c-bg-zebra; - justify-content: space-between; - img.picture { - order: 3; + /*justify-content: space-between; + img { + order: 1; } + .overview { + order: 2; + }*/ } &:hover { background: mix($c-link, $c-bg-box, 15%); } - img.picture { - flex: 0 0 128px; + img { + width: 128px; + //flex: 0 0 128px; - display: block; + //display: block; } - span { - display: flex; - flex-flow: row nowrap; - align-items: center; - * { - margin-right: 6px; - } + h2 { + font-size: 1.4em; + font-weight: bold; + color: $c-font-clearer; + } + p { + margin-left: 1em; } .overview { - margin: 20px 10px 10px 2.5vw; + padding: 1em; display: flex; + justify-content: space-around; + align-self: stretch; flex: auto; flex-flow: column; - justify-content: space-between; - padding-bottom: 15px; } } diff --git a/ui/localPlay/package.json b/ui/localPlay/package.json index 75dccf1e64623..296cff3aa675a 100644 --- a/ui/localPlay/package.json +++ b/ui/localPlay/package.json @@ -16,8 +16,7 @@ "puz": "workspace:*", "round": "workspace:*", "snabbdom": "^3.5.1", - "tree": "workspace:*", - "zerofish": "^0.0.14" + "tree": "workspace:*" }, "scripts": { "compile": "tsc", diff --git a/ui/localPlay/src/botVsBot/bvbCtrl.ts b/ui/localPlay/src/botVsBot/bvbCtrl.ts index 3b23a5f633759..b586f98fc3e97 100644 --- a/ui/localPlay/src/botVsBot/bvbCtrl.ts +++ b/ui/localPlay/src/botVsBot/bvbCtrl.ts @@ -3,7 +3,7 @@ import { PromotionCtrl } from 'chess/promotion'; import { makeFen /*, parseFen*/ } from 'chessops/fen'; import { Chess, Role } from 'chessops'; import * as Chops from 'chessops'; -import makeZerofish, { Zerofish, PV } from 'zerofish'; +//import makeZerofish, { Zerofish, PV } from 'zerofish'; import { Api as CgApi } from 'chessground/api'; import { Config as CgConfig } from 'chessground/config'; import { Key } from 'chessground/types'; @@ -11,6 +11,24 @@ import { Engines } from 'ceval'; type Player = 'human' | 'zero' | 'fish'; +interface Zerofish { + goZero(fen: string): Promise; + goFish(fen: string, opts: { pvs?: number; depth?: number }): Promise; + reset(): void; + setNet(name: string, weights: Uint8Array): void; +} + +interface PV { + moves: string[]; + score: number; +} + +function makeZerofish() { + return new Promise((resolve, reject) => { + resolve({} as Zerofish); + }); +} + export class BvbCtrl implements CgHost { cg: CgApi; path = ''; diff --git a/ui/localPlay/src/ctrl.ts b/ui/localPlay/src/ctrl.ts index b8fb2b865ec19..dad9587e8122c 100644 --- a/ui/localPlay/src/ctrl.ts +++ b/ui/localPlay/src/ctrl.ts @@ -1,24 +1,27 @@ import { LocalPlayOpts } from './interfaces'; -import { Coral } from 'libot/bots/coral'; -import { Ctrl as LibotCtrl } from 'libot'; -import { makeRounds } from './data'; +import { type Ctrl as LibotCtrl } from 'libot'; +//import { makeRound } from './data'; import { makeFen /*, parseFen*/ } from 'chessops/fen'; import { makeSanAndPlay } from 'chessops/san'; import { Chess } from 'chessops'; import * as Chops from 'chessops'; export class Ctrl { - //bot?: Libot = new Coral(); + libot: LibotCtrl; chess = Chess.default(); tellRound: SocketSend; fiftyMovePly = 0; + loaded: Promise; threefoldFens: Map = new Map(); constructor( readonly opts: LocalPlayOpts, readonly redraw: () => void, ) { - makeRounds(this).then(sender => (this.tellRound = sender)); + this.loaded = lichess.loadEsm('libot').then(libot => { + this.libot = libot; + this.libot.setBot('coral'); + }); } reset(/*fen: string*/) { @@ -71,7 +74,7 @@ export class Ctrl { } async botMove() { - //this.move(await this.bot!.move(this.fen)); + this.move(await this.libot!.move(this.fen)); } fifty(move?: Chops.Move) { diff --git a/ui/localPlay/src/data.ts b/ui/localPlay/src/data.ts index 28aa593e3e6f8..554f6bd87f19b 100644 --- a/ui/localPlay/src/data.ts +++ b/ui/localPlay/src/data.ts @@ -1,5 +1,4 @@ -import { RoundOpts, RoundData } from 'round'; -import { Ctrl } from './ctrl'; +import { RoundData } from 'round'; //import { Player, GameData } from 'game'; /*interface RoundApi { @@ -7,7 +6,7 @@ import { Ctrl } from './ctrl'; moveOn: MoveOn; }*/ -const data: RoundData = { +export const fakeData: RoundData = { game: { id: 'x7hgwoir', variant: { key: 'standard', name: 'Standard', short: 'Std' }, @@ -85,25 +84,3 @@ const data: RoundData = { takebackable: true, moretimeable: true, }; - -export async function makeRounds(ctrl: Ctrl): Promise { - const moves: string[] = []; - console.log(ctrl.dests); - for (const from in ctrl.dests) { - moves.push(from + ctrl.dests[from]); - } - const opts: RoundOpts = { - element: document.querySelector('.round__app') as HTMLElement, - data: { ...data, possibleMoves: moves.join(' ') }, - socketSend: (t: string, d: any) => { - if (t === 'move') { - ctrl.userMove(d.u); - } - }, - crosstableEl: document.querySelector('.cross-table') as HTMLElement, - i18n: {}, - onChange: (d: RoundData) => console.log(d), - local: true, - }; - return lichess.loadEsm('round', { init: opts }); -} diff --git a/ui/localPlay/src/interfaces.ts b/ui/localPlay/src/interfaces.ts index 8fccec7b5a5c0..56fe677fbc7db 100644 --- a/ui/localPlay/src/interfaces.ts +++ b/ui/localPlay/src/interfaces.ts @@ -1,5 +1,8 @@ +import { RoundData } from 'round'; + export interface LocalPlayOpts { mode: 'vsBot' | 'botVsBot'; pref: any; i18n: any; + data: RoundData; } diff --git a/ui/localPlay/src/main.ts b/ui/localPlay/src/main.ts index 1e16d4152b196..b8818483d8d76 100644 --- a/ui/localPlay/src/main.ts +++ b/ui/localPlay/src/main.ts @@ -1,8 +1,10 @@ import { attributesModule, classModule, init } from 'snabbdom'; +import { RoundOpts, RoundData } from 'round'; import { Ctrl } from './ctrl'; import view from './view'; import initBvb from './botVsBot/bvbMain'; import { LocalPlayOpts } from './interfaces'; +import { fakeData as data } from './data'; const patch = init([classModule, attributesModule]); @@ -10,6 +12,9 @@ export async function initModule(opts: LocalPlayOpts) { if (opts.mode === 'botVsBot') return initBvb(opts); const ctrl = new Ctrl(opts, () => {}); + ctrl.tellRound = await makeRound(ctrl); + await ctrl.loaded; + console.log(ctrl.libot, ctrl.libot.bots); const blueprint = view(ctrl); const element = document.querySelector('#bot-view') as HTMLElement; element.innerHTML = ''; @@ -20,3 +25,26 @@ export async function initModule(opts: LocalPlayOpts) { } redraw(); } + +export async function makeRound(ctrl: Ctrl): Promise { + const moves: string[] = []; + console.log(ctrl.dests); + for (const from in ctrl.dests) { + moves.push(from + ctrl.dests[from]); + } + const opts: RoundOpts = { + element: document.querySelector('.round__app') as HTMLElement, + data: { ...data, possibleMoves: moves.join(' ') }, + socketSend: (t: string, d: any) => { + if (t === 'move') { + console.log('movin on up', t, d); + ctrl.userMove(d.u); + } + }, + crosstableEl: document.querySelector('.cross-table') as HTMLElement, + i18n: {}, + onChange: (d: RoundData) => console.log(d), + local: true, + }; + return lichess.loadEsm('round', { init: opts }); +} diff --git a/ui/localPlay/src/view.ts b/ui/localPlay/src/view.ts index c46b618d3d547..65cbc8a7292be 100644 --- a/ui/localPlay/src/view.ts +++ b/ui/localPlay/src/view.ts @@ -1,7 +1,7 @@ import { h, VNode } from 'snabbdom'; //import * as licon from 'common/licon'; //import { bind } from 'common/snabbdom'; -import { localBots } from 'libot'; +import { type Libot } from 'libot'; import { Ctrl } from './ctrl'; export default function (ctrl: Ctrl): VNode { @@ -11,16 +11,15 @@ export default function (ctrl: Ctrl): VNode { 'div#bot-content', h( 'div#bot-list', - Object.values(localBots).map(bot => botView(ctrl, bot)), + Object.values(ctrl.libot.bots).map(bot => botView(ctrl, bot)), ), ), ]); } -function botView(ctrl: Ctrl, bot: any): VNode { +function botView(ctrl: Ctrl, bot: Libot): VNode { return h('div.fancy-bot', [ - h('h1', bot.name), - h('p', bot.description), - h('img', { attrs: { src: lichess.assetUrl(bot.image, { noVersion: true }) } }), + h('img', { attrs: { src: bot.imageUrl } }), + h('div.overview', [h('h2', bot.name), h('p', bot.description)]), ]); } diff --git a/ui/round/package.json b/ui/round/package.json index a3e564f5399d5..b8715dfc33d12 100644 --- a/ui/round/package.json +++ b/ui/round/package.json @@ -21,8 +21,7 @@ "voice": "workspace:*", "nvui": "workspace:*", "board": "workspace:*", - "snabbdom": "^3.5.1", - "zerofish": "0.0.11" + "snabbdom": "^3.5.1" }, "scripts": { "compile": "tsc", diff --git a/ui/round/src/ctrl.ts b/ui/round/src/ctrl.ts index 94b72ac699f0a..1bd74fb891f7d 100644 --- a/ui/round/src/ctrl.ts +++ b/ui/round/src/ctrl.ts @@ -33,8 +33,8 @@ import * as wakeLock from 'common/wakeLock'; import { opposite, uciToMove } from 'chessground/util'; import * as Prefs from 'common/prefs'; -import makeZerofish, { Zerofish } from 'zerofish'; -import * as Ch from 'chess'; +//import makeZerofish, { Zerofish } from 'zerofish'; +//import * as Ch from 'chess'; import { RoundOpts, @@ -93,7 +93,7 @@ export default class RoundController { preDrop?: cg.Role; sign: string = Math.random().toString(36); keyboardHelp: boolean = location.hash === '#keyboard'; - zerofish?: Zerofish; + //zerofish?: Zerofish; constructor( readonly opts: RoundOpts, @@ -168,10 +168,10 @@ export default class RoundController { if (!this.opts.noab && this.isPlaying()) ab.init(this); - makeZerofish({ pbUrl: '/assets/lifat/bots/weights/maia-1900.pb' }).then(zf => (this.zerofish = zf)); + //makeZerofish({ pbUrl: '/assets/lifat/bots/weights/maia-1900.pb' }).then(zf => (this.zerofish = zf)); } - private async updateZero(fen: string) { + /*private async updateZero(fen: string) { if (this.ply !== this.lastPly() || this.data.player.color !== (this.ply % 2 === 0 ? 'white' : 'black')) return; if (fen.split(' ')[0] === fen) fen += this.ply % 2 === 0 ? ' w' : ' b'; @@ -180,7 +180,7 @@ export default class RoundController { this.sendMove(uci?.slice(0, 2) as Key, uci?.slice(2, 4) as Key, Ch.charRole(uci!.slice(4)), { premove: false, }); - } + }*/ private showExpiration = () => { if (!this.data.expiration) return; @@ -293,7 +293,7 @@ export default class RoundController { this.autoScroll(); const canMove = ply === this.lastPly() && this.data.player.color === config.turnColor; - this.updateZero(s.fen); + //this.updateZero(s.fen); this.voiceMove?.update(s.fen, canMove); this.keyboardMove?.update(s), canMove; lichess.pubsub.emit('ply', ply); @@ -513,7 +513,7 @@ export default class RoundController { this.autoScroll(); this.onChange(); - this.updateZero(step.fen); //, playedColor != d.player.color); + //this.updateZero(step.fen); //, playedColor != d.player.color); this.keyboardMove?.update(step, playedColor != d.player.color); this.voiceMove?.update(step.fen, playedColor != d.player.color); lichess.sound.move({ ...o, filter: 'music' }); @@ -551,7 +551,7 @@ export default class RoundController { this.autoScroll(); this.onChange(); this.setLoading(false); - this.updateZero(d.steps[d.steps.length - 1].fen); //, true); + //this.updateZero(d.steps[d.steps.length - 1].fen); //, true); this.keyboardMove?.update(d.steps[d.steps.length - 1]); this.voiceMove?.update(d.steps[d.steps.length - 1].fen, true); }; @@ -907,7 +907,7 @@ export default class RoundController { location.href = '/page/play-extensions'; } }, 1000); - this.updateZero(d.game.fen); + //this.updateZero(d.game.fen); this.onChange(); }, 800); }; diff --git a/ui/round/src/view/user.ts b/ui/round/src/view/user.ts index c875c81e389e8..2a0333341834a 100644 --- a/ui/round/src/view/user.ts +++ b/ui/round/src/view/user.ts @@ -18,7 +18,8 @@ export function botHtml(ctrl: RoundController, player: Player, position: Positio }, }, [ - h('span', [h('img', { attrs: { src: player.image!, width: 64, height: 64 } }), h('name', player.name)]), + h('i.line', [h('img', { attrs: { src: player.image! } })]), + h('a.text', h('name', player.name)), h('rating', player.rating), //h('rating', player.ratingDiff), ], From 560df8bffe7b7e4c74eba9207b45800a35acea56 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Sat, 11 Nov 2023 06:59:29 -0600 Subject: [PATCH 063/174] gah --- ui/libot/@build/index.mjs | 15 +++ ui/libot/src/bots/agatha.ts | 23 +++++ ui/libot/src/bots/benny.ts | 23 +++++ ui/libot/src/bots/danny.ts | 23 +++++ ui/libot/src/bots/dansby.ts | 23 +++++ ui/libot/src/bots/gary.ts | 23 +++++ ui/libot/src/bots/ghost.ts | 23 +++++ ui/libot/src/bots/greta.ts | 23 +++++ ui/libot/src/bots/grunt.ts | 23 +++++ ui/libot/src/bots/helena.ts | 23 +++++ ui/libot/src/bots/henry.ts | 23 +++++ ui/libot/src/bots/larry.ts | 23 +++++ ui/libot/src/bots/listress.ts | 23 +++++ ui/libot/src/bots/louise.ts | 23 +++++ ui/libot/src/bots/maia.ts | 23 +++++ ui/libot/src/bots/marco.ts | 23 +++++ ui/libot/src/bots/mitsoko.ts | 23 +++++ ui/libot/src/bots/nacho.ts | 23 +++++ ui/libot/src/bots/owen.ts | 23 +++++ ui/libot/src/bots/sabine.ts | 23 +++++ ui/libot/src/bots/shark.ts | 23 +++++ ui/libot/src/bots/terrence.ts | 23 +++++ ui/libot/src/bots/torso.ts | 23 +++++ ui/libot/src/index.gen.ts | 30 ++++++ ui/libot/src/makeBot.mjs | 172 ++++++++++++++++++++++++++++++++++ 25 files changed, 723 insertions(+) create mode 100644 ui/libot/@build/index.mjs create mode 100644 ui/libot/src/bots/agatha.ts create mode 100644 ui/libot/src/bots/benny.ts create mode 100644 ui/libot/src/bots/danny.ts create mode 100644 ui/libot/src/bots/dansby.ts create mode 100644 ui/libot/src/bots/gary.ts create mode 100644 ui/libot/src/bots/ghost.ts create mode 100644 ui/libot/src/bots/greta.ts create mode 100644 ui/libot/src/bots/grunt.ts create mode 100644 ui/libot/src/bots/helena.ts create mode 100644 ui/libot/src/bots/henry.ts create mode 100644 ui/libot/src/bots/larry.ts create mode 100644 ui/libot/src/bots/listress.ts create mode 100644 ui/libot/src/bots/louise.ts create mode 100644 ui/libot/src/bots/maia.ts create mode 100644 ui/libot/src/bots/marco.ts create mode 100644 ui/libot/src/bots/mitsoko.ts create mode 100644 ui/libot/src/bots/nacho.ts create mode 100644 ui/libot/src/bots/owen.ts create mode 100644 ui/libot/src/bots/sabine.ts create mode 100644 ui/libot/src/bots/shark.ts create mode 100644 ui/libot/src/bots/terrence.ts create mode 100644 ui/libot/src/bots/torso.ts create mode 100644 ui/libot/src/index.gen.ts create mode 100644 ui/libot/src/makeBot.mjs diff --git a/ui/libot/@build/index.mjs b/ui/libot/@build/index.mjs new file mode 100644 index 0000000000000..0b3adf19675ed --- /dev/null +++ b/ui/libot/@build/index.mjs @@ -0,0 +1,15 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const srcDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '../src'); +const prelude = + '// Generated by @build/index.mjs to export the bots directory from src/index.ts\n//\n' + + '// Relaunch ui/build to regenerate (the bots directory is not watched).\n\n'; +const barrel = fs + .readdirSync(path.join(srcDir, 'bots')) + .filter(file => file.endsWith('.ts')) + .map(file => `export * from './bots/${path.basename(file, '.ts')}';`) + .join('\n'); + +fs.writeFileSync(path.join(srcDir, 'index.gen.ts'), prelude + barrel); diff --git a/ui/libot/src/bots/agatha.ts b/ui/libot/src/bots/agatha.ts new file mode 100644 index 0000000000000..87def10b236b8 --- /dev/null +++ b/ui/libot/src/bots/agatha.ts @@ -0,0 +1,23 @@ +import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class Agatha implements Libot { + name = 'Agatha'; + description = 'Agatha is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/witch1.webp', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.agatha = Agatha; diff --git a/ui/libot/src/bots/benny.ts b/ui/libot/src/bots/benny.ts new file mode 100644 index 0000000000000..b81305e711679 --- /dev/null +++ b/ui/libot/src/bots/benny.ts @@ -0,0 +1,23 @@ +import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class Benny implements Libot { + name = 'Benny'; + description = 'Benny is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/benny.webp', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.benny = Benny; diff --git a/ui/libot/src/bots/danny.ts b/ui/libot/src/bots/danny.ts new file mode 100644 index 0000000000000..ba10a63f717a5 --- /dev/null +++ b/ui/libot/src/bots/danny.ts @@ -0,0 +1,23 @@ +import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class Danny implements Libot { + name = 'Danny'; + description = 'Danny is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/danny.webp', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.danny = Danny; diff --git a/ui/libot/src/bots/dansby.ts b/ui/libot/src/bots/dansby.ts new file mode 100644 index 0000000000000..90f916dfa5425 --- /dev/null +++ b/ui/libot/src/bots/dansby.ts @@ -0,0 +1,23 @@ +import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class Dansby implements Libot { + name = 'Dansby'; + description = 'Dansby is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/dansby.webp', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.dansby = Dansby; diff --git a/ui/libot/src/bots/gary.ts b/ui/libot/src/bots/gary.ts new file mode 100644 index 0000000000000..bbcaa7d022ab0 --- /dev/null +++ b/ui/libot/src/bots/gary.ts @@ -0,0 +1,23 @@ +import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class Gary implements Libot { + name = 'Gary'; + description = 'Gary is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/gary.webp', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.gary = Gary; diff --git a/ui/libot/src/bots/ghost.ts b/ui/libot/src/bots/ghost.ts new file mode 100644 index 0000000000000..a58112bda1b01 --- /dev/null +++ b/ui/libot/src/bots/ghost.ts @@ -0,0 +1,23 @@ +import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class Ghost implements Libot { + name = 'Ghost'; + description = 'Ghost is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/specops-lady.webp', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.ghost = Ghost; diff --git a/ui/libot/src/bots/greta.ts b/ui/libot/src/bots/greta.ts new file mode 100644 index 0000000000000..2c0a63957f4a1 --- /dev/null +++ b/ui/libot/src/bots/greta.ts @@ -0,0 +1,23 @@ +import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class Greta implements Libot { + name = 'Greta'; + description = 'Greta is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/greta.webp', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.greta = Greta; diff --git a/ui/libot/src/bots/grunt.ts b/ui/libot/src/bots/grunt.ts new file mode 100644 index 0000000000000..c436f7468d23d --- /dev/null +++ b/ui/libot/src/bots/grunt.ts @@ -0,0 +1,23 @@ +import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class Grunt implements Libot { + name = 'Grunt'; + description = 'Grunt is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/grunt.webp', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.grunt = Grunt; diff --git a/ui/libot/src/bots/helena.ts b/ui/libot/src/bots/helena.ts new file mode 100644 index 0000000000000..50c4c37201401 --- /dev/null +++ b/ui/libot/src/bots/helena.ts @@ -0,0 +1,23 @@ +import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class Helena implements Libot { + name = 'Helena'; + description = 'Helena is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/helena.webp', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.helena = Helena; diff --git a/ui/libot/src/bots/henry.ts b/ui/libot/src/bots/henry.ts new file mode 100644 index 0000000000000..fd635568c39b1 --- /dev/null +++ b/ui/libot/src/bots/henry.ts @@ -0,0 +1,23 @@ +import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class Henry implements Libot { + name = 'Henry'; + description = 'Henry is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/henry.webp', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.henry = Henry; diff --git a/ui/libot/src/bots/larry.ts b/ui/libot/src/bots/larry.ts new file mode 100644 index 0000000000000..b0dc909cb83b7 --- /dev/null +++ b/ui/libot/src/bots/larry.ts @@ -0,0 +1,23 @@ +import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class Larry implements Libot { + name = 'Larry'; + description = 'Larry is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/larry.webp', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.larry = Larry; diff --git a/ui/libot/src/bots/listress.ts b/ui/libot/src/bots/listress.ts new file mode 100644 index 0000000000000..aa813df0ddbc9 --- /dev/null +++ b/ui/libot/src/bots/listress.ts @@ -0,0 +1,23 @@ +import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class Listress implements Libot { + name = 'Listress'; + description = 'Listress is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/listress.webp', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.listress = Listress; diff --git a/ui/libot/src/bots/louise.ts b/ui/libot/src/bots/louise.ts new file mode 100644 index 0000000000000..afc8a1bd328a6 --- /dev/null +++ b/ui/libot/src/bots/louise.ts @@ -0,0 +1,23 @@ +import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class Louise implements Libot { + name = 'Louise'; + description = 'Louise is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/louise.webp', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.louise = Louise; diff --git a/ui/libot/src/bots/maia.ts b/ui/libot/src/bots/maia.ts new file mode 100644 index 0000000000000..e9b1a4c82d67f --- /dev/null +++ b/ui/libot/src/bots/maia.ts @@ -0,0 +1,23 @@ +import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class Maia implements Libot { + name = 'Maia'; + description = 'Maia is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/maia.webp', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.maia = Maia; diff --git a/ui/libot/src/bots/marco.ts b/ui/libot/src/bots/marco.ts new file mode 100644 index 0000000000000..dabf6115070d3 --- /dev/null +++ b/ui/libot/src/bots/marco.ts @@ -0,0 +1,23 @@ +import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class Marco implements Libot { + name = 'Marco'; + description = 'Marco is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/marco.webp', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.marco = Marco; diff --git a/ui/libot/src/bots/mitsoko.ts b/ui/libot/src/bots/mitsoko.ts new file mode 100644 index 0000000000000..37a0a9ae616e1 --- /dev/null +++ b/ui/libot/src/bots/mitsoko.ts @@ -0,0 +1,23 @@ +import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class Mitsoko implements Libot { + name = 'Mitsoko'; + description = 'Mitsoko is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/mitsoko.webp', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.mitsoko = Mitsoko; diff --git a/ui/libot/src/bots/nacho.ts b/ui/libot/src/bots/nacho.ts new file mode 100644 index 0000000000000..1a0067e098b4e --- /dev/null +++ b/ui/libot/src/bots/nacho.ts @@ -0,0 +1,23 @@ +import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class Nacho implements Libot { + name = 'Nacho'; + description = 'Nacho is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/nacho.webp', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.nacho = Nacho; diff --git a/ui/libot/src/bots/owen.ts b/ui/libot/src/bots/owen.ts new file mode 100644 index 0000000000000..c988e5b90b464 --- /dev/null +++ b/ui/libot/src/bots/owen.ts @@ -0,0 +1,23 @@ +import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class Owen implements Libot { + name = 'Owen'; + description = 'Owen is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/owen.webp', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.owen = Owen; diff --git a/ui/libot/src/bots/sabine.ts b/ui/libot/src/bots/sabine.ts new file mode 100644 index 0000000000000..7fc20ab9b6f72 --- /dev/null +++ b/ui/libot/src/bots/sabine.ts @@ -0,0 +1,23 @@ +import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class Sabine implements Libot { + name = 'Sabine'; + description = 'Sabine is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/witch2.webp', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.sabine = Sabine; diff --git a/ui/libot/src/bots/shark.ts b/ui/libot/src/bots/shark.ts new file mode 100644 index 0000000000000..8fa8090aeb683 --- /dev/null +++ b/ui/libot/src/bots/shark.ts @@ -0,0 +1,23 @@ +import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class Shark implements Libot { + name = 'Shark'; + description = 'Shark is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/shark.webp', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.shark = Shark; diff --git a/ui/libot/src/bots/terrence.ts b/ui/libot/src/bots/terrence.ts new file mode 100644 index 0000000000000..60ad65dfcfc4c --- /dev/null +++ b/ui/libot/src/bots/terrence.ts @@ -0,0 +1,23 @@ +import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class Terrence implements Libot { + name = 'Terrence'; + description = 'Terrence is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/terrence.webp', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.terrence = Terrence; diff --git a/ui/libot/src/bots/torso.ts b/ui/libot/src/bots/torso.ts new file mode 100644 index 0000000000000..f6b94bf1b4183 --- /dev/null +++ b/ui/libot/src/bots/torso.ts @@ -0,0 +1,23 @@ +import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class Torso implements Libot { + name = 'Torso'; + description = 'Torso is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/soldier-torso.webp', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.torso = Torso; diff --git a/ui/libot/src/index.gen.ts b/ui/libot/src/index.gen.ts new file mode 100644 index 0000000000000..07cd5f06685a5 --- /dev/null +++ b/ui/libot/src/index.gen.ts @@ -0,0 +1,30 @@ +// Generated by @build/index.mjs to export the bots directory from src/index.ts +// +// Relaunch ui/build to regenerate (the bots directory is not watched). + +export * from './bots/agatha'; +export * from './bots/babyHoward'; +export * from './bots/beatrice'; +export * from './bots/benny'; +export * from './bots/coral'; +export * from './bots/danny'; +export * from './bots/dansby'; +export * from './bots/elsieZero'; +export * from './bots/gary'; +export * from './bots/ghost'; +export * from './bots/greta'; +export * from './bots/grunt'; +export * from './bots/helena'; +export * from './bots/henry'; +export * from './bots/larry'; +export * from './bots/listress'; +export * from './bots/louise'; +export * from './bots/maia'; +export * from './bots/marco'; +export * from './bots/mitsoko'; +export * from './bots/nacho'; +export * from './bots/owen'; +export * from './bots/sabine'; +export * from './bots/shark'; +export * from './bots/terrence'; +export * from './bots/torso'; diff --git a/ui/libot/src/makeBot.mjs b/ui/libot/src/makeBot.mjs new file mode 100644 index 0000000000000..cf22962943077 --- /dev/null +++ b/ui/libot/src/makeBot.mjs @@ -0,0 +1,172 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const srcDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'bots'); + +const localBots = { + coral: { + name: 'Coral', + description: 'Coral is a simple bot that plays random moves.', + image: 'coral.webp', + }, + babyHoward: { + name: 'Baby Howard', + description: 'Baby Howard is a bot that plays random moves.', + image: 'baby-howard.webp', + }, + elsieZero: { + name: 'Elsie Zero', + description: 'Elsie Zero is a bot that plays random moves.', + image: 'baby-robot.webp', + }, + beatrice: { + name: 'Beatrice', + description: 'Beatrice is a bot that plays random moves.', + image: 'beatrice.webp', + }, + benny: { + name: 'Benny', + description: '', + image: 'benny.webp', + }, + danny: { + name: 'Danny', + description: '', + image: 'danny.webp', + }, + dansby: { + name: 'Dansby', + description: '', + image: 'dansby.webp', + }, + gary: { + name: 'Gary', + description: '', + image: 'gary.webp', + }, + greta: { + name: 'Greta', + description: '', + image: 'greta.webp', + }, + grunt: { + name: 'Grunt', + description: '', + image: 'grunt.webp', + }, + helena: { + name: 'Helena', + description: '', + image: 'helena.webp', + }, + henry: { + name: 'Henry', + description: '', + image: 'henry.webp', + }, + larry: { + name: 'Larry', + description: '', + image: 'larry.webp', + }, + listress: { + name: 'Listress', + description: '', + image: 'listress.webp', + }, + louise: { + name: 'Louise', + description: '', + image: 'louise.webp', + }, + maia: { + name: 'Maia', + description: '', + image: 'maia.webp', + }, + marco: { + name: 'Marco', + description: '', + image: 'marco.webp', + }, + mitsoko: { + name: 'Mitsoko', + description: '', + image: 'mitsoko.webp', + }, + nacho: { + name: 'Nacho', + description: '', + image: 'nacho.webp', + }, + owen: { + name: 'Owen', + description: '', + image: 'owen.webp', + }, + shark: { + name: 'Shark', + description: '', + image: 'shark.webp', + }, + torso: { + name: 'Torso', + description: '', + image: 'soldier-torso.webp', + }, + ghost: { + name: 'Ghost', + description: '', + image: 'specops-lady.webp', + }, + terrence: { + name: 'Terrence', + description: '', + image: 'terrence.webp', + }, + agatha: { + name: 'Agatha', + description: '', + image: 'witch1.webp', + }, + sabine: { + name: 'Sabine', + description: '', + image: 'witch2.webp', + }, +}; + +for (const k in localBots) { + const bot = localBots[k]; + const ext = makeBot(k, bot); + fs.writeFileSync(path.join(srcDir, `${k}.ts`), ext); +} + +function makeBot(k, bot) { + const clz = bot.name.replace(/ /g, ''); + return `import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class ${clz} implements Libot { + name = '${bot.name}'; + description = '${bot.name} is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/${bot.image}', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.${k} = ${clz}; +`; +} From 195f180e2dfc5d5fc337e16e9326aa677dc41f06 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Mon, 13 Nov 2023 13:51:10 -0600 Subject: [PATCH 064/174] . --- ui/localPlay/css/_vs-bot.scss | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/ui/localPlay/css/_vs-bot.scss b/ui/localPlay/css/_vs-bot.scss index d4e048c1e8222..613fc91616d42 100644 --- a/ui/localPlay/css/_vs-bot.scss +++ b/ui/localPlay/css/_vs-bot.scss @@ -25,13 +25,6 @@ } &:nth-child(even) { background: $c-bg-zebra; - /*justify-content: space-between; - img { - order: 1; - } - .overview { - order: 2; - }*/ } &:hover { @@ -40,9 +33,6 @@ img { width: 128px; - //flex: 0 0 128px; - - //display: block; } h2 { font-size: 1.4em; From 506f69483269ded5d03d2e89fb0bf82a517772cc Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Thu, 16 Nov 2023 15:30:43 -0600 Subject: [PATCH 065/174] . --- pnpm-lock.yaml | 38 +++++++- ui/@build/src/build.ts | 8 +- ui/libot/@build/make.mjs | 45 +++++++++ ui/libot/defs/bots.json | 156 +++++++++++++++++++++++++++++++ ui/libot/package.json | 20 ++-- ui/libot/src/bots/agatha.ts | 25 ----- ui/libot/src/bots/babyHoward.ts | 25 ----- ui/libot/src/bots/beatrice.ts | 25 ----- ui/libot/src/bots/coral.ts | 25 ----- ui/libot/src/bots/dansby.ts | 25 ----- ui/libot/src/bots/elsieZero.ts | 25 ----- ui/libot/src/bots/gary.ts | 25 ----- ui/libot/src/bots/ghost.ts | 25 ----- ui/libot/src/bots/greta.ts | 25 ----- ui/libot/src/bots/grunt.ts | 25 ----- ui/libot/src/bots/helena.ts | 25 ----- ui/libot/src/bots/henry.ts | 25 ----- ui/libot/src/bots/larry.ts | 25 ----- ui/libot/src/bots/listress.ts | 25 ----- ui/libot/src/bots/louise.ts | 25 ----- ui/libot/src/bots/maia.ts | 25 ----- ui/libot/src/bots/marco.ts | 25 ----- ui/libot/src/bots/mitsoko.ts | 25 ----- ui/libot/src/bots/nacho.ts | 25 ----- ui/libot/src/bots/owen.ts | 25 ----- ui/libot/src/bots/shark.ts | 25 ----- ui/libot/src/bots/spectre.ts | 25 ----- ui/libot/src/bots/terrence.ts | 25 ----- ui/libot/src/ctrl.ts | 6 +- ui/libot/src/index.gen.ts | 27 ------ ui/libot/src/interfaces.ts | 10 +- ui/libot/src/main.ts | 14 ++- ui/libot/src/makeBot.mjs | 161 -------------------------------- ui/libot/src/zerobot.ts | 26 ++++++ ui/localPlay/src/ctrl.ts | 7 +- ui/localPlay/src/main.ts | 4 +- 36 files changed, 302 insertions(+), 795 deletions(-) create mode 100644 ui/libot/@build/make.mjs create mode 100644 ui/libot/defs/bots.json delete mode 100644 ui/libot/src/bots/agatha.ts delete mode 100644 ui/libot/src/bots/babyHoward.ts delete mode 100644 ui/libot/src/bots/beatrice.ts delete mode 100644 ui/libot/src/bots/coral.ts delete mode 100644 ui/libot/src/bots/dansby.ts delete mode 100644 ui/libot/src/bots/elsieZero.ts delete mode 100644 ui/libot/src/bots/gary.ts delete mode 100644 ui/libot/src/bots/ghost.ts delete mode 100644 ui/libot/src/bots/greta.ts delete mode 100644 ui/libot/src/bots/grunt.ts delete mode 100644 ui/libot/src/bots/helena.ts delete mode 100644 ui/libot/src/bots/henry.ts delete mode 100644 ui/libot/src/bots/larry.ts delete mode 100644 ui/libot/src/bots/listress.ts delete mode 100644 ui/libot/src/bots/louise.ts delete mode 100644 ui/libot/src/bots/maia.ts delete mode 100644 ui/libot/src/bots/marco.ts delete mode 100644 ui/libot/src/bots/mitsoko.ts delete mode 100644 ui/libot/src/bots/nacho.ts delete mode 100644 ui/libot/src/bots/owen.ts delete mode 100644 ui/libot/src/bots/shark.ts delete mode 100644 ui/libot/src/bots/spectre.ts delete mode 100644 ui/libot/src/bots/terrence.ts delete mode 100644 ui/libot/src/index.gen.ts delete mode 100644 ui/libot/src/makeBot.mjs create mode 100644 ui/libot/src/zerobot.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0eb3abc79742..c35635dd18ec2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -389,8 +389,8 @@ importers: specifier: workspace:* version: link:../tree zerofish: - specifier: link:/home/gamblej/ws/lichess/zerofish - version: link:../../../zerofish + specifier: ^0.0.16 + version: 0.0.16 ui/lobby: dependencies: @@ -1992,6 +1992,10 @@ packages: resolution: {integrity: sha512-rWr/ryzOUi9r/zUA2GK2qLWGBIBmDeIojBQXuvR76pulHUoEGMJ2A7NWShUaA5AE90ha+l9tlsyGz2UioQE9cg==} dev: false + /@types/emscripten@1.39.10: + resolution: {integrity: sha512-TB/6hBkYQJxsZHSqyeuO1Jt0AB/bW6G7rHt9g7lML7SOF6lbgcHvw/Lr+69iqN0qxgXLhWKScAon73JNnptuDw==} + dev: false + /@types/fnando__sparkline@0.3.4: resolution: {integrity: sha512-FWU1zw7CVJYVeDk77FGphTUabfPims4F/Yq+WFB0Gh647lLtiXHWn8vpfT95Fl65IsNBDOhEbxJdhmERMGubNQ==} dev: false @@ -2051,6 +2055,12 @@ packages: resolution: {integrity: sha512-Mgq7eCtoTjT89FqNoTzzXg2XvCi5VMhRV6+I2aYanc6kQCBImeNaAYRs/DyoVqk1YEUJK5gN9VO7HRIdz4Wo3Q==} dev: false + /@types/node@20.9.0: + resolution: {integrity: sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==} + dependencies: + undici-types: 5.26.5 + dev: false + /@types/prettier@2.7.1: resolution: {integrity: sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==} dev: false @@ -2087,6 +2097,10 @@ packages: resolution: {integrity: sha512-cHKZeeX0WtKTI7zHH7LSUKxi7yv4hEGWVHRJlMziRxRiUenzZ1Pg4X77VUWNyCFBce5Nlm+usLTndn7o7YvQKg==} dev: false + /@types/web@0.0.119: + resolution: {integrity: sha512-CQVOcEWrxr0MXbQbR3rrw6GHo2mcr8WlhLHQkOKDhhySTjz15/35jk0Zm2FbHRyCvSEjr/J7A2iDD5GRrGxE2A==} + dev: false + /@types/webrtc@0.0.33: resolution: {integrity: sha512-xjN6BelzkY3lzXjIjXGqJVDS6XDleEsvp1bVIyNccXCcMoTH3wvUXFew4/qflwJdNqjmq98Zc5VcALV+XBKBvg==} dev: false @@ -4603,6 +4617,12 @@ packages: hasBin: true dev: false + /prettier@3.1.0: + resolution: {integrity: sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==} + engines: {node: '>=14'} + hasBin: true + dev: false + /pretty-format@29.3.1: resolution: {integrity: sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5185,6 +5205,10 @@ packages: hasBin: true dev: false + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: false + /universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} @@ -5395,6 +5419,16 @@ packages: engines: {node: '>=10'} dev: false + /zerofish@0.0.16: + resolution: {integrity: sha512-4Xgbic2zijBYIeCapuyBeSmahG4wb4Bv4gJEa2B9qjdczx/1gjm5aOGu8yLSs8Q2R9wmBs9MJ6cPSLGSwDbTCA==} + dependencies: + '@types/emscripten': 1.39.10 + '@types/node': 20.9.0 + '@types/web': 0.0.119 + prettier: 3.1.0 + typescript: 5.2.2 + dev: false + /zxcvbn@4.4.2: resolution: {integrity: sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ==} dev: false diff --git a/ui/@build/src/build.ts b/ui/@build/src/build.ts index 9a9ab31344398..22a489da7678a 100644 --- a/ui/@build/src/build.ts +++ b/ui/@build/src/build.ts @@ -78,7 +78,7 @@ export async function copies() { watcher.on('change', () => { updated.add(dir); clearTimeout(watchTimeout); - watchTimeout = setTimeout(fire, 600); + watchTimeout = setTimeout(fire, 2000); }); watcher.on('error', (err: Error) => env.error(err)); } @@ -100,13 +100,15 @@ async function globCopy(cp: Copy): Promise> { const srcs = await globArray(cp.src, { cwd: cp.mod.root, abs: false }); watchDirs.add(path.join(cp.mod.root, globRoot)); - env.log(`[${c.grey(cp.mod.name)}] - Copy '${c.cyan(cp.src)}' to '${c.cyan(cp.dest)}'`); + env.log(`[${c.grey(cp.mod.name)}] - Sync '${c.cyan(cp.src)}' to '${c.cyan(cp.dest)}'`); + const fileCopies = []; for (const src of srcs) { const srcPath = path.join(cp.mod.root, src); watchDirs.add(path.dirname(srcPath)); const destPath = path.join(dest, src.slice(globRoot.length)); - await copyOne(srcPath, destPath, cp.mod.name); + fileCopies.push(copyOne(srcPath, destPath, cp.mod.name)); } + await Promise.all(fileCopies); return watchDirs; } diff --git a/ui/libot/@build/make.mjs b/ui/libot/@build/make.mjs new file mode 100644 index 0000000000000..6c44027d1e49f --- /dev/null +++ b/ui/libot/@build/make.mjs @@ -0,0 +1,45 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const buildDir = path.join(path.dirname(fileURLToPath(import.meta.url))); +const srcDir = path.join(buildDir, '../src/bots'); + +const localBots = JSON.parse(fs.readFileSync(path.join(buildDir, 'bots.json'), 'utf8')); +let ordinal = 0; + +for (const k in localBots) { + const bot = localBots[k]; + const ext = makeBot(k, bot, ordinal++); + fs.writeFileSync(path.join(srcDir, `${k}.ts`), ext); +} + +function makeBot(k, bot, ordinal) { + const clz = bot.name.replace(/ /g, ''); + return `import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class ${clz} implements Libot { + name = '${bot.name}'; + uid = '#${k}'; + ordinal = ${ordinal}; + description = '${bot.name} is a bot that plays random moves.'; + imageUrl = lichess.assetUrl('lifat/bots/images/${bot.image}', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.${k} = ${clz}; +`; +} diff --git a/ui/libot/defs/bots.json b/ui/libot/defs/bots.json new file mode 100644 index 0000000000000..7827e3d4f1ec3 --- /dev/null +++ b/ui/libot/defs/bots.json @@ -0,0 +1,156 @@ +[ + { + "uid": "#babyhoward", + "name": "Baby Howard", + "description": "Baby Howard is a bot that plays random moves.", + "image": "baby-howard.webp", + "netName": "maia-1100.pb" + }, + { + "uid": "#coral", + "name": "Coral", + "description": "Coral is a simple bot that plays random moves.", + "image": "coral.webp", + "netName": "maia-1100.pb" + }, + { + "uid": "#owen", + "name": "Owen", + "description": "", + "image": "owen.webp", + "netName": "maia-1100.pb" + }, + { + "uid": "#beatrice", + "name": "Beatrice", + "description": "Beatrice is a bot that plays random moves.", + "image": "beatrice.webp", + "netName": "maia-1200.pb" + }, + { + "uid": "#nacho", + "name": "Nacho", + "description": "", + "image": "nacho.webp", + "netName": "maia-1200.pb" + }, + { + "uid": "#louise", + "name": "Louise", + "description": "", + "image": "louise.webp", + "netName": "maia-1300.pb" + }, + { + "uid": "#terrence", + "name": "Terrence", + "description": "", + "image": "terrence.webp", + "netName": "maia-1400.pb" + }, + { + "uid": "#elsiezero", + "name": "Elsie Zero", + "description": "Elsie Zero is a bot that plays random moves.", + "image": "baby-robot.webp", + "netName": "maia-1300.pb" + }, + { + "uid": "#henry", + "name": "Henry", + "description": "", + "image": "henry.webp", + "netName": "maia-1400.pb" + }, + { + "uid": "#greta", + "name": "Greta", + "description": "", + "image": "greta.webp", + "netName": "maia-1700.pb" + }, + { + "uid": "#ronald", + "name": "Ronald", + "description": "", + "image": "shark.webp", + "netName": "maia-1500.pb" + }, + { + "uid": "#marco", + "name": "Marco", + "description": "", + "image": "marco.webp", + "netName": "maia-1600.pb" + }, + { + "uid": "#ivanka", + "name": "Ivanka", + "description": "", + "image": "larry.webp", + "netName": "maia-1500.pb" + }, + { + "uid": "#dansby", + "name": "Dansby", + "description": "", + "image": "dansby.webp", + "netName": "maia-1500.pb" + }, + { + "uid": "#agatha", + "name": "Agatha", + "description": "", + "image": "witch1.webp", + "netName": "maia-1600.pb" + }, + { + "uid": "#grunt", + "name": "Grunt", + "description": "", + "image": "grunt.webp", + "netName": "maia-1700.pb" + }, + { + "uid": "#helena", + "name": "Helena", + "description": "", + "image": "helena.webp", + "netName": "maia-1700.pb" + }, + { + "uid": "#ghost", + "name": "Ghost", + "description": "", + "image": "specops-lady.webp", + "netName": "maia-1900.pb" + }, + { + "uid": "#mitsoko", + "name": "Mitsoko", + "description": "", + "image": "mitsoko.webp", + "netName": "maia-1800.pb" + }, + { + "uid": "#maia", + "name": "Maia", + "description": "", + "image": "maia.webp", + "netName": "maia-1900.pb" + }, + { + "uid": "#spectre", + "name": "Spectre", + "description": "", + "image": "soldier-torso.webp", + "netName": "maia-1800.pb" + }, + { + "uid": "#listress", + "name": "Listress", + "description": "", + "image": "listress.webp", + "netName": "maia-1900.pb" + } +] diff --git a/ui/libot/package.json b/ui/libot/package.json index b6384bb537ecf..64d9a66be7edc 100644 --- a/ui/libot/package.json +++ b/ui/libot/package.json @@ -10,20 +10,24 @@ "chessops": "^0.12.7", "common": "workspace:*", "tree": "workspace:*", - "zerofish": "link:/home/gamblej/ws/lichess/zerofish" - }, - "scripts": { - "pre": "node @build/index.mjs" + "zerofish": "^0.0.16" }, + "scripts": {}, "lichess": { "modules": { "esm": { "src/main.ts": "libot" } }, - "copy": { - "src": "node_modules/zerofish/dist/zerofishEngine.*", - "dest": "../../public/npm" - } + "copy": [ + { + "src": "node_modules/zerofish/dist/zerofishEngine.*", + "dest": "../../public/npm" + }, + { + "src": "defs/bots.json", + "dest": "../../public" + } + ] } } diff --git a/ui/libot/src/bots/agatha.ts b/ui/libot/src/bots/agatha.ts deleted file mode 100644 index df89bfec42640..0000000000000 --- a/ui/libot/src/bots/agatha.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class Agatha implements Libot { - name = 'Agatha'; - uid = '#agatha'; - ordinal = 14; - description = 'Agatha is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/witch1.webp', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.agatha = Agatha; diff --git a/ui/libot/src/bots/babyHoward.ts b/ui/libot/src/bots/babyHoward.ts deleted file mode 100644 index c5c1b56973b5a..0000000000000 --- a/ui/libot/src/bots/babyHoward.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class BabyHoward implements Libot { - name = 'Baby Howard'; - uid = '#babyHoward'; - ordinal = 0; - description = 'Baby Howard is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/baby-howard.webp', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.babyHoward = BabyHoward; diff --git a/ui/libot/src/bots/beatrice.ts b/ui/libot/src/bots/beatrice.ts deleted file mode 100644 index cb10e96a85612..0000000000000 --- a/ui/libot/src/bots/beatrice.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class Beatrice implements Libot { - name = 'Beatrice'; - uid = '#beatrice'; - ordinal = 3; - description = 'Beatrice is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/beatrice.webp', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.beatrice = Beatrice; diff --git a/ui/libot/src/bots/coral.ts b/ui/libot/src/bots/coral.ts deleted file mode 100644 index ec6f322e52da9..0000000000000 --- a/ui/libot/src/bots/coral.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class Coral implements Libot { - name = 'Coral'; - uid = '#coral'; - ordinal = 1; - description = 'Coral is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/coral.webp', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.coral = Coral; diff --git a/ui/libot/src/bots/dansby.ts b/ui/libot/src/bots/dansby.ts deleted file mode 100644 index e3accf8deb806..0000000000000 --- a/ui/libot/src/bots/dansby.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class Dansby implements Libot { - name = 'Dansby'; - uid = '#dansby'; - ordinal = 11; - description = 'Dansby is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/dansby.webp', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.dansby = Dansby; diff --git a/ui/libot/src/bots/elsieZero.ts b/ui/libot/src/bots/elsieZero.ts deleted file mode 100644 index 1e912de47b67d..0000000000000 --- a/ui/libot/src/bots/elsieZero.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class ElsieZero implements Libot { - name = 'Elsie Zero'; - uid = '#elsieZero'; - ordinal = 6; - description = 'Elsie Zero is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/baby-robot.webp', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.elsieZero = ElsieZero; diff --git a/ui/libot/src/bots/gary.ts b/ui/libot/src/bots/gary.ts deleted file mode 100644 index aa7215154c34a..0000000000000 --- a/ui/libot/src/bots/gary.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class Gary implements Libot { - name = 'Gary'; - uid = '#gary'; - ordinal = 7; - description = 'Gary is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/gary.webp', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.gary = Gary; diff --git a/ui/libot/src/bots/ghost.ts b/ui/libot/src/bots/ghost.ts deleted file mode 100644 index c93d8ea099afb..0000000000000 --- a/ui/libot/src/bots/ghost.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class Ghost implements Libot { - name = 'Ghost'; - uid = '#ghost'; - ordinal = 20; - description = 'Ghost is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/specops-lady.webp', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.ghost = Ghost; diff --git a/ui/libot/src/bots/greta.ts b/ui/libot/src/bots/greta.ts deleted file mode 100644 index e8ee61b7a8896..0000000000000 --- a/ui/libot/src/bots/greta.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class Greta implements Libot { - name = 'Greta'; - uid = '#greta'; - ordinal = 15; - description = 'Greta is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/greta.webp', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.greta = Greta; diff --git a/ui/libot/src/bots/grunt.ts b/ui/libot/src/bots/grunt.ts deleted file mode 100644 index faad34724793e..0000000000000 --- a/ui/libot/src/bots/grunt.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class Grunt implements Libot { - name = 'Grunt'; - uid = '#grunt'; - ordinal = 16; - description = 'Grunt is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/grunt.webp', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.grunt = Grunt; diff --git a/ui/libot/src/bots/helena.ts b/ui/libot/src/bots/helena.ts deleted file mode 100644 index 148e2d0108b2f..0000000000000 --- a/ui/libot/src/bots/helena.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class Helena implements Libot { - name = 'Helena'; - uid = '#helena'; - ordinal = 17; - description = 'Helena is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/helena.webp', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.helena = Helena; diff --git a/ui/libot/src/bots/henry.ts b/ui/libot/src/bots/henry.ts deleted file mode 100644 index df3b28c784b4a..0000000000000 --- a/ui/libot/src/bots/henry.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class Henry implements Libot { - name = 'Henry'; - uid = '#henry'; - ordinal = 8; - description = 'Henry is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/henry.webp', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.henry = Henry; diff --git a/ui/libot/src/bots/larry.ts b/ui/libot/src/bots/larry.ts deleted file mode 100644 index 2f1199b5986b3..0000000000000 --- a/ui/libot/src/bots/larry.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class Larry implements Libot { - name = 'Larry'; - uid = '#larry'; - ordinal = 12; - description = 'Larry is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/larry.webp', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.larry = Larry; diff --git a/ui/libot/src/bots/listress.ts b/ui/libot/src/bots/listress.ts deleted file mode 100644 index 00368282513e1..0000000000000 --- a/ui/libot/src/bots/listress.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class Listress implements Libot { - name = 'Listress'; - uid = '#listress'; - ordinal = 22; - description = 'Listress is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/listress.webp', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.listress = Listress; diff --git a/ui/libot/src/bots/louise.ts b/ui/libot/src/bots/louise.ts deleted file mode 100644 index 9d43811eb8373..0000000000000 --- a/ui/libot/src/bots/louise.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class Louise implements Libot { - name = 'Louise'; - uid = '#louise'; - ordinal = 5; - description = 'Louise is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/louise.webp', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.louise = Louise; diff --git a/ui/libot/src/bots/maia.ts b/ui/libot/src/bots/maia.ts deleted file mode 100644 index 3967453142b45..0000000000000 --- a/ui/libot/src/bots/maia.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class Maia implements Libot { - name = 'Maia'; - uid = '#maia'; - ordinal = 18; - description = 'Maia is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/maia.webp', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.maia = Maia; diff --git a/ui/libot/src/bots/marco.ts b/ui/libot/src/bots/marco.ts deleted file mode 100644 index 5f4056da7ef6d..0000000000000 --- a/ui/libot/src/bots/marco.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class Marco implements Libot { - name = 'Marco'; - uid = '#marco'; - ordinal = 13; - description = 'Marco is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/marco.webp', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.marco = Marco; diff --git a/ui/libot/src/bots/mitsoko.ts b/ui/libot/src/bots/mitsoko.ts deleted file mode 100644 index b97d3c5bf2501..0000000000000 --- a/ui/libot/src/bots/mitsoko.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class Mitsoko implements Libot { - name = 'Mitsoko'; - uid = '#mitsoko'; - ordinal = 19; - description = 'Mitsoko is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/mitsoko.webp', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.mitsoko = Mitsoko; diff --git a/ui/libot/src/bots/nacho.ts b/ui/libot/src/bots/nacho.ts deleted file mode 100644 index fea802e4b67fc..0000000000000 --- a/ui/libot/src/bots/nacho.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class Nacho implements Libot { - name = 'Nacho'; - uid = '#nacho'; - ordinal = 4; - description = 'Nacho is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/nacho.webp', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.nacho = Nacho; diff --git a/ui/libot/src/bots/owen.ts b/ui/libot/src/bots/owen.ts deleted file mode 100644 index ebd7e78b7e378..0000000000000 --- a/ui/libot/src/bots/owen.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class Owen implements Libot { - name = 'Owen'; - uid = '#owen'; - ordinal = 2; - description = 'Owen is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/owen.webp', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.owen = Owen; diff --git a/ui/libot/src/bots/shark.ts b/ui/libot/src/bots/shark.ts deleted file mode 100644 index d618fc944533a..0000000000000 --- a/ui/libot/src/bots/shark.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class Shark implements Libot { - name = 'Shark'; - uid = '#shark'; - ordinal = 10; - description = 'Shark is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/shark.webp', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.shark = Shark; diff --git a/ui/libot/src/bots/spectre.ts b/ui/libot/src/bots/spectre.ts deleted file mode 100644 index c326a94004866..0000000000000 --- a/ui/libot/src/bots/spectre.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class Spectre implements Libot { - name = 'Spectre'; - uid = '#spectre'; - ordinal = 21; - description = 'Spectre is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/soldier-torso.webp', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.spectre = Spectre; diff --git a/ui/libot/src/bots/terrence.ts b/ui/libot/src/bots/terrence.ts deleted file mode 100644 index f8b6ada6f347d..0000000000000 --- a/ui/libot/src/bots/terrence.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class Terrence implements Libot { - name = 'Terrence'; - uid = '#terrence'; - ordinal = 9; - description = 'Terrence is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/terrence.webp', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.terrence = Terrence; diff --git a/ui/libot/src/ctrl.ts b/ui/libot/src/ctrl.ts index 9a4cdcbb2fbb7..239d764090d74 100644 --- a/ui/libot/src/ctrl.ts +++ b/ui/libot/src/ctrl.ts @@ -9,10 +9,6 @@ export interface Ctrl { move(fen: string): Promise; } -type Constructor = new (...args: any[]) => T; - -export const registry: { [k: string]: Constructor } = {}; - export async function makeCtrl(libots: Libots, zf: Zerofish): Promise { const nets = new Map(); let bot: Libot; @@ -38,7 +34,7 @@ export async function makeCtrl(libots: Libots, zf: Zerofish): Promise { } async function fetchNet(netName: string): Promise { - return fetch(lichess.assetUrl(`lifat/bots/weights/${netName}.pb`, { noVersion: true })) + return fetch(lichess.assetUrl(`lifat/bots/weights/${netName}`, { noVersion: true })) .then(res => res.arrayBuffer()) .then(buf => new Uint8Array(buf)); } diff --git a/ui/libot/src/index.gen.ts b/ui/libot/src/index.gen.ts deleted file mode 100644 index e4abf9eb09c39..0000000000000 --- a/ui/libot/src/index.gen.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Generated by @build/index.mjs to export the bots directory from src/index.ts -// -// Relaunch ui/build to regenerate (the bots directory is not watched). - -export * from './bots/agatha'; -export * from './bots/babyHoward'; -export * from './bots/beatrice'; -export * from './bots/coral'; -export * from './bots/dansby'; -export * from './bots/elsieZero'; -export * from './bots/gary'; -export * from './bots/ghost'; -export * from './bots/greta'; -export * from './bots/grunt'; -export * from './bots/helena'; -export * from './bots/henry'; -export * from './bots/larry'; -export * from './bots/listress'; -export * from './bots/louise'; -export * from './bots/maia'; -export * from './bots/marco'; -export * from './bots/mitsoko'; -export * from './bots/nacho'; -export * from './bots/owen'; -export * from './bots/shark'; -export * from './bots/spectre'; -export * from './bots/terrence'; diff --git a/ui/libot/src/interfaces.ts b/ui/libot/src/interfaces.ts index 5eacefda89b90..51ac250b103ed 100644 --- a/ui/libot/src/interfaces.ts +++ b/ui/libot/src/interfaces.ts @@ -1,15 +1,17 @@ -export interface Libot { +export interface BotInfo { readonly name: string; readonly uid: string; - readonly ordinal: number; readonly description: string; - readonly imageUrl: string; + readonly image: string; readonly netName?: string; +} +export interface Libot extends BotInfo { + readonly imageUrl: string; readonly ratings: Map; + readonly ordinal: number; move: (fen: string) => Promise; } - export interface Libots { bots: { [id: string]: Libot }; sort(): Libot[]; diff --git a/ui/libot/src/main.ts b/ui/libot/src/main.ts index 46a381bcfe50c..150198f93869b 100644 --- a/ui/libot/src/main.ts +++ b/ui/libot/src/main.ts @@ -1,18 +1,22 @@ import { Libots } from './interfaces'; import makeZerofish from 'zerofish'; -import { makeCtrl, registry } from './ctrl'; +import { makeCtrl } from './ctrl'; +import { ZeroBot } from './zerobot'; export { type Ctrl as LibotCtrl } from './ctrl'; export * from './interfaces'; -export * from './index.gen'; export async function initModule(stubs = false) { const libots: Libots = { sort: () => Object.values(libots.bots).sort((a, b) => (a.ordinal < b.ordinal ? -1 : 1)), bots: {}, }; - const zf = !stubs ? await makeZerofish({ root: lichess.assetUrl('npm') }) : undefined; - for (const name in registry) { - libots.bots[name] = new registry[name](zf); + + const zfPromise = !stubs ? makeZerofish({ root: lichess.assetUrl('npm') }) : Promise.resolve(undefined); + const botsPromise = fetch(lichess.assetUrl('bots.json')).then(x => x.json()); + const [zf, bots] = await Promise.all([zfPromise, botsPromise]); + for (const bot of bots) { + if (zf) libots.bots[bot.uid.slice(1)] = new ZeroBot(bot, zf); + else libots.bots[bot.uid.slice(1)] = bot; } if (zf) return makeCtrl(libots, zf); else return libots; diff --git a/ui/libot/src/makeBot.mjs b/ui/libot/src/makeBot.mjs deleted file mode 100644 index 30230e32afd7e..0000000000000 --- a/ui/libot/src/makeBot.mjs +++ /dev/null @@ -1,161 +0,0 @@ -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const srcDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'bots'); - -const localBots = { - babyHoward: { - name: 'Baby Howard', - description: 'Baby Howard is a bot that plays random moves.', - image: 'baby-howard.webp', - }, - coral: { - name: 'Coral', - description: 'Coral is a simple bot that plays random moves.', - image: 'coral.webp', - }, - owen: { - name: 'Owen', - description: '', - image: 'owen.webp', - }, - beatrice: { - name: 'Beatrice', - description: 'Beatrice is a bot that plays random moves.', - image: 'beatrice.webp', - }, - nacho: { - name: 'Nacho', - description: '', - image: 'nacho.webp', - }, - louise: { - name: 'Louise', - description: '', - image: 'louise.webp', - }, - elsieZero: { - name: 'Elsie Zero', - description: 'Elsie Zero is a bot that plays random moves.', - image: 'baby-robot.webp', - }, - gary: { - name: 'Gary', - description: '', - image: 'gary.webp', - }, - henry: { - name: 'Henry', - description: '', - image: 'henry.webp', - }, - terrence: { - name: 'Terrence', - description: '', - image: 'terrence.webp', - }, - shark: { - name: 'Shark', - description: '', - image: 'shark.webp', - }, - dansby: { - name: 'Dansby', - description: '', - image: 'dansby.webp', - }, - larry: { - name: 'Larry', - description: '', - image: 'larry.webp', - }, - marco: { - name: 'Marco', - description: '', - image: 'marco.webp', - }, - agatha: { - name: 'Agatha', - description: '', - image: 'witch1.webp', - }, - greta: { - name: 'Greta', - description: '', - image: 'greta.webp', - }, - grunt: { - name: 'Grunt', - description: '', - image: 'grunt.webp', - }, - helena: { - name: 'Helena', - description: '', - image: 'helena.webp', - }, - mitsoko: { - name: 'Mitsoko', - description: '', - image: 'mitsoko.webp', - }, - spectre: { - name: 'Spectre', - description: '', - image: 'soldier-torso.webp', - }, - ghost: { - name: 'Ghost', - description: '', - image: 'specops-lady.webp', - }, - maia: { - name: 'Maia', - description: '', - image: 'maia.webp', - }, - listress: { - name: 'Listress', - description: '', - image: 'listress.webp', - }, -}; - -let ordinal = 0; - -for (const k in localBots) { - const bot = localBots[k]; - const ext = makeBot(k, bot, ordinal++); - fs.writeFileSync(path.join(srcDir, `${k}.ts`), ext); -} - -function makeBot(k, bot, ordinal) { - const clz = bot.name.replace(/ /g, ''); - return `import { type Zerofish } from 'zerofish'; -import { Libot } from '../interfaces'; -import { registry } from '../ctrl'; - -export class ${clz} implements Libot { - name = '${bot.name}'; - uid = '#${k}'; - ordinal = ${ordinal}; - description = '${bot.name} is a bot that plays random moves.'; - imageUrl = lichess.assetUrl('lifat/bots/images/${bot.image}', { noVersion: true }); - netName = 'maia-1100'; - ratings = new Map(); - zf: Zerofish; - - constructor(zf: Zerofish, opts?: any) { - opts; - this.zf = zf; - } - - async move(fen: string) { - return await this.zf.goZero(fen); - } -} - -registry.${k} = ${clz}; -`; -} diff --git a/ui/libot/src/zerobot.ts b/ui/libot/src/zerobot.ts new file mode 100644 index 0000000000000..06992daf75e66 --- /dev/null +++ b/ui/libot/src/zerobot.ts @@ -0,0 +1,26 @@ +import { type Zerofish } from 'zerofish'; +import { Libot, BotInfo } from './interfaces'; + +let ordinal = 0; +export class ZeroBot implements Libot { + readonly name: string; + readonly uid: string; + readonly description: string; + readonly image: string; + readonly netName?: string; + ratings = new Map(); + ordinal: number; + zf: Zerofish; + get imageUrl() { + return lichess.assetUrl(`lifat/bots/images/${this.image}`, { noVersion: true }); + } + constructor(info: BotInfo, zf: Zerofish) { + Object.assign(this, info); + this.zf = zf; + this.ordinal = ordinal++; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} diff --git a/ui/localPlay/src/ctrl.ts b/ui/localPlay/src/ctrl.ts index f6fa6df213fb5..3cca179e1f0a6 100644 --- a/ui/localPlay/src/ctrl.ts +++ b/ui/localPlay/src/ctrl.ts @@ -19,7 +19,6 @@ export class Ctrl { readonly redraw: () => void, ) { this.loaded = lichess.loadEsm('libot').then(libot => { - console.log('libot loaded'); this.libot = libot; this.libot.setBot('coral'); }); @@ -58,6 +57,7 @@ export class Ctrl { } move(uci: Uci) { + console.log('move', uci); const move = Chops.parseUci(uci); if (!move || !this.chess.isLegal(move)) throw new Error(`illegal move ${uci}, ${this.fen}}`); const san = makeSanAndPlay(this.chess, move); @@ -75,7 +75,10 @@ export class Ctrl { } async botMove() { - this.move(await this.libot!.move(this.fen)); + console.log('bot move'); + const uci = await this.libot!.move(this.fen); + console.log('got bot move', uci); + this.move(uci); } fifty(move?: Chops.Move) { diff --git a/ui/localPlay/src/main.ts b/ui/localPlay/src/main.ts index a90617066cef0..d16a223cd8eb9 100644 --- a/ui/localPlay/src/main.ts +++ b/ui/localPlay/src/main.ts @@ -14,7 +14,6 @@ export async function initModule(opts: LocalPlayOpts) { const ctrl = new LocalPlayCtrl(opts, () => {}); ctrl.tellRound = await makeRound(ctrl); await ctrl.loaded; - console.log('done', ctrl.libot, ctrl.libot.bots); const blueprint = view(ctrl); const element = document.querySelector('#bot-view') as HTMLElement; element.innerHTML = ''; @@ -28,7 +27,6 @@ export async function initModule(opts: LocalPlayOpts) { export async function makeRound(ctrl: LocalPlayCtrl): Promise { const moves: string[] = []; - console.log(ctrl.dests); for (const from in ctrl.dests) { moves.push(from + ctrl.dests[from]); } @@ -43,7 +41,7 @@ export async function makeRound(ctrl: LocalPlayCtrl): Promise { }, crosstableEl: document.querySelector('.cross-table') as HTMLElement, i18n: {}, - onChange: (d: RoundData) => console.log(d), + onChange: (d: RoundData) => d, //console.log(d), local: true, }; return lichess.loadEsm('round', { init: opts }); From 51a135a0294d9fe8dbbb0289852353333d3184bc Mon Sep 17 00:00:00 2001 From: Jonathan Gamble <101470903+schlawg@users.noreply.github.com> Date: Wed, 22 Nov 2023 12:56:32 -0600 Subject: [PATCH 066/174] gah --- pnpm-lock.yaml | 4 +- ui/libot/package.json | 2 +- ui/libot/src/interfaces.ts | 11 +++++ ui/libot/src/zerobot.ts | 93 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 104 insertions(+), 6 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f309c99c1fff3..a41653c2c1da5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' settings: autoInstallPeers: true @@ -371,7 +371,7 @@ importers: specifier: workspace:* version: link:../chess chessops: - specifier: ^0.12.7 + specifier: ^0.12.8 version: 0.12.8 common: specifier: workspace:* diff --git a/ui/libot/package.json b/ui/libot/package.json index 64d9a66be7edc..d7ecc23aa1c3e 100644 --- a/ui/libot/package.json +++ b/ui/libot/package.json @@ -7,7 +7,7 @@ "typings": "dist/main.d.ts", "dependencies": { "chess": "workspace:*", - "chessops": "^0.12.7", + "chessops": "^0.12.8", "common": "workspace:*", "tree": "workspace:*", "zerofish": "^0.0.16" diff --git a/ui/libot/src/interfaces.ts b/ui/libot/src/interfaces.ts index 51ac250b103ed..a6a89c462e60f 100644 --- a/ui/libot/src/interfaces.ts +++ b/ui/libot/src/interfaces.ts @@ -1,9 +1,20 @@ +export interface ZeroBotConfig { + fishMix: number; + cpBias: number; + cpThreshold: number; + searchDepth?: number; + searchMs?: number; + searchWidth: number; + aggression: number; +} + export interface BotInfo { readonly name: string; readonly uid: string; readonly description: string; readonly image: string; readonly netName?: string; + readonly zbcfg?: ZeroBotConfig; } export interface Libot extends BotInfo { readonly imageUrl: string; diff --git a/ui/libot/src/zerobot.ts b/ui/libot/src/zerobot.ts index 06992daf75e66..7a19b07b23a24 100644 --- a/ui/libot/src/zerobot.ts +++ b/ui/libot/src/zerobot.ts @@ -1,26 +1,113 @@ -import { type Zerofish } from 'zerofish'; -import { Libot, BotInfo } from './interfaces'; +import { type Zerofish, type PV } from 'zerofish'; +import * as Chops from 'chessops'; +import { Libot, BotInfo, ZeroBotConfig } from './interfaces'; let ordinal = 0; + export class ZeroBot implements Libot { readonly name: string; readonly uid: string; readonly description: string; readonly image: string; + readonly zfcfg: ZeroBotConfig; readonly netName?: string; ratings = new Map(); ordinal: number; zf: Zerofish; + get imageUrl() { return lichess.assetUrl(`lifat/bots/images/${this.image}`, { noVersion: true }); } + constructor(info: BotInfo, zf: Zerofish) { + const infoCfg = info.zbcfg; Object.assign(this, info); + this.zfcfg = infoCfg ? Object.assign({}, defaultCfg, infoCfg) : defaultCfg; this.zf = zf; this.ordinal = ordinal++; } + weigh(material: Chops.Material) { + let score = 0; + for (const [role, price] of Object.entries(prices) as [Chops.Role, number][]) { + score += price * material.count(role); + } + return score; + } + async move(fen: string) { - return await this.zf.goZero(fen); + const zeroMove = this.zfcfg.fishMix > 0 ? this.zf.goZero(fen) : Promise.resolve(undefined); + const fishMove = + this.zfcfg.fishMix < 1 + ? this.zf.goFish(fen, { + depth: this.zfcfg.searchDepth, + ms: !this.zfcfg.searchDepth ? this.zfcfg.searchMs : undefined, + pvs: this.zfcfg.searchWidth, + }) + : Promise.resolve([]); + const [zero, fishPvs] = await Promise.all([zeroMove, fishMove]); + const chess = Chops.Chess.fromSetup(Chops.fen.parseFen(fen).unwrap()).unwrap(); + const aggression: [number, PV][] = []; + //const before = this.weigh(Chops.Material.fromBoard(chess.board)); + for (const pv of fishPvs) { + const pvChess = chess.clone(); + for (const move of pv.moves) pvChess.play(Chops.parseUci(move)!); + const after = this.weigh(Chops.Material.fromBoard(pvChess.board)); + aggression.push([after, pv]); + } + const [low, high] = fishPvs.reduce( + ([low, high], pv) => { + const score = pv.score; + if (score < low) low = score; + if (score > high) high = score; + return [low, high]; + }, + [Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER], + ); + aggression.sort((a, b) => b[0] - a[0]); + const zeroIndex = aggression.findIndex(([_, pv]) => pv.moves[0] === zero); + const zeroScore = zeroIndex >= 0 ? aggression[zeroIndex]?.[0] : -0.1; + return zero ?? ''; } } + +const defaultCfg: ZeroBotConfig = { + fishMix: 0, // [0 zero, 1 fish] + cpBias: 0, + cpThreshold: 0.4, + searchMs: 800, + searchWidth: 8, // multiPV + aggression: 0.5, // [0 passive, 1 aggressive] +}; + +const prices: { [r: Chops.Role]: number } = { + pawn: 1, + knight: 2.8, + bishop: 3, + rook: 5, + queen: 9, +}; + +function sq2key(sq: number): Key { + return Chops.makeSquare(sq); +} + +function splitUci(uci: Uci): { from: Key; to: Key; role?: Chops.Role } { + return { from: uci.slice(0, 2) as Key, to: uci.slice(2, 4) as Key, role: Chops.charToRole(uci.slice(4)) }; +} + +function linesWithin(move: string, lines: PV[], bias = 0, threshold = 50) { + const zeroScore = lines.find(line => line.moves[0] === move)?.score ?? Number.NaN; + return lines.filter(fish => Math.abs(fish.score - bias - zeroScore) < threshold && fish.moves.length); +} + +function randomSprinkle(move: string, lines: PV[]) { + lines = linesWithin(move, lines, 0, 20); + if (!lines.length) return move; + return lines[Math.floor(Math.random() * lines.length)].moves[0] ?? move; +} + +/* +function occurs(chance: number) { + return Math.random() < chance; +}*/ From 53c7947861a4cbde620534a30a396b39bd06c51e Mon Sep 17 00:00:00 2001 From: Jonathan Gamble <101470903+schlawg@users.noreply.github.com> Date: Wed, 22 Nov 2023 18:07:07 -0600 Subject: [PATCH 067/174] gah --- ui/libot/src/zerobot.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ui/libot/src/zerobot.ts b/ui/libot/src/zerobot.ts index 7a19b07b23a24..bd876ac581e25 100644 --- a/ui/libot/src/zerobot.ts +++ b/ui/libot/src/zerobot.ts @@ -71,6 +71,12 @@ export class ZeroBot implements Libot { } } +const dimensions = { + aggression: 0, + zeroFit: 1, + survival: 2, + threshhold: 3, +}; const defaultCfg: ZeroBotConfig = { fishMix: 0, // [0 zero, 1 fish] cpBias: 0, @@ -80,7 +86,7 @@ const defaultCfg: ZeroBotConfig = { aggression: 0.5, // [0 passive, 1 aggressive] }; -const prices: { [r: Chops.Role]: number } = { +const prices: { [role in Chops.Role]?: number } = { pawn: 1, knight: 2.8, bishop: 3, From 0c76831fb388619073265a8e19d4423eeb96cd92 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Tue, 28 Nov 2023 16:15:23 -0600 Subject: [PATCH 068/174] merge --- pnpm-lock.yaml | 10 +++++----- ui/common/src/dialog.ts | 35 ++++++++++++++++++----------------- ui/libot/package.json | 2 +- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index efe5092f3af56..333b7ea2d20f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.1' +lockfileVersion: '6.0' settings: autoInstallPeers: true @@ -380,8 +380,8 @@ importers: specifier: workspace:* version: link:../tree zerofish: - specifier: ^0.0.16 - version: 0.0.16 + specifier: ^0.0.17 + version: 0.0.17 ui/lobby: dependencies: @@ -4832,8 +4832,8 @@ packages: engines: {node: '>=10'} dev: false - /zerofish@0.0.16: - resolution: {integrity: sha512-4Xgbic2zijBYIeCapuyBeSmahG4wb4Bv4gJEa2B9qjdczx/1gjm5aOGu8yLSs8Q2R9wmBs9MJ6cPSLGSwDbTCA==} + /zerofish@0.0.17: + resolution: {integrity: sha512-r9u4dOJVI8Yxq16zxiFjKz7xZ/Tv5V2jPA5W3sbMQhc0fauCvWz4g8fNswbYY0sybIbbC90JtHXomwRzBm/MRw==} dependencies: '@types/emscripten': 1.39.10 '@types/node': 20.9.1 diff --git a/ui/common/src/dialog.ts b/ui/common/src/dialog.ts index 4f8e36ce380a7..bbbd4c83a0b7c 100644 --- a/ui/common/src/dialog.ts +++ b/ui/common/src/dialog.ts @@ -4,16 +4,6 @@ import { isTouchDevice } from './device'; import * as xhr from './xhr'; import * as licon from './licon'; -let dialogPolyfill: { registerDialog: (dialog: HTMLDialogElement) => void }; - -export const ready = lichess.load.then(async () => { - window.addEventListener('resize', onResize); - if (window.HTMLDialogElement) return true; - dialogPolyfill = (await import(lichess.assetUrl('npm/dialog-polyfill.esm.js')).catch(() => undefined)) - ?.default; - return dialogPolyfill !== undefined; -}); - export interface Dialog { readonly open: boolean; // is visible? readonly view: HTMLElement; // your content div @@ -24,8 +14,8 @@ export interface Dialog { close(): void; } -interface DialogOpts { - class?: string; // zero or more classes (period separated) for your view div +export interface DialogOpts { + class?: string; // zero or more classes which are appended to your view div cssPath?: string; // for themed css craplets cash?: Cash; // content, will be cloned and any 'none' class removed htmlUrl?: string; // content, url will be xhr'd @@ -33,7 +23,7 @@ interface DialogOpts { attrs?: { dialog?: Attrs; view?: Attrs }; // optional attrs for dialog and view div action?: Action | Action[]; // if present, add handlers to action buttons onClose?: (dialog: Dialog) => void; // called when dialog closes - noCloseButton?: boolean; // if true, no upper right corener close button + noCloseButton?: boolean; // if true, no upper right corner close button noClickAway?: boolean; // if true, no click-away-to-close } @@ -44,11 +34,11 @@ export interface DomDialogOpts extends DialogOpts { export interface SnabDialogOpts extends DialogOpts { vnodes?: MaybeVNodes; // snabDialog automatically shows as 'modal' on redraw unless.. - onInsert?: (dialog: Dialog) => void; // if supplied, call show() or showModal() manually + onInsert?: (dialog: Dialog) => void; // if onInsert supplied, call show()/showModal() manually } // Action can be any "clickable" client button, usually to dismiss the dialog -interface Action { +export interface Action { selector: string; // selector, click handler will be installed action?: string | ((dialog: Dialog, action: Action) => void); // if action not provided, just close @@ -56,6 +46,15 @@ interface Action { // if function, it will be called on click and YOU must close the dialog } +// await this promise before showing a dialog on page load (for old browsers) +export const ready = lichess.load.then(async () => { + window.addEventListener('resize', onResize); + if (window.HTMLDialogElement) return true; + dialogPolyfill = (await import(lichess.assetUrl('npm/dialog-polyfill.esm.js')).catch(() => undefined)) + ?.default; + return dialogPolyfill !== undefined; +}); + // if no 'show' in opts, you must call show or showModal on the resolved promise export async function domDialog(o: DomDialogOpts): Promise { const [html] = await assets(o); @@ -72,7 +71,7 @@ export async function domDialog(o: DomDialogOpts): Promise { } const view = $as('
'); - if (o.class) view.classList.add(...o.class.split('.')); + if (o.class) view.classList.add(...o.class.split(/[ .]/)); for (const [k, v] of Object.entries(o.attrs?.view ?? {})) view.setAttribute(k, String(v)); if (html) view.innerHTML = html; @@ -110,7 +109,7 @@ export function snabDialog(o: SnabDialogOpts): VNode { h( 'div.scrollable', h( - 'div.dialog-content' + (o.class ? `.${o.class}` : ''), + 'div.dialog-content' + (o.class ? `.${o.class.split(/[ .]/).join('.')}` : ''), { attrs: o.attrs?.view, hook: onInsert(async view => { @@ -241,3 +240,5 @@ const focusQuery = ['button', 'input', 'select', 'textarea'] .map(sel => `${sel}:not(:disabled)`) .concat(['[href]', '[tabindex="0"]', '[role="tab"]']) .join(','); + +let dialogPolyfill: { registerDialog: (dialog: HTMLDialogElement) => void }; diff --git a/ui/libot/package.json b/ui/libot/package.json index 64d9a66be7edc..4d411a034d790 100644 --- a/ui/libot/package.json +++ b/ui/libot/package.json @@ -10,7 +10,7 @@ "chessops": "^0.12.7", "common": "workspace:*", "tree": "workspace:*", - "zerofish": "^0.0.16" + "zerofish": "^0.0.17" }, "scripts": {}, "lichess": { From 5251374e500a61bd44886239eb74db295f984496 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Wed, 29 Nov 2023 14:56:47 -0600 Subject: [PATCH 069/174] gah --- public/bots.json | 7 ---- ui/libot/defs/bots.json | 7 ---- ui/libot/src/behavior.ts | 73 ++++++++++++++++++++++++++++++++++++++++ ui/libot/src/zerobot.ts | 48 ++++---------------------- 4 files changed, 79 insertions(+), 56 deletions(-) diff --git a/public/bots.json b/public/bots.json index 7827e3d4f1ec3..18235a68a4cf6 100644 --- a/public/bots.json +++ b/public/bots.json @@ -13,13 +13,6 @@ "image": "coral.webp", "netName": "maia-1100.pb" }, - { - "uid": "#owen", - "name": "Owen", - "description": "", - "image": "owen.webp", - "netName": "maia-1100.pb" - }, { "uid": "#beatrice", "name": "Beatrice", diff --git a/ui/libot/defs/bots.json b/ui/libot/defs/bots.json index 7827e3d4f1ec3..18235a68a4cf6 100644 --- a/ui/libot/defs/bots.json +++ b/ui/libot/defs/bots.json @@ -13,13 +13,6 @@ "image": "coral.webp", "netName": "maia-1100.pb" }, - { - "uid": "#owen", - "name": "Owen", - "description": "", - "image": "owen.webp", - "netName": "maia-1100.pb" - }, { "uid": "#beatrice", "name": "Beatrice", diff --git a/ui/libot/src/behavior.ts b/ui/libot/src/behavior.ts index 28db33c8c32a0..6bc35c814360c 100644 --- a/ui/libot/src/behavior.ts +++ b/ui/libot/src/behavior.ts @@ -1,4 +1,5 @@ import { type Score } from 'zerofish'; +import * as Chops from 'chessops'; export function linesWithin(move: string, lines: Score[], bias = 0, threshold = 50) { const zeroScore = lines.find(line => line.moves[0] === move)?.score ?? Number.NaN; @@ -14,3 +15,75 @@ export function randomSprinkle(move: string, lines: Score[]) { export function occurs(chance: number) { return Math.random() < chance; } + +export function scores(lines: Score[][], move?: string) { + const matches: Score[] = []; + if (!move) return matches; + for (const history of lines) { + for (const line of history) { + if (line.moves[0] === move) matches.push(line); + } + } + return matches; +} + +export function deepScores(lines: Score[][], move?: string) { + const matches: Score[] = []; + if (!move) return matches; + let deepest = 0; + for (const history of lines) { + for (const line of history) { + if (line.moves[0] !== move || line.depth < deepest) continue; + if (line.depth > deepest) matches.length = 0; + matches.push(line); + deepest = line.depth; + } + } + return matches; +} + +export function shallowScores(lines: Score[][], move?: string) { + const matches: Score[] = []; + if (!move) return matches; + let shallowest = 99; + for (const history of lines) { + for (const line of history) { + if (line.moves[0] !== move || line.depth > shallowest) continue; + if (line.depth > shallowest) matches.length = 0; + matches.push(line); + shallowest = line.depth; + } + } + return matches; +} + +export function byDestruction(lines: Score[][], fen: string) { + const chess = Chops.Chess.fromSetup(Chops.fen.parseFen(fen).unwrap()).unwrap(); + const before = weigh(Chops.Material.fromBoard(chess.board)); + const aggression: [number, Score][] = []; + for (const history of lines) { + for (const pv of history) { + const pvChess = chess.clone(); + for (const move of pv.moves) pvChess.play(Chops.parseUci(move)!); + const destruction = (before - weigh(Chops.Material.fromBoard(pvChess.board))) / pv.moves.length; + if (destruction > 0) aggression.push([destruction, pv]); + } + } + return aggression; +} + +const prices: { [role in Chops.Role]?: number } = { + pawn: 1, + knight: 2.8, + bishop: 3, + rook: 5, + queen: 9, +}; + +function weigh(material: Chops.Material) { + let score = 0; + for (const [role, price] of Object.entries(prices) as [Chops.Role, number][]) { + score += price * material.count(role); + } + return score; +} diff --git a/ui/libot/src/zerobot.ts b/ui/libot/src/zerobot.ts index 72c8517cb3e4c..034785dbb58b6 100644 --- a/ui/libot/src/zerobot.ts +++ b/ui/libot/src/zerobot.ts @@ -1,7 +1,7 @@ import { type Zerofish, type Score } from 'zerofish'; import * as Chops from 'chessops'; import { Libot, BotInfo, ZeroBotConfig } from './interfaces'; - +import { deepScores, byDestruction, shallowScores, scores } from './behavior'; let ordinal = 0; export class ZeroBot implements Libot { @@ -27,14 +27,6 @@ export class ZeroBot implements Libot { this.ordinal = ordinal++; } - weigh(material: Chops.Material) { - let score = 0; - for (const [role, price] of Object.entries(prices) as [Chops.Role, number][]) { - score += price * material.count(role); - } - return score; - } - async move(fen: string) { const zeroMove = this.zfcfg.fishMix < 1 ? this.zf.goZero(fen) : Promise.resolve(undefined); const fishMove = @@ -45,32 +37,12 @@ export class ZeroBot implements Libot { pvs: this.zfcfg.searchWidth, }) : Promise.resolve([]); - const [zero, fishMoves] = await Promise.all([zeroMove, fishMove]); - const deepScores = fishMoves.map(fish => fish[fish.length - 1]); - console.log(zero); - console.log(fishMoves); - console.log(deepScores); - const chess = Chops.Chess.fromSetup(Chops.fen.parseFen(fen).unwrap()).unwrap(); - const aggression: [number, Score][] = []; - //const before = this.weigh(Chops.Material.fromBoard(chess.board)); - for (const pv of deepScores) { - const pvChess = chess.clone(); - for (const move of pv.moves) pvChess.play(Chops.parseUci(move)!); - const after = this.weigh(Chops.Material.fromBoard(pvChess.board)); - aggression.push([after, pv]); - } - /*const [low, high] = bestScores.reduce( - ([low, high], pv) => { - const score = pv.score; - if (score < low) low = score; - if (score > high) high = score; - return [low, high]; - }, - [Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER], - );*/ + const [zero, fishResult] = await Promise.all([zeroMove, fishMove]); + const aggression = byDestruction(fishResult, fen); aggression.sort((a, b) => b[0] - a[0]); const zeroIndex = aggression.findIndex(([_, pv]) => pv.moves[0] === zero); - const zeroScore = zeroIndex >= 0 ? aggression[zeroIndex]?.[0] : -0.1; + const zeroDestruction = zeroIndex >= 0 ? aggression[zeroIndex]?.[0] : -0.1; + console.log(zeroDestruction, aggression, deepScores(fishResult, zero)); return zero ?? ''; } } @@ -83,7 +55,7 @@ const dimensions = { }; const defaultCfg: ZeroBotConfig = { - fishMix: 0, // [0 zero, 1 fish] + fishMix: 0.5, // [0 zero, 1 fish] cpBias: 0, cpThreshold: 0.4, searchMs: 800, @@ -91,14 +63,6 @@ const defaultCfg: ZeroBotConfig = { aggression: 0.5, // [0 passive, 1 aggressive] }; -const prices: { [role in Chops.Role]?: number } = { - pawn: 1, - knight: 2.8, - bishop: 3, - rook: 5, - queen: 9, -}; - function sq2key(sq: number): Key { return Chops.makeSquare(sq); } From a32ff2171165c1f77cf0728fcc08cf36b20cef23 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Thu, 30 Nov 2023 18:31:54 -0600 Subject: [PATCH 070/174] gah --- bin/gen/licon.py | 10 ++++------ modules/common/src/main/Licon.scala | 3 --- ui/common/css/abstract/_licon.scss | 3 --- ui/common/src/licon.ts | 3 --- ui/libot/src/behavior.ts | 24 ++++++++++++++++-------- ui/libot/src/zerobot.ts | 3 ++- ui/site/src/diagnostic.ts | 5 +++-- ui/site/src/site.ts | 18 +++++------------- 8 files changed, 30 insertions(+), 39 deletions(-) diff --git a/bin/gen/licon.py b/bin/gen/licon.py index 6198d35491dfe..4a20bed9c00f2 100755 --- a/bin/gen/licon.py +++ b/bin/gen/licon.py @@ -18,9 +18,6 @@ * * To make these characters visible in your editor, install the lichess.ttf font (which is also generated by * licon.py) and then add it to your editor's font list. - * - * You could google 'how to install fonts', but it is generally considered best practice to ping @1vader on - * discord about these matters. */ """ @@ -46,7 +43,8 @@ def main(): gen_sources(codes) print('Generated:\n public/font/lichess.woff\n public/font/lichess.woff2\n public/font/lichess.ttf') - print(' modules/common/src/main/Licon.scala\n ui/common/src/licon.ts\n') + print(' modules/common/src/main/Licon.scala\n ui/common/src/licon.ts') + print(' ui/common/css/abstract/_licon.scss\n') args = parser.parse_args() if args.check or args.replace: @@ -54,8 +52,8 @@ def main(): find_replace_chars({chr(v): k for k, v in codes.items()}, args.replace) else: print('Note:') - print(' bin/licon.py --check # report any embedded licon literals in your sources') - print(' bin/licon.py --replace # replace embedded licon literals with `licon.`') + print(' bin/gen/licon.py --check # report any embedded licon literals in your sources') + print(' bin/gen/licon.py --replace # replace embedded licon literals with `licon.`') print("\nDon't forget to install lichess.ttf in your system & editor!\n") diff --git a/modules/common/src/main/Licon.scala b/modules/common/src/main/Licon.scala index 29e327b7b1b67..2b672bf353214 100644 --- a/modules/common/src/main/Licon.scala +++ b/modules/common/src/main/Licon.scala @@ -8,9 +8,6 @@ * * To make these characters visible in your editor, install the lichess.ttf font (which is also generated by * licon.py) and then add it to your editor's font list. - * - * You could google 'how to install fonts', but it is generally considered best practice to ping @1vader on - * discord about these matters. */ package lila.common diff --git a/ui/common/css/abstract/_licon.scss b/ui/common/css/abstract/_licon.scss index 16bdd3e05e7a5..06fd4865fb243 100644 --- a/ui/common/css/abstract/_licon.scss +++ b/ui/common/css/abstract/_licon.scss @@ -8,9 +8,6 @@ * * To make these characters visible in your editor, install the lichess.ttf font (which is also generated by * licon.py) and then add it to your editor's font list. - * - * You could google 'how to install fonts', but it is generally considered best practice to ping @1vader on - * discord about these matters. */ $licon-Tools: ''; // e000 diff --git a/ui/common/src/licon.ts b/ui/common/src/licon.ts index 13f143d774c43..98e60c4b26402 100644 --- a/ui/common/src/licon.ts +++ b/ui/common/src/licon.ts @@ -8,9 +8,6 @@ * * To make these characters visible in your editor, install the lichess.ttf font (which is also generated by * licon.py) and then add it to your editor's font list. - * - * You could google 'how to install fonts', but it is generally considered best practice to ping @1vader on - * discord about these matters. */ export const Tools = ''; // e000 diff --git a/ui/libot/src/behavior.ts b/ui/libot/src/behavior.ts index 6bc35c814360c..4922147948e41 100644 --- a/ui/libot/src/behavior.ts +++ b/ui/libot/src/behavior.ts @@ -57,16 +57,24 @@ export function shallowScores(lines: Score[][], move?: string) { return matches; } -export function byDestruction(lines: Score[][], fen: string) { +export function byDestruction(lines: Score[][], fen: string, mutual = false) { const chess = Chops.Chess.fromSetup(Chops.fen.parseFen(fen).unwrap()).unwrap(); - const before = weigh(Chops.Material.fromBoard(chess.board)); + const beforeMaterial = Chops.Material.fromBoard(chess.board); + const opponent = Chops.opposite(chess.turn); + const before = weigh(mutual ? beforeMaterial : beforeMaterial[opponent]); const aggression: [number, Score][] = []; for (const history of lines) { for (const pv of history) { - const pvChess = chess.clone(); - for (const move of pv.moves) pvChess.play(Chops.parseUci(move)!); - const destruction = (before - weigh(Chops.Material.fromBoard(pvChess.board))) / pv.moves.length; - if (destruction > 0) aggression.push([destruction, pv]); + try { + const pvChess = chess.clone(); + for (const move of pv.moves) pvChess.play(Chops.parseUci(move)!); + const afterMaterial = Chops.Material.fromBoard(pvChess.board); + const destruction = + (before - weigh(mutual ? afterMaterial : afterMaterial[opponent])) / pv.moves.length; + if (destruction > 0) aggression.push([destruction, pv]); + } catch (e) { + console.error(e, pv.moves); + } } } return aggression; @@ -80,10 +88,10 @@ const prices: { [role in Chops.Role]?: number } = { queen: 9, }; -function weigh(material: Chops.Material) { +function weigh(material: Chops.Material | Chops.MaterialSide) { let score = 0; for (const [role, price] of Object.entries(prices) as [Chops.Role, number][]) { - score += price * material.count(role); + score += price * ('white' in material ? material.count(role) : material[role]); } return score; } diff --git a/ui/libot/src/zerobot.ts b/ui/libot/src/zerobot.ts index 034785dbb58b6..969a763cdafd2 100644 --- a/ui/libot/src/zerobot.ts +++ b/ui/libot/src/zerobot.ts @@ -28,6 +28,7 @@ export class ZeroBot implements Libot { } async move(fen: string) { + lichess.log("It's the spinner.gif. The spinner.gif is crashing chrome, ok?"); const zeroMove = this.zfcfg.fishMix < 1 ? this.zf.goZero(fen) : Promise.resolve(undefined); const fishMove = this.zfcfg.fishMix > 0 @@ -43,7 +44,7 @@ export class ZeroBot implements Libot { const zeroIndex = aggression.findIndex(([_, pv]) => pv.moves[0] === zero); const zeroDestruction = zeroIndex >= 0 ? aggression[zeroIndex]?.[0] : -0.1; console.log(zeroDestruction, aggression, deepScores(fishResult, zero)); - return zero ?? ''; + return aggression[0]?.[1]?.moves[0] ?? zero; } } diff --git a/ui/site/src/diagnostic.ts b/ui/site/src/diagnostic.ts index 285b104aad375..aba812924cf3e 100644 --- a/ui/site/src/diagnostic.ts +++ b/ui/site/src/diagnostic.ts @@ -1,8 +1,9 @@ import { isTouchDevice } from 'common/device'; -import { domDialog } from 'common/dialog'; +import { domDialog, ready } from 'common/dialog'; export default async function initModule() { - const logs = await lichess.log.get(); + console.log('here we is'); + const [logs] = await Promise.all([lichess.log.get(), ready]); const text = `Browser: ${navigator.userAgent}\n` + `Cores: ${navigator.hardwareConcurrency}\n` + diff --git a/ui/site/src/site.ts b/ui/site/src/site.ts index 56b44c615e459..4315c56234341 100644 --- a/ui/site/src/site.ts +++ b/ui/site/src/site.ts @@ -14,7 +14,6 @@ import serviceWorker from './component/serviceWorker'; import StrongSocket from './component/socket'; import topBar from './component/top-bar'; import watchers from './component/watchers'; -import { reload } from './component/reload'; import { requestIdleCallback } from './component/functions'; import { userComplete } from './component/assets'; import { siteTrans } from './component/trans'; @@ -28,6 +27,8 @@ lichess.info = info; lichess.load.then(() => { $('#user_tag').removeAttr('href'); + const setBlind = location.hash === '#blind'; + const showDebug = location.hash === '#debug'; requestAnimationFrame(() => { miniBoard.initAll(); @@ -135,18 +136,9 @@ lichess.load.then(() => { el.setAttribute('content', el.getAttribute('content') + ',maximum-scale=1.0'); } - if (location.hash === '#debug') lichess.loadEsm('diagnostic'); - - if (location.hash === '#blind' && !lichess.blindMode) - xhr - .text('/toggle-blind-mode', { - method: 'post', - body: xhr.form({ - enable: 1, - redirect: '/', - }), - }) - .then(reload); + if (setBlind && !lichess.blindMode) setTimeout(() => $('#blind-mode button').trigger('click'), 1500); + + if (showDebug) lichess.loadEsm('diagnostic'); const pageAnnounce = document.body.getAttribute('data-announce'); if (pageAnnounce) announce(JSON.parse(pageAnnounce)); From cda72b85a91e82d3c320aa155a8baa7b15a5fdcf Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Sat, 2 Dec 2023 18:29:57 -0600 Subject: [PATCH 071/174] gah --- project/plugins.sbt | 2 +- ui/libot/src/behavior.ts | 97 ------------------ ui/libot/src/interfaces.ts | 26 +++-- ui/libot/src/main.ts | 4 +- ui/libot/src/zerobot.ts | 89 ----------------- ui/libot/src/zfbot.ts | 197 +++++++++++++++++++++++++++++++++++++ ui/round/src/socket.ts | 4 +- ui/site/src/diagnostic.ts | 1 - 8 files changed, 219 insertions(+), 201 deletions(-) delete mode 100644 ui/libot/src/behavior.ts delete mode 100644 ui/libot/src/zerobot.ts create mode 100644 ui/libot/src/zfbot.ts diff --git a/project/plugins.sbt b/project/plugins.sbt index 3770ce8da8891..5937942b9a465 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -4,4 +4,4 @@ resolvers += Resolver.url( )(Resolver.ivyStylePatterns) addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.18-lila_1.21") // scala2 branch addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") -addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.5.12") +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.5.11") diff --git a/ui/libot/src/behavior.ts b/ui/libot/src/behavior.ts deleted file mode 100644 index 4922147948e41..0000000000000 --- a/ui/libot/src/behavior.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { type Score } from 'zerofish'; -import * as Chops from 'chessops'; - -export function linesWithin(move: string, lines: Score[], bias = 0, threshold = 50) { - const zeroScore = lines.find(line => line.moves[0] === move)?.score ?? Number.NaN; - return lines.filter(fish => Math.abs(fish.score - bias - zeroScore) < threshold && fish.moves.length); -} - -export function randomSprinkle(move: string, lines: Score[]) { - lines = linesWithin(move, lines, 0, 20); - if (!lines.length) return move; - return lines[Math.floor(Math.random() * lines.length)].moves[0] ?? move; -} - -export function occurs(chance: number) { - return Math.random() < chance; -} - -export function scores(lines: Score[][], move?: string) { - const matches: Score[] = []; - if (!move) return matches; - for (const history of lines) { - for (const line of history) { - if (line.moves[0] === move) matches.push(line); - } - } - return matches; -} - -export function deepScores(lines: Score[][], move?: string) { - const matches: Score[] = []; - if (!move) return matches; - let deepest = 0; - for (const history of lines) { - for (const line of history) { - if (line.moves[0] !== move || line.depth < deepest) continue; - if (line.depth > deepest) matches.length = 0; - matches.push(line); - deepest = line.depth; - } - } - return matches; -} - -export function shallowScores(lines: Score[][], move?: string) { - const matches: Score[] = []; - if (!move) return matches; - let shallowest = 99; - for (const history of lines) { - for (const line of history) { - if (line.moves[0] !== move || line.depth > shallowest) continue; - if (line.depth > shallowest) matches.length = 0; - matches.push(line); - shallowest = line.depth; - } - } - return matches; -} - -export function byDestruction(lines: Score[][], fen: string, mutual = false) { - const chess = Chops.Chess.fromSetup(Chops.fen.parseFen(fen).unwrap()).unwrap(); - const beforeMaterial = Chops.Material.fromBoard(chess.board); - const opponent = Chops.opposite(chess.turn); - const before = weigh(mutual ? beforeMaterial : beforeMaterial[opponent]); - const aggression: [number, Score][] = []; - for (const history of lines) { - for (const pv of history) { - try { - const pvChess = chess.clone(); - for (const move of pv.moves) pvChess.play(Chops.parseUci(move)!); - const afterMaterial = Chops.Material.fromBoard(pvChess.board); - const destruction = - (before - weigh(mutual ? afterMaterial : afterMaterial[opponent])) / pv.moves.length; - if (destruction > 0) aggression.push([destruction, pv]); - } catch (e) { - console.error(e, pv.moves); - } - } - } - return aggression; -} - -const prices: { [role in Chops.Role]?: number } = { - pawn: 1, - knight: 2.8, - bishop: 3, - rook: 5, - queen: 9, -}; - -function weigh(material: Chops.Material | Chops.MaterialSide) { - let score = 0; - for (const [role, price] of Object.entries(prices) as [Chops.Role, number][]) { - score += price * ('white' in material ? material.count(role) : material[role]); - } - return score; -} diff --git a/ui/libot/src/interfaces.ts b/ui/libot/src/interfaces.ts index a6a89c462e60f..839de2f0c2e21 100644 --- a/ui/libot/src/interfaces.ts +++ b/ui/libot/src/interfaces.ts @@ -1,11 +1,18 @@ -export interface ZeroBotConfig { - fishMix: number; - cpBias: number; - cpThreshold: number; - searchDepth?: number; - searchMs?: number; - searchWidth: number; - aggression: number; +import { Material } from 'chessops/setup'; + +export interface ZfBotConfig { + zeroChance: (p?: ZfParam) => number; + zeroCpDefault: (p?: ZfParam) => number; // default cp offset for an lc0 move not found in stockfish search + cpThreshold: (p?: ZfParam) => number; + searchDepth?: (p?: ZfParam) => number; + scoreDepth?: (p?: ZfParam) => number; + searchWidth: (p?: ZfParam) => number; + aggression: (p?: ZfParam) => number; // [0 passive, 1 aggressive] .5 noop +} + +export interface ZfParam { + ply: number; + material: Material; } export interface BotInfo { @@ -14,8 +21,9 @@ export interface BotInfo { readonly description: string; readonly image: string; readonly netName?: string; - readonly zbcfg?: ZeroBotConfig; + readonly zfcfg?: ZfBotConfig; } + export interface Libot extends BotInfo { readonly imageUrl: string; readonly ratings: Map; diff --git a/ui/libot/src/main.ts b/ui/libot/src/main.ts index 37f0a88f6c017..0a376a92b8887 100644 --- a/ui/libot/src/main.ts +++ b/ui/libot/src/main.ts @@ -1,7 +1,7 @@ import { Libots } from './interfaces'; import makeZerofish from 'zerofish'; import { makeCtrl } from './ctrl'; -import { ZeroBot } from './zerobot'; +import { ZfBot } from './zfbot'; export { type Ctrl as LibotCtrl } from './ctrl'; export * from './interfaces'; @@ -18,7 +18,7 @@ export async function initModule(stubs = false) { const [zf, bots] = await Promise.all([zfAsync, botsAsync]); for (const bot of bots) { - if (zf) libots.bots[bot.uid.slice(1)] = new ZeroBot(bot, zf); + if (zf) libots.bots[bot.uid.slice(1)] = new ZfBot(bot, zf); else libots.bots[bot.uid.slice(1)] = bot; } return zf ? makeCtrl(libots, zf) : libots; diff --git a/ui/libot/src/zerobot.ts b/ui/libot/src/zerobot.ts deleted file mode 100644 index 969a763cdafd2..0000000000000 --- a/ui/libot/src/zerobot.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { type Zerofish, type Score } from 'zerofish'; -import * as Chops from 'chessops'; -import { Libot, BotInfo, ZeroBotConfig } from './interfaces'; -import { deepScores, byDestruction, shallowScores, scores } from './behavior'; -let ordinal = 0; - -export class ZeroBot implements Libot { - readonly name: string; - readonly uid: string; - readonly description: string; - readonly image: string; - readonly zfcfg: ZeroBotConfig; - readonly netName?: string; - ratings = new Map(); - ordinal: number; - zf: Zerofish; - - get imageUrl() { - return lichess.assetUrl(`lifat/bots/images/${this.image}`, { noVersion: true }); - } - - constructor(info: BotInfo, zf: Zerofish) { - const infoCfg = info.zbcfg; - Object.assign(this, info); - this.zfcfg = infoCfg ? Object.assign({}, defaultCfg, infoCfg) : defaultCfg; - this.zf = zf; - this.ordinal = ordinal++; - } - - async move(fen: string) { - lichess.log("It's the spinner.gif. The spinner.gif is crashing chrome, ok?"); - const zeroMove = this.zfcfg.fishMix < 1 ? this.zf.goZero(fen) : Promise.resolve(undefined); - const fishMove = - this.zfcfg.fishMix > 0 - ? this.zf.goFish(fen, { - depth: this.zfcfg.searchDepth, - ms: !this.zfcfg.searchDepth ? this.zfcfg.searchMs : undefined, - pvs: this.zfcfg.searchWidth, - }) - : Promise.resolve([]); - const [zero, fishResult] = await Promise.all([zeroMove, fishMove]); - const aggression = byDestruction(fishResult, fen); - aggression.sort((a, b) => b[0] - a[0]); - const zeroIndex = aggression.findIndex(([_, pv]) => pv.moves[0] === zero); - const zeroDestruction = zeroIndex >= 0 ? aggression[zeroIndex]?.[0] : -0.1; - console.log(zeroDestruction, aggression, deepScores(fishResult, zero)); - return aggression[0]?.[1]?.moves[0] ?? zero; - } -} - -const dimensions = { - aggression: 0, - zeroFit: 1, - survival: 2, - threshhold: 3, -}; - -const defaultCfg: ZeroBotConfig = { - fishMix: 0.5, // [0 zero, 1 fish] - cpBias: 0, - cpThreshold: 0.4, - searchMs: 800, - searchWidth: 8, // multiPV - aggression: 0.5, // [0 passive, 1 aggressive] -}; - -function sq2key(sq: number): Key { - return Chops.makeSquare(sq); -} - -function splitUci(uci: Uci): { from: Key; to: Key; role?: Chops.Role } { - return { from: uci.slice(0, 2) as Key, to: uci.slice(2, 4) as Key, role: Chops.charToRole(uci.slice(4)) }; -} - -function linesWithin(move: string, lines: Score[], bias = 0, threshold = 50) { - const zeroScore = lines.find(line => line.moves[0] === move)?.score ?? Number.NaN; - return lines.filter(fish => Math.abs(fish.score - bias - zeroScore) < threshold && fish.moves.length); -} - -function randomSprinkle(move: string, lines: Score[]) { - lines = linesWithin(move, lines, 0, 20); - if (!lines.length) return move; - return lines[Math.floor(Math.random() * lines.length)].moves[0] ?? move; -} - -/* -function occurs(chance: number) { - return Math.random() < chance; -}*/ diff --git a/ui/libot/src/zfbot.ts b/ui/libot/src/zfbot.ts new file mode 100644 index 0000000000000..25b1cd69420e7 --- /dev/null +++ b/ui/libot/src/zfbot.ts @@ -0,0 +1,197 @@ +import { type Zerofish, type Score } from 'zerofish'; +import * as Chops from 'chessops'; +import { Libot, BotInfo, ZfBotConfig } from './interfaces'; + +let ordinal = 0; + +export class ZfBot implements Libot { + readonly name: string; + readonly uid: string; + readonly description: string; + readonly image: string; + readonly ctx: ZfBotConfig; + readonly netName?: string; + ratings = new Map(); + ordinal: number; + zf: Zerofish; + + get imageUrl() { + return lichess.assetUrl(`lifat/bots/images/${this.image}`, { noVersion: true }); + } + + constructor(info: BotInfo, zf: Zerofish) { + const infoCfg = info.zfcfg; + Object.assign(this, info); + this.ctx = infoCfg ? Object.assign({}, defaultCfg, infoCfg) : defaultCfg; + this.zf = zf; + this.ordinal = ordinal++; + } + + async move(fen: string) { + const ctx = this.ctx; + const chess = Chops.Chess.fromSetup(Chops.fen.parseFen(fen).unwrap()).unwrap(); + const p = { ply: chess.halfmoves, material: Chops.Material.fromBoard(chess.board) }; + + const zeroMove = this.netName ? this.zf.goZero(fen) : Promise.resolve(undefined); + if (chance(ctx.zeroChance(p))) return (await zeroMove) ?? '0000'; + const fishMove = this.zf.goFish(fen, { + depth: this.ctx.searchDepth?.(p) ?? 12, + pvs: this.ctx.searchWidth(p), + }); + let before = performance.now(); + const [zero, fish] = await Promise.all([zeroMove, fishMove]); + //const cp = score(fish[0]); + console.log('zf', performance.now() - before); + before = performance.now(); + const biasCp = biasScore(fish, { move: zero, bias: ctx.zeroCpDefault(p), depth: ctx.scoreDepth?.(p) }); + console.log('cp', score(fish[0]), 'biasCp', biasCp, performance.now() - before); + before = performance.now(); + const threshold = ctx.cpThreshold(p); + const filtered = filter(fish, biasCp, threshold); + if (!chance(ctx.aggression(p))) { + const mv = filtered[Math.floor(Math.random() * filtered.length)]?.moves[0] ?? zero; + console.log('returning ', mv, filtered); + } + const aggression = byDestruction(fish, fen); + aggression.sort((a, b) => b[0] - a[0]); + const zeroIndex = aggression.findIndex(([_, pv]) => pv.moves[0] === zero); + const zeroDestruction = zeroIndex >= 0 ? aggression[zeroIndex]?.[0] : -0.1; + console.log(zeroDestruction, aggression, performance.now() - before); + return aggression[0]?.[1]?.moves[0] ?? zero; + } +} + +const defaultCfg: ZfBotConfig = { + // if no opening book moves are selected, these parameters govern how a zerobot chooses a move + zeroChance: constant(0), // [0 computed, 1 always lc0] + zeroCpDefault: constant(-1), + // first, if chance(zeroChance) then the lc0 move is chosen and we are done + cpThreshold: constant(50), // a limiter for the number of centipawns we can lose vs fish[0][max] + searchDepth: constant(8), // how deep to go + scoreDepth: constant(99), // prefer scores at this depth + searchWidth: constant(16), // multiPV + aggression: constant(0.1), // [0 passive, 1 aggressive] +}; + +function constant(x: number) { + return () => x; +} + +function filter(pvs: Score[][], bias: number, threshold: number): Score[] { + const matches: Score[] = []; + for (const history of pvs) { + for (const line of history) { + if (Math.abs(line.score - bias) < threshold) matches.push({ ...line }); + } + } + return matches; +} + +function biasScore(pvs: Score[][], evalCriteria?: { move?: string; bias?: number; depth?: number }) { + //const best = score(pvs[0]); + //console.log(pvs); + if (pvs.length === 0 || !evalCriteria?.move) return evalCriteria?.bias ?? 0; + let fit = { score: evalCriteria?.bias ?? 0, depth: -100 }; + const targetDepth = evalCriteria?.depth ?? 99; + for (const history of pvs) { + for (const line of history) { + if (line.moves[0] === evalCriteria.move) { + //console.log(Math.abs(targetDepth - line.depth), Math.abs(targetDepth - fit.depth), fit.score); + if (Math.abs(targetDepth - line.depth) < Math.abs(targetDepth - fit.depth)) fit = line; + } + } + } + return fit.score; +} + +function byDestruction(lines: Score[][], fen: string, mutual = false) { + const chess = Chops.Chess.fromSetup(Chops.fen.parseFen(fen).unwrap()).unwrap(); + const beforeMaterial = Chops.Material.fromBoard(chess.board); + const opponent = Chops.opposite(chess.turn); + const before = weigh(mutual ? beforeMaterial : beforeMaterial[opponent]); + const aggression: [number, Score][] = []; + for (const history of lines) { + for (const pv of history) { + try { + const pvChess = chess.clone(); + for (const move of pv.moves) pvChess.play(Chops.parseUci(move)!); + const afterMaterial = Chops.Material.fromBoard(pvChess.board); + const destruction = + (before - weigh(mutual ? afterMaterial : afterMaterial[opponent])) / pv.moves.length; + if (destruction > 0) aggression.push([destruction, pv]); + } catch (e) { + console.error(e, pv.moves); + } + } + } + return aggression; +} + +const prices: { [role in Chops.Role]?: number } = { + pawn: 1, + knight: 2.8, + bishop: 3, + rook: 5, + queen: 9, +}; + +function weigh(material: Chops.Material | Chops.MaterialSide) { + let score = 0; + for (const [role, price] of Object.entries(prices) as [Chops.Role, number][]) { + score += price * ('white' in material ? material.count(role) : material[role]); + } + return score; +} + +function chance(chance: number) { + return Math.random() < chance; +} + +function scores(lines: Score[][], move?: string) { + const matches: Score[] = []; + if (!move) return matches; + for (const history of lines) { + for (const line of history) { + if (line.moves[0] === move) matches.push(line); + } + } + return matches; +} + +function deepScores(lines: Score[][], move?: string) { + const matches: Score[] = []; + if (!move) return matches; + let deepest = 0; + for (const history of lines) { + for (const line of history) { + if (line.moves[0] !== move || line.depth < deepest) continue; + if (line.depth > deepest) matches.length = 0; + matches.push(line); + deepest = line.depth; + } + } + return matches; +} + +function score(line: Score[]) { + return line[line.length - 1]?.score ?? 0; +} + +/*function deepScore(lines: Score[][], move?: string) { + return Math.min(...(deepScores(lines, move).map(line => line.score) ?? [])); +} + +function shallowScores(lines: Score[][], move?: string) { + const matches: Score[] = []; + if (!move) return matches; + let shallowest = 99; + for (const history of lines) { + for (const line of history) { + if (line.moves[0] !== move || line.depth > shallowest) continue; + if (line.depth > shallowest) matches.length = 0; + matches.push(line); + shallowest = line.depth; + } + } + return matches; +}*/ diff --git a/ui/round/src/socket.ts b/ui/round/src/socket.ts index 24cb8084eae80..f3c740a65c547 100644 --- a/ui/round/src/socket.ts +++ b/ui/round/src/socket.ts @@ -157,7 +157,7 @@ export function make(send: SocketSend, ctrl: RoundController): RoundSocket { return { send: (typ: string, data?: any, opts?: any, noRetry?: boolean) => { - console.log('socket send', typ, JSON.stringify(data, undefined, 2)); + //console.log('socket send', typ, JSON.stringify(data, undefined, 2)); if (opts) console.log('send opts', JSON.stringify(opts, undefined, 2)); if (noRetry !== undefined) console.log('send noRetry', noRetry); send(typ, data, opts, noRetry); @@ -171,7 +171,7 @@ export function make(send: SocketSend, ctrl: RoundController): RoundSocket { send(typ, data); }, receive(typ: string, data: any): boolean { - console.log('socket receive', typ, JSON.stringify(data, undefined, 2)); + //console.log('socket receive', typ, JSON.stringify(data, undefined, 2)); const handler = handlers[typ]; if (handler) { handler(data); diff --git a/ui/site/src/diagnostic.ts b/ui/site/src/diagnostic.ts index aba812924cf3e..f7a3378cb2ecb 100644 --- a/ui/site/src/diagnostic.ts +++ b/ui/site/src/diagnostic.ts @@ -2,7 +2,6 @@ import { isTouchDevice } from 'common/device'; import { domDialog, ready } from 'common/dialog'; export default async function initModule() { - console.log('here we is'); const [logs] = await Promise.all([lichess.log.get(), ready]); const text = `Browser: ${navigator.userAgent}\n` + From a8f2cc46cac108b1bdde0550f3e4d7a4806dd5e2 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Fri, 29 Dec 2023 10:23:54 -0600 Subject: [PATCH 072/174] murge --- .github/workflows/assets.yml | 9 +- .github/workflows/flair.yml | 16 + .github/workflows/server.yml | 11 +- app/LilaComponents.scala | 5 +- app/controllers/Account.scala | 10 +- app/controllers/Api.scala | 47 +- app/controllers/Appeal.scala | 7 +- app/controllers/Blog.scala | 3 +- app/controllers/Challenge.scala | 14 +- app/controllers/DailyFeed.scala | 61 + app/controllers/Dev.scala | 5 +- app/controllers/Game.scala | 20 +- app/controllers/GameMod.scala | 2 +- app/controllers/LilaController.scala | 4 +- app/controllers/Mod.scala | 15 +- app/controllers/Pref.scala | 2 +- app/controllers/Push.scala | 4 +- app/controllers/RelayRound.scala | 45 +- app/controllers/Round.scala | 17 +- app/controllers/Setup.scala | 2 +- app/controllers/Streamer.scala | 4 +- app/controllers/Study.scala | 4 +- app/controllers/Swiss.scala | 38 +- app/controllers/Team.scala | 164 +- app/controllers/TeamApi.scala | 125 + app/controllers/Timeline.scala | 20 +- app/controllers/Tournament.scala | 28 +- app/controllers/Ublog.scala | 55 +- app/controllers/User.scala | 5 +- app/controllers/Video.scala | 3 +- app/http/ContentSecurityPolicy.scala | 6 + app/http/CtrlErrors.scala | 3 + app/http/KeyPages.scala | 2 +- app/http/RequestContext.scala | 1 - app/http/RequestGetter.scala | 2 +- app/http/ResponseBuilder.scala | 15 +- app/mashup/Preload.scala | 7 +- app/mashup/UserInfo.scala | 2 +- app/router.scala | 2 + app/templating/AssetHelper.scala | 16 +- app/templating/DateHelper.scala | 11 +- app/templating/Environment.scala | 7 +- app/templating/FormHelper.scala | 35 +- app/templating/HtmlHelper.scala | 15 + app/templating/I18nHelper.scala | 2 +- app/templating/SetupHelper.scala | 2 +- app/templating/TeamHelper.scala | 37 +- app/templating/UserHelper.scala | 30 +- app/ui/scalatags.scala | 1 + app/views/account/kid.scala | 2 +- app/views/account/profile.scala | 36 +- app/views/appeal/tree.scala | 42 +- app/views/auth/bits.scala | 33 +- app/views/base/layout.scala | 40 +- app/views/base/topnav.scala | 3 +- app/views/blog/bits.scala | 8 +- app/views/blog/index.scala | 11 +- app/views/blog/show.scala | 2 +- app/views/board/userAnalysis.scala | 2 +- app/views/challenge/bits.scala | 2 +- app/views/challenge/mine.scala | 20 +- app/views/chat.scala | 4 +- app/views/dailyFeed.scala | 172 + app/views/dev.scala | 6 +- app/views/event.scala | 18 +- app/views/forum/categ.scala | 28 +- app/views/forum/topic.scala | 45 +- app/views/game/importGame.scala | 13 +- app/views/game/mini.scala | 10 +- app/views/gathering.scala | 2 +- app/views/kaladin.scala | 3 +- app/views/lobby/bits.scala | 99 +- app/views/lobby/home.scala | 5 +- app/views/mod/communication.scala | 2 +- app/views/opening/show.scala | 2 +- app/views/plan/indexStripe.scala | 6 + app/views/puzzle/bits.scala | 8 +- app/views/relay/bits.scala | 11 +- app/views/relay/roundForm.scala | 18 +- app/views/relay/show.scala | 26 +- app/views/relay/tourForm.scala | 58 +- app/views/simul/bits.scala | 4 +- app/views/site/bits.scala | 9 +- app/views/site/faq.scala | 4 +- app/views/site/page.scala | 1 + app/views/streamer/bits.scala | 2 +- app/views/streamer/picture.scala | 2 +- app/views/study/show.scala | 2 +- app/views/swiss/form.scala | 3 +- app/views/swiss/home.scala | 22 +- app/views/swiss/show.scala | 22 +- app/views/team/admin.scala | 21 +- app/views/team/bits.scala | 22 +- app/views/team/declinedRequest.scala | 9 +- app/views/team/form.scala | 29 +- app/views/team/list.scala | 7 +- app/views/team/members.scala | 13 +- app/views/team/request.scala | 9 +- app/views/team/show.scala | 45 +- app/views/team/tournaments.scala | 22 +- app/views/tournament/bits.scala | 11 +- app/views/tournament/crud.scala | 2 +- app/views/tournament/homepageSpotlight.scala | 2 +- app/views/tournament/teamBattle.scala | 34 +- app/views/ublog/blog.scala | 7 +- app/views/ublog/form.scala | 11 +- app/views/ublog/index.scala | 42 +- app/views/ublog/post.scala | 20 + app/views/user/mini.scala | 11 +- app/views/user/mod.scala | 6 +- app/views/user/show/newPlayer.scala | 54 +- app/views/video/bits.scala | 17 +- app/views/video/layout.scala | 3 +- bin/deploy | 14 +- bin/trans-dump | 10 +- bin/validate-flair | 50 + build.sbt | 7 +- conf/base.conf | 10 +- conf/routes | 72 +- conf/team.routes | 31 + modules/activity/src/main/Env.scala | 7 +- modules/activity/src/main/JsonView.scala | 21 +- modules/api/src/main/Cli.scala | 3 +- modules/api/src/main/Context.scala | 4 + modules/api/src/main/GameApi.scala | 49 +- modules/api/src/main/GameApiV2.scala | 73 +- modules/api/src/main/RoundApi.scala | 11 +- modules/blog/src/main/BlogPost.scala | 3 +- modules/blog/src/main/DailyFeed.scala | 90 + modules/blog/src/main/Env.scala | 9 +- modules/bot/src/main/GameStateStream.scala | 21 +- modules/challenge/src/main/ChallengeApi.scala | 9 +- .../challenge/src/main/ChallengeJoiner.scala | 6 +- modules/chat/src/main/Chat.scala | 12 +- modules/chat/src/main/ChatApi.scala | 12 +- modules/chat/src/main/Env.scala | 2 +- modules/chat/src/main/JsonView.scala | 16 +- modules/chat/src/main/package.scala | 3 + modules/clas/src/main/ClasMarkup.scala | 15 +- modules/clas/src/main/ClasMatesCache.scala | 21 +- modules/clas/src/main/ClasStudentCache.scala | 3 +- modules/common/src/main/Chronometer.scala | 1 + modules/common/src/main/HTTPRequest.scala | 18 +- modules/common/src/main/LightUser.scala | 2 +- modules/common/src/main/base/LilaModel.scala | 11 +- modules/common/src/main/config.scala | 14 + modules/common/src/main/model.scala | 14 +- modules/common/src/main/mon.scala | 10 +- modules/db/src/main/Handlers.scala | 3 + modules/db/src/main/dsl.scala | 3 + modules/evalCache/src/main/Env.scala | 3 +- modules/evalCache/src/main/EvalCacheApi.scala | 13 +- modules/event/src/main/BsonHandlers.scala | 2 +- modules/event/src/main/Event.scala | 11 +- modules/event/src/main/EventApi.scala | 25 +- modules/event/src/main/EventForm.scala | 14 +- modules/fishnet/src/main/Client.scala | 2 - modules/fishnet/src/main/JsonApi.scala | 1 - modules/game/src/main/Game.scala | 1 + modules/game/src/main/GameRepo.scala | 37 +- modules/game/src/main/JsonView.scala | 9 +- modules/game/src/main/Pov.scala | 18 +- modules/game/src/main/Rematches.scala | 2 +- modules/game/src/main/actorApi.scala | 2 + modules/game/src/test/PgnDumpTest.scala | 10 +- .../hub/src/main/AsyncActorSequencer.scala | 3 +- modules/hub/src/main/BoundedAsyncActor.scala | 36 +- .../hub/src/main/EarlyMultiThrottler.scala | 10 +- modules/hub/src/main/Types.scala | 19 +- modules/i18n/src/main/I18nKeys.scala | 40 +- modules/i18n/src/main/I18nLangPicker.scala | 4 +- modules/i18n/src/main/I18nQuantity.scala | 8 +- modules/i18n/src/main/LangForm.scala | 18 + modules/i18n/src/main/LangList.scala | 30 +- modules/i18n/src/main/package.scala | 24 +- modules/irc/src/main/IrcApi.scala | 6 +- modules/mailer/src/main/AutomaticEmail.scala | 14 +- modules/memo/src/main/Picfit.scala | 1 + modules/memo/src/main/SettingStore.scala | 22 +- modules/mod/src/main/Modlog.scala | 2 + modules/mod/src/main/ModlogApi.scala | 3 + modules/oauth/src/main/Env.scala | 3 +- modules/pref/src/main/PrefForm.scala | 6 +- modules/pref/src/main/Theme.scala | 2 +- modules/push/src/main/Device.scala | 11 +- modules/push/src/main/DeviceApi.scala | 6 +- modules/push/src/main/Env.scala | 29 +- modules/push/src/main/FirebasePush.scala | 153 +- modules/push/src/main/PushApi.scala | 481 ++- modules/push/src/main/WebPush.scala | 30 +- modules/rating/src/main/PerfType.scala | 1 + modules/relay/src/main/BSONHandlers.scala | 4 +- modules/relay/src/main/Env.scala | 38 +- modules/relay/src/main/JsonView.scala | 73 +- modules/relay/src/main/RelayApi.scala | 132 +- modules/relay/src/main/RelayDelay.scala | 15 +- modules/relay/src/main/RelayFetch.scala | 175 +- modules/relay/src/main/RelayFormat.scala | 85 +- modules/relay/src/main/RelayGame.scala | 4 +- modules/relay/src/main/RelayLeaderboard.scala | 20 +- modules/relay/src/main/RelayPush.scala | 49 +- modules/relay/src/main/RelayRound.scala | 13 +- modules/relay/src/main/RelaySync.scala | 9 +- modules/relay/src/main/RelayTour.scala | 9 +- modules/relay/src/main/RelayTourForm.scala | 21 +- modules/relay/src/main/SyncLog.scala | 4 +- modules/room/src/main/RoomSocket.scala | 4 +- modules/round/src/main/Env.scala | 10 +- modules/round/src/main/Rematcher.scala | 17 +- modules/round/src/main/RoundAsyncActor.scala | 27 +- modules/round/src/main/RoundMobile.scala | 129 +- modules/round/src/main/RoundSocket.scala | 25 +- modules/round/src/main/actorApi.scala | 2 + modules/security/src/main/Permission.scala | 7 +- modules/shutup/src/main/Dictionary.scala | 10 +- modules/simul/src/main/Env.scala | 2 +- modules/simul/src/main/SimulRepo.scala | 2 +- modules/simul/src/main/SimulSocket.scala | 2 +- modules/streamer/src/main/LiveStream.scala | 6 +- modules/streamer/src/main/Stream.scala | 13 +- modules/streamer/src/main/Streamer.scala | 3 +- modules/streamer/src/main/StreamerApi.scala | 6 +- modules/streamer/src/main/Streaming.scala | 5 +- modules/study/src/main/BSONHandlers.scala | 2 +- modules/study/src/main/Env.scala | 2 +- modules/study/src/main/MultiPgn.scala | 13 +- modules/study/src/main/StudyFlatTree.scala | 45 +- modules/study/src/main/StudyForm.scala | 5 +- modules/study/src/main/StudyMultiBoard.scala | 93 +- modules/study/src/main/StudyRepo.scala | 13 +- modules/study/src/main/StudySocket.scala | 2 +- modules/study/src/main/StudyTopic.scala | 6 +- modules/swiss/src/main/Env.scala | 2 +- modules/swiss/src/main/SwissFeature.scala | 14 +- modules/swiss/src/main/SwissSocket.scala | 2 +- modules/swiss/src/main/model.scala | 3 +- modules/team/src/main/Cached.scala | 24 +- modules/team/src/main/Env.scala | 7 +- modules/team/src/main/Team.scala | 20 +- modules/team/src/main/TeamApi.scala | 23 +- modules/team/src/main/TeamForm.scala | 17 +- modules/team/src/main/TeamRepo.scala | 16 +- modules/team/src/main/TeamSocket.scala | 2 +- modules/team/src/main/package.scala | 5 - modules/timeline/src/main/EntryApi.scala | 41 +- modules/tournament/src/main/Env.scala | 2 +- modules/tournament/src/main/JsonView.scala | 14 +- .../tournament/src/main/TournamentApi.scala | 3 +- .../src/main/TournamentSocket.scala | 2 +- modules/ublog/src/main/Env.scala | 4 +- modules/ublog/src/main/UblogApi.scala | 8 + .../ublog/src/main/UblogBsonHandlers.scala | 2 +- modules/ublog/src/main/UblogForm.scala | 17 +- modules/ublog/src/main/UblogPaginator.scala | 10 +- modules/ublog/src/main/UblogPost.scala | 6 +- modules/ublog/src/main/UblogRank.scala | 89 +- modules/user/src/main/Env.scala | 2 +- modules/user/src/main/Flags.scala | 6 +- modules/user/src/main/FlairApi.scala | 54 + modules/user/src/main/LightUserApi.scala | 2 +- modules/user/src/main/User.scala | 11 +- modules/user/src/main/UserFlair.scala | 56 - modules/user/src/main/UserForm.scala | 10 +- modules/user/src/main/UserPerfs.scala | 2 +- modules/user/src/main/UserRepo.scala | 2 +- pnpm-lock.yaml | 88 +- project/Dependencies.scala | 7 +- project/plugins.sbt | 2 +- public/bots.json | 8 +- public/flair/README.md | 65 + .../flair/img/activity.1st-place-medal.webp | Bin 0 -> 1672 bytes public/flair/img/activity.2024.webp | Bin 0 -> 4176 bytes .../flair/img/activity.2nd-place-medal.webp | Bin 0 -> 1502 bytes .../flair/img/activity.3rd-place-medal.webp | Bin 0 -> 1732 bytes .../flair/img/activity.admission-tickets.webp | Bin 0 -> 904 bytes .../flair/img/activity.american-football.webp | Bin 0 -> 1858 bytes public/flair/img/activity.artist-palette.webp | Bin 0 -> 1944 bytes public/flair/img/activity.badminton.webp | Bin 0 -> 2494 bytes public/flair/img/activity.balloon.webp | Bin 0 -> 1410 bytes public/flair/img/activity.baseball.webp | Bin 0 -> 2006 bytes public/flair/img/activity.basketball.webp | Bin 0 -> 2088 bytes public/flair/img/activity.bowling.webp | Bin 0 -> 1990 bytes public/flair/img/activity.boxing-glove.webp | Bin 0 -> 1222 bytes public/flair/img/activity.carp-streamer.webp | Bin 0 -> 2808 bytes public/flair/img/activity.chess-pawn.webp | Bin 0 -> 1124 bytes public/flair/img/activity.chess.webp | Bin 0 -> 1760 bytes public/flair/img/activity.christmas-tree.webp | Bin 0 -> 2096 bytes public/flair/img/activity.club-suit.webp | Bin 0 -> 1176 bytes public/flair/img/activity.confetti-ball.webp | Bin 0 -> 3786 bytes public/flair/img/activity.cricket-game.webp | Bin 0 -> 1662 bytes public/flair/img/activity.crystal-ball.webp | Bin 0 -> 1656 bytes public/flair/img/activity.curling-stone.webp | Bin 0 -> 1558 bytes public/flair/img/activity.diamond-suit.webp | Bin 0 -> 1228 bytes public/flair/img/activity.direct-hit.webp | Bin 0 -> 2186 bytes public/flair/img/activity.diving-mask.webp | Bin 0 -> 2146 bytes public/flair/img/activity.field-hockey.webp | Bin 0 -> 1748 bytes public/flair/img/activity.firecracker.webp | Bin 0 -> 1974 bytes public/flair/img/activity.fireworks.webp | Bin 0 -> 2076 bytes public/flair/img/activity.fishing-pole.webp | Bin 0 -> 2132 bytes public/flair/img/activity.flag-in-hole.webp | Bin 0 -> 1638 bytes .../img/activity.flower-playing-cards.webp | Bin 0 -> 816 bytes public/flair/img/activity.flying-disc.webp | Bin 0 -> 2296 bytes public/flair/img/activity.framed-picture.webp | Bin 0 -> 880 bytes public/flair/img/activity.game-die.webp | Bin 0 -> 1502 bytes public/flair/img/activity.goal-net.webp | Bin 0 -> 2880 bytes public/flair/img/activity.heart-suit.webp | Bin 0 -> 1350 bytes public/flair/img/activity.ice-hockey.webp | Bin 0 -> 1590 bytes public/flair/img/activity.ice-skate.webp | Bin 0 -> 1666 bytes public/flair/img/activity.jack-o-lantern.webp | Bin 0 -> 1886 bytes public/flair/img/activity.japanese-dolls.webp | Bin 0 -> 2500 bytes public/flair/img/activity.joker.webp | Bin 0 -> 1204 bytes public/flair/img/activity.joystick.webp | Bin 0 -> 1512 bytes public/flair/img/activity.kite.webp | Bin 0 -> 2324 bytes public/flair/img/activity.lacrosse.webp | Bin 0 -> 2242 bytes .../flair/img/activity.lichess-berserk.webp | Bin 0 -> 2728 bytes public/flair/img/activity.lichess-blitz.webp | Bin 0 -> 3099 bytes public/flair/img/activity.lichess-bullet.webp | Bin 0 -> 2398 bytes .../flair/img/activity.lichess-classical.webp | Bin 0 -> 4574 bytes .../img/activity.lichess-correspondence.webp | Bin 0 -> 3668 bytes public/flair/img/activity.lichess-hogger.webp | Bin 0 -> 1562 bytes public/flair/img/activity.lichess-horsey.webp | Bin 0 -> 2132 bytes public/flair/img/activity.lichess-rapid.webp | Bin 0 -> 2980 bytes .../img/activity.lichess-ultrabullet.webp | Bin 0 -> 3240 bytes .../img/activity.lichess-variant-960.webp | Bin 0 -> 1242 bytes .../activity.lichess-variant-antichess.webp | Bin 0 -> 2100 bytes .../img/activity.lichess-variant-atomic.webp | Bin 0 -> 2252 bytes .../activity.lichess-variant-crazyhouse.webp | Bin 0 -> 916 bytes .../img/activity.lichess-variant-horde.webp | Bin 0 -> 1454 bytes ...vity.lichess-variant-king-of-the-hill.webp | Bin 0 -> 1048 bytes ...activity.lichess-variant-racing-kings.webp | Bin 0 -> 1472 bytes .../activity.lichess-variant-three-check.webp | Bin 0 -> 1556 bytes public/flair/img/activity.lichess.webp | Bin 0 -> 2736 bytes public/flair/img/activity.magic-wand.webp | Bin 0 -> 2466 bytes .../img/activity.mahjong-red-dragon.webp | Bin 0 -> 1236 bytes .../img/activity.martial-arts-uniform.webp | Bin 0 -> 1932 bytes public/flair/img/activity.military-medal.webp | Bin 0 -> 1736 bytes public/flair/img/activity.mirror-ball.webp | Bin 0 -> 2400 bytes .../img/activity.moon-viewing-ceremony.webp | Bin 0 -> 1520 bytes public/flair/img/activity.nesting-dolls.webp | Bin 0 -> 2018 bytes public/flair/img/activity.party-popper.webp | Bin 0 -> 3448 bytes .../flair/img/activity.performing-arts.webp | Bin 0 -> 2044 bytes public/flair/img/activity.pinata.webp | Bin 0 -> 3510 bytes .../flair/img/activity.pine-decoration.webp | Bin 0 -> 1534 bytes public/flair/img/activity.ping-pong.webp | Bin 0 -> 1508 bytes public/flair/img/activity.pistol.webp | Bin 0 -> 1580 bytes public/flair/img/activity.pool-8-ball.webp | Bin 0 -> 1266 bytes public/flair/img/activity.puzzle-piece.webp | Bin 0 -> 2032 bytes public/flair/img/activity.red-envelope.webp | Bin 0 -> 1070 bytes public/flair/img/activity.rugby-football.webp | Bin 0 -> 1706 bytes public/flair/img/activity.running-shirt.webp | Bin 0 -> 1624 bytes public/flair/img/activity.shogi-bigsby.webp | Bin 0 -> 2720 bytes public/flair/img/activity.shogi-king.webp | Bin 0 -> 1034 bytes public/flair/img/activity.skis.webp | Bin 0 -> 1236 bytes public/flair/img/activity.sled.webp | Bin 0 -> 2108 bytes public/flair/img/activity.slot-machine.webp | Bin 0 -> 1982 bytes public/flair/img/activity.soccer-ball.webp | Bin 0 -> 1652 bytes public/flair/img/activity.softball.webp | Bin 0 -> 2200 bytes public/flair/img/activity.spade-suit.webp | Bin 0 -> 1084 bytes public/flair/img/activity.sparkler.webp | Bin 0 -> 1906 bytes public/flair/img/activity.sparkles.webp | Bin 0 -> 2142 bytes public/flair/img/activity.sports-medal.webp | Bin 0 -> 1634 bytes public/flair/img/activity.tanabata-tree.webp | Bin 0 -> 2798 bytes public/flair/img/activity.tennis.webp | Bin 0 -> 2844 bytes public/flair/img/activity.ticket.webp | Bin 0 -> 990 bytes public/flair/img/activity.trophy.webp | Bin 0 -> 2282 bytes public/flair/img/activity.video-game.webp | Bin 0 -> 1200 bytes public/flair/img/activity.volleyball.webp | Bin 0 -> 1662 bytes public/flair/img/activity.wind-chime.webp | Bin 0 -> 1724 bytes public/flair/img/activity.wrapped-gift.webp | Bin 0 -> 1900 bytes .../img/activity.xmas-lichess-horsey.webp | Bin 0 -> 4558 bytes public/flair/img/activity.yo-yo.webp | Bin 0 -> 1998 bytes public/flair/img/food-drink.amphora.webp | Bin 0 -> 1742 bytes public/flair/img/food-drink.avocado.webp | Bin 0 -> 1660 bytes public/flair/img/food-drink.baby-bottle.webp | Bin 0 -> 1538 bytes public/flair/img/food-drink.bacon.webp | Bin 0 -> 1642 bytes public/flair/img/food-drink.bagel.webp | Bin 0 -> 2134 bytes .../flair/img/food-drink.baguette-bread.webp | Bin 0 -> 1638 bytes public/flair/img/food-drink.banana.webp | Bin 0 -> 1912 bytes public/flair/img/food-drink.beans.webp | Bin 0 -> 1804 bytes public/flair/img/food-drink.beer-mug.webp | Bin 0 -> 1944 bytes public/flair/img/food-drink.bell-pepper.webp | Bin 0 -> 1462 bytes public/flair/img/food-drink.bento-box.webp | Bin 0 -> 1466 bytes public/flair/img/food-drink.beverage-box.webp | Bin 0 -> 1642 bytes .../flair/img/food-drink.birthday-cake.webp | Bin 0 -> 2296 bytes public/flair/img/food-drink.blueberries.webp | Bin 0 -> 1450 bytes .../food-drink.bottle-with-popping-cork.webp | Bin 0 -> 1650 bytes .../flair/img/food-drink.bowl-with-spoon.webp | Bin 0 -> 1484 bytes public/flair/img/food-drink.bread.webp | Bin 0 -> 1646 bytes public/flair/img/food-drink.broccoli.webp | Bin 0 -> 1860 bytes .../flair/img/food-drink.brown-mushroom.webp | Bin 0 -> 1408 bytes public/flair/img/food-drink.bubble-tea.webp | Bin 0 -> 1810 bytes public/flair/img/food-drink.burrito.webp | Bin 0 -> 1668 bytes public/flair/img/food-drink.butter.webp | Bin 0 -> 1506 bytes public/flair/img/food-drink.candy.webp | Bin 0 -> 1480 bytes public/flair/img/food-drink.canned-food.webp | Bin 0 -> 1362 bytes public/flair/img/food-drink.carrot.webp | Bin 0 -> 1420 bytes public/flair/img/food-drink.cheese-wedge.webp | Bin 0 -> 1572 bytes public/flair/img/food-drink.cherries.webp | Bin 0 -> 1872 bytes public/flair/img/food-drink.chestnut.webp | Bin 0 -> 1438 bytes .../flair/img/food-drink.chocolate-bar.webp | Bin 0 -> 1564 bytes public/flair/img/food-drink.chopsticks.webp | Bin 0 -> 1760 bytes .../img/food-drink.clinking-beer-mugs.webp | Bin 0 -> 2572 bytes .../img/food-drink.clinking-glasses.webp | Bin 0 -> 2602 bytes .../flair/img/food-drink.cocktail-glass.webp | Bin 0 -> 1876 bytes public/flair/img/food-drink.coconut.webp | Bin 0 -> 1764 bytes public/flair/img/food-drink.cooked-rice.webp | Bin 0 -> 1498 bytes public/flair/img/food-drink.cookie.webp | Bin 0 -> 1840 bytes public/flair/img/food-drink.cooking.webp | Bin 0 -> 1470 bytes public/flair/img/food-drink.croissant.webp | Bin 0 -> 1816 bytes public/flair/img/food-drink.cucumber.webp | Bin 0 -> 1366 bytes .../flair/img/food-drink.cup-with-straw.webp | Bin 0 -> 1248 bytes public/flair/img/food-drink.cupcake.webp | Bin 0 -> 1910 bytes public/flair/img/food-drink.curry-rice.webp | Bin 0 -> 1690 bytes public/flair/img/food-drink.custard.webp | Bin 0 -> 1538 bytes public/flair/img/food-drink.cut-of-meat.webp | Bin 0 -> 1984 bytes public/flair/img/food-drink.dango.webp | Bin 0 -> 1506 bytes public/flair/img/food-drink.doughnut.webp | Bin 0 -> 1824 bytes public/flair/img/food-drink.dumpling.webp | Bin 0 -> 1390 bytes public/flair/img/food-drink.ear-of-corn.webp | Bin 0 -> 2026 bytes public/flair/img/food-drink.egg.webp | Bin 0 -> 1336 bytes public/flair/img/food-drink.falafel.webp | Bin 0 -> 1844 bytes .../img/food-drink.fish-cake-with-swirl.webp | Bin 0 -> 1640 bytes public/flair/img/food-drink.flatbread.webp | Bin 0 -> 1746 bytes public/flair/img/food-drink.fondue.webp | Bin 0 -> 2344 bytes .../food-drink.fork-and-knife-with-plate.webp | Bin 0 -> 1756 bytes .../flair/img/food-drink.fork-and-knife.webp | Bin 0 -> 1290 bytes .../flair/img/food-drink.fortune-cookie.webp | Bin 0 -> 1748 bytes public/flair/img/food-drink.french-fries.webp | Bin 0 -> 2026 bytes public/flair/img/food-drink.fried-shrimp.webp | Bin 0 -> 1892 bytes public/flair/img/food-drink.garlic.webp | Bin 0 -> 1294 bytes public/flair/img/food-drink.ginger.webp | Bin 0 -> 2090 bytes .../flair/img/food-drink.glass-of-milk.webp | Bin 0 -> 1514 bytes public/flair/img/food-drink.grapes.webp | Bin 0 -> 1806 bytes public/flair/img/food-drink.green-apple.webp | Bin 0 -> 1588 bytes public/flair/img/food-drink.green-salad.webp | Bin 0 -> 1820 bytes public/flair/img/food-drink.hamburger.webp | Bin 0 -> 2054 bytes public/flair/img/food-drink.honey-pot.webp | Bin 0 -> 1722 bytes public/flair/img/food-drink.hot-beverage.webp | Bin 0 -> 1802 bytes public/flair/img/food-drink.hot-dog.webp | Bin 0 -> 1616 bytes public/flair/img/food-drink.hot-pepper.webp | Bin 0 -> 1694 bytes public/flair/img/food-drink.ice-cream.webp | Bin 0 -> 1662 bytes public/flair/img/food-drink.ice.webp | Bin 0 -> 2774 bytes public/flair/img/food-drink.jar.webp | Bin 0 -> 1590 bytes .../flair/img/food-drink.kitchen-knife.webp | Bin 0 -> 1150 bytes public/flair/img/food-drink.kiwi-fruit.webp | Bin 0 -> 1926 bytes public/flair/img/food-drink.leafy-green.webp | Bin 0 -> 1726 bytes public/flair/img/food-drink.lemon.webp | Bin 0 -> 1540 bytes public/flair/img/food-drink.lime.webp | Bin 0 -> 1650 bytes public/flair/img/food-drink.lollipop.webp | Bin 0 -> 2180 bytes public/flair/img/food-drink.mango.webp | Bin 0 -> 1690 bytes public/flair/img/food-drink.mate.webp | Bin 0 -> 1308 bytes public/flair/img/food-drink.meat-on-bone.webp | Bin 0 -> 1798 bytes public/flair/img/food-drink.melon.webp | Bin 0 -> 1964 bytes public/flair/img/food-drink.moon-cake.webp | Bin 0 -> 2224 bytes public/flair/img/food-drink.oden.webp | Bin 0 -> 2024 bytes public/flair/img/food-drink.olive.webp | Bin 0 -> 1988 bytes public/flair/img/food-drink.onion.webp | Bin 0 -> 1638 bytes public/flair/img/food-drink.pancakes.webp | Bin 0 -> 2250 bytes public/flair/img/food-drink.pea-pod.webp | Bin 0 -> 1704 bytes public/flair/img/food-drink.peanuts.webp | Bin 0 -> 1606 bytes public/flair/img/food-drink.pear.webp | Bin 0 -> 1320 bytes public/flair/img/food-drink.pie.webp | Bin 0 -> 1588 bytes public/flair/img/food-drink.pineapple.webp | Bin 0 -> 1690 bytes public/flair/img/food-drink.pizza.webp | Bin 0 -> 1872 bytes public/flair/img/food-drink.popcorn.webp | Bin 0 -> 2176 bytes public/flair/img/food-drink.pot-of-food.webp | Bin 0 -> 1570 bytes public/flair/img/food-drink.potato.webp | Bin 0 -> 1418 bytes public/flair/img/food-drink.poultry-leg.webp | Bin 0 -> 1390 bytes .../flair/img/food-drink.pouring-liquid.webp | Bin 0 -> 1804 bytes public/flair/img/food-drink.pretzel.webp | Bin 0 -> 2338 bytes public/flair/img/food-drink.red-apple.webp | Bin 0 -> 1768 bytes public/flair/img/food-drink.rice-ball.webp | Bin 0 -> 1394 bytes public/flair/img/food-drink.rice-cracker.webp | Bin 0 -> 1720 bytes .../img/food-drink.roasted-sweet-potato.webp | Bin 0 -> 1658 bytes public/flair/img/food-drink.sake.webp | Bin 0 -> 1372 bytes public/flair/img/food-drink.salt.webp | Bin 0 -> 1308 bytes public/flair/img/food-drink.sandwich.webp | Bin 0 -> 1972 bytes .../img/food-drink.shallow-pan-of-food.webp | Bin 0 -> 2228 bytes public/flair/img/food-drink.shaved-ice.webp | Bin 0 -> 1454 bytes public/flair/img/food-drink.shortcake.webp | Bin 0 -> 1926 bytes .../flair/img/food-drink.soft-ice-cream.webp | Bin 0 -> 1362 bytes public/flair/img/food-drink.spaghetti.webp | Bin 0 -> 2102 bytes public/flair/img/food-drink.spoon.webp | Bin 0 -> 1098 bytes .../flair/img/food-drink.steaming-bowl.webp | Bin 0 -> 2176 bytes public/flair/img/food-drink.strawberry.webp | Bin 0 -> 1872 bytes .../img/food-drink.stuffed-flatbread.webp | Bin 0 -> 2034 bytes public/flair/img/food-drink.sushi.webp | Bin 0 -> 2046 bytes public/flair/img/food-drink.taco.webp | Bin 0 -> 2030 bytes public/flair/img/food-drink.takeout-box.webp | Bin 0 -> 1622 bytes public/flair/img/food-drink.tamale.webp | Bin 0 -> 1956 bytes public/flair/img/food-drink.tangerine.webp | Bin 0 -> 1682 bytes .../img/food-drink.teacup-without-handle.webp | Bin 0 -> 1344 bytes public/flair/img/food-drink.teapot.webp | Bin 0 -> 1604 bytes public/flair/img/food-drink.tomato.webp | Bin 0 -> 1752 bytes .../flair/img/food-drink.tropical-drink.webp | Bin 0 -> 2002 bytes .../flair/img/food-drink.tumbler-glass.webp | Bin 0 -> 2206 bytes public/flair/img/food-drink.waffle.webp | Bin 0 -> 1788 bytes public/flair/img/food-drink.watermelon.webp | Bin 0 -> 1690 bytes public/flair/img/food-drink.wine-glass.webp | Bin 0 -> 1812 bytes public/flair/img/nature.ant.webp | Bin 0 -> 1680 bytes public/flair/img/nature.baby-chick.webp | Bin 0 -> 1678 bytes public/flair/img/nature.badger.webp | Bin 0 -> 1266 bytes public/flair/img/nature.bat.webp | Bin 0 -> 1430 bytes public/flair/img/nature.bear.webp | Bin 0 -> 1456 bytes public/flair/img/nature.beaver.webp | Bin 0 -> 1564 bytes public/flair/img/nature.beetle.webp | Bin 0 -> 1962 bytes public/flair/img/nature.bird.webp | Bin 0 -> 1572 bytes public/flair/img/nature.bison.webp | Bin 0 -> 1808 bytes public/flair/img/nature.black-bird.webp | Bin 0 -> 1268 bytes public/flair/img/nature.black-cat.webp | Bin 0 -> 1738 bytes public/flair/img/nature.blossom.webp | Bin 0 -> 2234 bytes public/flair/img/nature.blowfish.webp | Bin 0 -> 2014 bytes public/flair/img/nature.boar.webp | Bin 0 -> 1584 bytes public/flair/img/nature.bouquet.webp | Bin 0 -> 2136 bytes public/flair/img/nature.bug.webp | Bin 0 -> 1866 bytes public/flair/img/nature.butterfly.webp | Bin 0 -> 2286 bytes public/flair/img/nature.cactus.webp | Bin 0 -> 2108 bytes public/flair/img/nature.camel.webp | Bin 0 -> 2152 bytes public/flair/img/nature.cat-face.webp | Bin 0 -> 2172 bytes public/flair/img/nature.cat.webp | Bin 0 -> 2002 bytes public/flair/img/nature.cherry-blossom.webp | Bin 0 -> 1694 bytes public/flair/img/nature.chicken.webp | Bin 0 -> 1776 bytes public/flair/img/nature.chipmunk.webp | Bin 0 -> 1830 bytes public/flair/img/nature.closed-umbrella.webp | Bin 0 -> 1648 bytes .../nature.cloud-with-lightning-and-rain.webp | Bin 0 -> 1888 bytes .../img/nature.cloud-with-lightning.webp | Bin 0 -> 1318 bytes public/flair/img/nature.cloud-with-rain.webp | Bin 0 -> 1872 bytes public/flair/img/nature.cloud-with-snow.webp | Bin 0 -> 1992 bytes public/flair/img/nature.cloud.webp | Bin 0 -> 1030 bytes public/flair/img/nature.cockroach.webp | Bin 0 -> 2542 bytes public/flair/img/nature.comet.webp | Bin 0 -> 2278 bytes public/flair/img/nature.coral.webp | Bin 0 -> 2656 bytes public/flair/img/nature.cow-face.webp | Bin 0 -> 1800 bytes public/flair/img/nature.cow.webp | Bin 0 -> 1768 bytes public/flair/img/nature.crab.webp | Bin 0 -> 2514 bytes public/flair/img/nature.crescent-moon.webp | Bin 0 -> 1564 bytes public/flair/img/nature.cricket.webp | Bin 0 -> 2058 bytes public/flair/img/nature.crocodile.webp | Bin 0 -> 1778 bytes public/flair/img/nature.cyclone.webp | Bin 0 -> 2716 bytes public/flair/img/nature.deciduous-tree.webp | Bin 0 -> 1428 bytes public/flair/img/nature.deer.webp | Bin 0 -> 1912 bytes public/flair/img/nature.dodo.webp | Bin 0 -> 1750 bytes public/flair/img/nature.dog-face.webp | Bin 0 -> 1586 bytes public/flair/img/nature.dog.webp | Bin 0 -> 1960 bytes public/flair/img/nature.dolphin.webp | Bin 0 -> 1964 bytes public/flair/img/nature.donkey.webp | Bin 0 -> 1540 bytes public/flair/img/nature.dove.webp | Bin 0 -> 1742 bytes public/flair/img/nature.dragon-face.webp | Bin 0 -> 2186 bytes public/flair/img/nature.dragon.webp | Bin 0 -> 2648 bytes public/flair/img/nature.droplet.webp | Bin 0 -> 1236 bytes public/flair/img/nature.duck.webp | Bin 0 -> 1682 bytes public/flair/img/nature.eagle.webp | Bin 0 -> 1788 bytes public/flair/img/nature.elephant.webp | Bin 0 -> 1604 bytes public/flair/img/nature.empty-nest.webp | Bin 0 -> 1616 bytes public/flair/img/nature.evergreen-tree.webp | Bin 0 -> 1720 bytes public/flair/img/nature.ewe.webp | Bin 0 -> 1570 bytes public/flair/img/nature.fallen-leaf.webp | Bin 0 -> 2162 bytes public/flair/img/nature.feather.webp | Bin 0 -> 1638 bytes public/flair/img/nature.fire.webp | Bin 0 -> 1680 bytes .../img/nature.first-quarter-moon-face.webp | Bin 0 -> 1678 bytes .../flair/img/nature.first-quarter-moon.webp | Bin 0 -> 1142 bytes public/flair/img/nature.fish.webp | Bin 0 -> 1484 bytes public/flair/img/nature.flamingo.webp | Bin 0 -> 1516 bytes public/flair/img/nature.fly.webp | Bin 0 -> 2140 bytes public/flair/img/nature.fog.webp | Bin 0 -> 520 bytes public/flair/img/nature.four-leaf-clover.webp | Bin 0 -> 1924 bytes public/flair/img/nature.fox.webp | Bin 0 -> 1874 bytes public/flair/img/nature.frog.webp | Bin 0 -> 1698 bytes .../img/nature.front-facing-baby-chick.webp | Bin 0 -> 1702 bytes public/flair/img/nature.full-moon-face.webp | Bin 0 -> 1578 bytes public/flair/img/nature.full-moon.webp | Bin 0 -> 1242 bytes public/flair/img/nature.giraffe.webp | Bin 0 -> 1924 bytes public/flair/img/nature.glowing-star.webp | Bin 0 -> 2262 bytes public/flair/img/nature.goat.webp | Bin 0 -> 1674 bytes public/flair/img/nature.goose.webp | Bin 0 -> 1640 bytes public/flair/img/nature.gorilla.webp | Bin 0 -> 1582 bytes public/flair/img/nature.guide-dog.webp | Bin 0 -> 2484 bytes public/flair/img/nature.hamster.webp | Bin 0 -> 1892 bytes public/flair/img/nature.hatching-chick.webp | Bin 0 -> 1618 bytes public/flair/img/nature.hedgehog.webp | Bin 0 -> 1674 bytes public/flair/img/nature.herb.webp | Bin 0 -> 2302 bytes public/flair/img/nature.hibiscus.webp | Bin 0 -> 1962 bytes public/flair/img/nature.high-voltage.webp | Bin 0 -> 1594 bytes public/flair/img/nature.hippopotamus.webp | Bin 0 -> 1474 bytes public/flair/img/nature.honeybee.webp | Bin 0 -> 2504 bytes public/flair/img/nature.horse-face.webp | Bin 0 -> 1736 bytes public/flair/img/nature.horse.webp | Bin 0 -> 2152 bytes public/flair/img/nature.hyacinth.webp | Bin 0 -> 1924 bytes public/flair/img/nature.jellyfish.webp | Bin 0 -> 2980 bytes public/flair/img/nature.kangaroo.webp | Bin 0 -> 1674 bytes public/flair/img/nature.koala.webp | Bin 0 -> 1296 bytes public/flair/img/nature.lady-beetle.webp | Bin 0 -> 2052 bytes .../img/nature.last-quarter-moon-face.webp | Bin 0 -> 1712 bytes .../flair/img/nature.last-quarter-moon.webp | Bin 0 -> 1134 bytes .../img/nature.leaf-fluttering-in-wind.webp | Bin 0 -> 1794 bytes public/flair/img/nature.leopard.webp | Bin 0 -> 2232 bytes public/flair/img/nature.lion.webp | Bin 0 -> 1812 bytes public/flair/img/nature.lizard.webp | Bin 0 -> 2046 bytes public/flair/img/nature.llama.webp | Bin 0 -> 1750 bytes public/flair/img/nature.lobster.webp | Bin 0 -> 2560 bytes public/flair/img/nature.lotus.webp | Bin 0 -> 1902 bytes public/flair/img/nature.mammoth.webp | Bin 0 -> 1842 bytes public/flair/img/nature.maple-leaf.webp | Bin 0 -> 1940 bytes public/flair/img/nature.microbe.webp | Bin 0 -> 2560 bytes public/flair/img/nature.milky-way.webp | Bin 0 -> 1332 bytes public/flair/img/nature.monkey-face.webp | Bin 0 -> 1482 bytes public/flair/img/nature.monkey.webp | Bin 0 -> 2232 bytes public/flair/img/nature.moose.webp | Bin 0 -> 1988 bytes public/flair/img/nature.mosquito.webp | Bin 0 -> 2026 bytes public/flair/img/nature.mouse-face.webp | Bin 0 -> 1424 bytes public/flair/img/nature.mouse.webp | Bin 0 -> 1570 bytes public/flair/img/nature.mushroom.webp | Bin 0 -> 2040 bytes public/flair/img/nature.nest-with-eggs.webp | Bin 0 -> 1796 bytes public/flair/img/nature.new-moon-face.webp | Bin 0 -> 1232 bytes public/flair/img/nature.new-moon.webp | Bin 0 -> 904 bytes public/flair/img/nature.octopus-howard.webp | Bin 0 -> 2956 bytes public/flair/img/nature.octopus.webp | Bin 0 -> 2304 bytes public/flair/img/nature.orangutan.webp | Bin 0 -> 1914 bytes public/flair/img/nature.otter.webp | Bin 0 -> 1540 bytes public/flair/img/nature.owl.webp | Bin 0 -> 1660 bytes public/flair/img/nature.ox.webp | Bin 0 -> 1664 bytes public/flair/img/nature.oyster.webp | Bin 0 -> 1550 bytes public/flair/img/nature.palm-tree.webp | Bin 0 -> 1944 bytes public/flair/img/nature.panda.webp | Bin 0 -> 1640 bytes public/flair/img/nature.parrot.webp | Bin 0 -> 1870 bytes public/flair/img/nature.paw-prints.webp | Bin 0 -> 1762 bytes public/flair/img/nature.peacock.webp | Bin 0 -> 2240 bytes public/flair/img/nature.penguin.webp | Bin 0 -> 1320 bytes public/flair/img/nature.phoenix-bird.webp | Bin 0 -> 2530 bytes public/flair/img/nature.pig-face.webp | Bin 0 -> 1706 bytes public/flair/img/nature.pig-nose.webp | Bin 0 -> 1392 bytes public/flair/img/nature.pig.webp | Bin 0 -> 1752 bytes public/flair/img/nature.polar-bear.webp | Bin 0 -> 1566 bytes public/flair/img/nature.poodle.webp | Bin 0 -> 1818 bytes public/flair/img/nature.potted-plant.webp | Bin 0 -> 2772 bytes public/flair/img/nature.rabbit-face.webp | Bin 0 -> 1712 bytes public/flair/img/nature.rabbit.webp | Bin 0 -> 1538 bytes public/flair/img/nature.raccoon.webp | Bin 0 -> 1546 bytes public/flair/img/nature.rainbow.webp | Bin 0 -> 1502 bytes public/flair/img/nature.ram.webp | Bin 0 -> 1614 bytes public/flair/img/nature.rat.webp | Bin 0 -> 1864 bytes public/flair/img/nature.rhinoceros.webp | Bin 0 -> 1614 bytes public/flair/img/nature.ringed-planet.webp | Bin 0 -> 1448 bytes public/flair/img/nature.rock.webp | Bin 0 -> 1158 bytes public/flair/img/nature.rooster.webp | Bin 0 -> 1918 bytes public/flair/img/nature.rose.webp | Bin 0 -> 1474 bytes public/flair/img/nature.rosette.webp | Bin 0 -> 2052 bytes public/flair/img/nature.rubber-duck.webp | Bin 0 -> 1786 bytes public/flair/img/nature.sauropod.webp | Bin 0 -> 1972 bytes public/flair/img/nature.scorpion.webp | Bin 0 -> 2278 bytes public/flair/img/nature.seal.webp | Bin 0 -> 1552 bytes public/flair/img/nature.seedling.webp | Bin 0 -> 1506 bytes public/flair/img/nature.service-dog.webp | Bin 0 -> 2234 bytes public/flair/img/nature.shamrock.webp | Bin 0 -> 1846 bytes public/flair/img/nature.shark.webp | Bin 0 -> 1598 bytes public/flair/img/nature.sheaf-of-rice.webp | Bin 0 -> 2408 bytes public/flair/img/nature.shooting-star.webp | Bin 0 -> 1210 bytes public/flair/img/nature.shrimp.webp | Bin 0 -> 2450 bytes public/flair/img/nature.skunk.webp | Bin 0 -> 1418 bytes public/flair/img/nature.sloth.webp | Bin 0 -> 2004 bytes public/flair/img/nature.snail.webp | Bin 0 -> 1766 bytes public/flair/img/nature.snake.webp | Bin 0 -> 1744 bytes public/flair/img/nature.snowflake.webp | Bin 0 -> 2634 bytes .../img/nature.snowman-without-snow.webp | Bin 0 -> 1344 bytes public/flair/img/nature.snowman.webp | Bin 0 -> 1978 bytes public/flair/img/nature.spider-web.webp | Bin 0 -> 2450 bytes public/flair/img/nature.spider.webp | Bin 0 -> 1858 bytes public/flair/img/nature.spiral-shell.webp | Bin 0 -> 1284 bytes public/flair/img/nature.spouting-whale.webp | Bin 0 -> 1994 bytes public/flair/img/nature.squid.webp | Bin 0 -> 2348 bytes public/flair/img/nature.star.webp | Bin 0 -> 1608 bytes public/flair/img/nature.sun-behind-cloud.webp | Bin 0 -> 1614 bytes .../img/nature.sun-behind-large-cloud.webp | Bin 0 -> 1354 bytes .../img/nature.sun-behind-rain-cloud.webp | Bin 0 -> 1898 bytes .../img/nature.sun-behind-small-cloud.webp | Bin 0 -> 2028 bytes public/flair/img/nature.sun-with-face.webp | Bin 0 -> 2242 bytes public/flair/img/nature.sun.webp | Bin 0 -> 2272 bytes public/flair/img/nature.sunflower.webp | Bin 0 -> 2078 bytes public/flair/img/nature.swan.webp | Bin 0 -> 1556 bytes public/flair/img/nature.t-rex.webp | Bin 0 -> 1916 bytes public/flair/img/nature.tiger-face.webp | Bin 0 -> 2510 bytes public/flair/img/nature.tiger.webp | Bin 0 -> 2252 bytes public/flair/img/nature.tornado.webp | Bin 0 -> 1780 bytes public/flair/img/nature.tropical-fish.webp | Bin 0 -> 2068 bytes public/flair/img/nature.tulip.webp | Bin 0 -> 1640 bytes public/flair/img/nature.turkey.webp | Bin 0 -> 2162 bytes public/flair/img/nature.turtle.webp | Bin 0 -> 1558 bytes public/flair/img/nature.two-hump-camel.webp | Bin 0 -> 2140 bytes .../flair/img/nature.umbrella-on-ground.webp | Bin 0 -> 1938 bytes .../img/nature.umbrella-with-rain-drops.webp | Bin 0 -> 2038 bytes public/flair/img/nature.umbrella.webp | Bin 0 -> 1556 bytes public/flair/img/nature.unicorn.webp | Bin 0 -> 1854 bytes .../img/nature.waning-crescent-moon.webp | Bin 0 -> 1254 bytes .../flair/img/nature.waning-gibbous-moon.webp | Bin 0 -> 1304 bytes public/flair/img/nature.water-buffalo.webp | Bin 0 -> 1508 bytes public/flair/img/nature.water-wave.webp | Bin 0 -> 1962 bytes .../img/nature.waxing-crescent-moon.webp | Bin 0 -> 1272 bytes .../flair/img/nature.waxing-gibbous-moon.webp | Bin 0 -> 1294 bytes public/flair/img/nature.whale.webp | Bin 0 -> 1700 bytes public/flair/img/nature.white-flower.webp | Bin 0 -> 1954 bytes public/flair/img/nature.wilted-flower.webp | Bin 0 -> 2194 bytes public/flair/img/nature.wind-face.webp | Bin 0 -> 1854 bytes public/flair/img/nature.wing.webp | Bin 0 -> 1556 bytes public/flair/img/nature.wolf.webp | Bin 0 -> 1520 bytes public/flair/img/nature.wood.webp | Bin 0 -> 1364 bytes public/flair/img/nature.worm.webp | Bin 0 -> 1498 bytes public/flair/img/nature.xmas-tree.webp | Bin 0 -> 2560 bytes public/flair/img/nature.zebra.webp | Bin 0 -> 2024 bytes public/flair/img/objects.abacus.webp | Bin 0 -> 2668 bytes public/flair/img/objects.accordion.webp | Bin 0 -> 1998 bytes .../flair/img/objects.adhesive-bandage.webp | Bin 0 -> 1688 bytes public/flair/img/objects.alarm-clock.webp | Bin 0 -> 2300 bytes public/flair/img/objects.alembic.webp | Bin 0 -> 2428 bytes public/flair/img/objects.backpack.webp | Bin 0 -> 2142 bytes public/flair/img/objects.balance-scale.webp | Bin 0 -> 2558 bytes public/flair/img/objects.ballet-shoes.webp | Bin 0 -> 2402 bytes .../img/objects.ballot-box-with-ballot.webp | Bin 0 -> 1456 bytes public/flair/img/objects.banjo.webp | Bin 0 -> 1560 bytes public/flair/img/objects.bar-chart.webp | Bin 0 -> 782 bytes public/flair/img/objects.basket.webp | Bin 0 -> 2342 bytes public/flair/img/objects.bathtub.webp | Bin 0 -> 1938 bytes public/flair/img/objects.battery.webp | Bin 0 -> 902 bytes public/flair/img/objects.bed.webp | Bin 0 -> 864 bytes public/flair/img/objects.bell-with-slash.webp | Bin 0 -> 1952 bytes public/flair/img/objects.bell.webp | Bin 0 -> 1780 bytes public/flair/img/objects.bellhop-bell.webp | Bin 0 -> 1598 bytes public/flair/img/objects.bikini.webp | Bin 0 -> 2216 bytes public/flair/img/objects.billed-cap.webp | Bin 0 -> 1390 bytes public/flair/img/objects.black-nib.webp | Bin 0 -> 1474 bytes public/flair/img/objects.blue-book.webp | Bin 0 -> 592 bytes public/flair/img/objects.bookmark-tabs.webp | Bin 0 -> 1144 bytes public/flair/img/objects.bookmark.webp | Bin 0 -> 1650 bytes public/flair/img/objects.books.webp | Bin 0 -> 2100 bytes public/flair/img/objects.boomerang.webp | Bin 0 -> 2528 bytes public/flair/img/objects.bow-and-arrow.webp | Bin 0 -> 2480 bytes public/flair/img/objects.briefcase.webp | Bin 0 -> 1158 bytes public/flair/img/objects.briefs.webp | Bin 0 -> 1174 bytes public/flair/img/objects.broken-chain.webp | Bin 0 -> 2356 bytes public/flair/img/objects.broom.webp | Bin 0 -> 1320 bytes public/flair/img/objects.bubbles.webp | Bin 0 -> 2790 bytes public/flair/img/objects.bucket.webp | Bin 0 -> 1898 bytes public/flair/img/objects.calendar.webp | Bin 0 -> 1176 bytes .../flair/img/objects.camera-with-flash.webp | Bin 0 -> 1612 bytes public/flair/img/objects.camera.webp | Bin 0 -> 1038 bytes public/flair/img/objects.candle.webp | Bin 0 -> 1220 bytes public/flair/img/objects.card-file-box.webp | Bin 0 -> 1024 bytes .../img/objects.card-index-dividers.webp | Bin 0 -> 728 bytes public/flair/img/objects.card-index.webp | Bin 0 -> 1544 bytes public/flair/img/objects.carpentry-saw.webp | Bin 0 -> 1674 bytes public/flair/img/objects.chains.webp | Bin 0 -> 2166 bytes public/flair/img/objects.chair.webp | Bin 0 -> 1572 bytes .../flair/img/objects.chart-decreasing.webp | Bin 0 -> 1090 bytes .../objects.chart-increasing-with-yen.webp | Bin 0 -> 1002 bytes .../flair/img/objects.chart-increasing.webp | Bin 0 -> 1102 bytes public/flair/img/objects.cigarette.webp | Bin 0 -> 1666 bytes public/flair/img/objects.clamp.webp | Bin 0 -> 1706 bytes public/flair/img/objects.clapper-board.webp | Bin 0 -> 1292 bytes public/flair/img/objects.clipboard.webp | Bin 0 -> 858 bytes public/flair/img/objects.closed-book.webp | Bin 0 -> 600 bytes ...ects.closed-mailbox-with-lowered-flag.webp | Bin 0 -> 942 bytes ...jects.closed-mailbox-with-raised-flag.webp | Bin 0 -> 1112 bytes public/flair/img/objects.clutch-bag.webp | Bin 0 -> 1282 bytes public/flair/img/objects.coat.webp | Bin 0 -> 1694 bytes public/flair/img/objects.coffin.webp | Bin 0 -> 1462 bytes public/flair/img/objects.coin.webp | Bin 0 -> 1852 bytes public/flair/img/objects.computer-disk.webp | Bin 0 -> 804 bytes public/flair/img/objects.computer-mouse.webp | Bin 0 -> 942 bytes public/flair/img/objects.control-knobs.webp | Bin 0 -> 1102 bytes public/flair/img/objects.couch-and-lamp.webp | Bin 0 -> 1540 bytes public/flair/img/objects.crayon.webp | Bin 0 -> 1264 bytes public/flair/img/objects.credit-card.webp | Bin 0 -> 720 bytes public/flair/img/objects.crossed-swords.webp | Bin 0 -> 2054 bytes public/flair/img/objects.crown.webp | Bin 0 -> 2672 bytes public/flair/img/objects.crutch.webp | Bin 0 -> 1542 bytes .../flair/img/objects.desktop-computer.webp | Bin 0 -> 1144 bytes public/flair/img/objects.diya-lamp.webp | Bin 0 -> 2090 bytes public/flair/img/objects.dollar-banknote.webp | Bin 0 -> 786 bytes public/flair/img/objects.door.webp | Bin 0 -> 424 bytes public/flair/img/objects.dress.webp | Bin 0 -> 1330 bytes public/flair/img/objects.drum.webp | Bin 0 -> 2232 bytes public/flair/img/objects.dvd.webp | Bin 0 -> 1774 bytes public/flair/img/objects.e-mail.webp | Bin 0 -> 1044 bytes public/flair/img/objects.eight-oclock.webp | Bin 0 -> 1448 bytes public/flair/img/objects.eight-thirty.webp | Bin 0 -> 1454 bytes public/flair/img/objects.electric-plug.webp | Bin 0 -> 1244 bytes public/flair/img/objects.eleven-oclock.webp | Bin 0 -> 1450 bytes public/flair/img/objects.eleven-thirty.webp | Bin 0 -> 1456 bytes .../img/objects.envelope-with-arrow.webp | Bin 0 -> 1196 bytes public/flair/img/objects.envelope.webp | Bin 0 -> 860 bytes public/flair/img/objects.euro-banknote.webp | Bin 0 -> 940 bytes public/flair/img/objects.fax-machine.webp | Bin 0 -> 1414 bytes public/flair/img/objects.file-cabinet.webp | Bin 0 -> 628 bytes public/flair/img/objects.file-folder.webp | Bin 0 -> 1186 bytes public/flair/img/objects.film-frames.webp | Bin 0 -> 1232 bytes public/flair/img/objects.film-projector.webp | Bin 0 -> 2142 bytes .../flair/img/objects.fire-extinguisher.webp | Bin 0 -> 1610 bytes public/flair/img/objects.five-oclock.webp | Bin 0 -> 1444 bytes public/flair/img/objects.five-thirty.webp | Bin 0 -> 1454 bytes public/flair/img/objects.flashlight.webp | Bin 0 -> 1512 bytes public/flair/img/objects.flat-shoe.webp | Bin 0 -> 1178 bytes public/flair/img/objects.floppy-disk.webp | Bin 0 -> 570 bytes public/flair/img/objects.flute.webp | Bin 0 -> 1636 bytes .../flair/img/objects.folding-hand-fan.webp | Bin 0 -> 1688 bytes public/flair/img/objects.fountain-pen.webp | Bin 0 -> 1578 bytes public/flair/img/objects.four-oclock.webp | Bin 0 -> 1442 bytes public/flair/img/objects.four-thirty.webp | Bin 0 -> 1452 bytes public/flair/img/objects.funeral-urn.webp | Bin 0 -> 1398 bytes public/flair/img/objects.gear.webp | Bin 0 -> 1450 bytes public/flair/img/objects.gem-stone.webp | Bin 0 -> 1484 bytes public/flair/img/objects.glasses.webp | Bin 0 -> 1340 bytes public/flair/img/objects.gloves.webp | Bin 0 -> 1844 bytes public/flair/img/objects.goggles.webp | Bin 0 -> 2036 bytes public/flair/img/objects.graduation-cap.webp | Bin 0 -> 1406 bytes public/flair/img/objects.green-book.webp | Bin 0 -> 554 bytes public/flair/img/objects.guitar.webp | Bin 0 -> 1944 bytes public/flair/img/objects.hair-pick.webp | Bin 0 -> 1678 bytes public/flair/img/objects.hammer-and-pick.webp | Bin 0 -> 2288 bytes .../flair/img/objects.hammer-and-wrench.webp | Bin 0 -> 2436 bytes public/flair/img/objects.hammer.webp | Bin 0 -> 1470 bytes public/flair/img/objects.hamsa.webp | Bin 0 -> 1502 bytes public/flair/img/objects.handbag.webp | Bin 0 -> 1732 bytes public/flair/img/objects.headphone.webp | Bin 0 -> 1730 bytes public/flair/img/objects.headstone.webp | Bin 0 -> 720 bytes .../flair/img/objects.high-heeled-shoe.webp | Bin 0 -> 1726 bytes public/flair/img/objects.hiking-boot.webp | Bin 0 -> 1722 bytes public/flair/img/objects.hook.webp | Bin 0 -> 1802 bytes public/flair/img/objects.hourglass-done.webp | Bin 0 -> 2002 bytes .../flair/img/objects.hourglass-not-done.webp | Bin 0 -> 2178 bytes .../img/objects.identification-card.webp | Bin 0 -> 720 bytes public/flair/img/objects.inbox-tray.webp | Bin 0 -> 1282 bytes .../flair/img/objects.incoming-envelope.webp | Bin 0 -> 1238 bytes public/flair/img/objects.jeans.webp | Bin 0 -> 1096 bytes public/flair/img/objects.key.webp | Bin 0 -> 1756 bytes public/flair/img/objects.keyboard.webp | Bin 0 -> 920 bytes public/flair/img/objects.kimono.webp | Bin 0 -> 2042 bytes public/flair/img/objects.knot.webp | Bin 0 -> 1986 bytes public/flair/img/objects.lab-coat.webp | Bin 0 -> 1880 bytes public/flair/img/objects.label.webp | Bin 0 -> 1730 bytes public/flair/img/objects.ladder.webp | Bin 0 -> 1092 bytes public/flair/img/objects.laptop.webp | Bin 0 -> 1026 bytes public/flair/img/objects.ledger.webp | Bin 0 -> 1326 bytes public/flair/img/objects.level-slider.webp | Bin 0 -> 854 bytes public/flair/img/objects.light-bulb.webp | Bin 0 -> 1592 bytes public/flair/img/objects.link.webp | Bin 0 -> 1838 bytes .../flair/img/objects.linked-paperclips.webp | Bin 0 -> 3306 bytes public/flair/img/objects.lipstick.webp | Bin 0 -> 1072 bytes public/flair/img/objects.locked-with-key.webp | Bin 0 -> 1920 bytes public/flair/img/objects.locked-with-pen.webp | Bin 0 -> 1992 bytes public/flair/img/objects.locked.webp | Bin 0 -> 1318 bytes public/flair/img/objects.long-drum.webp | Bin 0 -> 1824 bytes public/flair/img/objects.lotion-bottle.webp | Bin 0 -> 1310 bytes public/flair/img/objects.loudspeaker.webp | Bin 0 -> 1814 bytes public/flair/img/objects.low-battery.webp | Bin 0 -> 940 bytes public/flair/img/objects.luggage.webp | Bin 0 -> 1890 bytes public/flair/img/objects.magnet.webp | Bin 0 -> 1168 bytes .../objects.magnifying-glass-tilted-left.webp | Bin 0 -> 1928 bytes ...objects.magnifying-glass-tilted-right.webp | Bin 0 -> 1926 bytes public/flair/img/objects.mans-shoe.webp | Bin 0 -> 1124 bytes .../flair/img/objects.mantelpiece-clock.webp | Bin 0 -> 1574 bytes public/flair/img/objects.maracas.webp | Bin 0 -> 2580 bytes public/flair/img/objects.megaphone.webp | Bin 0 -> 1676 bytes public/flair/img/objects.memo.webp | Bin 0 -> 1216 bytes public/flair/img/objects.microphone.webp | Bin 0 -> 1222 bytes public/flair/img/objects.microscope.webp | Bin 0 -> 1854 bytes public/flair/img/objects.mirror.webp | Bin 0 -> 2104 bytes .../img/objects.mobile-phone-with-arrow.webp | Bin 0 -> 1408 bytes public/flair/img/objects.mobile-phone.webp | Bin 0 -> 1038 bytes public/flair/img/objects.money-bag.webp | Bin 0 -> 1916 bytes .../flair/img/objects.money-with-wings.webp | Bin 0 -> 2048 bytes public/flair/img/objects.mouse-trap.webp | Bin 0 -> 1698 bytes public/flair/img/objects.movie-camera.webp | Bin 0 -> 1288 bytes .../flair/img/objects.musical-keyboard.webp | Bin 0 -> 694 bytes public/flair/img/objects.musical-note.webp | Bin 0 -> 880 bytes public/flair/img/objects.musical-notes.webp | Bin 0 -> 1594 bytes public/flair/img/objects.musical-score.webp | Bin 0 -> 1824 bytes public/flair/img/objects.muted-speaker.webp | Bin 0 -> 1782 bytes public/flair/img/objects.nazar-amulet.webp | Bin 0 -> 1916 bytes public/flair/img/objects.necktie.webp | Bin 0 -> 1218 bytes public/flair/img/objects.newspaper.webp | Bin 0 -> 868 bytes public/flair/img/objects.nine-oclock.webp | Bin 0 -> 1424 bytes public/flair/img/objects.nine-thirty.webp | Bin 0 -> 1456 bytes ...bjects.notebook-with-decorative-cover.webp | Bin 0 -> 722 bytes public/flair/img/objects.notebook.webp | Bin 0 -> 1422 bytes public/flair/img/objects.nut-and-bolt.webp | Bin 0 -> 1536 bytes public/flair/img/objects.old-key.webp | Bin 0 -> 1738 bytes public/flair/img/objects.one-oclock.webp | Bin 0 -> 1450 bytes .../flair/img/objects.one-piece-swimsuit.webp | Bin 0 -> 1452 bytes public/flair/img/objects.one-thirty.webp | Bin 0 -> 1456 bytes public/flair/img/objects.open-book.webp | Bin 0 -> 1494 bytes .../flair/img/objects.open-file-folder.webp | Bin 0 -> 1252 bytes ...bjects.open-mailbox-with-lowered-flag.webp | Bin 0 -> 1040 bytes ...objects.open-mailbox-with-raised-flag.webp | Bin 0 -> 1366 bytes public/flair/img/objects.optical-disk.webp | Bin 0 -> 1592 bytes public/flair/img/objects.orange-book.webp | Bin 0 -> 624 bytes public/flair/img/objects.outbox-tray.webp | Bin 0 -> 1346 bytes public/flair/img/objects.package.webp | Bin 0 -> 1242 bytes public/flair/img/objects.page-facing-up.webp | Bin 0 -> 620 bytes public/flair/img/objects.page-with-curl.webp | Bin 0 -> 662 bytes public/flair/img/objects.pager.webp | Bin 0 -> 1114 bytes public/flair/img/objects.paintbrush.webp | Bin 0 -> 1428 bytes public/flair/img/objects.paperclip.webp | Bin 0 -> 2402 bytes public/flair/img/objects.pen.webp | Bin 0 -> 1578 bytes public/flair/img/objects.pencil.webp | Bin 0 -> 1348 bytes public/flair/img/objects.petri-dish.webp | Bin 0 -> 1750 bytes public/flair/img/objects.pick.webp | Bin 0 -> 1564 bytes public/flair/img/objects.pill.webp | Bin 0 -> 1338 bytes public/flair/img/objects.placard.webp | Bin 0 -> 650 bytes public/flair/img/objects.plunger.webp | Bin 0 -> 1352 bytes public/flair/img/objects.postal-horn.webp | Bin 0 -> 2220 bytes public/flair/img/objects.postbox.webp | Bin 0 -> 982 bytes public/flair/img/objects.pound-banknote.webp | Bin 0 -> 830 bytes public/flair/img/objects.prayer-beads.webp | Bin 0 -> 2570 bytes public/flair/img/objects.printer.webp | Bin 0 -> 1248 bytes public/flair/img/objects.purse.webp | Bin 0 -> 1294 bytes public/flair/img/objects.pushpin.webp | Bin 0 -> 1636 bytes public/flair/img/objects.radio.webp | Bin 0 -> 1668 bytes public/flair/img/objects.razor.webp | Bin 0 -> 1550 bytes public/flair/img/objects.receipt.webp | Bin 0 -> 1446 bytes .../flair/img/objects.red-paper-lantern.webp | Bin 0 -> 1308 bytes public/flair/img/objects.reminder-ribbon.webp | Bin 0 -> 1890 bytes .../img/objects.rescue-workers-helmet.webp | Bin 0 -> 1930 bytes public/flair/img/objects.ribbon.webp | Bin 0 -> 2120 bytes public/flair/img/objects.ring.webp | Bin 0 -> 1816 bytes public/flair/img/objects.roll-of-paper.webp | Bin 0 -> 968 bytes .../img/objects.rolled-up-newspaper.webp | Bin 0 -> 1564 bytes public/flair/img/objects.round-pushpin.webp | Bin 0 -> 1018 bytes public/flair/img/objects.running-shoe.webp | Bin 0 -> 1604 bytes public/flair/img/objects.safety-pin.webp | Bin 0 -> 1992 bytes public/flair/img/objects.safety-vest.webp | Bin 0 -> 1768 bytes public/flair/img/objects.sari.webp | Bin 0 -> 2064 bytes .../flair/img/objects.satellite-antenna.webp | Bin 0 -> 1916 bytes public/flair/img/objects.saxophone.webp | Bin 0 -> 1764 bytes public/flair/img/objects.scarf.webp | Bin 0 -> 1842 bytes public/flair/img/objects.scissors.webp | Bin 0 -> 2266 bytes public/flair/img/objects.screwdriver.webp | Bin 0 -> 1292 bytes public/flair/img/objects.scroll.webp | Bin 0 -> 1156 bytes public/flair/img/objects.seven-oclock.webp | Bin 0 -> 1440 bytes public/flair/img/objects.seven-thirty.webp | Bin 0 -> 1462 bytes public/flair/img/objects.sewing-needle.webp | Bin 0 -> 2656 bytes public/flair/img/objects.shield.webp | Bin 0 -> 1476 bytes public/flair/img/objects.shopping-bags.webp | Bin 0 -> 1872 bytes public/flair/img/objects.shopping-cart.webp | Bin 0 -> 2346 bytes public/flair/img/objects.shorts.webp | Bin 0 -> 1852 bytes public/flair/img/objects.shower.webp | Bin 0 -> 2692 bytes public/flair/img/objects.six-oclock.webp | Bin 0 -> 1426 bytes public/flair/img/objects.six-thirty.webp | Bin 0 -> 1454 bytes public/flair/img/objects.soap.webp | Bin 0 -> 1792 bytes public/flair/img/objects.socks.webp | Bin 0 -> 1926 bytes .../img/objects.speaker-high-volume.webp | Bin 0 -> 2222 bytes .../flair/img/objects.speaker-low-volume.webp | Bin 0 -> 1224 bytes .../img/objects.speaker-medium-volume.webp | Bin 0 -> 1444 bytes public/flair/img/objects.spiral-calendar.webp | Bin 0 -> 1786 bytes public/flair/img/objects.spiral-notepad.webp | Bin 0 -> 1330 bytes public/flair/img/objects.sponge.webp | Bin 0 -> 2150 bytes public/flair/img/objects.stethoscope.webp | Bin 0 -> 2552 bytes public/flair/img/objects.stopwatch.webp | Bin 0 -> 1548 bytes public/flair/img/objects.straight-ruler.webp | Bin 0 -> 1260 bytes .../flair/img/objects.studio-microphone.webp | Bin 0 -> 1684 bytes public/flair/img/objects.sunglasses.webp | Bin 0 -> 886 bytes public/flair/img/objects.syringe.webp | Bin 0 -> 1318 bytes public/flair/img/objects.t-shirt.webp | Bin 0 -> 1314 bytes .../flair/img/objects.tear-off-calendar.webp | Bin 0 -> 1324 bytes public/flair/img/objects.teddy-bear.webp | Bin 0 -> 1670 bytes .../flair/img/objects.telephone-receiver.webp | Bin 0 -> 1058 bytes public/flair/img/objects.telephone.webp | Bin 0 -> 1602 bytes public/flair/img/objects.telescope.webp | Bin 0 -> 1790 bytes public/flair/img/objects.television.webp | Bin 0 -> 1064 bytes public/flair/img/objects.ten-oclock.webp | Bin 0 -> 1448 bytes public/flair/img/objects.ten-thirty.webp | Bin 0 -> 1442 bytes public/flair/img/objects.test-tube.webp | Bin 0 -> 1646 bytes public/flair/img/objects.thermometer.webp | Bin 0 -> 1152 bytes public/flair/img/objects.thong-sandal.webp | Bin 0 -> 1302 bytes public/flair/img/objects.thread.webp | Bin 0 -> 1580 bytes public/flair/img/objects.three-oclock.webp | Bin 0 -> 1426 bytes public/flair/img/objects.three-thirty.webp | Bin 0 -> 1448 bytes public/flair/img/objects.timer-clock.webp | Bin 0 -> 1486 bytes public/flair/img/objects.toilet.webp | Bin 0 -> 1192 bytes public/flair/img/objects.toolbox.webp | Bin 0 -> 1014 bytes public/flair/img/objects.toothbrush.webp | Bin 0 -> 1506 bytes public/flair/img/objects.top-hat.webp | Bin 0 -> 1222 bytes public/flair/img/objects.trackball.webp | Bin 0 -> 1324 bytes .../flair/img/objects.triangular-ruler.webp | Bin 0 -> 1148 bytes public/flair/img/objects.trumpet.webp | Bin 0 -> 2082 bytes public/flair/img/objects.twelve-oclock.webp | Bin 0 -> 1414 bytes public/flair/img/objects.twelve-thirty.webp | Bin 0 -> 1454 bytes public/flair/img/objects.two-oclocktime.webp | Bin 0 -> 1462 bytes public/flair/img/objects.two-thirty.webp | Bin 0 -> 1452 bytes public/flair/img/objects.unlocked.webp | Bin 0 -> 1398 bytes public/flair/img/objects.video-camera.webp | Bin 0 -> 1038 bytes public/flair/img/objects.videocassette.webp | Bin 0 -> 746 bytes public/flair/img/objects.violin.webp | Bin 0 -> 2546 bytes public/flair/img/objects.wastebasket.webp | Bin 0 -> 2930 bytes public/flair/img/objects.watch.webp | Bin 0 -> 1462 bytes public/flair/img/objects.white-cane.webp | Bin 0 -> 1094 bytes public/flair/img/objects.window.webp | Bin 0 -> 838 bytes public/flair/img/objects.womans-boot.webp | Bin 0 -> 1386 bytes public/flair/img/objects.womans-clothes.webp | Bin 0 -> 1380 bytes public/flair/img/objects.womans-hat.webp | Bin 0 -> 1730 bytes public/flair/img/objects.womans-sandal.webp | Bin 0 -> 1124 bytes public/flair/img/objects.wrench.webp | Bin 0 -> 1476 bytes public/flair/img/objects.x-ray.webp | Bin 0 -> 1686 bytes public/flair/img/objects.xmas-bell.webp | Bin 0 -> 3562 bytes public/flair/img/objects.xmas-candle.webp | Bin 0 -> 2014 bytes public/flair/img/objects.xmas-hat.webp | Bin 0 -> 1948 bytes public/flair/img/objects.yarn.webp | Bin 0 -> 2422 bytes public/flair/img/objects.yen-banknote.webp | Bin 0 -> 828 bytes public/flair/img/people.anatomical-heart.webp | Bin 0 -> 1646 bytes .../img/people.artist-dark-skin-tone.webp | Bin 0 -> 1982 bytes .../img/people.artist-light-skin-tone.webp | Bin 0 -> 2146 bytes .../people.artist-medium-dark-skin-tone.webp | Bin 0 -> 2040 bytes .../people.artist-medium-light-skin-tone.webp | Bin 0 -> 2180 bytes .../img/people.artist-medium-skin-tone.webp | Bin 0 -> 2060 bytes public/flair/img/people.artist.webp | Bin 0 -> 2246 bytes .../img/people.astronaut-dark-skin-tone.webp | Bin 0 -> 1412 bytes .../img/people.astronaut-light-skin-tone.webp | Bin 0 -> 1578 bytes ...eople.astronaut-medium-dark-skin-tone.webp | Bin 0 -> 1496 bytes ...ople.astronaut-medium-light-skin-tone.webp | Bin 0 -> 1576 bytes .../people.astronaut-medium-skin-tone.webp | Bin 0 -> 1500 bytes public/flair/img/people.astronaut.webp | Bin 0 -> 1698 bytes .../img/people.baby-angel-dark-skin-tone.webp | Bin 0 -> 1908 bytes .../people.baby-angel-light-skin-tone.webp | Bin 0 -> 2046 bytes ...ople.baby-angel-medium-dark-skin-tone.webp | Bin 0 -> 2002 bytes ...ple.baby-angel-medium-light-skin-tone.webp | Bin 0 -> 1946 bytes .../people.baby-angel-medium-skin-tone.webp | Bin 0 -> 2000 bytes public/flair/img/people.baby-angel.webp | Bin 0 -> 2206 bytes .../flair/img/people.baby-dark-skin-tone.webp | Bin 0 -> 1142 bytes .../img/people.baby-light-skin-tone.webp | Bin 0 -> 1422 bytes .../people.baby-medium-dark-skin-tone.webp | Bin 0 -> 1314 bytes .../people.baby-medium-light-skin-tone.webp | Bin 0 -> 1364 bytes .../img/people.baby-medium-skin-tone.webp | Bin 0 -> 1326 bytes public/flair/img/people.baby.webp | Bin 0 -> 1638 bytes ...nd-index-pointing-down-dark-skin-tone.webp | Bin 0 -> 1084 bytes ...d-index-pointing-down-light-skin-tone.webp | Bin 0 -> 1298 bytes ...x-pointing-down-medium-dark-skin-tone.webp | Bin 0 -> 1190 bytes ...-pointing-down-medium-light-skin-tone.webp | Bin 0 -> 1266 bytes ...-index-pointing-down-medium-skin-tone.webp | Bin 0 -> 1218 bytes .../people.backhand-index-pointing-down.webp | Bin 0 -> 1424 bytes ...nd-index-pointing-left-dark-skin-tone.webp | Bin 0 -> 1046 bytes ...d-index-pointing-left-light-skin-tone.webp | Bin 0 -> 1296 bytes ...x-pointing-left-medium-dark-skin-tone.webp | Bin 0 -> 1206 bytes ...-pointing-left-medium-light-skin-tone.webp | Bin 0 -> 1278 bytes ...-index-pointing-left-medium-skin-tone.webp | Bin 0 -> 1234 bytes .../people.backhand-index-pointing-left.webp | Bin 0 -> 1470 bytes ...d-index-pointing-right-dark-skin-tone.webp | Bin 0 -> 1046 bytes ...-index-pointing-right-light-skin-tone.webp | Bin 0 -> 1304 bytes ...-pointing-right-medium-dark-skin-tone.webp | Bin 0 -> 1182 bytes ...pointing-right-medium-light-skin-tone.webp | Bin 0 -> 1270 bytes ...index-pointing-right-medium-skin-tone.webp | Bin 0 -> 1212 bytes .../people.backhand-index-pointing-right.webp | Bin 0 -> 1434 bytes ...hand-index-pointing-up-dark-skin-tone.webp | Bin 0 -> 1082 bytes ...and-index-pointing-up-light-skin-tone.webp | Bin 0 -> 1320 bytes ...dex-pointing-up-medium-dark-skin-tone.webp | Bin 0 -> 1228 bytes ...ex-pointing-up-medium-light-skin-tone.webp | Bin 0 -> 1304 bytes ...nd-index-pointing-up-medium-skin-tone.webp | Bin 0 -> 1244 bytes .../people.backhand-index-pointing-up.webp | Bin 0 -> 1482 bytes public/flair/img/people.bald.webp | Bin 0 -> 1880 bytes public/flair/img/people.biting-lip.webp | Bin 0 -> 1180 bytes public/flair/img/people.bone.webp | Bin 0 -> 1254 bytes .../flair/img/people.boy-dark-skin-tone.webp | Bin 0 -> 1218 bytes .../flair/img/people.boy-light-skin-tone.webp | Bin 0 -> 1558 bytes .../img/people.boy-medium-dark-skin-tone.webp | Bin 0 -> 1368 bytes .../people.boy-medium-light-skin-tone.webp | Bin 0 -> 1536 bytes .../img/people.boy-medium-skin-tone.webp | Bin 0 -> 1400 bytes public/flair/img/people.boy.webp | Bin 0 -> 1746 bytes public/flair/img/people.brain.webp | Bin 0 -> 1732 bytes .../people.breast-feeding-dark-skin-tone.webp | Bin 0 -> 1446 bytes ...people.breast-feeding-light-skin-tone.webp | Bin 0 -> 1580 bytes ....breast-feeding-medium-dark-skin-tone.webp | Bin 0 -> 1500 bytes ...breast-feeding-medium-light-skin-tone.webp | Bin 0 -> 1628 bytes ...eople.breast-feeding-medium-skin-tone.webp | Bin 0 -> 1516 bytes public/flair/img/people.breast-feeding.webp | Bin 0 -> 1660 bytes .../flair/img/people.bust-in-silhouette.webp | Bin 0 -> 1212 bytes .../flair/img/people.busts-in-silhouette.webp | Bin 0 -> 1462 bytes .../people.call-me-hand-dark-skin-tone.webp | Bin 0 -> 1138 bytes .../people.call-me-hand-light-skin-tone.webp | Bin 0 -> 1406 bytes ...le.call-me-hand-medium-dark-skin-tone.webp | Bin 0 -> 1290 bytes ...e.call-me-hand-medium-light-skin-tone.webp | Bin 0 -> 1372 bytes .../people.call-me-hand-medium-skin-tone.webp | Bin 0 -> 1332 bytes public/flair/img/people.call-me-hand.webp | Bin 0 -> 1550 bytes .../img/people.child-dark-skin-tone.webp | Bin 0 -> 1276 bytes .../img/people.child-light-skin-tone.webp | Bin 0 -> 1642 bytes .../people.child-medium-dark-skin-tone.webp | Bin 0 -> 1432 bytes .../people.child-medium-light-skin-tone.webp | Bin 0 -> 1636 bytes .../img/people.child-medium-skin-tone.webp | Bin 0 -> 1480 bytes public/flair/img/people.child.webp | Bin 0 -> 1836 bytes .../people.clapping-hands-dark-skin-tone.webp | Bin 0 -> 1774 bytes ...people.clapping-hands-light-skin-tone.webp | Bin 0 -> 2040 bytes ....clapping-hands-medium-dark-skin-tone.webp | Bin 0 -> 1940 bytes ...clapping-hands-medium-light-skin-tone.webp | Bin 0 -> 1976 bytes ...eople.clapping-hands-medium-skin-tone.webp | Bin 0 -> 1950 bytes public/flair/img/people.clapping-hands.webp | Bin 0 -> 2158 bytes ...le.construction-worker-dark-skin-tone.webp | Bin 0 -> 1914 bytes ...e.construction-worker-light-skin-tone.webp | Bin 0 -> 2044 bytes ...truction-worker-medium-dark-skin-tone.webp | Bin 0 -> 1946 bytes ...ruction-worker-medium-light-skin-tone.webp | Bin 0 -> 1972 bytes ....construction-worker-medium-skin-tone.webp | Bin 0 -> 1950 bytes .../flair/img/people.construction-worker.webp | Bin 0 -> 2076 bytes .../flair/img/people.cook-dark-skin-tone.webp | Bin 0 -> 1894 bytes .../img/people.cook-light-skin-tone.webp | Bin 0 -> 2020 bytes .../people.cook-medium-dark-skin-tone.webp | Bin 0 -> 1974 bytes .../people.cook-medium-light-skin-tone.webp | Bin 0 -> 2024 bytes .../img/people.cook-medium-skin-tone.webp | Bin 0 -> 1966 bytes public/flair/img/people.cook.webp | Bin 0 -> 2174 bytes ...ople.couple-with-heart-dark-skin-tone.webp | Bin 0 -> 2082 bytes ...ple.couple-with-heart-light-skin-tone.webp | Bin 0 -> 2308 bytes ...an-man-dark-skin-tone-light-skin-tone.webp | Bin 0 -> 2102 bytes ...-dark-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2014 bytes ...dark-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2086 bytes ...n-man-dark-skin-tone-medium-skin-tone.webp | Bin 0 -> 2016 bytes ...ple-with-heart-man-man-dark-skin-tone.webp | Bin 0 -> 1966 bytes ...an-man-light-skin-tone-dark-skin-tone.webp | Bin 0 -> 2120 bytes ...light-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2152 bytes ...ight-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2228 bytes ...-man-light-skin-tone-medium-skin-tone.webp | Bin 0 -> 2158 bytes ...le-with-heart-man-man-light-skin-tone.webp | Bin 0 -> 2226 bytes ...-medium-dark-skin-tone-dark-skin-tone.webp | Bin 0 -> 2018 bytes ...medium-dark-skin-tone-light-skin-tone.webp | Bin 0 -> 2148 bytes ...dark-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2128 bytes ...edium-dark-skin-tone-medium-skin-tone.webp | Bin 0 -> 2054 bytes ...h-heart-man-man-medium-dark-skin-tone.webp | Bin 0 -> 2048 bytes ...medium-light-skin-tone-dark-skin-tone.webp | Bin 0 -> 2098 bytes ...edium-light-skin-tone-light-skin-tone.webp | Bin 0 -> 2222 bytes ...light-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2136 bytes ...dium-light-skin-tone-medium-skin-tone.webp | Bin 0 -> 2128 bytes ...-heart-man-man-medium-light-skin-tone.webp | Bin 0 -> 2180 bytes ...n-man-medium-skin-tone-dark-skin-tone.webp | Bin 0 -> 2046 bytes ...-man-medium-skin-tone-light-skin-tone.webp | Bin 0 -> 2172 bytes ...edium-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2086 bytes ...dium-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2160 bytes ...e-with-heart-man-man-medium-skin-tone.webp | Bin 0 -> 2082 bytes .../img/people.couple-with-heart-man-man.webp | Bin 0 -> 2324 bytes ...uple-with-heart-medium-dark-skin-tone.webp | Bin 0 -> 2128 bytes ...ple-with-heart-medium-light-skin-tone.webp | Bin 0 -> 2240 bytes ...le.couple-with-heart-medium-skin-tone.webp | Bin 0 -> 2150 bytes ...person-dark-skin-tone-light-skin-tone.webp | Bin 0 -> 2194 bytes ...-dark-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2112 bytes ...dark-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2188 bytes ...erson-dark-skin-tone-medium-skin-tone.webp | Bin 0 -> 2122 bytes ...person-light-skin-tone-dark-skin-tone.webp | Bin 0 -> 2214 bytes ...light-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2230 bytes ...ight-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2306 bytes ...rson-light-skin-tone-medium-skin-tone.webp | Bin 0 -> 2242 bytes ...-medium-dark-skin-tone-dark-skin-tone.webp | Bin 0 -> 2124 bytes ...medium-dark-skin-tone-light-skin-tone.webp | Bin 0 -> 2218 bytes ...dark-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2206 bytes ...edium-dark-skin-tone-medium-skin-tone.webp | Bin 0 -> 2142 bytes ...medium-light-skin-tone-dark-skin-tone.webp | Bin 0 -> 2202 bytes ...edium-light-skin-tone-light-skin-tone.webp | Bin 0 -> 2306 bytes ...light-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2218 bytes ...dium-light-skin-tone-medium-skin-tone.webp | Bin 0 -> 2220 bytes ...erson-medium-skin-tone-dark-skin-tone.webp | Bin 0 -> 2142 bytes ...rson-medium-skin-tone-light-skin-tone.webp | Bin 0 -> 2244 bytes ...edium-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2156 bytes ...dium-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2222 bytes ...an-man-dark-skin-tone-light-skin-tone.webp | Bin 0 -> 2068 bytes ...-dark-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 1968 bytes ...dark-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2052 bytes ...n-man-dark-skin-tone-medium-skin-tone.webp | Bin 0 -> 1982 bytes ...e-with-heart-woman-man-dark-skin-tone.webp | Bin 0 -> 1928 bytes ...an-man-light-skin-tone-dark-skin-tone.webp | Bin 0 -> 2048 bytes ...light-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2084 bytes ...ight-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2166 bytes ...-man-light-skin-tone-medium-skin-tone.webp | Bin 0 -> 2096 bytes ...-with-heart-woman-man-light-skin-tone.webp | Bin 0 -> 2174 bytes ...-medium-dark-skin-tone-dark-skin-tone.webp | Bin 0 -> 1964 bytes ...medium-dark-skin-tone-light-skin-tone.webp | Bin 0 -> 2096 bytes ...dark-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2084 bytes ...edium-dark-skin-tone-medium-skin-tone.webp | Bin 0 -> 2012 bytes ...heart-woman-man-medium-dark-skin-tone.webp | Bin 0 -> 2000 bytes ...medium-light-skin-tone-dark-skin-tone.webp | Bin 0 -> 2066 bytes ...edium-light-skin-tone-light-skin-tone.webp | Bin 0 -> 2188 bytes ...light-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2098 bytes ...dium-light-skin-tone-medium-skin-tone.webp | Bin 0 -> 2102 bytes ...eart-woman-man-medium-light-skin-tone.webp | Bin 0 -> 2140 bytes ...n-man-medium-skin-tone-dark-skin-tone.webp | Bin 0 -> 1990 bytes ...-man-medium-skin-tone-light-skin-tone.webp | Bin 0 -> 2124 bytes ...edium-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2020 bytes ...dium-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2108 bytes ...with-heart-woman-man-medium-skin-tone.webp | Bin 0 -> 2030 bytes .../people.couple-with-heart-woman-man.webp | Bin 0 -> 2256 bytes ...-woman-dark-skin-tone-light-skin-tone.webp | Bin 0 -> 1970 bytes ...-dark-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 1874 bytes ...dark-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 1994 bytes ...woman-dark-skin-tone-medium-skin-tone.webp | Bin 0 -> 1908 bytes ...with-heart-woman-woman-dark-skin-tone.webp | Bin 0 -> 1858 bytes ...-woman-light-skin-tone-dark-skin-tone.webp | Bin 0 -> 1988 bytes ...light-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 1998 bytes ...ight-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2112 bytes ...oman-light-skin-tone-medium-skin-tone.webp | Bin 0 -> 2030 bytes ...ith-heart-woman-woman-light-skin-tone.webp | Bin 0 -> 2078 bytes ...-medium-dark-skin-tone-dark-skin-tone.webp | Bin 0 -> 1902 bytes ...medium-dark-skin-tone-light-skin-tone.webp | Bin 0 -> 1998 bytes ...dark-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2034 bytes ...edium-dark-skin-tone-medium-skin-tone.webp | Bin 0 -> 1942 bytes ...art-woman-woman-medium-dark-skin-tone.webp | Bin 0 -> 1908 bytes ...medium-light-skin-tone-dark-skin-tone.webp | Bin 0 -> 2012 bytes ...edium-light-skin-tone-light-skin-tone.webp | Bin 0 -> 2110 bytes ...light-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2012 bytes ...dium-light-skin-tone-medium-skin-tone.webp | Bin 0 -> 2034 bytes ...rt-woman-woman-medium-light-skin-tone.webp | Bin 0 -> 2084 bytes ...woman-medium-skin-tone-dark-skin-tone.webp | Bin 0 -> 1932 bytes ...oman-medium-skin-tone-light-skin-tone.webp | Bin 0 -> 2028 bytes ...edium-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 1940 bytes ...dium-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2048 bytes ...th-heart-woman-woman-medium-skin-tone.webp | Bin 0 -> 1954 bytes .../people.couple-with-heart-woman-woman.webp | Bin 0 -> 2154 bytes .../flair/img/people.couple-with-heart.webp | Bin 0 -> 2358 bytes ...people.crossed-fingers-dark-skin-tone.webp | Bin 0 -> 1084 bytes ...eople.crossed-fingers-light-skin-tone.webp | Bin 0 -> 1304 bytes ...crossed-fingers-medium-dark-skin-tone.webp | Bin 0 -> 1206 bytes ...rossed-fingers-medium-light-skin-tone.webp | Bin 0 -> 1300 bytes ...ople.crossed-fingers-medium-skin-tone.webp | Bin 0 -> 1248 bytes public/flair/img/people.crossed-fingers.webp | Bin 0 -> 1454 bytes public/flair/img/people.curly-hair.webp | Bin 0 -> 1430 bytes public/flair/img/people.dark-skin-tone.webp | Bin 0 -> 206 bytes .../img/people.deaf-man-dark-skin-tone.webp | Bin 0 -> 1550 bytes .../img/people.deaf-man-light-skin-tone.webp | Bin 0 -> 1834 bytes ...people.deaf-man-medium-dark-skin-tone.webp | Bin 0 -> 1642 bytes ...eople.deaf-man-medium-light-skin-tone.webp | Bin 0 -> 1722 bytes .../img/people.deaf-man-medium-skin-tone.webp | Bin 0 -> 1664 bytes public/flair/img/people.deaf-man.webp | Bin 0 -> 1934 bytes .../people.deaf-person-dark-skin-tone.webp | Bin 0 -> 1598 bytes .../people.deaf-person-light-skin-tone.webp | Bin 0 -> 1806 bytes ...ple.deaf-person-medium-dark-skin-tone.webp | Bin 0 -> 1626 bytes ...le.deaf-person-medium-light-skin-tone.webp | Bin 0 -> 1662 bytes .../people.deaf-person-medium-skin-tone.webp | Bin 0 -> 1644 bytes public/flair/img/people.deaf-person.webp | Bin 0 -> 1856 bytes .../img/people.deaf-woman-dark-skin-tone.webp | Bin 0 -> 1302 bytes .../people.deaf-woman-light-skin-tone.webp | Bin 0 -> 1614 bytes ...ople.deaf-woman-medium-dark-skin-tone.webp | Bin 0 -> 1398 bytes ...ple.deaf-woman-medium-light-skin-tone.webp | Bin 0 -> 1470 bytes .../people.deaf-woman-medium-skin-tone.webp | Bin 0 -> 1422 bytes public/flair/img/people.deaf-woman.webp | Bin 0 -> 1696 bytes .../img/people.detective-dark-skin-tone.webp | Bin 0 -> 1616 bytes .../img/people.detective-light-skin-tone.webp | Bin 0 -> 1830 bytes ...eople.detective-medium-dark-skin-tone.webp | Bin 0 -> 1716 bytes ...ople.detective-medium-light-skin-tone.webp | Bin 0 -> 1816 bytes .../people.detective-medium-skin-tone.webp | Bin 0 -> 1714 bytes public/flair/img/people.detective.webp | Bin 0 -> 1962 bytes public/flair/img/people.dna.webp | Bin 0 -> 2516 bytes public/flair/img/people.drop-of-blood.webp | Bin 0 -> 1162 bytes .../flair/img/people.ear-dark-skin-tone.webp | Bin 0 -> 994 bytes .../flair/img/people.ear-light-skin-tone.webp | Bin 0 -> 1188 bytes .../img/people.ear-medium-dark-skin-tone.webp | Bin 0 -> 1132 bytes .../people.ear-medium-light-skin-tone.webp | Bin 0 -> 1192 bytes .../img/people.ear-medium-skin-tone.webp | Bin 0 -> 1172 bytes ...e.ear-with-hearing-aid-dark-skin-tone.webp | Bin 0 -> 1584 bytes ....ear-with-hearing-aid-light-skin-tone.webp | Bin 0 -> 1732 bytes ...ith-hearing-aid-medium-dark-skin-tone.webp | Bin 0 -> 1652 bytes ...th-hearing-aid-medium-light-skin-tone.webp | Bin 0 -> 1720 bytes ...ear-with-hearing-aid-medium-skin-tone.webp | Bin 0 -> 1682 bytes .../img/people.ear-with-hearing-aid.webp | Bin 0 -> 1888 bytes public/flair/img/people.ear.webp | Bin 0 -> 1422 bytes .../flair/img/people.elf-dark-skin-tone.webp | Bin 0 -> 1792 bytes .../flair/img/people.elf-light-skin-tone.webp | Bin 0 -> 1814 bytes .../img/people.elf-medium-dark-skin-tone.webp | Bin 0 -> 1810 bytes .../people.elf-medium-light-skin-tone.webp | Bin 0 -> 1786 bytes .../img/people.elf-medium-skin-tone.webp | Bin 0 -> 1786 bytes public/flair/img/people.elf.webp | Bin 0 -> 1884 bytes public/flair/img/people.eye.webp | Bin 0 -> 1298 bytes public/flair/img/people.eyes.webp | Bin 0 -> 1354 bytes .../people.factory-worker-dark-skin-tone.webp | Bin 0 -> 2126 bytes ...people.factory-worker-light-skin-tone.webp | Bin 0 -> 2304 bytes ....factory-worker-medium-dark-skin-tone.webp | Bin 0 -> 2196 bytes ...factory-worker-medium-light-skin-tone.webp | Bin 0 -> 2262 bytes ...eople.factory-worker-medium-skin-tone.webp | Bin 0 -> 2204 bytes public/flair/img/people.factory-worker.webp | Bin 0 -> 2374 bytes .../img/people.fairy-dark-skin-tone.webp | Bin 0 -> 2162 bytes .../img/people.fairy-light-skin-tone.webp | Bin 0 -> 2192 bytes .../people.fairy-medium-dark-skin-tone.webp | Bin 0 -> 2172 bytes .../people.fairy-medium-light-skin-tone.webp | Bin 0 -> 2170 bytes .../img/people.fairy-medium-skin-tone.webp | Bin 0 -> 2168 bytes public/flair/img/people.fairy.webp | Bin 0 -> 2260 bytes ...people.family-adult-adult-child-child.webp | Bin 0 -> 1072 bytes .../img/people.family-adult-adult-child.webp | Bin 0 -> 894 bytes .../img/people.family-adult-child-child.webp | Bin 0 -> 820 bytes .../flair/img/people.family-adult-child.webp | Bin 0 -> 700 bytes .../flair/img/people.family-man-boy-boy.webp | Bin 0 -> 820 bytes public/flair/img/people.family-man-boy.webp | Bin 0 -> 700 bytes .../flair/img/people.family-man-girl-boy.webp | Bin 0 -> 874 bytes .../img/people.family-man-girl-girl.webp | Bin 0 -> 944 bytes public/flair/img/people.family-man-girl.webp | Bin 0 -> 762 bytes .../img/people.family-man-man-boy-boy.webp | Bin 0 -> 1072 bytes .../flair/img/people.family-man-man-boy.webp | Bin 0 -> 894 bytes .../img/people.family-man-man-girl-boy.webp | Bin 0 -> 1102 bytes .../img/people.family-man-man-girl-girl.webp | Bin 0 -> 1090 bytes .../flair/img/people.family-man-man-girl.webp | Bin 0 -> 986 bytes .../img/people.family-man-woman-boy-boy.webp | Bin 0 -> 1114 bytes .../img/people.family-man-woman-boy.webp | Bin 0 -> 944 bytes .../img/people.family-man-woman-girl-boy.webp | Bin 0 -> 1150 bytes .../people.family-man-woman-girl-girl.webp | Bin 0 -> 1136 bytes .../img/people.family-man-woman-girl.webp | Bin 0 -> 1028 bytes .../img/people.family-woman-boy-boy.webp | Bin 0 -> 848 bytes public/flair/img/people.family-woman-boy.webp | Bin 0 -> 718 bytes .../img/people.family-woman-girl-boy.webp | Bin 0 -> 888 bytes .../img/people.family-woman-girl-girl.webp | Bin 0 -> 968 bytes .../flair/img/people.family-woman-girl.webp | Bin 0 -> 802 bytes .../people.family-woman-woman-boy-boy.webp | Bin 0 -> 1146 bytes .../img/people.family-woman-woman-boy.webp | Bin 0 -> 982 bytes .../people.family-woman-woman-girl-boy.webp | Bin 0 -> 1184 bytes .../people.family-woman-woman-girl-girl.webp | Bin 0 -> 1176 bytes .../img/people.family-woman-woman-girl.webp | Bin 0 -> 1064 bytes public/flair/img/people.family.webp | Bin 0 -> 894 bytes .../img/people.farmer-dark-skin-tone.webp | Bin 0 -> 2434 bytes .../img/people.farmer-light-skin-tone.webp | Bin 0 -> 2554 bytes .../people.farmer-medium-dark-skin-tone.webp | Bin 0 -> 2466 bytes .../people.farmer-medium-light-skin-tone.webp | Bin 0 -> 2428 bytes .../img/people.farmer-medium-skin-tone.webp | Bin 0 -> 2468 bytes public/flair/img/people.farmer.webp | Bin 0 -> 2594 bytes .../people.firefighter-dark-skin-tone.webp | Bin 0 -> 1930 bytes .../people.firefighter-light-skin-tone.webp | Bin 0 -> 2010 bytes ...ple.firefighter-medium-dark-skin-tone.webp | Bin 0 -> 1922 bytes ...le.firefighter-medium-light-skin-tone.webp | Bin 0 -> 1910 bytes .../people.firefighter-medium-skin-tone.webp | Bin 0 -> 1922 bytes public/flair/img/people.firefighter.webp | Bin 0 -> 1996 bytes .../people.flexed-biceps-dark-skin-tone.webp | Bin 0 -> 1256 bytes .../people.flexed-biceps-light-skin-tone.webp | Bin 0 -> 1504 bytes ...e.flexed-biceps-medium-dark-skin-tone.webp | Bin 0 -> 1422 bytes ....flexed-biceps-medium-light-skin-tone.webp | Bin 0 -> 1496 bytes ...people.flexed-biceps-medium-skin-tone.webp | Bin 0 -> 1440 bytes public/flair/img/people.flexed-biceps.webp | Bin 0 -> 1718 bytes .../people.folded-hands-dark-skin-tone.webp | Bin 0 -> 1424 bytes .../people.folded-hands-light-skin-tone.webp | Bin 0 -> 1608 bytes ...le.folded-hands-medium-dark-skin-tone.webp | Bin 0 -> 1484 bytes ...e.folded-hands-medium-light-skin-tone.webp | Bin 0 -> 1588 bytes .../people.folded-hands-medium-skin-tone.webp | Bin 0 -> 1530 bytes public/flair/img/people.folded-hands.webp | Bin 0 -> 1702 bytes .../flair/img/people.foot-dark-skin-tone.webp | Bin 0 -> 1082 bytes .../img/people.foot-light-skin-tone.webp | Bin 0 -> 1350 bytes .../people.foot-medium-dark-skin-tone.webp | Bin 0 -> 1254 bytes .../people.foot-medium-light-skin-tone.webp | Bin 0 -> 1308 bytes .../img/people.foot-medium-skin-tone.webp | Bin 0 -> 1268 bytes public/flair/img/people.foot.webp | Bin 0 -> 1498 bytes public/flair/img/people.footprints.webp | Bin 0 -> 1910 bytes public/flair/img/people.genie.webp | Bin 0 -> 2958 bytes .../flair/img/people.girl-dark-skin-tone.webp | Bin 0 -> 1388 bytes .../img/people.girl-light-skin-tone.webp | Bin 0 -> 1684 bytes .../people.girl-medium-dark-skin-tone.webp | Bin 0 -> 1526 bytes .../people.girl-medium-light-skin-tone.webp | Bin 0 -> 1770 bytes .../img/people.girl-medium-skin-tone.webp | Bin 0 -> 1556 bytes public/flair/img/people.girl.webp | Bin 0 -> 1902 bytes .../img/people.guard-dark-skin-tone.webp | Bin 0 -> 1274 bytes .../img/people.guard-light-skin-tone.webp | Bin 0 -> 1398 bytes .../people.guard-medium-dark-skin-tone.webp | Bin 0 -> 1316 bytes .../people.guard-medium-light-skin-tone.webp | Bin 0 -> 1444 bytes .../img/people.guard-medium-skin-tone.webp | Bin 0 -> 1344 bytes public/flair/img/people.guard.webp | Bin 0 -> 1446 bytes ...d-with-fingers-splayed-dark-skin-tone.webp | Bin 0 -> 1506 bytes ...-with-fingers-splayed-light-skin-tone.webp | Bin 0 -> 1796 bytes ...fingers-splayed-medium-dark-skin-tone.webp | Bin 0 -> 1656 bytes ...ingers-splayed-medium-light-skin-tone.webp | Bin 0 -> 1768 bytes ...with-fingers-splayed-medium-skin-tone.webp | Bin 0 -> 1706 bytes .../img/people.hand-with-fingers-splayed.webp | Bin 0 -> 1950 bytes ...nger-and-thumb-crossed-dark-skin-tone.webp | Bin 0 -> 1208 bytes ...ger-and-thumb-crossed-light-skin-tone.webp | Bin 0 -> 1538 bytes ...d-thumb-crossed-medium-dark-skin-tone.webp | Bin 0 -> 1364 bytes ...-thumb-crossed-medium-light-skin-tone.webp | Bin 0 -> 1472 bytes ...er-and-thumb-crossed-medium-skin-tone.webp | Bin 0 -> 1388 bytes ...d-with-index-finger-and-thumb-crossed.webp | Bin 0 -> 1640 bytes ...dshake-dark-skin-tone-light-skin-tone.webp | Bin 0 -> 1274 bytes ...-dark-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 1190 bytes ...dark-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 1238 bytes ...shake-dark-skin-tone-medium-skin-tone.webp | Bin 0 -> 1190 bytes .../img/people.handshake-dark-skin-tone.webp | Bin 0 -> 1062 bytes ...dshake-light-skin-tone-dark-skin-tone.webp | Bin 0 -> 1326 bytes ...light-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 1374 bytes ...ight-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 1350 bytes ...hake-light-skin-tone-medium-skin-tone.webp | Bin 0 -> 1366 bytes .../img/people.handshake-light-skin-tone.webp | Bin 0 -> 1326 bytes ...-medium-dark-skin-tone-dark-skin-tone.webp | Bin 0 -> 1254 bytes ...medium-dark-skin-tone-light-skin-tone.webp | Bin 0 -> 1324 bytes ...dark-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 1296 bytes ...edium-dark-skin-tone-medium-skin-tone.webp | Bin 0 -> 1280 bytes ...eople.handshake-medium-dark-skin-tone.webp | Bin 0 -> 1262 bytes ...medium-light-skin-tone-dark-skin-tone.webp | Bin 0 -> 1296 bytes ...edium-light-skin-tone-light-skin-tone.webp | Bin 0 -> 1288 bytes ...light-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 1330 bytes ...dium-light-skin-tone-medium-skin-tone.webp | Bin 0 -> 1312 bytes ...ople.handshake-medium-light-skin-tone.webp | Bin 0 -> 1294 bytes ...shake-medium-skin-tone-dark-skin-tone.webp | Bin 0 -> 1246 bytes ...hake-medium-skin-tone-light-skin-tone.webp | Bin 0 -> 1278 bytes ...edium-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 1272 bytes ...dium-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 1276 bytes .../people.handshake-medium-skin-tone.webp | Bin 0 -> 1272 bytes public/flair/img/people.handshake.webp | Bin 0 -> 1448 bytes .../people.health-worker-dark-skin-tone.webp | Bin 0 -> 1564 bytes .../people.health-worker-light-skin-tone.webp | Bin 0 -> 1732 bytes ...e.health-worker-medium-dark-skin-tone.webp | Bin 0 -> 1648 bytes ....health-worker-medium-light-skin-tone.webp | Bin 0 -> 1798 bytes ...people.health-worker-medium-skin-tone.webp | Bin 0 -> 1674 bytes public/flair/img/people.health-worker.webp | Bin 0 -> 1874 bytes .../people.heart-hands-dark-skin-tone.webp | Bin 0 -> 1366 bytes .../people.heart-hands-light-skin-tone.webp | Bin 0 -> 1664 bytes ...ple.heart-hands-medium-dark-skin-tone.webp | Bin 0 -> 1540 bytes ...le.heart-hands-medium-light-skin-tone.webp | Bin 0 -> 1640 bytes .../people.heart-hands-medium-skin-tone.webp | Bin 0 -> 1582 bytes public/flair/img/people.heart-hands.webp | Bin 0 -> 1830 bytes .../people.horse-racing-dark-skin-tone.webp | Bin 0 -> 2248 bytes .../people.horse-racing-light-skin-tone.webp | Bin 0 -> 2282 bytes ...le.horse-racing-medium-dark-skin-tone.webp | Bin 0 -> 2258 bytes ...e.horse-racing-medium-light-skin-tone.webp | Bin 0 -> 2270 bytes .../people.horse-racing-medium-skin-tone.webp | Bin 0 -> 2258 bytes public/flair/img/people.horse-racing.webp | Bin 0 -> 2286 bytes ...pointing-at-the-viewer-dark-skin-tone.webp | Bin 0 -> 1214 bytes ...ointing-at-the-viewer-light-skin-tone.webp | Bin 0 -> 1504 bytes ...g-at-the-viewer-medium-dark-skin-tone.webp | Bin 0 -> 1350 bytes ...-at-the-viewer-medium-light-skin-tone.webp | Bin 0 -> 1484 bytes ...inting-at-the-viewer-medium-skin-tone.webp | Bin 0 -> 1410 bytes .../people.index-pointing-at-the-viewer.webp | Bin 0 -> 1680 bytes ...ople.index-pointing-up-dark-skin-tone.webp | Bin 0 -> 1070 bytes ...ple.index-pointing-up-light-skin-tone.webp | Bin 0 -> 1286 bytes ...dex-pointing-up-medium-dark-skin-tone.webp | Bin 0 -> 1160 bytes ...ex-pointing-up-medium-light-skin-tone.webp | Bin 0 -> 1278 bytes ...le.index-pointing-up-medium-skin-tone.webp | Bin 0 -> 1214 bytes .../flair/img/people.index-pointing-up.webp | Bin 0 -> 1414 bytes .../img/people.judge-dark-skin-tone.webp | Bin 0 -> 1854 bytes .../img/people.judge-light-skin-tone.webp | Bin 0 -> 2074 bytes .../people.judge-medium-dark-skin-tone.webp | Bin 0 -> 1956 bytes .../people.judge-medium-light-skin-tone.webp | Bin 0 -> 2096 bytes .../img/people.judge-medium-skin-tone.webp | Bin 0 -> 1972 bytes public/flair/img/people.judge.webp | Bin 0 -> 2240 bytes .../flair/img/people.kiss-dark-skin-tone.webp | Bin 0 -> 1574 bytes .../img/people.kiss-light-skin-tone.webp | Bin 0 -> 1890 bytes .../people.kiss-medium-dark-skin-tone.webp | Bin 0 -> 1698 bytes .../people.kiss-medium-light-skin-tone.webp | Bin 0 -> 1758 bytes .../img/people.kiss-medium-skin-tone.webp | Bin 0 -> 1712 bytes public/flair/img/people.kiss.webp | Bin 0 -> 2028 bytes ...eople.left-facing-fist-dark-skin-tone.webp | Bin 0 -> 944 bytes ...ople.left-facing-fist-light-skin-tone.webp | Bin 0 -> 1166 bytes ...eft-facing-fist-medium-dark-skin-tone.webp | Bin 0 -> 1072 bytes ...ft-facing-fist-medium-light-skin-tone.webp | Bin 0 -> 1168 bytes ...ple.left-facing-fist-medium-skin-tone.webp | Bin 0 -> 1102 bytes public/flair/img/people.left-facing-fist.webp | Bin 0 -> 1282 bytes .../people.leftwards-hand-dark-skin-tone.webp | Bin 0 -> 1136 bytes ...people.leftwards-hand-light-skin-tone.webp | Bin 0 -> 1402 bytes ....leftwards-hand-medium-dark-skin-tone.webp | Bin 0 -> 1276 bytes ...leftwards-hand-medium-light-skin-tone.webp | Bin 0 -> 1372 bytes ...eople.leftwards-hand-medium-skin-tone.webp | Bin 0 -> 1308 bytes public/flair/img/people.leftwards-hand.webp | Bin 0 -> 1534 bytes ...leftwards-pushing-hand-dark-skin-tone.webp | Bin 0 -> 1046 bytes ...eftwards-pushing-hand-light-skin-tone.webp | Bin 0 -> 1294 bytes ...ds-pushing-hand-medium-dark-skin-tone.webp | Bin 0 -> 1152 bytes ...s-pushing-hand-medium-light-skin-tone.webp | Bin 0 -> 1244 bytes ...ftwards-pushing-hand-medium-skin-tone.webp | Bin 0 -> 1192 bytes .../img/people.leftwards-pushing-hand.webp | Bin 0 -> 1400 bytes .../flair/img/people.leg-dark-skin-tone.webp | Bin 0 -> 1126 bytes .../flair/img/people.leg-light-skin-tone.webp | Bin 0 -> 1354 bytes .../img/people.leg-medium-dark-skin-tone.webp | Bin 0 -> 1270 bytes .../people.leg-medium-light-skin-tone.webp | Bin 0 -> 1344 bytes .../img/people.leg-medium-skin-tone.webp | Bin 0 -> 1296 bytes public/flair/img/people.leg.webp | Bin 0 -> 1512 bytes public/flair/img/people.light-skin-tone.webp | Bin 0 -> 228 bytes ...eople.love-you-gesture-dark-skin-tone.webp | Bin 0 -> 1288 bytes ...ople.love-you-gesture-light-skin-tone.webp | Bin 0 -> 1564 bytes ...ove-you-gesture-medium-dark-skin-tone.webp | Bin 0 -> 1430 bytes ...ve-you-gesture-medium-light-skin-tone.webp | Bin 0 -> 1542 bytes ...ple.love-you-gesture-medium-skin-tone.webp | Bin 0 -> 1468 bytes public/flair/img/people.love-you-gesture.webp | Bin 0 -> 1706 bytes public/flair/img/people.lungs.webp | Bin 0 -> 1992 bytes .../flair/img/people.mage-dark-skin-tone.webp | Bin 0 -> 2252 bytes .../img/people.mage-light-skin-tone.webp | Bin 0 -> 2276 bytes .../people.mage-medium-dark-skin-tone.webp | Bin 0 -> 2286 bytes .../people.mage-medium-light-skin-tone.webp | Bin 0 -> 2282 bytes .../img/people.mage-medium-skin-tone.webp | Bin 0 -> 2278 bytes public/flair/img/people.mage.webp | Bin 0 -> 2380 bytes .../img/people.man-artist-dark-skin-tone.webp | Bin 0 -> 1926 bytes .../people.man-artist-light-skin-tone.webp | Bin 0 -> 2120 bytes ...ople.man-artist-medium-dark-skin-tone.webp | Bin 0 -> 1986 bytes ...ple.man-artist-medium-light-skin-tone.webp | Bin 0 -> 2104 bytes .../people.man-artist-medium-skin-tone.webp | Bin 0 -> 2018 bytes public/flair/img/people.man-artist.webp | Bin 0 -> 2214 bytes .../people.man-astronaut-dark-skin-tone.webp | Bin 0 -> 1418 bytes .../people.man-astronaut-light-skin-tone.webp | Bin 0 -> 1598 bytes ...e.man-astronaut-medium-dark-skin-tone.webp | Bin 0 -> 1520 bytes ....man-astronaut-medium-light-skin-tone.webp | Bin 0 -> 1574 bytes ...people.man-astronaut-medium-skin-tone.webp | Bin 0 -> 1532 bytes public/flair/img/people.man-astronaut.webp | Bin 0 -> 1722 bytes public/flair/img/people.man-bald.webp | Bin 0 -> 1652 bytes public/flair/img/people.man-beard.webp | Bin 0 -> 1686 bytes .../img/people.man-biking-dark-skin-tone.webp | Bin 0 -> 2604 bytes .../people.man-biking-light-skin-tone.webp | Bin 0 -> 2644 bytes ...ople.man-biking-medium-dark-skin-tone.webp | Bin 0 -> 2618 bytes ...ple.man-biking-medium-light-skin-tone.webp | Bin 0 -> 2636 bytes .../people.man-biking-medium-skin-tone.webp | Bin 0 -> 2626 bytes public/flair/img/people.man-biking.webp | Bin 0 -> 2676 bytes public/flair/img/people.man-blond-hair.webp | Bin 0 -> 1666 bytes ...ople.man-bouncing-ball-dark-skin-tone.webp | Bin 0 -> 1852 bytes ...ple.man-bouncing-ball-light-skin-tone.webp | Bin 0 -> 2046 bytes ...n-bouncing-ball-medium-dark-skin-tone.webp | Bin 0 -> 1926 bytes ...-bouncing-ball-medium-light-skin-tone.webp | Bin 0 -> 2034 bytes ...le.man-bouncing-ball-medium-skin-tone.webp | Bin 0 -> 1946 bytes .../flair/img/people.man-bouncing-ball.webp | Bin 0 -> 2170 bytes .../img/people.man-bowing-dark-skin-tone.webp | Bin 0 -> 1706 bytes .../people.man-bowing-light-skin-tone.webp | Bin 0 -> 1928 bytes ...ople.man-bowing-medium-dark-skin-tone.webp | Bin 0 -> 1768 bytes ...ple.man-bowing-medium-light-skin-tone.webp | Bin 0 -> 1856 bytes .../people.man-bowing-medium-skin-tone.webp | Bin 0 -> 1798 bytes public/flair/img/people.man-bowing.webp | Bin 0 -> 2012 bytes ...eople.man-cartwheeling-dark-skin-tone.webp | Bin 0 -> 1480 bytes ...ople.man-cartwheeling-light-skin-tone.webp | Bin 0 -> 1690 bytes ...an-cartwheeling-medium-dark-skin-tone.webp | Bin 0 -> 1602 bytes ...n-cartwheeling-medium-light-skin-tone.webp | Bin 0 -> 1684 bytes ...ple.man-cartwheeling-medium-skin-tone.webp | Bin 0 -> 1606 bytes public/flair/img/people.man-cartwheeling.webp | Bin 0 -> 1858 bytes .../people.man-climbing-dark-skin-tone.webp | Bin 0 -> 1904 bytes .../people.man-climbing-light-skin-tone.webp | Bin 0 -> 2040 bytes ...le.man-climbing-medium-dark-skin-tone.webp | Bin 0 -> 1942 bytes ...e.man-climbing-medium-light-skin-tone.webp | Bin 0 -> 2006 bytes .../people.man-climbing-medium-skin-tone.webp | Bin 0 -> 1958 bytes public/flair/img/people.man-climbing.webp | Bin 0 -> 2086 bytes ...an-construction-worker-dark-skin-tone.webp | Bin 0 -> 1900 bytes ...n-construction-worker-light-skin-tone.webp | Bin 0 -> 2006 bytes ...truction-worker-medium-dark-skin-tone.webp | Bin 0 -> 1926 bytes ...ruction-worker-medium-light-skin-tone.webp | Bin 0 -> 1920 bytes ...-construction-worker-medium-skin-tone.webp | Bin 0 -> 1918 bytes .../img/people.man-construction-worker.webp | Bin 0 -> 2032 bytes .../img/people.man-cook-dark-skin-tone.webp | Bin 0 -> 1894 bytes .../img/people.man-cook-light-skin-tone.webp | Bin 0 -> 2022 bytes ...people.man-cook-medium-dark-skin-tone.webp | Bin 0 -> 1974 bytes ...eople.man-cook-medium-light-skin-tone.webp | Bin 0 -> 1972 bytes .../img/people.man-cook-medium-skin-tone.webp | Bin 0 -> 1970 bytes public/flair/img/people.man-cook.webp | Bin 0 -> 2170 bytes public/flair/img/people.man-curly-hair.webp | Bin 0 -> 1726 bytes .../people.man-dancing-dark-skin-tone.webp | Bin 0 -> 1882 bytes .../people.man-dancing-light-skin-tone.webp | Bin 0 -> 1954 bytes ...ple.man-dancing-medium-dark-skin-tone.webp | Bin 0 -> 1926 bytes ...le.man-dancing-medium-light-skin-tone.webp | Bin 0 -> 1976 bytes .../people.man-dancing-medium-skin-tone.webp | Bin 0 -> 1930 bytes public/flair/img/people.man-dancing.webp | Bin 0 -> 2012 bytes .../img/people.man-dark-skin-tone-bald.webp | Bin 0 -> 1306 bytes .../img/people.man-dark-skin-tone-beard.webp | Bin 0 -> 1148 bytes .../people.man-dark-skin-tone-blond-hair.webp | Bin 0 -> 1588 bytes .../people.man-dark-skin-tone-curly-hair.webp | Bin 0 -> 1410 bytes .../people.man-dark-skin-tone-red-hair.webp | Bin 0 -> 1566 bytes .../people.man-dark-skin-tone-white-hair.webp | Bin 0 -> 1464 bytes .../flair/img/people.man-dark-skin-tone.webp | Bin 0 -> 1186 bytes .../people.man-detective-dark-skin-tone.webp | Bin 0 -> 1706 bytes .../people.man-detective-light-skin-tone.webp | Bin 0 -> 1924 bytes ...e.man-detective-medium-dark-skin-tone.webp | Bin 0 -> 1792 bytes ....man-detective-medium-light-skin-tone.webp | Bin 0 -> 1862 bytes ...people.man-detective-medium-skin-tone.webp | Bin 0 -> 1802 bytes public/flair/img/people.man-detective.webp | Bin 0 -> 2064 bytes .../img/people.man-elf-dark-skin-tone.webp | Bin 0 -> 1758 bytes .../img/people.man-elf-light-skin-tone.webp | Bin 0 -> 1818 bytes .../people.man-elf-medium-dark-skin-tone.webp | Bin 0 -> 1810 bytes ...people.man-elf-medium-light-skin-tone.webp | Bin 0 -> 1796 bytes .../img/people.man-elf-medium-skin-tone.webp | Bin 0 -> 1794 bytes public/flair/img/people.man-elf.webp | Bin 0 -> 1940 bytes ...people.man-facepalming-dark-skin-tone.webp | Bin 0 -> 1312 bytes ...eople.man-facepalming-light-skin-tone.webp | Bin 0 -> 1558 bytes ...man-facepalming-medium-dark-skin-tone.webp | Bin 0 -> 1410 bytes ...an-facepalming-medium-light-skin-tone.webp | Bin 0 -> 1538 bytes ...ople.man-facepalming-medium-skin-tone.webp | Bin 0 -> 1422 bytes public/flair/img/people.man-facepalming.webp | Bin 0 -> 1648 bytes ...ple.man-factory-worker-dark-skin-tone.webp | Bin 0 -> 2100 bytes ...le.man-factory-worker-light-skin-tone.webp | Bin 0 -> 2282 bytes ...-factory-worker-medium-dark-skin-tone.webp | Bin 0 -> 2170 bytes ...factory-worker-medium-light-skin-tone.webp | Bin 0 -> 2222 bytes ...e.man-factory-worker-medium-skin-tone.webp | Bin 0 -> 2186 bytes .../flair/img/people.man-factory-worker.webp | Bin 0 -> 2364 bytes .../img/people.man-fairy-dark-skin-tone.webp | Bin 0 -> 2134 bytes .../img/people.man-fairy-light-skin-tone.webp | Bin 0 -> 2156 bytes ...eople.man-fairy-medium-dark-skin-tone.webp | Bin 0 -> 2178 bytes ...ople.man-fairy-medium-light-skin-tone.webp | Bin 0 -> 2132 bytes .../people.man-fairy-medium-skin-tone.webp | Bin 0 -> 2140 bytes public/flair/img/people.man-fairy.webp | Bin 0 -> 2264 bytes .../img/people.man-farmer-dark-skin-tone.webp | Bin 0 -> 2450 bytes .../people.man-farmer-light-skin-tone.webp | Bin 0 -> 2542 bytes ...ople.man-farmer-medium-dark-skin-tone.webp | Bin 0 -> 2464 bytes ...ple.man-farmer-medium-light-skin-tone.webp | Bin 0 -> 2432 bytes .../people.man-farmer-medium-skin-tone.webp | Bin 0 -> 2466 bytes public/flair/img/people.man-farmer.webp | Bin 0 -> 2592 bytes ...eople.man-feeding-baby-dark-skin-tone.webp | Bin 0 -> 1612 bytes ...ople.man-feeding-baby-light-skin-tone.webp | Bin 0 -> 1756 bytes ...an-feeding-baby-medium-dark-skin-tone.webp | Bin 0 -> 1654 bytes ...n-feeding-baby-medium-light-skin-tone.webp | Bin 0 -> 1732 bytes ...ple.man-feeding-baby-medium-skin-tone.webp | Bin 0 -> 1658 bytes public/flair/img/people.man-feeding-baby.webp | Bin 0 -> 1826 bytes ...people.man-firefighter-dark-skin-tone.webp | Bin 0 -> 1888 bytes ...eople.man-firefighter-light-skin-tone.webp | Bin 0 -> 1962 bytes ...man-firefighter-medium-dark-skin-tone.webp | Bin 0 -> 1866 bytes ...an-firefighter-medium-light-skin-tone.webp | Bin 0 -> 1886 bytes ...ople.man-firefighter-medium-skin-tone.webp | Bin 0 -> 1882 bytes public/flair/img/people.man-firefighter.webp | Bin 0 -> 1962 bytes .../people.man-frowning-dark-skin-tone.webp | Bin 0 -> 1170 bytes .../people.man-frowning-light-skin-tone.webp | Bin 0 -> 1398 bytes ...le.man-frowning-medium-dark-skin-tone.webp | Bin 0 -> 1264 bytes ...e.man-frowning-medium-light-skin-tone.webp | Bin 0 -> 1362 bytes .../people.man-frowning-medium-skin-tone.webp | Bin 0 -> 1266 bytes public/flair/img/people.man-frowning.webp | Bin 0 -> 1504 bytes public/flair/img/people.man-genie.webp | Bin 0 -> 2872 bytes ...eople.man-gesturing-no-dark-skin-tone.webp | Bin 0 -> 1594 bytes ...ople.man-gesturing-no-light-skin-tone.webp | Bin 0 -> 1814 bytes ...an-gesturing-no-medium-dark-skin-tone.webp | Bin 0 -> 1658 bytes ...n-gesturing-no-medium-light-skin-tone.webp | Bin 0 -> 1808 bytes ...ple.man-gesturing-no-medium-skin-tone.webp | Bin 0 -> 1700 bytes public/flair/img/people.man-gesturing-no.webp | Bin 0 -> 1936 bytes ...eople.man-gesturing-ok-dark-skin-tone.webp | Bin 0 -> 1872 bytes ...ople.man-gesturing-ok-light-skin-tone.webp | Bin 0 -> 2120 bytes ...an-gesturing-ok-medium-dark-skin-tone.webp | Bin 0 -> 1940 bytes ...n-gesturing-ok-medium-light-skin-tone.webp | Bin 0 -> 1968 bytes ...ple.man-gesturing-ok-medium-skin-tone.webp | Bin 0 -> 1926 bytes public/flair/img/people.man-gesturing-ok.webp | Bin 0 -> 2188 bytes ...le.man-getting-haircut-dark-skin-tone.webp | Bin 0 -> 1876 bytes ...e.man-getting-haircut-light-skin-tone.webp | Bin 0 -> 2084 bytes ...getting-haircut-medium-dark-skin-tone.webp | Bin 0 -> 1946 bytes ...etting-haircut-medium-light-skin-tone.webp | Bin 0 -> 2012 bytes ....man-getting-haircut-medium-skin-tone.webp | Bin 0 -> 1944 bytes .../flair/img/people.man-getting-haircut.webp | Bin 0 -> 2156 bytes ...le.man-getting-massage-dark-skin-tone.webp | Bin 0 -> 1512 bytes ...e.man-getting-massage-light-skin-tone.webp | Bin 0 -> 1836 bytes ...getting-massage-medium-dark-skin-tone.webp | Bin 0 -> 1656 bytes ...etting-massage-medium-light-skin-tone.webp | Bin 0 -> 1670 bytes ....man-getting-massage-medium-skin-tone.webp | Bin 0 -> 1664 bytes .../flair/img/people.man-getting-massage.webp | Bin 0 -> 1950 bytes .../people.man-golfing-dark-skin-tone.webp | Bin 0 -> 1674 bytes .../people.man-golfing-light-skin-tone.webp | Bin 0 -> 1720 bytes ...ple.man-golfing-medium-dark-skin-tone.webp | Bin 0 -> 1684 bytes ...le.man-golfing-medium-light-skin-tone.webp | Bin 0 -> 1704 bytes .../people.man-golfing-medium-skin-tone.webp | Bin 0 -> 1684 bytes public/flair/img/people.man-golfing.webp | Bin 0 -> 1742 bytes .../img/people.man-guard-dark-skin-tone.webp | Bin 0 -> 1312 bytes .../img/people.man-guard-light-skin-tone.webp | Bin 0 -> 1460 bytes ...eople.man-guard-medium-dark-skin-tone.webp | Bin 0 -> 1376 bytes ...ople.man-guard-medium-light-skin-tone.webp | Bin 0 -> 1440 bytes .../people.man-guard-medium-skin-tone.webp | Bin 0 -> 1398 bytes public/flair/img/people.man-guard.webp | Bin 0 -> 1510 bytes ...ople.man-health-worker-dark-skin-tone.webp | Bin 0 -> 1524 bytes ...ple.man-health-worker-light-skin-tone.webp | Bin 0 -> 1710 bytes ...n-health-worker-medium-dark-skin-tone.webp | Bin 0 -> 1612 bytes ...-health-worker-medium-light-skin-tone.webp | Bin 0 -> 1704 bytes ...le.man-health-worker-medium-skin-tone.webp | Bin 0 -> 1638 bytes .../flair/img/people.man-health-worker.webp | Bin 0 -> 1846 bytes ....man-in-lotus-position-dark-skin-tone.webp | Bin 0 -> 1594 bytes ...man-in-lotus-position-light-skin-tone.webp | Bin 0 -> 1882 bytes ...-lotus-position-medium-dark-skin-tone.webp | Bin 0 -> 1692 bytes ...lotus-position-medium-light-skin-tone.webp | Bin 0 -> 1838 bytes ...an-in-lotus-position-medium-skin-tone.webp | Bin 0 -> 1724 bytes .../img/people.man-in-lotus-position.webp | Bin 0 -> 2014 bytes ...n-in-manual-wheelchair-dark-skin-tone.webp | Bin 0 -> 1786 bytes ...heelchair-facing-right-dark-skin-tone.webp | Bin 0 -> 1776 bytes ...eelchair-facing-right-light-skin-tone.webp | Bin 0 -> 1868 bytes ...ir-facing-right-medium-dark-skin-tone.webp | Bin 0 -> 1804 bytes ...r-facing-right-medium-light-skin-tone.webp | Bin 0 -> 1868 bytes ...elchair-facing-right-medium-skin-tone.webp | Bin 0 -> 1816 bytes ...man-in-manual-wheelchair-facing-right.webp | Bin 0 -> 1904 bytes ...-in-manual-wheelchair-light-skin-tone.webp | Bin 0 -> 1884 bytes ...nual-wheelchair-medium-dark-skin-tone.webp | Bin 0 -> 1808 bytes ...ual-wheelchair-medium-light-skin-tone.webp | Bin 0 -> 1886 bytes ...in-manual-wheelchair-medium-skin-tone.webp | Bin 0 -> 1830 bytes .../img/people.man-in-manual-wheelchair.webp | Bin 0 -> 1916 bytes ...n-motorized-wheelchair-dark-skin-tone.webp | Bin 0 -> 1732 bytes ...heelchair-facing-right-dark-skin-tone.webp | Bin 0 -> 1714 bytes ...eelchair-facing-right-light-skin-tone.webp | Bin 0 -> 1814 bytes ...ir-facing-right-medium-dark-skin-tone.webp | Bin 0 -> 1758 bytes ...r-facing-right-medium-light-skin-tone.webp | Bin 0 -> 1820 bytes ...elchair-facing-right-medium-skin-tone.webp | Bin 0 -> 1784 bytes ...-in-motorized-wheelchair-facing-right.webp | Bin 0 -> 1868 bytes ...-motorized-wheelchair-light-skin-tone.webp | Bin 0 -> 1832 bytes ...ized-wheelchair-medium-dark-skin-tone.webp | Bin 0 -> 1774 bytes ...zed-wheelchair-medium-light-skin-tone.webp | Bin 0 -> 1840 bytes ...motorized-wheelchair-medium-skin-tone.webp | Bin 0 -> 1808 bytes .../people.man-in-motorized-wheelchair.webp | Bin 0 -> 1884 bytes ...ple.man-in-steamy-room-dark-skin-tone.webp | Bin 0 -> 2032 bytes ...le.man-in-steamy-room-light-skin-tone.webp | Bin 0 -> 2060 bytes ...-in-steamy-room-medium-dark-skin-tone.webp | Bin 0 -> 2064 bytes ...in-steamy-room-medium-light-skin-tone.webp | Bin 0 -> 1980 bytes ...e.man-in-steamy-room-medium-skin-tone.webp | Bin 0 -> 2042 bytes .../flair/img/people.man-in-steamy-room.webp | Bin 0 -> 2170 bytes .../people.man-in-tuxedo-dark-skin-tone.webp | Bin 0 -> 1346 bytes .../people.man-in-tuxedo-light-skin-tone.webp | Bin 0 -> 1564 bytes ...e.man-in-tuxedo-medium-dark-skin-tone.webp | Bin 0 -> 1458 bytes ....man-in-tuxedo-medium-light-skin-tone.webp | Bin 0 -> 1558 bytes ...people.man-in-tuxedo-medium-skin-tone.webp | Bin 0 -> 1486 bytes public/flair/img/people.man-in-tuxedo.webp | Bin 0 -> 1726 bytes .../img/people.man-judge-dark-skin-tone.webp | Bin 0 -> 1752 bytes .../img/people.man-judge-light-skin-tone.webp | Bin 0 -> 1962 bytes ...eople.man-judge-medium-dark-skin-tone.webp | Bin 0 -> 1854 bytes ...ople.man-judge-medium-light-skin-tone.webp | Bin 0 -> 1946 bytes .../people.man-judge-medium-skin-tone.webp | Bin 0 -> 1866 bytes public/flair/img/people.man-judge.webp | Bin 0 -> 2118 bytes .../people.man-juggling-dark-skin-tone.webp | Bin 0 -> 2194 bytes .../people.man-juggling-light-skin-tone.webp | Bin 0 -> 2306 bytes ...le.man-juggling-medium-dark-skin-tone.webp | Bin 0 -> 2230 bytes ...e.man-juggling-medium-light-skin-tone.webp | Bin 0 -> 2264 bytes .../people.man-juggling-medium-skin-tone.webp | Bin 0 -> 2242 bytes public/flair/img/people.man-juggling.webp | Bin 0 -> 2324 bytes .../people.man-kneeling-dark-skin-tone.webp | Bin 0 -> 1348 bytes ...-kneeling-facing-right-dark-skin-tone.webp | Bin 0 -> 1364 bytes ...kneeling-facing-right-light-skin-tone.webp | Bin 0 -> 1462 bytes ...ng-facing-right-medium-dark-skin-tone.webp | Bin 0 -> 1386 bytes ...g-facing-right-medium-light-skin-tone.webp | Bin 0 -> 1472 bytes ...neeling-facing-right-medium-skin-tone.webp | Bin 0 -> 1418 bytes .../img/people.man-kneeling-facing-right.webp | Bin 0 -> 1518 bytes .../people.man-kneeling-light-skin-tone.webp | Bin 0 -> 1458 bytes ...le.man-kneeling-medium-dark-skin-tone.webp | Bin 0 -> 1374 bytes ...e.man-kneeling-medium-light-skin-tone.webp | Bin 0 -> 1482 bytes .../people.man-kneeling-medium-skin-tone.webp | Bin 0 -> 1410 bytes public/flair/img/people.man-kneeling.webp | Bin 0 -> 1528 bytes ...le.man-lifting-weights-dark-skin-tone.webp | Bin 0 -> 2012 bytes ...e.man-lifting-weights-light-skin-tone.webp | Bin 0 -> 2178 bytes ...lifting-weights-medium-dark-skin-tone.webp | Bin 0 -> 2094 bytes ...ifting-weights-medium-light-skin-tone.webp | Bin 0 -> 2162 bytes ....man-lifting-weights-medium-skin-tone.webp | Bin 0 -> 2084 bytes .../flair/img/people.man-lifting-weights.webp | Bin 0 -> 2322 bytes .../img/people.man-light-skin-tone-bald.webp | Bin 0 -> 1544 bytes .../img/people.man-light-skin-tone-beard.webp | Bin 0 -> 1510 bytes ...people.man-light-skin-tone-blond-hair.webp | Bin 0 -> 1658 bytes ...people.man-light-skin-tone-curly-hair.webp | Bin 0 -> 1626 bytes .../people.man-light-skin-tone-red-hair.webp | Bin 0 -> 1734 bytes ...people.man-light-skin-tone-white-hair.webp | Bin 0 -> 1526 bytes .../flair/img/people.man-light-skin-tone.webp | Bin 0 -> 1488 bytes .../img/people.man-mage-dark-skin-tone.webp | Bin 0 -> 2024 bytes .../img/people.man-mage-light-skin-tone.webp | Bin 0 -> 2040 bytes ...people.man-mage-medium-dark-skin-tone.webp | Bin 0 -> 2034 bytes ...eople.man-mage-medium-light-skin-tone.webp | Bin 0 -> 2034 bytes .../img/people.man-mage-medium-skin-tone.webp | Bin 0 -> 2022 bytes public/flair/img/people.man-mage.webp | Bin 0 -> 2104 bytes .../people.man-mechanic-dark-skin-tone.webp | Bin 0 -> 1964 bytes .../people.man-mechanic-light-skin-tone.webp | Bin 0 -> 2198 bytes ...le.man-mechanic-medium-dark-skin-tone.webp | Bin 0 -> 2028 bytes ...e.man-mechanic-medium-light-skin-tone.webp | Bin 0 -> 2100 bytes .../people.man-mechanic-medium-skin-tone.webp | Bin 0 -> 2042 bytes public/flair/img/people.man-mechanic.webp | Bin 0 -> 2276 bytes ...people.man-medium-dark-skin-tone-bald.webp | Bin 0 -> 1414 bytes ...eople.man-medium-dark-skin-tone-beard.webp | Bin 0 -> 1296 bytes ....man-medium-dark-skin-tone-blond-hair.webp | Bin 0 -> 1612 bytes ....man-medium-dark-skin-tone-curly-hair.webp | Bin 0 -> 1504 bytes ...le.man-medium-dark-skin-tone-red-hair.webp | Bin 0 -> 1584 bytes ....man-medium-dark-skin-tone-white-hair.webp | Bin 0 -> 1512 bytes .../img/people.man-medium-dark-skin-tone.webp | Bin 0 -> 1366 bytes ...eople.man-medium-light-skin-tone-bald.webp | Bin 0 -> 1470 bytes ...ople.man-medium-light-skin-tone-beard.webp | Bin 0 -> 1482 bytes ...man-medium-light-skin-tone-blond-hair.webp | Bin 0 -> 1592 bytes ...man-medium-light-skin-tone-curly-hair.webp | Bin 0 -> 1582 bytes ...e.man-medium-light-skin-tone-red-hair.webp | Bin 0 -> 1630 bytes ...man-medium-light-skin-tone-white-hair.webp | Bin 0 -> 1486 bytes .../people.man-medium-light-skin-tone.webp | Bin 0 -> 1474 bytes .../img/people.man-medium-skin-tone-bald.webp | Bin 0 -> 1418 bytes .../people.man-medium-skin-tone-beard.webp | Bin 0 -> 1352 bytes ...eople.man-medium-skin-tone-blond-hair.webp | Bin 0 -> 1594 bytes ...eople.man-medium-skin-tone-curly-hair.webp | Bin 0 -> 1508 bytes .../people.man-medium-skin-tone-red-hair.webp | Bin 0 -> 1588 bytes ...eople.man-medium-skin-tone-white-hair.webp | Bin 0 -> 1490 bytes .../img/people.man-medium-skin-tone.webp | Bin 0 -> 1386 bytes ...le.man-mountain-biking-dark-skin-tone.webp | Bin 0 -> 1626 bytes ...e.man-mountain-biking-light-skin-tone.webp | Bin 0 -> 1652 bytes ...mountain-biking-medium-dark-skin-tone.webp | Bin 0 -> 1624 bytes ...ountain-biking-medium-light-skin-tone.webp | Bin 0 -> 1628 bytes ....man-mountain-biking-medium-skin-tone.webp | Bin 0 -> 1608 bytes .../flair/img/people.man-mountain-biking.webp | Bin 0 -> 1672 bytes ...ople.man-office-worker-dark-skin-tone.webp | Bin 0 -> 1346 bytes ...ple.man-office-worker-light-skin-tone.webp | Bin 0 -> 1548 bytes ...n-office-worker-medium-dark-skin-tone.webp | Bin 0 -> 1440 bytes ...-office-worker-medium-light-skin-tone.webp | Bin 0 -> 1532 bytes ...le.man-office-worker-medium-skin-tone.webp | Bin 0 -> 1466 bytes .../flair/img/people.man-office-worker.webp | Bin 0 -> 1702 bytes .../img/people.man-pilot-dark-skin-tone.webp | Bin 0 -> 1550 bytes .../img/people.man-pilot-light-skin-tone.webp | Bin 0 -> 1758 bytes ...eople.man-pilot-medium-dark-skin-tone.webp | Bin 0 -> 1642 bytes ...ople.man-pilot-medium-light-skin-tone.webp | Bin 0 -> 1706 bytes .../people.man-pilot-medium-skin-tone.webp | Bin 0 -> 1656 bytes public/flair/img/people.man-pilot.webp | Bin 0 -> 1876 bytes ...e.man-playing-handball-dark-skin-tone.webp | Bin 0 -> 1820 bytes ....man-playing-handball-light-skin-tone.webp | Bin 0 -> 1970 bytes ...laying-handball-medium-dark-skin-tone.webp | Bin 0 -> 1892 bytes ...aying-handball-medium-light-skin-tone.webp | Bin 0 -> 1994 bytes ...man-playing-handball-medium-skin-tone.webp | Bin 0 -> 1914 bytes .../img/people.man-playing-handball.webp | Bin 0 -> 2120 bytes ...man-playing-water-polo-dark-skin-tone.webp | Bin 0 -> 1858 bytes ...an-playing-water-polo-light-skin-tone.webp | Bin 0 -> 2030 bytes ...ying-water-polo-medium-dark-skin-tone.webp | Bin 0 -> 1882 bytes ...ing-water-polo-medium-light-skin-tone.webp | Bin 0 -> 1980 bytes ...n-playing-water-polo-medium-skin-tone.webp | Bin 0 -> 1912 bytes .../img/people.man-playing-water-polo.webp | Bin 0 -> 2082 bytes ...ple.man-police-officer-dark-skin-tone.webp | Bin 0 -> 1748 bytes ...le.man-police-officer-light-skin-tone.webp | Bin 0 -> 1978 bytes ...-police-officer-medium-dark-skin-tone.webp | Bin 0 -> 1814 bytes ...police-officer-medium-light-skin-tone.webp | Bin 0 -> 1894 bytes ...e.man-police-officer-medium-skin-tone.webp | Bin 0 -> 1840 bytes .../flair/img/people.man-police-officer.webp | Bin 0 -> 2042 bytes .../people.man-pouting-dark-skin-tone.webp | Bin 0 -> 1428 bytes .../people.man-pouting-light-skin-tone.webp | Bin 0 -> 1638 bytes ...ple.man-pouting-medium-dark-skin-tone.webp | Bin 0 -> 1476 bytes ...le.man-pouting-medium-light-skin-tone.webp | Bin 0 -> 1580 bytes .../people.man-pouting-medium-skin-tone.webp | Bin 0 -> 1496 bytes public/flair/img/people.man-pouting.webp | Bin 0 -> 1734 bytes ...eople.man-raising-hand-dark-skin-tone.webp | Bin 0 -> 1652 bytes ...ople.man-raising-hand-light-skin-tone.webp | Bin 0 -> 1910 bytes ...an-raising-hand-medium-dark-skin-tone.webp | Bin 0 -> 1746 bytes ...n-raising-hand-medium-light-skin-tone.webp | Bin 0 -> 1810 bytes ...ple.man-raising-hand-medium-skin-tone.webp | Bin 0 -> 1756 bytes public/flair/img/people.man-raising-hand.webp | Bin 0 -> 2018 bytes public/flair/img/people.man-red-hair.webp | Bin 0 -> 1726 bytes ...people.man-rowing-boat-dark-skin-tone.webp | Bin 0 -> 1604 bytes ...eople.man-rowing-boat-light-skin-tone.webp | Bin 0 -> 1690 bytes ...man-rowing-boat-medium-dark-skin-tone.webp | Bin 0 -> 1626 bytes ...an-rowing-boat-medium-light-skin-tone.webp | Bin 0 -> 1708 bytes ...ople.man-rowing-boat-medium-skin-tone.webp | Bin 0 -> 1652 bytes public/flair/img/people.man-rowing-boat.webp | Bin 0 -> 1724 bytes .../people.man-running-dark-skin-tone.webp | Bin 0 -> 1402 bytes ...n-running-facing-right-dark-skin-tone.webp | Bin 0 -> 1386 bytes ...-running-facing-right-light-skin-tone.webp | Bin 0 -> 1500 bytes ...ng-facing-right-medium-dark-skin-tone.webp | Bin 0 -> 1418 bytes ...g-facing-right-medium-light-skin-tone.webp | Bin 0 -> 1502 bytes ...running-facing-right-medium-skin-tone.webp | Bin 0 -> 1434 bytes .../img/people.man-running-facing-right.webp | Bin 0 -> 1570 bytes .../people.man-running-light-skin-tone.webp | Bin 0 -> 1514 bytes ...ple.man-running-medium-dark-skin-tone.webp | Bin 0 -> 1438 bytes ...le.man-running-medium-light-skin-tone.webp | Bin 0 -> 1524 bytes .../people.man-running-medium-skin-tone.webp | Bin 0 -> 1456 bytes public/flair/img/people.man-running.webp | Bin 0 -> 1600 bytes .../people.man-scientist-dark-skin-tone.webp | Bin 0 -> 1998 bytes .../people.man-scientist-light-skin-tone.webp | Bin 0 -> 2062 bytes ...e.man-scientist-medium-dark-skin-tone.webp | Bin 0 -> 2068 bytes ....man-scientist-medium-light-skin-tone.webp | Bin 0 -> 2122 bytes ...people.man-scientist-medium-skin-tone.webp | Bin 0 -> 2080 bytes public/flair/img/people.man-scientist.webp | Bin 0 -> 2226 bytes .../people.man-shrugging-dark-skin-tone.webp | Bin 0 -> 1594 bytes .../people.man-shrugging-light-skin-tone.webp | Bin 0 -> 1834 bytes ...e.man-shrugging-medium-dark-skin-tone.webp | Bin 0 -> 1686 bytes ....man-shrugging-medium-light-skin-tone.webp | Bin 0 -> 1826 bytes ...people.man-shrugging-medium-skin-tone.webp | Bin 0 -> 1726 bytes public/flair/img/people.man-shrugging.webp | Bin 0 -> 1970 bytes .../img/people.man-singer-dark-skin-tone.webp | Bin 0 -> 1936 bytes .../people.man-singer-light-skin-tone.webp | Bin 0 -> 2182 bytes ...ople.man-singer-medium-dark-skin-tone.webp | Bin 0 -> 2022 bytes ...ple.man-singer-medium-light-skin-tone.webp | Bin 0 -> 2164 bytes .../people.man-singer-medium-skin-tone.webp | Bin 0 -> 2056 bytes public/flair/img/people.man-singer.webp | Bin 0 -> 2294 bytes .../people.man-standing-dark-skin-tone.webp | Bin 0 -> 1286 bytes .../people.man-standing-light-skin-tone.webp | Bin 0 -> 1450 bytes ...le.man-standing-medium-dark-skin-tone.webp | Bin 0 -> 1348 bytes ...e.man-standing-medium-light-skin-tone.webp | Bin 0 -> 1446 bytes .../people.man-standing-medium-skin-tone.webp | Bin 0 -> 1362 bytes public/flair/img/people.man-standing.webp | Bin 0 -> 1514 bytes .../people.man-student-dark-skin-tone.webp | Bin 0 -> 1518 bytes .../people.man-student-light-skin-tone.webp | Bin 0 -> 1766 bytes ...ple.man-student-medium-dark-skin-tone.webp | Bin 0 -> 1642 bytes ...le.man-student-medium-light-skin-tone.webp | Bin 0 -> 1742 bytes .../people.man-student-medium-skin-tone.webp | Bin 0 -> 1666 bytes public/flair/img/people.man-student.webp | Bin 0 -> 1902 bytes .../people.man-superhero-dark-skin-tone.webp | Bin 0 -> 1798 bytes .../people.man-superhero-light-skin-tone.webp | Bin 0 -> 1820 bytes ...e.man-superhero-medium-dark-skin-tone.webp | Bin 0 -> 1792 bytes ....man-superhero-medium-light-skin-tone.webp | Bin 0 -> 1818 bytes ...people.man-superhero-medium-skin-tone.webp | Bin 0 -> 1792 bytes public/flair/img/people.man-superhero.webp | Bin 0 -> 1822 bytes ...eople.man-supervillain-dark-skin-tone.webp | Bin 0 -> 1948 bytes ...ople.man-supervillain-light-skin-tone.webp | Bin 0 -> 1988 bytes ...an-supervillain-medium-dark-skin-tone.webp | Bin 0 -> 1956 bytes ...n-supervillain-medium-light-skin-tone.webp | Bin 0 -> 2004 bytes ...ple.man-supervillain-medium-skin-tone.webp | Bin 0 -> 1970 bytes public/flair/img/people.man-supervillain.webp | Bin 0 -> 1986 bytes .../people.man-surfing-dark-skin-tone.webp | Bin 0 -> 1960 bytes .../people.man-surfing-light-skin-tone.webp | Bin 0 -> 2030 bytes ...ple.man-surfing-medium-dark-skin-tone.webp | Bin 0 -> 1962 bytes ...le.man-surfing-medium-light-skin-tone.webp | Bin 0 -> 2018 bytes .../people.man-surfing-medium-skin-tone.webp | Bin 0 -> 1956 bytes public/flair/img/people.man-surfing.webp | Bin 0 -> 2062 bytes .../people.man-swimming-dark-skin-tone.webp | Bin 0 -> 1252 bytes .../people.man-swimming-light-skin-tone.webp | Bin 0 -> 1422 bytes ...le.man-swimming-medium-dark-skin-tone.webp | Bin 0 -> 1292 bytes ...e.man-swimming-medium-light-skin-tone.webp | Bin 0 -> 1368 bytes .../people.man-swimming-medium-skin-tone.webp | Bin 0 -> 1308 bytes public/flair/img/people.man-swimming.webp | Bin 0 -> 1494 bytes .../people.man-teacher-dark-skin-tone.webp | Bin 0 -> 1416 bytes .../people.man-teacher-light-skin-tone.webp | Bin 0 -> 1634 bytes ...ple.man-teacher-medium-dark-skin-tone.webp | Bin 0 -> 1498 bytes ...le.man-teacher-medium-light-skin-tone.webp | Bin 0 -> 1580 bytes .../people.man-teacher-medium-skin-tone.webp | Bin 0 -> 1512 bytes public/flair/img/people.man-teacher.webp | Bin 0 -> 1690 bytes ...eople.man-technologist-dark-skin-tone.webp | Bin 0 -> 1464 bytes ...ople.man-technologist-light-skin-tone.webp | Bin 0 -> 1606 bytes ...an-technologist-medium-dark-skin-tone.webp | Bin 0 -> 1512 bytes ...n-technologist-medium-light-skin-tone.webp | Bin 0 -> 1582 bytes ...ple.man-technologist-medium-skin-tone.webp | Bin 0 -> 1504 bytes public/flair/img/people.man-technologist.webp | Bin 0 -> 1696 bytes ...eople.man-tipping-hand-dark-skin-tone.webp | Bin 0 -> 1610 bytes ...ople.man-tipping-hand-light-skin-tone.webp | Bin 0 -> 1862 bytes ...an-tipping-hand-medium-dark-skin-tone.webp | Bin 0 -> 1706 bytes ...n-tipping-hand-medium-light-skin-tone.webp | Bin 0 -> 1820 bytes ...ple.man-tipping-hand-medium-skin-tone.webp | Bin 0 -> 1722 bytes public/flair/img/people.man-tipping-hand.webp | Bin 0 -> 1974 bytes .../people.man-vampire-dark-skin-tone.webp | Bin 0 -> 1520 bytes .../people.man-vampire-light-skin-tone.webp | Bin 0 -> 1726 bytes ...ple.man-vampire-medium-dark-skin-tone.webp | Bin 0 -> 1576 bytes ...le.man-vampire-medium-light-skin-tone.webp | Bin 0 -> 1684 bytes .../people.man-vampire-medium-skin-tone.webp | Bin 0 -> 1592 bytes public/flair/img/people.man-vampire.webp | Bin 0 -> 1800 bytes .../people.man-walking-dark-skin-tone.webp | Bin 0 -> 1196 bytes ...n-walking-facing-right-dark-skin-tone.webp | Bin 0 -> 1176 bytes ...-walking-facing-right-light-skin-tone.webp | Bin 0 -> 1264 bytes ...ng-facing-right-medium-dark-skin-tone.webp | Bin 0 -> 1210 bytes ...g-facing-right-medium-light-skin-tone.webp | Bin 0 -> 1274 bytes ...walking-facing-right-medium-skin-tone.webp | Bin 0 -> 1226 bytes .../img/people.man-walking-facing-right.webp | Bin 0 -> 1316 bytes .../people.man-walking-light-skin-tone.webp | Bin 0 -> 1288 bytes ...ple.man-walking-medium-dark-skin-tone.webp | Bin 0 -> 1218 bytes ...le.man-walking-medium-light-skin-tone.webp | Bin 0 -> 1298 bytes .../people.man-walking-medium-skin-tone.webp | Bin 0 -> 1246 bytes public/flair/img/people.man-walking.webp | Bin 0 -> 1336 bytes ...ple.man-wearing-turban-dark-skin-tone.webp | Bin 0 -> 1422 bytes ...le.man-wearing-turban-light-skin-tone.webp | Bin 0 -> 1562 bytes ...-wearing-turban-medium-dark-skin-tone.webp | Bin 0 -> 1528 bytes ...wearing-turban-medium-light-skin-tone.webp | Bin 0 -> 1518 bytes ...e.man-wearing-turban-medium-skin-tone.webp | Bin 0 -> 1518 bytes .../flair/img/people.man-wearing-turban.webp | Bin 0 -> 1710 bytes public/flair/img/people.man-white-hair.webp | Bin 0 -> 1628 bytes .../people.man-with-veil-dark-skin-tone.webp | Bin 0 -> 1562 bytes .../people.man-with-veil-light-skin-tone.webp | Bin 0 -> 1648 bytes ...e.man-with-veil-medium-dark-skin-tone.webp | Bin 0 -> 1612 bytes ....man-with-veil-medium-light-skin-tone.webp | Bin 0 -> 1638 bytes ...people.man-with-veil-medium-skin-tone.webp | Bin 0 -> 1594 bytes public/flair/img/people.man-with-veil.webp | Bin 0 -> 1780 bytes ...le.man-with-white-cane-dark-skin-tone.webp | Bin 0 -> 1606 bytes ...hite-cane-facing-right-dark-skin-tone.webp | Bin 0 -> 1552 bytes ...ite-cane-facing-right-light-skin-tone.webp | Bin 0 -> 1636 bytes ...ne-facing-right-medium-dark-skin-tone.webp | Bin 0 -> 1572 bytes ...e-facing-right-medium-light-skin-tone.webp | Bin 0 -> 1630 bytes ...te-cane-facing-right-medium-skin-tone.webp | Bin 0 -> 1588 bytes ...ople.man-with-white-cane-facing-right.webp | Bin 0 -> 1670 bytes ...e.man-with-white-cane-light-skin-tone.webp | Bin 0 -> 1690 bytes ...with-white-cane-medium-dark-skin-tone.webp | Bin 0 -> 1642 bytes ...ith-white-cane-medium-light-skin-tone.webp | Bin 0 -> 1714 bytes ....man-with-white-cane-medium-skin-tone.webp | Bin 0 -> 1666 bytes .../flair/img/people.man-with-white-cane.webp | Bin 0 -> 1758 bytes public/flair/img/people.man-zombie.webp | Bin 0 -> 2222 bytes public/flair/img/people.man.webp | Bin 0 -> 1688 bytes .../img/people.mechanic-dark-skin-tone.webp | Bin 0 -> 2000 bytes .../img/people.mechanic-light-skin-tone.webp | Bin 0 -> 2216 bytes ...people.mechanic-medium-dark-skin-tone.webp | Bin 0 -> 2070 bytes ...eople.mechanic-medium-light-skin-tone.webp | Bin 0 -> 2156 bytes .../img/people.mechanic-medium-skin-tone.webp | Bin 0 -> 2090 bytes public/flair/img/people.mechanic.webp | Bin 0 -> 2286 bytes public/flair/img/people.mechanical-arm.webp | Bin 0 -> 1748 bytes public/flair/img/people.mechanical-leg.webp | Bin 0 -> 1476 bytes .../img/people.medium-dark-skin-tone.webp | Bin 0 -> 218 bytes .../img/people.medium-light-skin-tone.webp | Bin 0 -> 218 bytes public/flair/img/people.medium-skin-tone.webp | Bin 0 -> 216 bytes ...-hands-dark-skin-tone-light-skin-tone.webp | Bin 0 -> 2412 bytes ...-dark-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2314 bytes ...dark-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2400 bytes ...hands-dark-skin-tone-medium-skin-tone.webp | Bin 0 -> 2324 bytes ...ople.men-holding-hands-dark-skin-tone.webp | Bin 0 -> 2248 bytes ...-hands-light-skin-tone-dark-skin-tone.webp | Bin 0 -> 2454 bytes ...light-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2504 bytes ...ight-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2562 bytes ...ands-light-skin-tone-medium-skin-tone.webp | Bin 0 -> 2504 bytes ...ple.men-holding-hands-light-skin-tone.webp | Bin 0 -> 2550 bytes ...-medium-dark-skin-tone-dark-skin-tone.webp | Bin 0 -> 2346 bytes ...medium-dark-skin-tone-light-skin-tone.webp | Bin 0 -> 2488 bytes ...dark-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2478 bytes ...edium-dark-skin-tone-medium-skin-tone.webp | Bin 0 -> 2400 bytes ...n-holding-hands-medium-dark-skin-tone.webp | Bin 0 -> 2376 bytes ...medium-light-skin-tone-dark-skin-tone.webp | Bin 0 -> 2456 bytes ...edium-light-skin-tone-light-skin-tone.webp | Bin 0 -> 2560 bytes ...light-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2496 bytes ...dium-light-skin-tone-medium-skin-tone.webp | Bin 0 -> 2504 bytes ...-holding-hands-medium-light-skin-tone.webp | Bin 0 -> 2544 bytes ...hands-medium-skin-tone-dark-skin-tone.webp | Bin 0 -> 2386 bytes ...ands-medium-skin-tone-light-skin-tone.webp | Bin 0 -> 2514 bytes ...edium-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2416 bytes ...dium-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2504 bytes ...le.men-holding-hands-medium-skin-tone.webp | Bin 0 -> 2426 bytes .../flair/img/people.men-holding-hands.webp | Bin 0 -> 2688 bytes .../flair/img/people.men-with-bunny-ears.webp | Bin 0 -> 3072 bytes public/flair/img/people.men-wrestling.webp | Bin 0 -> 2496 bytes .../img/people.mermaid-dark-skin-tone.webp | Bin 0 -> 1922 bytes .../img/people.mermaid-light-skin-tone.webp | Bin 0 -> 2072 bytes .../people.mermaid-medium-dark-skin-tone.webp | Bin 0 -> 1902 bytes ...people.mermaid-medium-light-skin-tone.webp | Bin 0 -> 2014 bytes .../img/people.mermaid-medium-skin-tone.webp | Bin 0 -> 1932 bytes public/flair/img/people.mermaid.webp | Bin 0 -> 2102 bytes .../img/people.merman-dark-skin-tone.webp | Bin 0 -> 2118 bytes .../img/people.merman-light-skin-tone.webp | Bin 0 -> 2268 bytes .../people.merman-medium-dark-skin-tone.webp | Bin 0 -> 2186 bytes .../people.merman-medium-light-skin-tone.webp | Bin 0 -> 2250 bytes .../img/people.merman-medium-skin-tone.webp | Bin 0 -> 2186 bytes public/flair/img/people.merman.webp | Bin 0 -> 2322 bytes .../img/people.merperson-dark-skin-tone.webp | Bin 0 -> 1492 bytes .../img/people.merperson-light-skin-tone.webp | Bin 0 -> 1568 bytes ...eople.merperson-medium-dark-skin-tone.webp | Bin 0 -> 1478 bytes ...ople.merperson-medium-light-skin-tone.webp | Bin 0 -> 1534 bytes .../people.merperson-medium-skin-tone.webp | Bin 0 -> 1492 bytes public/flair/img/people.merperson.webp | Bin 0 -> 1562 bytes public/flair/img/people.mouth.webp | Bin 0 -> 1256 bytes .../img/people.mrs-claus-dark-skin-tone.webp | Bin 0 -> 1862 bytes .../img/people.mrs-claus-light-skin-tone.webp | Bin 0 -> 1868 bytes ...eople.mrs-claus-medium-dark-skin-tone.webp | Bin 0 -> 1900 bytes ...ople.mrs-claus-medium-light-skin-tone.webp | Bin 0 -> 1872 bytes .../people.mrs-claus-medium-skin-tone.webp | Bin 0 -> 1862 bytes public/flair/img/people.mrs-claus.webp | Bin 0 -> 1968 bytes .../img/people.mx-claus-dark-skin-tone.webp | Bin 0 -> 1834 bytes .../img/people.mx-claus-light-skin-tone.webp | Bin 0 -> 1844 bytes ...people.mx-claus-medium-dark-skin-tone.webp | Bin 0 -> 1856 bytes ...eople.mx-claus-medium-light-skin-tone.webp | Bin 0 -> 1818 bytes .../img/people.mx-claus-medium-skin-tone.webp | Bin 0 -> 1834 bytes public/flair/img/people.mx-claus.webp | Bin 0 -> 1930 bytes .../people.nail-polish-dark-skin-tone.webp | Bin 0 -> 1624 bytes .../people.nail-polish-light-skin-tone.webp | Bin 0 -> 1746 bytes ...ple.nail-polish-medium-dark-skin-tone.webp | Bin 0 -> 1572 bytes ...le.nail-polish-medium-light-skin-tone.webp | Bin 0 -> 1684 bytes .../people.nail-polish-medium-skin-tone.webp | Bin 0 -> 1638 bytes public/flair/img/people.nail-polish.webp | Bin 0 -> 1738 bytes .../img/people.ninja-dark-skin-tone.webp | Bin 0 -> 1664 bytes .../img/people.ninja-light-skin-tone.webp | Bin 0 -> 1756 bytes .../people.ninja-medium-dark-skin-tone.webp | Bin 0 -> 1696 bytes .../people.ninja-medium-light-skin-tone.webp | Bin 0 -> 1732 bytes .../img/people.ninja-medium-skin-tone.webp | Bin 0 -> 1710 bytes public/flair/img/people.ninja.webp | Bin 0 -> 1808 bytes .../flair/img/people.nose-dark-skin-tone.webp | Bin 0 -> 968 bytes .../img/people.nose-light-skin-tone.webp | Bin 0 -> 1182 bytes .../people.nose-medium-dark-skin-tone.webp | Bin 0 -> 1108 bytes .../people.nose-medium-light-skin-tone.webp | Bin 0 -> 1164 bytes .../img/people.nose-medium-skin-tone.webp | Bin 0 -> 1114 bytes public/flair/img/people.nose.webp | Bin 0 -> 1350 bytes .../people.office-worker-dark-skin-tone.webp | Bin 0 -> 1434 bytes .../people.office-worker-light-skin-tone.webp | Bin 0 -> 1612 bytes ...e.office-worker-medium-dark-skin-tone.webp | Bin 0 -> 1524 bytes ....office-worker-medium-light-skin-tone.webp | Bin 0 -> 1678 bytes ...people.office-worker-medium-skin-tone.webp | Bin 0 -> 1544 bytes public/flair/img/people.office-worker.webp | Bin 0 -> 1774 bytes .../img/people.ok-hand-dark-skin-tone.webp | Bin 0 -> 1230 bytes .../img/people.ok-hand-light-skin-tone.webp | Bin 0 -> 1544 bytes .../people.ok-hand-medium-dark-skin-tone.webp | Bin 0 -> 1396 bytes ...people.ok-hand-medium-light-skin-tone.webp | Bin 0 -> 1520 bytes .../img/people.ok-hand-medium-skin-tone.webp | Bin 0 -> 1444 bytes public/flair/img/people.ok-hand.webp | Bin 0 -> 1690 bytes .../img/people.old-man-dark-skin-tone.webp | Bin 0 -> 1274 bytes .../img/people.old-man-light-skin-tone.webp | Bin 0 -> 1464 bytes .../people.old-man-medium-dark-skin-tone.webp | Bin 0 -> 1422 bytes ...people.old-man-medium-light-skin-tone.webp | Bin 0 -> 1432 bytes .../img/people.old-man-medium-skin-tone.webp | Bin 0 -> 1402 bytes public/flair/img/people.old-man.webp | Bin 0 -> 1686 bytes .../img/people.old-woman-dark-skin-tone.webp | Bin 0 -> 1330 bytes .../img/people.old-woman-light-skin-tone.webp | Bin 0 -> 1494 bytes ...eople.old-woman-medium-dark-skin-tone.webp | Bin 0 -> 1428 bytes ...ople.old-woman-medium-light-skin-tone.webp | Bin 0 -> 1448 bytes .../people.old-woman-medium-skin-tone.webp | Bin 0 -> 1406 bytes public/flair/img/people.old-woman.webp | Bin 0 -> 1674 bytes .../people.older-person-dark-skin-tone.webp | Bin 0 -> 1662 bytes .../people.older-person-light-skin-tone.webp | Bin 0 -> 1724 bytes ...le.older-person-medium-dark-skin-tone.webp | Bin 0 -> 1690 bytes ...e.older-person-medium-light-skin-tone.webp | Bin 0 -> 1648 bytes .../people.older-person-medium-skin-tone.webp | Bin 0 -> 1618 bytes public/flair/img/people.older-person.webp | Bin 0 -> 1862 bytes .../people.oncoming-fist-dark-skin-tone.webp | Bin 0 -> 1178 bytes .../people.oncoming-fist-light-skin-tone.webp | Bin 0 -> 1472 bytes ...e.oncoming-fist-medium-dark-skin-tone.webp | Bin 0 -> 1322 bytes ....oncoming-fist-medium-light-skin-tone.webp | Bin 0 -> 1418 bytes ...people.oncoming-fist-medium-skin-tone.webp | Bin 0 -> 1350 bytes public/flair/img/people.oncoming-fist.webp | Bin 0 -> 1604 bytes .../img/people.open-hands-dark-skin-tone.webp | Bin 0 -> 1468 bytes .../people.open-hands-light-skin-tone.webp | Bin 0 -> 1744 bytes ...ople.open-hands-medium-dark-skin-tone.webp | Bin 0 -> 1582 bytes ...ple.open-hands-medium-light-skin-tone.webp | Bin 0 -> 1718 bytes .../people.open-hands-medium-skin-tone.webp | Bin 0 -> 1642 bytes public/flair/img/people.open-hands.webp | Bin 0 -> 1852 bytes .../people.palm-down-hand-dark-skin-tone.webp | Bin 0 -> 966 bytes ...people.palm-down-hand-light-skin-tone.webp | Bin 0 -> 1154 bytes ....palm-down-hand-medium-dark-skin-tone.webp | Bin 0 -> 1082 bytes ...palm-down-hand-medium-light-skin-tone.webp | Bin 0 -> 1152 bytes ...eople.palm-down-hand-medium-skin-tone.webp | Bin 0 -> 1110 bytes public/flair/img/people.palm-down-hand.webp | Bin 0 -> 1290 bytes .../people.palm-up-hand-dark-skin-tone.webp | Bin 0 -> 1002 bytes .../people.palm-up-hand-light-skin-tone.webp | Bin 0 -> 1212 bytes ...le.palm-up-hand-medium-dark-skin-tone.webp | Bin 0 -> 1118 bytes ...e.palm-up-hand-medium-light-skin-tone.webp | Bin 0 -> 1190 bytes .../people.palm-up-hand-medium-skin-tone.webp | Bin 0 -> 1156 bytes public/flair/img/people.palm-up-hand.webp | Bin 0 -> 1332 bytes ...ople.palms-up-together-dark-skin-tone.webp | Bin 0 -> 1304 bytes ...ple.palms-up-together-light-skin-tone.webp | Bin 0 -> 1532 bytes ...lms-up-together-medium-dark-skin-tone.webp | Bin 0 -> 1418 bytes ...ms-up-together-medium-light-skin-tone.webp | Bin 0 -> 1566 bytes ...le.palms-up-together-medium-skin-tone.webp | Bin 0 -> 1492 bytes .../flair/img/people.palms-up-together.webp | Bin 0 -> 1704 bytes ...-hands-dark-skin-tone-light-skin-tone.webp | Bin 0 -> 2544 bytes ...-dark-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2450 bytes ...dark-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2528 bytes ...hands-dark-skin-tone-medium-skin-tone.webp | Bin 0 -> 2462 bytes ...e.people-holding-hands-dark-skin-tone.webp | Bin 0 -> 2422 bytes ...-hands-light-skin-tone-dark-skin-tone.webp | Bin 0 -> 2544 bytes ...light-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2546 bytes ...ight-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2604 bytes ...ands-light-skin-tone-medium-skin-tone.webp | Bin 0 -> 2554 bytes ....people-holding-hands-light-skin-tone.webp | Bin 0 -> 2596 bytes ...-medium-dark-skin-tone-dark-skin-tone.webp | Bin 0 -> 2474 bytes ...medium-dark-skin-tone-light-skin-tone.webp | Bin 0 -> 2578 bytes ...dark-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2554 bytes ...edium-dark-skin-tone-medium-skin-tone.webp | Bin 0 -> 2490 bytes ...e-holding-hands-medium-dark-skin-tone.webp | Bin 0 -> 2460 bytes ...medium-light-skin-tone-dark-skin-tone.webp | Bin 0 -> 2552 bytes ...edium-light-skin-tone-light-skin-tone.webp | Bin 0 -> 2620 bytes ...light-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2554 bytes ...dium-light-skin-tone-medium-skin-tone.webp | Bin 0 -> 2562 bytes ...-holding-hands-medium-light-skin-tone.webp | Bin 0 -> 2598 bytes ...hands-medium-skin-tone-dark-skin-tone.webp | Bin 0 -> 2484 bytes ...ands-medium-skin-tone-light-skin-tone.webp | Bin 0 -> 2578 bytes ...edium-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2482 bytes ...dium-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2548 bytes ...people-holding-hands-medium-skin-tone.webp | Bin 0 -> 2482 bytes .../img/people.people-holding-hands.webp | Bin 0 -> 2700 bytes public/flair/img/people.people-hugging.webp | Bin 0 -> 1432 bytes .../img/people.people-with-bunny-ears.webp | Bin 0 -> 3098 bytes public/flair/img/people.people-wrestling.webp | Bin 0 -> 2570 bytes public/flair/img/people.person-bald.webp | Bin 0 -> 1574 bytes public/flair/img/people.person-beard.webp | Bin 0 -> 1768 bytes .../people.person-biking-dark-skin-tone.webp | Bin 0 -> 2652 bytes .../people.person-biking-light-skin-tone.webp | Bin 0 -> 2680 bytes ...e.person-biking-medium-dark-skin-tone.webp | Bin 0 -> 2654 bytes ....person-biking-medium-light-skin-tone.webp | Bin 0 -> 2666 bytes ...people.person-biking-medium-skin-tone.webp | Bin 0 -> 2652 bytes public/flair/img/people.person-biking.webp | Bin 0 -> 2698 bytes .../flair/img/people.person-blond-hair.webp | Bin 0 -> 1692 bytes ...e.person-bouncing-ball-dark-skin-tone.webp | Bin 0 -> 1874 bytes ....person-bouncing-ball-light-skin-tone.webp | Bin 0 -> 2026 bytes ...n-bouncing-ball-medium-dark-skin-tone.webp | Bin 0 -> 1924 bytes ...-bouncing-ball-medium-light-skin-tone.webp | Bin 0 -> 2002 bytes ...person-bouncing-ball-medium-skin-tone.webp | Bin 0 -> 1926 bytes .../img/people.person-bouncing-ball.webp | Bin 0 -> 2128 bytes .../people.person-bowing-dark-skin-tone.webp | Bin 0 -> 1838 bytes .../people.person-bowing-light-skin-tone.webp | Bin 0 -> 1956 bytes ...e.person-bowing-medium-dark-skin-tone.webp | Bin 0 -> 1832 bytes ....person-bowing-medium-light-skin-tone.webp | Bin 0 -> 1904 bytes ...people.person-bowing-medium-skin-tone.webp | Bin 0 -> 1852 bytes public/flair/img/people.person-bowing.webp | Bin 0 -> 1958 bytes ...le.person-cartwheeling-dark-skin-tone.webp | Bin 0 -> 1490 bytes ...e.person-cartwheeling-light-skin-tone.webp | Bin 0 -> 1680 bytes ...on-cartwheeling-medium-dark-skin-tone.webp | Bin 0 -> 1588 bytes ...n-cartwheeling-medium-light-skin-tone.webp | Bin 0 -> 1654 bytes ....person-cartwheeling-medium-skin-tone.webp | Bin 0 -> 1598 bytes .../flair/img/people.person-cartwheeling.webp | Bin 0 -> 1832 bytes ...people.person-climbing-dark-skin-tone.webp | Bin 0 -> 1954 bytes ...eople.person-climbing-light-skin-tone.webp | Bin 0 -> 2072 bytes ...person-climbing-medium-dark-skin-tone.webp | Bin 0 -> 1984 bytes ...erson-climbing-medium-light-skin-tone.webp | Bin 0 -> 2042 bytes ...ople.person-climbing-medium-skin-tone.webp | Bin 0 -> 1996 bytes public/flair/img/people.person-climbing.webp | Bin 0 -> 2110 bytes .../flair/img/people.person-curly-hair.webp | Bin 0 -> 1674 bytes .../people.person-dark-skin-tone-bald.webp | Bin 0 -> 1322 bytes .../people.person-dark-skin-tone-beard.webp | Bin 0 -> 1276 bytes ...ople.person-dark-skin-tone-blond-hair.webp | Bin 0 -> 1864 bytes ...ople.person-dark-skin-tone-curly-hair.webp | Bin 0 -> 1450 bytes ...people.person-dark-skin-tone-red-hair.webp | Bin 0 -> 1814 bytes ...ople.person-dark-skin-tone-white-hair.webp | Bin 0 -> 1676 bytes .../img/people.person-dark-skin-tone.webp | Bin 0 -> 1340 bytes ...ple.person-facepalming-dark-skin-tone.webp | Bin 0 -> 1448 bytes ...le.person-facepalming-light-skin-tone.webp | Bin 0 -> 1600 bytes ...son-facepalming-medium-dark-skin-tone.webp | Bin 0 -> 1496 bytes ...on-facepalming-medium-light-skin-tone.webp | Bin 0 -> 1606 bytes ...e.person-facepalming-medium-skin-tone.webp | Bin 0 -> 1524 bytes .../flair/img/people.person-facepalming.webp | Bin 0 -> 1664 bytes ...le.person-feeding-baby-dark-skin-tone.webp | Bin 0 -> 1752 bytes ...e.person-feeding-baby-light-skin-tone.webp | Bin 0 -> 1816 bytes ...on-feeding-baby-medium-dark-skin-tone.webp | Bin 0 -> 1738 bytes ...n-feeding-baby-medium-light-skin-tone.webp | Bin 0 -> 1810 bytes ....person-feeding-baby-medium-skin-tone.webp | Bin 0 -> 1754 bytes .../flair/img/people.person-feeding-baby.webp | Bin 0 -> 1842 bytes public/flair/img/people.person-fencing.webp | Bin 0 -> 1940 bytes ...people.person-frowning-dark-skin-tone.webp | Bin 0 -> 1300 bytes ...eople.person-frowning-light-skin-tone.webp | Bin 0 -> 1462 bytes ...person-frowning-medium-dark-skin-tone.webp | Bin 0 -> 1360 bytes ...erson-frowning-medium-light-skin-tone.webp | Bin 0 -> 1448 bytes ...ople.person-frowning-medium-skin-tone.webp | Bin 0 -> 1372 bytes public/flair/img/people.person-frowning.webp | Bin 0 -> 1534 bytes ...le.person-gesturing-no-dark-skin-tone.webp | Bin 0 -> 1630 bytes ...e.person-gesturing-no-light-skin-tone.webp | Bin 0 -> 1858 bytes ...on-gesturing-no-medium-dark-skin-tone.webp | Bin 0 -> 1698 bytes ...n-gesturing-no-medium-light-skin-tone.webp | Bin 0 -> 1802 bytes ....person-gesturing-no-medium-skin-tone.webp | Bin 0 -> 1732 bytes .../flair/img/people.person-gesturing-no.webp | Bin 0 -> 1932 bytes ...le.person-gesturing-ok-dark-skin-tone.webp | Bin 0 -> 1960 bytes ...e.person-gesturing-ok-light-skin-tone.webp | Bin 0 -> 2156 bytes ...on-gesturing-ok-medium-dark-skin-tone.webp | Bin 0 -> 1980 bytes ...n-gesturing-ok-medium-light-skin-tone.webp | Bin 0 -> 1896 bytes ....person-gesturing-ok-medium-skin-tone.webp | Bin 0 -> 1962 bytes .../flair/img/people.person-gesturing-ok.webp | Bin 0 -> 2148 bytes ...person-getting-haircut-dark-skin-tone.webp | Bin 0 -> 1936 bytes ...erson-getting-haircut-light-skin-tone.webp | Bin 0 -> 2098 bytes ...getting-haircut-medium-dark-skin-tone.webp | Bin 0 -> 1978 bytes ...etting-haircut-medium-light-skin-tone.webp | Bin 0 -> 2072 bytes ...rson-getting-haircut-medium-skin-tone.webp | Bin 0 -> 1988 bytes .../img/people.person-getting-haircut.webp | Bin 0 -> 2150 bytes ...person-getting-massage-dark-skin-tone.webp | Bin 0 -> 1590 bytes ...erson-getting-massage-light-skin-tone.webp | Bin 0 -> 1902 bytes ...getting-massage-medium-dark-skin-tone.webp | Bin 0 -> 1718 bytes ...etting-massage-medium-light-skin-tone.webp | Bin 0 -> 1758 bytes ...rson-getting-massage-medium-skin-tone.webp | Bin 0 -> 1718 bytes .../img/people.person-getting-massage.webp | Bin 0 -> 1994 bytes .../people.person-golfing-dark-skin-tone.webp | Bin 0 -> 1722 bytes ...people.person-golfing-light-skin-tone.webp | Bin 0 -> 1742 bytes ....person-golfing-medium-dark-skin-tone.webp | Bin 0 -> 1726 bytes ...person-golfing-medium-light-skin-tone.webp | Bin 0 -> 1756 bytes ...eople.person-golfing-medium-skin-tone.webp | Bin 0 -> 1726 bytes public/flair/img/people.person-golfing.webp | Bin 0 -> 1740 bytes .../people.person-in-bed-dark-skin-tone.webp | Bin 0 -> 1058 bytes .../people.person-in-bed-light-skin-tone.webp | Bin 0 -> 1076 bytes ...e.person-in-bed-medium-dark-skin-tone.webp | Bin 0 -> 1072 bytes ....person-in-bed-medium-light-skin-tone.webp | Bin 0 -> 1068 bytes ...people.person-in-bed-medium-skin-tone.webp | Bin 0 -> 1062 bytes public/flair/img/people.person-in-bed.webp | Bin 0 -> 1078 bytes ...rson-in-lotus-position-dark-skin-tone.webp | Bin 0 -> 1656 bytes ...son-in-lotus-position-light-skin-tone.webp | Bin 0 -> 1890 bytes ...-lotus-position-medium-dark-skin-tone.webp | Bin 0 -> 1722 bytes ...lotus-position-medium-light-skin-tone.webp | Bin 0 -> 1854 bytes ...on-in-lotus-position-medium-skin-tone.webp | Bin 0 -> 1738 bytes .../img/people.person-in-lotus-position.webp | Bin 0 -> 2006 bytes ...n-in-manual-wheelchair-dark-skin-tone.webp | Bin 0 -> 1820 bytes ...heelchair-facing-right-dark-skin-tone.webp | Bin 0 -> 1810 bytes ...eelchair-facing-right-light-skin-tone.webp | Bin 0 -> 1868 bytes ...ir-facing-right-medium-dark-skin-tone.webp | Bin 0 -> 1816 bytes ...r-facing-right-medium-light-skin-tone.webp | Bin 0 -> 1876 bytes ...elchair-facing-right-medium-skin-tone.webp | Bin 0 -> 1828 bytes ...son-in-manual-wheelchair-facing-right.webp | Bin 0 -> 1890 bytes ...-in-manual-wheelchair-light-skin-tone.webp | Bin 0 -> 1886 bytes ...nual-wheelchair-medium-dark-skin-tone.webp | Bin 0 -> 1848 bytes ...ual-wheelchair-medium-light-skin-tone.webp | Bin 0 -> 1896 bytes ...in-manual-wheelchair-medium-skin-tone.webp | Bin 0 -> 1844 bytes .../people.person-in-manual-wheelchair.webp | Bin 0 -> 1914 bytes ...n-motorized-wheelchair-dark-skin-tone.webp | Bin 0 -> 1780 bytes ...heelchair-facing-right-dark-skin-tone.webp | Bin 0 -> 1758 bytes ...eelchair-facing-right-light-skin-tone.webp | Bin 0 -> 1828 bytes ...ir-facing-right-medium-dark-skin-tone.webp | Bin 0 -> 1790 bytes ...r-facing-right-medium-light-skin-tone.webp | Bin 0 -> 1834 bytes ...elchair-facing-right-medium-skin-tone.webp | Bin 0 -> 1784 bytes ...-in-motorized-wheelchair-facing-right.webp | Bin 0 -> 1862 bytes ...-motorized-wheelchair-light-skin-tone.webp | Bin 0 -> 1854 bytes ...ized-wheelchair-medium-dark-skin-tone.webp | Bin 0 -> 1810 bytes ...zed-wheelchair-medium-light-skin-tone.webp | Bin 0 -> 1856 bytes ...motorized-wheelchair-medium-skin-tone.webp | Bin 0 -> 1810 bytes ...people.person-in-motorized-wheelchair.webp | Bin 0 -> 1898 bytes ....person-in-steamy-room-dark-skin-tone.webp | Bin 0 -> 2072 bytes ...person-in-steamy-room-light-skin-tone.webp | Bin 0 -> 2138 bytes ...-in-steamy-room-medium-dark-skin-tone.webp | Bin 0 -> 2114 bytes ...in-steamy-room-medium-light-skin-tone.webp | Bin 0 -> 2010 bytes ...erson-in-steamy-room-medium-skin-tone.webp | Bin 0 -> 2096 bytes .../img/people.person-in-steamy-room.webp | Bin 0 -> 2258 bytes ...son-in-suit-levitating-dark-skin-tone.webp | Bin 0 -> 1038 bytes ...on-in-suit-levitating-light-skin-tone.webp | Bin 0 -> 1084 bytes ...suit-levitating-medium-dark-skin-tone.webp | Bin 0 -> 1080 bytes ...uit-levitating-medium-light-skin-tone.webp | Bin 0 -> 1118 bytes ...n-in-suit-levitating-medium-skin-tone.webp | Bin 0 -> 1082 bytes .../img/people.person-in-suit-levitating.webp | Bin 0 -> 1152 bytes ...eople.person-in-tuxedo-dark-skin-tone.webp | Bin 0 -> 1434 bytes ...ople.person-in-tuxedo-light-skin-tone.webp | Bin 0 -> 1606 bytes ...erson-in-tuxedo-medium-dark-skin-tone.webp | Bin 0 -> 1524 bytes ...rson-in-tuxedo-medium-light-skin-tone.webp | Bin 0 -> 1720 bytes ...ple.person-in-tuxedo-medium-skin-tone.webp | Bin 0 -> 1554 bytes public/flair/img/people.person-in-tuxedo.webp | Bin 0 -> 1790 bytes ...people.person-juggling-dark-skin-tone.webp | Bin 0 -> 2254 bytes ...eople.person-juggling-light-skin-tone.webp | Bin 0 -> 2334 bytes ...person-juggling-medium-dark-skin-tone.webp | Bin 0 -> 2276 bytes ...erson-juggling-medium-light-skin-tone.webp | Bin 0 -> 2314 bytes ...ople.person-juggling-medium-skin-tone.webp | Bin 0 -> 2286 bytes public/flair/img/people.person-juggling.webp | Bin 0 -> 2340 bytes ...people.person-kneeling-dark-skin-tone.webp | Bin 0 -> 1422 bytes ...-kneeling-facing-right-dark-skin-tone.webp | Bin 0 -> 1436 bytes ...kneeling-facing-right-light-skin-tone.webp | Bin 0 -> 1504 bytes ...ng-facing-right-medium-dark-skin-tone.webp | Bin 0 -> 1424 bytes ...g-facing-right-medium-light-skin-tone.webp | Bin 0 -> 1522 bytes ...neeling-facing-right-medium-skin-tone.webp | Bin 0 -> 1448 bytes .../people.person-kneeling-facing-right.webp | Bin 0 -> 1528 bytes ...eople.person-kneeling-light-skin-tone.webp | Bin 0 -> 1498 bytes ...person-kneeling-medium-dark-skin-tone.webp | Bin 0 -> 1422 bytes ...erson-kneeling-medium-light-skin-tone.webp | Bin 0 -> 1510 bytes ...ople.person-kneeling-medium-skin-tone.webp | Bin 0 -> 1434 bytes public/flair/img/people.person-kneeling.webp | Bin 0 -> 1524 bytes ...person-lifting-weights-dark-skin-tone.webp | Bin 0 -> 2026 bytes ...erson-lifting-weights-light-skin-tone.webp | Bin 0 -> 2166 bytes ...lifting-weights-medium-dark-skin-tone.webp | Bin 0 -> 2082 bytes ...ifting-weights-medium-light-skin-tone.webp | Bin 0 -> 2106 bytes ...rson-lifting-weights-medium-skin-tone.webp | Bin 0 -> 2090 bytes .../img/people.person-lifting-weights.webp | Bin 0 -> 2286 bytes .../people.person-light-skin-tone-bald.webp | Bin 0 -> 1506 bytes .../people.person-light-skin-tone-beard.webp | Bin 0 -> 1576 bytes ...ple.person-light-skin-tone-blond-hair.webp | Bin 0 -> 1782 bytes ...ple.person-light-skin-tone-curly-hair.webp | Bin 0 -> 1612 bytes ...eople.person-light-skin-tone-red-hair.webp | Bin 0 -> 1928 bytes ...ple.person-light-skin-tone-white-hair.webp | Bin 0 -> 1604 bytes .../img/people.person-light-skin-tone.webp | Bin 0 -> 1640 bytes ...ple.person-medium-dark-skin-tone-bald.webp | Bin 0 -> 1406 bytes ...le.person-medium-dark-skin-tone-beard.webp | Bin 0 -> 1424 bytes ...rson-medium-dark-skin-tone-blond-hair.webp | Bin 0 -> 1814 bytes ...rson-medium-dark-skin-tone-curly-hair.webp | Bin 0 -> 1498 bytes ...person-medium-dark-skin-tone-red-hair.webp | Bin 0 -> 1764 bytes ...rson-medium-dark-skin-tone-white-hair.webp | Bin 0 -> 1688 bytes .../people.person-medium-dark-skin-tone.webp | Bin 0 -> 1494 bytes ...le.person-medium-light-skin-tone-bald.webp | Bin 0 -> 1428 bytes ...e.person-medium-light-skin-tone-beard.webp | Bin 0 -> 1682 bytes ...son-medium-light-skin-tone-blond-hair.webp | Bin 0 -> 1732 bytes ...son-medium-light-skin-tone-curly-hair.webp | Bin 0 -> 1606 bytes ...erson-medium-light-skin-tone-red-hair.webp | Bin 0 -> 1838 bytes ...son-medium-light-skin-tone-white-hair.webp | Bin 0 -> 1582 bytes .../people.person-medium-light-skin-tone.webp | Bin 0 -> 1746 bytes .../people.person-medium-skin-tone-bald.webp | Bin 0 -> 1408 bytes .../people.person-medium-skin-tone-beard.webp | Bin 0 -> 1472 bytes ...le.person-medium-skin-tone-blond-hair.webp | Bin 0 -> 1784 bytes ...le.person-medium-skin-tone-curly-hair.webp | Bin 0 -> 1522 bytes ...ople.person-medium-skin-tone-red-hair.webp | Bin 0 -> 1802 bytes ...le.person-medium-skin-tone-white-hair.webp | Bin 0 -> 1636 bytes .../img/people.person-medium-skin-tone.webp | Bin 0 -> 1516 bytes ...person-mountain-biking-dark-skin-tone.webp | Bin 0 -> 1640 bytes ...erson-mountain-biking-light-skin-tone.webp | Bin 0 -> 1658 bytes ...mountain-biking-medium-dark-skin-tone.webp | Bin 0 -> 1638 bytes ...ountain-biking-medium-light-skin-tone.webp | Bin 0 -> 1634 bytes ...rson-mountain-biking-medium-skin-tone.webp | Bin 0 -> 1610 bytes .../img/people.person-mountain-biking.webp | Bin 0 -> 1672 bytes ...erson-playing-handball-dark-skin-tone.webp | Bin 0 -> 1858 bytes ...rson-playing-handball-light-skin-tone.webp | Bin 0 -> 1996 bytes ...laying-handball-medium-dark-skin-tone.webp | Bin 0 -> 1926 bytes ...aying-handball-medium-light-skin-tone.webp | Bin 0 -> 2000 bytes ...son-playing-handball-medium-skin-tone.webp | Bin 0 -> 1922 bytes .../img/people.person-playing-handball.webp | Bin 0 -> 2128 bytes ...son-playing-water-polo-dark-skin-tone.webp | Bin 0 -> 2038 bytes ...on-playing-water-polo-light-skin-tone.webp | Bin 0 -> 2090 bytes ...ying-water-polo-medium-dark-skin-tone.webp | Bin 0 -> 2004 bytes ...ing-water-polo-medium-light-skin-tone.webp | Bin 0 -> 2030 bytes ...n-playing-water-polo-medium-skin-tone.webp | Bin 0 -> 1990 bytes .../img/people.person-playing-water-polo.webp | Bin 0 -> 2024 bytes .../people.person-pouting-dark-skin-tone.webp | Bin 0 -> 1552 bytes ...people.person-pouting-light-skin-tone.webp | Bin 0 -> 1682 bytes ....person-pouting-medium-dark-skin-tone.webp | Bin 0 -> 1554 bytes ...person-pouting-medium-light-skin-tone.webp | Bin 0 -> 1660 bytes ...eople.person-pouting-medium-skin-tone.webp | Bin 0 -> 1576 bytes public/flair/img/people.person-pouting.webp | Bin 0 -> 1694 bytes ...le.person-raising-hand-dark-skin-tone.webp | Bin 0 -> 1698 bytes ...e.person-raising-hand-light-skin-tone.webp | Bin 0 -> 1922 bytes ...on-raising-hand-medium-dark-skin-tone.webp | Bin 0 -> 1770 bytes ...n-raising-hand-medium-light-skin-tone.webp | Bin 0 -> 1802 bytes ....person-raising-hand-medium-skin-tone.webp | Bin 0 -> 1768 bytes .../flair/img/people.person-raising-hand.webp | Bin 0 -> 1968 bytes public/flair/img/people.person-red-hair.webp | Bin 0 -> 1864 bytes ...ple.person-rowing-boat-dark-skin-tone.webp | Bin 0 -> 1684 bytes ...le.person-rowing-boat-light-skin-tone.webp | Bin 0 -> 1754 bytes ...son-rowing-boat-medium-dark-skin-tone.webp | Bin 0 -> 1696 bytes ...on-rowing-boat-medium-light-skin-tone.webp | Bin 0 -> 1776 bytes ...e.person-rowing-boat-medium-skin-tone.webp | Bin 0 -> 1726 bytes .../flair/img/people.person-rowing-boat.webp | Bin 0 -> 1778 bytes .../people.person-running-dark-skin-tone.webp | Bin 0 -> 1446 bytes ...n-running-facing-right-dark-skin-tone.webp | Bin 0 -> 1434 bytes ...running-facing-right-light-skin-tonet.webp | Bin 0 -> 1520 bytes ...ng-facing-right-medium-dark-skin-tone.webp | Bin 0 -> 1458 bytes ...g-facing-right-medium-light-skin-tone.webp | Bin 0 -> 1522 bytes ...running-facing-right-medium-skin-tone.webp | Bin 0 -> 1458 bytes .../people.person-running-facing-right.webp | Bin 0 -> 1584 bytes ...people.person-running-light-skin-tone.webp | Bin 0 -> 1520 bytes ....person-running-medium-dark-skin-tone.webp | Bin 0 -> 1464 bytes ...person-running-medium-light-skin-tone.webp | Bin 0 -> 1530 bytes ...eople.person-running-medium-skin-tone.webp | Bin 0 -> 1460 bytes public/flair/img/people.person-running.webp | Bin 0 -> 1586 bytes ...eople.person-shrugging-dark-skin-tone.webp | Bin 0 -> 1632 bytes ...ople.person-shrugging-light-skin-tone.webp | Bin 0 -> 1846 bytes ...erson-shrugging-medium-dark-skin-tone.webp | Bin 0 -> 1698 bytes ...rson-shrugging-medium-light-skin-tone.webp | Bin 0 -> 1740 bytes ...ple.person-shrugging-medium-skin-tone.webp | Bin 0 -> 1708 bytes public/flair/img/people.person-shrugging.webp | Bin 0 -> 1922 bytes ...people.person-standing-dark-skin-tone.webp | Bin 0 -> 1386 bytes ...eople.person-standing-light-skin-tone.webp | Bin 0 -> 1476 bytes ...person-standing-medium-dark-skin-tone.webp | Bin 0 -> 1386 bytes ...erson-standing-medium-light-skin-tone.webp | Bin 0 -> 1466 bytes ...ople.person-standing-medium-skin-tone.webp | Bin 0 -> 1406 bytes public/flair/img/people.person-standing.webp | Bin 0 -> 1514 bytes .../people.person-surfing-dark-skin-tone.webp | Bin 0 -> 1982 bytes ...people.person-surfing-light-skin-tone.webp | Bin 0 -> 2032 bytes ....person-surfing-medium-dark-skin-tone.webp | Bin 0 -> 1986 bytes ...person-surfing-medium-light-skin-tone.webp | Bin 0 -> 2014 bytes ...eople.person-surfing-medium-skin-tone.webp | Bin 0 -> 1964 bytes public/flair/img/people.person-surfing.webp | Bin 0 -> 2054 bytes ...people.person-swimming-dark-skin-tone.webp | Bin 0 -> 1442 bytes ...eople.person-swimming-light-skin-tone.webp | Bin 0 -> 1476 bytes ...person-swimming-medium-dark-skin-tone.webp | Bin 0 -> 1428 bytes ...erson-swimming-medium-light-skin-tone.webp | Bin 0 -> 1438 bytes ...ople.person-swimming-medium-skin-tone.webp | Bin 0 -> 1426 bytes public/flair/img/people.person-swimming.webp | Bin 0 -> 1468 bytes ...ple.person-taking-bath-dark-skin-tone.webp | Bin 0 -> 2322 bytes ...le.person-taking-bath-light-skin-tone.webp | Bin 0 -> 2352 bytes ...son-taking-bath-medium-dark-skin-tone.webp | Bin 0 -> 2348 bytes ...on-taking-bath-medium-light-skin-tone.webp | Bin 0 -> 2350 bytes ...e.person-taking-bath-medium-skin-tone.webp | Bin 0 -> 2338 bytes .../flair/img/people.person-taking-bath.webp | Bin 0 -> 2380 bytes ...le.person-tipping-hand-dark-skin-tone.webp | Bin 0 -> 1664 bytes ...e.person-tipping-hand-light-skin-tone.webp | Bin 0 -> 1888 bytes ...on-tipping-hand-medium-dark-skin-tone.webp | Bin 0 -> 1738 bytes ...n-tipping-hand-medium-light-skin-tone.webp | Bin 0 -> 1816 bytes ....person-tipping-hand-medium-skin-tone.webp | Bin 0 -> 1756 bytes .../flair/img/people.person-tipping-hand.webp | Bin 0 -> 1972 bytes .../people.person-walking-dark-skin-tone.webp | Bin 0 -> 1252 bytes ...n-walking-facing-right-dark-skin-tone.webp | Bin 0 -> 1232 bytes ...-walking-facing-right-light-skin-tone.webp | Bin 0 -> 1294 bytes ...ng-facing-right-medium-dark-skin-tone.webp | Bin 0 -> 1244 bytes ...g-facing-right-medium-light-skin-tone.webp | Bin 0 -> 1308 bytes ...walking-facing-right-medium-skin-tone.webp | Bin 0 -> 1240 bytes .../people.person-walking-facing-right.webp | Bin 0 -> 1314 bytes ...people.person-walking-light-skin-tone.webp | Bin 0 -> 1308 bytes ....person-walking-medium-dark-skin-tone.webp | Bin 0 -> 1240 bytes ...person-walking-medium-light-skin-tone.webp | Bin 0 -> 1326 bytes ...eople.person-walking-medium-skin-tone.webp | Bin 0 -> 1250 bytes public/flair/img/people.person-walking.webp | Bin 0 -> 1330 bytes ....person-wearing-turban-dark-skin-tone.webp | Bin 0 -> 1464 bytes ...person-wearing-turban-light-skin-tone.webp | Bin 0 -> 1598 bytes ...-wearing-turban-medium-dark-skin-tone.webp | Bin 0 -> 1544 bytes ...wearing-turban-medium-light-skin-tone.webp | Bin 0 -> 1596 bytes ...erson-wearing-turban-medium-skin-tone.webp | Bin 0 -> 1550 bytes .../img/people.person-wearing-turban.webp | Bin 0 -> 1736 bytes .../flair/img/people.person-white-hair.webp | Bin 0 -> 1688 bytes ...ople.person-with-crown-dark-skin-tone.webp | Bin 0 -> 2088 bytes ...ple.person-with-crown-light-skin-tone.webp | Bin 0 -> 2280 bytes ...rson-with-crown-medium-dark-skin-tone.webp | Bin 0 -> 2158 bytes ...son-with-crown-medium-light-skin-tone.webp | Bin 0 -> 2174 bytes ...le.person-with-crown-medium-skin-tone.webp | Bin 0 -> 2168 bytes .../flair/img/people.person-with-crown.webp | Bin 0 -> 2344 bytes ....person-with-headscarf-dark-skin-tone.webp | Bin 0 -> 1360 bytes ...person-with-headscarf-light-skin-tone.webp | Bin 0 -> 1536 bytes ...-with-headscarf-medium-dark-skin-tone.webp | Bin 0 -> 1406 bytes ...with-headscarf-medium-light-skin-tone.webp | Bin 0 -> 1484 bytes ...erson-with-headscarf-medium-skin-tone.webp | Bin 0 -> 1432 bytes .../img/people.person-with-headscarf.webp | Bin 0 -> 1560 bytes ...e.person-with-skullcap-dark-skin-tone.webp | Bin 0 -> 1710 bytes ....person-with-skullcap-light-skin-tone.webp | Bin 0 -> 1874 bytes ...n-with-skullcap-medium-dark-skin-tone.webp | Bin 0 -> 1750 bytes ...-with-skullcap-medium-light-skin-tone.webp | Bin 0 -> 1850 bytes ...person-with-skullcap-medium-skin-tone.webp | Bin 0 -> 1772 bytes .../img/people.person-with-skullcap.webp | Bin 0 -> 1914 bytes ...eople.person-with-veil-dark-skin-tone.webp | Bin 0 -> 1682 bytes ...ople.person-with-veil-light-skin-tone.webp | Bin 0 -> 1836 bytes ...erson-with-veil-medium-dark-skin-tone.webp | Bin 0 -> 1758 bytes ...rson-with-veil-medium-light-skin-tone.webp | Bin 0 -> 1810 bytes ...ple.person-with-veil-medium-skin-tone.webp | Bin 0 -> 1754 bytes public/flair/img/people.person-with-veil.webp | Bin 0 -> 1978 bytes ...person-with-white-cane-dark-skin-tone.webp | Bin 0 -> 1638 bytes ...hite-cane-facing-right-dark-skin-tone.webp | Bin 0 -> 1596 bytes ...ite-cane-facing-right-light-skin-tone.webp | Bin 0 -> 1642 bytes ...ne-facing-right-medium-dark-skin-tone.webp | Bin 0 -> 1600 bytes ...e-facing-right-medium-light-skin-tone.webp | Bin 0 -> 1658 bytes ...te-cane-facing-right-medium-skin-tone.webp | Bin 0 -> 1604 bytes ...e.person-with-white-cane-facing-right.webp | Bin 0 -> 1666 bytes ...erson-with-white-cane-light-skin-tone.webp | Bin 0 -> 1692 bytes ...with-white-cane-medium-dark-skin-tone.webp | Bin 0 -> 1648 bytes ...ith-white-cane-medium-light-skin-tone.webp | Bin 0 -> 1706 bytes ...rson-with-white-cane-medium-skin-tone.webp | Bin 0 -> 1638 bytes .../img/people.person-with-white-cane.webp | Bin 0 -> 1724 bytes public/flair/img/people.person.webp | Bin 0 -> 1840 bytes .../img/people.pilot-dark-skin-tone.webp | Bin 0 -> 1594 bytes .../img/people.pilot-light-skin-tone.webp | Bin 0 -> 1754 bytes .../people.pilot-medium-dark-skin-tone.webp | Bin 0 -> 1674 bytes .../people.pilot-medium-light-skin-tone.webp | Bin 0 -> 1826 bytes .../img/people.pilot-medium-skin-tone.webp | Bin 0 -> 1686 bytes public/flair/img/people.pilot.webp | Bin 0 -> 1894 bytes ...people.pinched-fingers-dark-skin-tone.webp | Bin 0 -> 1216 bytes ...eople.pinched-fingers-light-skin-tone.webp | Bin 0 -> 1474 bytes ...pinched-fingers-medium-dark-skin-tone.webp | Bin 0 -> 1350 bytes ...inched-fingers-medium-light-skin-tone.webp | Bin 0 -> 1454 bytes ...ople.pinched-fingers-medium-skin-tone.webp | Bin 0 -> 1404 bytes public/flair/img/people.pinched-fingers.webp | Bin 0 -> 1632 bytes .../people.police-officer-dark-skin-tone.webp | Bin 0 -> 1820 bytes ...people.police-officer-light-skin-tone.webp | Bin 0 -> 1988 bytes ....police-officer-medium-dark-skin-tone.webp | Bin 0 -> 1864 bytes ...police-officer-medium-light-skin-tone.webp | Bin 0 -> 1998 bytes ...eople.police-officer-medium-skin-tone.webp | Bin 0 -> 1900 bytes public/flair/img/people.police-officer.webp | Bin 0 -> 2068 bytes .../people.pregnant-man-dark-skin-tone.webp | Bin 0 -> 1212 bytes .../people.pregnant-man-light-skin-tone.webp | Bin 0 -> 1378 bytes ...le.pregnant-man-medium-dark-skin-tone.webp | Bin 0 -> 1258 bytes ...e.pregnant-man-medium-light-skin-tone.webp | Bin 0 -> 1386 bytes .../people.pregnant-man-medium-skin-tone.webp | Bin 0 -> 1292 bytes public/flair/img/people.pregnant-man.webp | Bin 0 -> 1468 bytes ...people.pregnant-person-dark-skin-tone.webp | Bin 0 -> 1326 bytes ...eople.pregnant-person-light-skin-tone.webp | Bin 0 -> 1422 bytes ...pregnant-person-medium-dark-skin-tone.webp | Bin 0 -> 1328 bytes ...regnant-person-medium-light-skin-tone.webp | Bin 0 -> 1426 bytes ...ople.pregnant-person-medium-skin-tone.webp | Bin 0 -> 1312 bytes public/flair/img/people.pregnant-person.webp | Bin 0 -> 1436 bytes .../people.pregnant-woman-dark-skin-tone.webp | Bin 0 -> 1172 bytes ...people.pregnant-woman-light-skin-tone.webp | Bin 0 -> 1366 bytes ....pregnant-woman-medium-dark-skin-tone.webp | Bin 0 -> 1256 bytes ...pregnant-woman-medium-light-skin-tone.webp | Bin 0 -> 1404 bytes ...eople.pregnant-woman-medium-skin-tone.webp | Bin 0 -> 1296 bytes public/flair/img/people.pregnant-woman.webp | Bin 0 -> 1404 bytes .../img/people.prince-dark-skin-tone.webp | Bin 0 -> 2060 bytes .../img/people.prince-light-skin-tone.webp | Bin 0 -> 2274 bytes .../people.prince-medium-dark-skin-tone.webp | Bin 0 -> 2108 bytes .../people.prince-medium-light-skin-tone.webp | Bin 0 -> 2096 bytes .../img/people.prince-medium-skin-tone.webp | Bin 0 -> 2126 bytes public/flair/img/people.prince.webp | Bin 0 -> 2304 bytes .../img/people.princess-dark-skin-tone.webp | Bin 0 -> 1970 bytes .../img/people.princess-light-skin-tone.webp | Bin 0 -> 2130 bytes ...people.princess-medium-dark-skin-tone.webp | Bin 0 -> 2018 bytes ...eople.princess-medium-light-skin-tone.webp | Bin 0 -> 1976 bytes .../img/people.princess-medium-skin-tone.webp | Bin 0 -> 2012 bytes public/flair/img/people.princess.webp | Bin 0 -> 2166 bytes ...le.raised-back-of-hand-dark-skin-tone.webp | Bin 0 -> 1242 bytes ...e.raised-back-of-hand-light-skin-tone.webp | Bin 0 -> 1496 bytes ...ed-back-of-hand-medium-dark-skin-tone.webp | Bin 0 -> 1378 bytes ...d-back-of-hand-medium-light-skin-tone.webp | Bin 0 -> 1474 bytes ....raised-back-of-hand-medium-skin-tone.webp | Bin 0 -> 1412 bytes .../flair/img/people.raised-back-of-hand.webp | Bin 0 -> 1618 bytes .../people.raised-fist-dark-skin-tone.webp | Bin 0 -> 1242 bytes .../people.raised-fist-light-skin-tone.webp | Bin 0 -> 1528 bytes ...ple.raised-fist-medium-dark-skin-tone.webp | Bin 0 -> 1364 bytes ...le.raised-fist-medium-light-skin-tone.webp | Bin 0 -> 1508 bytes .../people.raised-fist-medium-skin-tone.webp | Bin 0 -> 1438 bytes public/flair/img/people.raised-fist.webp | Bin 0 -> 1664 bytes .../people.raised-hand-dark-skin-tone.webp | Bin 0 -> 1216 bytes .../people.raised-hand-light-skin-tone.webp | Bin 0 -> 1428 bytes ...ple.raised-hand-medium-dark-skin-tone.webp | Bin 0 -> 1308 bytes ...le.raised-hand-medium-light-skin-tone.webp | Bin 0 -> 1416 bytes .../people.raised-hand-medium-skin-tone.webp | Bin 0 -> 1362 bytes public/flair/img/people.raised-hand.webp | Bin 0 -> 1540 bytes .../people.raising-hands-dark-skin-tone.webp | Bin 0 -> 2030 bytes .../people.raising-hands-light-skin-tone.webp | Bin 0 -> 2300 bytes ...e.raising-hands-medium-dark-skin-tone.webp | Bin 0 -> 2158 bytes ....raising-hands-medium-light-skin-tone.webp | Bin 0 -> 2268 bytes ...people.raising-hands-medium-skin-tone.webp | Bin 0 -> 2198 bytes public/flair/img/people.raising-hands.webp | Bin 0 -> 2412 bytes public/flair/img/people.red-hair.webp | Bin 0 -> 1882 bytes ...ople.right-facing-fist-dark-skin-tone.webp | Bin 0 -> 952 bytes ...ple.right-facing-fist-light-skin-tone.webp | Bin 0 -> 1168 bytes ...ght-facing-fist-medium-dark-skin-tone.webp | Bin 0 -> 1072 bytes ...ht-facing-fist-medium-light-skin-tone.webp | Bin 0 -> 1154 bytes ...le.right-facing-fist-medium-skin-tone.webp | Bin 0 -> 1104 bytes .../flair/img/people.right-facing-fist.webp | Bin 0 -> 1284 bytes ...people.rightwards-hand-dark-skin-tone.webp | Bin 0 -> 1176 bytes ...eople.rightwards-hand-light-skin-tone.webp | Bin 0 -> 1418 bytes ...rightwards-hand-medium-dark-skin-tone.webp | Bin 0 -> 1284 bytes ...ightwards-hand-medium-light-skin-tone.webp | Bin 0 -> 1398 bytes ...ople.rightwards-hand-medium-skin-tone.webp | Bin 0 -> 1340 bytes public/flair/img/people.rightwards-hand.webp | Bin 0 -> 1528 bytes ...ightwards-pushing-hand-dark-skin-tone.webp | Bin 0 -> 1056 bytes ...ghtwards-pushing-hand-light-skin-tone.webp | Bin 0 -> 1284 bytes ...ds-pushing-hand-medium-dark-skin-tone.webp | Bin 0 -> 1186 bytes ...s-pushing-hand-medium-light-skin-tone.webp | Bin 0 -> 1254 bytes ...htwards-pushing-hand-medium-skin-tone.webp | Bin 0 -> 1202 bytes .../img/people.rightwards-pushing-hand.webp | Bin 0 -> 1412 bytes .../people.santa-claus-dark-skin-tone.webp | Bin 0 -> 1684 bytes .../people.santa-claus-light-skin-tone.webp | Bin 0 -> 1628 bytes ...ple.santa-claus-medium-dark-skin-tone.webp | Bin 0 -> 1710 bytes ...le.santa-claus-medium-light-skin-tone.webp | Bin 0 -> 1656 bytes .../people.santa-claus-medium-skin-tone.webp | Bin 0 -> 1690 bytes public/flair/img/people.santa-claus.webp | Bin 0 -> 1736 bytes .../img/people.scientist-dark-skin-tone.webp | Bin 0 -> 2050 bytes .../img/people.scientist-light-skin-tone.webp | Bin 0 -> 2134 bytes ...eople.scientist-medium-dark-skin-tone.webp | Bin 0 -> 2118 bytes ...ople.scientist-medium-light-skin-tone.webp | Bin 0 -> 2216 bytes .../people.scientist-medium-skin-tone.webp | Bin 0 -> 2118 bytes public/flair/img/people.scientist.webp | Bin 0 -> 2288 bytes .../img/people.selfie-dark-skin-tone.webp | Bin 0 -> 1442 bytes .../img/people.selfie-light-skin-tone.webp | Bin 0 -> 1498 bytes .../people.selfie-medium-dark-skin-tone.webp | Bin 0 -> 1442 bytes .../people.selfie-medium-light-skin-tone.webp | Bin 0 -> 1476 bytes .../img/people.selfie-medium-skin-tone.webp | Bin 0 -> 1440 bytes public/flair/img/people.selfie.webp | Bin 0 -> 1534 bytes ...ople.sign-of-the-horns-dark-skin-tone.webp | Bin 0 -> 1158 bytes ...ple.sign-of-the-horns-light-skin-tone.webp | Bin 0 -> 1384 bytes ...gn-of-the-horns-medium-dark-skin-tone.webp | Bin 0 -> 1258 bytes ...n-of-the-horns-medium-light-skin-tone.webp | Bin 0 -> 1376 bytes ...le.sign-of-the-horns-medium-skin-tone.webp | Bin 0 -> 1324 bytes .../flair/img/people.sign-of-the-horns.webp | Bin 0 -> 1540 bytes .../img/people.singer-dark-skin-tone.webp | Bin 0 -> 1982 bytes .../img/people.singer-light-skin-tone.webp | Bin 0 -> 2196 bytes .../people.singer-medium-dark-skin-tone.webp | Bin 0 -> 2036 bytes .../people.singer-medium-light-skin-tone.webp | Bin 0 -> 2160 bytes .../img/people.singer-medium-skin-tone.webp | Bin 0 -> 2082 bytes public/flair/img/people.singer.webp | Bin 0 -> 2260 bytes public/flair/img/people.skier.webp | Bin 0 -> 2084 bytes .../people.snowboarder-dark-skin-tone.webp | Bin 0 -> 2158 bytes .../people.snowboarder-light-skin-tone.webp | Bin 0 -> 2158 bytes ...ple.snowboarder-medium-dark-skin-tone.webp | Bin 0 -> 2158 bytes ...le.snowboarder-medium-light-skin-tone.webp | Bin 0 -> 2158 bytes .../people.snowboarder-medium-skin-tone.webp | Bin 0 -> 2158 bytes public/flair/img/people.snowboarder.webp | Bin 0 -> 2158 bytes public/flair/img/people.speaking-head.webp | Bin 0 -> 1518 bytes .../img/people.student-dark-skin-tone.webp | Bin 0 -> 1598 bytes .../img/people.student-light-skin-tone.webp | Bin 0 -> 1828 bytes .../people.student-medium-dark-skin-tone.webp | Bin 0 -> 1704 bytes ...people.student-medium-light-skin-tone.webp | Bin 0 -> 1868 bytes .../img/people.student-medium-skin-tone.webp | Bin 0 -> 1720 bytes public/flair/img/people.student.webp | Bin 0 -> 1956 bytes .../img/people.superhero-dark-skin-tone.webp | Bin 0 -> 1786 bytes .../img/people.superhero-light-skin-tone.webp | Bin 0 -> 1818 bytes ...eople.superhero-medium-dark-skin-tone.webp | Bin 0 -> 1786 bytes ...ople.superhero-medium-light-skin-tone.webp | Bin 0 -> 1816 bytes .../people.superhero-medium-skin-tone.webp | Bin 0 -> 1798 bytes public/flair/img/people.superhero.webp | Bin 0 -> 1810 bytes .../people.supervillain-dark-skin-tone.webp | Bin 0 -> 1952 bytes .../people.supervillain-light-skin-tone.webp | Bin 0 -> 2004 bytes ...le.supervillain-medium-dark-skin-tone.webp | Bin 0 -> 1960 bytes ...e.supervillain-medium-light-skin-tone.webp | Bin 0 -> 1992 bytes .../people.supervillain-medium-skin-tone.webp | Bin 0 -> 1968 bytes public/flair/img/people.supervillain.webp | Bin 0 -> 1984 bytes .../img/people.teacher-dark-skin-tone.webp | Bin 0 -> 1508 bytes .../img/people.teacher-light-skin-tone.webp | Bin 0 -> 1724 bytes .../people.teacher-medium-dark-skin-tone.webp | Bin 0 -> 1588 bytes ...people.teacher-medium-light-skin-tone.webp | Bin 0 -> 1684 bytes .../img/people.teacher-medium-skin-tone.webp | Bin 0 -> 1584 bytes public/flair/img/people.teacher.webp | Bin 0 -> 1752 bytes .../people.technologist-dark-skin-tone.webp | Bin 0 -> 1484 bytes .../people.technologist-light-skin-tone.webp | Bin 0 -> 1644 bytes ...le.technologist-medium-dark-skin-tone.webp | Bin 0 -> 1528 bytes ...e.technologist-medium-light-skin-tone.webp | Bin 0 -> 1596 bytes .../people.technologist-medium-skin-tone.webp | Bin 0 -> 1534 bytes public/flair/img/people.technologist.webp | Bin 0 -> 1716 bytes .../img/people.thumbs-up-dark-skin-tone.webp | Bin 0 -> 1174 bytes .../img/people.thumbs-up-light-skin-tone.webp | Bin 0 -> 1410 bytes ...eople.thumbs-up-medium-dark-skin-tone.webp | Bin 0 -> 1324 bytes ...ople.thumbs-up-medium-light-skin-tone.webp | Bin 0 -> 1404 bytes .../people.thumbs-up-medium-skin-tone.webp | Bin 0 -> 1366 bytes public/flair/img/people.thumbs-up.webp | Bin 0 -> 1586 bytes public/flair/img/people.tongue.webp | Bin 0 -> 1210 bytes public/flair/img/people.tooth.webp | Bin 0 -> 1234 bytes public/flair/img/people.troll.webp | Bin 0 -> 1834 bytes .../img/people.vampire-dark-skin-tone.webp | Bin 0 -> 1624 bytes .../img/people.vampire-light-skin-tone.webp | Bin 0 -> 1772 bytes .../people.vampire-medium-dark-skin-tone.webp | Bin 0 -> 1678 bytes ...people.vampire-medium-light-skin-tone.webp | Bin 0 -> 1802 bytes .../img/people.vampire-medium-skin-tone.webp | Bin 0 -> 1680 bytes public/flair/img/people.vampire.webp | Bin 0 -> 1856 bytes .../people.victory-hand-dark-skin-tone.webp | Bin 0 -> 1206 bytes .../people.victory-hand-light-skin-tone.webp | Bin 0 -> 1468 bytes ...le.victory-hand-medium-dark-skin-tone.webp | Bin 0 -> 1338 bytes ...e.victory-hand-medium-light-skin-tone.webp | Bin 0 -> 1460 bytes .../people.victory-hand-medium-skin-tone.webp | Bin 0 -> 1400 bytes public/flair/img/people.victory-hand.webp | Bin 0 -> 1596 bytes .../people.vulcan-salute-dark-skin-tone.webp | Bin 0 -> 1286 bytes .../people.vulcan-salute-light-skin-tone.webp | Bin 0 -> 1602 bytes ...e.vulcan-salute-medium-dark-skin-tone.webp | Bin 0 -> 1456 bytes ....vulcan-salute-medium-light-skin-tone.webp | Bin 0 -> 1550 bytes ...people.vulcan-salute-medium-skin-tone.webp | Bin 0 -> 1490 bytes public/flair/img/people.vulcan-salute.webp | Bin 0 -> 1742 bytes .../people.waving-hand-dark-skin-tone.webp | Bin 0 -> 1942 bytes .../people.waving-hand-light-skin-tone.webp | Bin 0 -> 2158 bytes ...ple.waving-hand-medium-dark-skin-tone.webp | Bin 0 -> 2070 bytes ...le.waving-hand-medium-light-skin-tone.webp | Bin 0 -> 2134 bytes .../people.waving-hand-medium-skin-tone.webp | Bin 0 -> 2104 bytes public/flair/img/people.waving-hand.webp | Bin 0 -> 2290 bytes public/flair/img/people.white-hair.webp | Bin 0 -> 1614 bytes ...-hands-dark-skin-tone-light-skin-tone.webp | Bin 0 -> 2408 bytes ...-dark-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2310 bytes ...dark-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2404 bytes ...hands-dark-skin-tone-medium-skin-tone.webp | Bin 0 -> 2332 bytes ...-and-man-holding-hands-dark-skin-tone.webp | Bin 0 -> 2246 bytes ...-hands-light-skin-tone-dark-skin-tone.webp | Bin 0 -> 2410 bytes ...light-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2454 bytes ...ight-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2518 bytes ...ands-light-skin-tone-medium-skin-tone.webp | Bin 0 -> 2458 bytes ...and-man-holding-hands-light-skin-tone.webp | Bin 0 -> 2506 bytes ...-medium-dark-skin-tone-dark-skin-tone.webp | Bin 0 -> 2318 bytes ...medium-dark-skin-tone-light-skin-tone.webp | Bin 0 -> 2470 bytes ...dark-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2462 bytes ...edium-dark-skin-tone-medium-skin-tone.webp | Bin 0 -> 2378 bytes ...n-holding-hands-medium-dark-skin-tone.webp | Bin 0 -> 2352 bytes ...medium-light-skin-tone-dark-skin-tone.webp | Bin 0 -> 2424 bytes ...edium-light-skin-tone-light-skin-tone.webp | Bin 0 -> 2530 bytes ...light-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2464 bytes ...dium-light-skin-tone-medium-skin-tone.webp | Bin 0 -> 2466 bytes ...-holding-hands-medium-light-skin-tone.webp | Bin 0 -> 2522 bytes ...hands-medium-skin-tone-dark-skin-tone.webp | Bin 0 -> 2344 bytes ...ands-medium-skin-tone-light-skin-tone.webp | Bin 0 -> 2486 bytes ...edium-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2386 bytes ...dium-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2466 bytes ...nd-man-holding-hands-medium-skin-tone.webp | Bin 0 -> 2388 bytes .../people.woman-and-man-holding-hands.webp | Bin 0 -> 2634 bytes .../people.woman-artist-dark-skin-tone.webp | Bin 0 -> 1782 bytes .../people.woman-artist-light-skin-tone.webp | Bin 0 -> 1950 bytes ...le.woman-artist-medium-dark-skin-tone.webp | Bin 0 -> 1846 bytes ...e.woman-artist-medium-light-skin-tone.webp | Bin 0 -> 1944 bytes .../people.woman-artist-medium-skin-tone.webp | Bin 0 -> 1868 bytes public/flair/img/people.woman-artist.webp | Bin 0 -> 2050 bytes ...people.woman-astronaut-dark-skin-tone.webp | Bin 0 -> 1452 bytes ...eople.woman-astronaut-light-skin-tone.webp | Bin 0 -> 1622 bytes ...woman-astronaut-medium-dark-skin-tone.webp | Bin 0 -> 1532 bytes ...oman-astronaut-medium-light-skin-tone.webp | Bin 0 -> 1572 bytes ...ople.woman-astronaut-medium-skin-tone.webp | Bin 0 -> 1536 bytes public/flair/img/people.woman-astronaut.webp | Bin 0 -> 1736 bytes public/flair/img/people.woman-bald.webp | Bin 0 -> 1624 bytes public/flair/img/people.woman-beard.webp | Bin 0 -> 1444 bytes .../people.woman-biking-dark-skin-tone.webp | Bin 0 -> 2652 bytes .../people.woman-biking-light-skin-tone.webp | Bin 0 -> 2700 bytes ...le.woman-biking-medium-dark-skin-tone.webp | Bin 0 -> 2672 bytes ...e.woman-biking-medium-light-skin-tone.webp | Bin 0 -> 2716 bytes .../people.woman-biking-medium-skin-tone.webp | Bin 0 -> 2674 bytes public/flair/img/people.woman-biking.webp | Bin 0 -> 2726 bytes public/flair/img/people.woman-blond-hair.webp | Bin 0 -> 1474 bytes ...le.woman-bouncing-ball-dark-skin-tone.webp | Bin 0 -> 1934 bytes ...e.woman-bouncing-ball-light-skin-tone.webp | Bin 0 -> 2128 bytes ...n-bouncing-ball-medium-dark-skin-tone.webp | Bin 0 -> 2018 bytes ...-bouncing-ball-medium-light-skin-tone.webp | Bin 0 -> 2154 bytes ....woman-bouncing-ball-medium-skin-tone.webp | Bin 0 -> 2050 bytes .../flair/img/people.woman-bouncing-ball.webp | Bin 0 -> 2254 bytes .../people.woman-bowing-dark-skin-tone.webp | Bin 0 -> 1624 bytes .../people.woman-bowing-light-skin-tone.webp | Bin 0 -> 1852 bytes ...le.woman-bowing-medium-dark-skin-tone.webp | Bin 0 -> 1718 bytes ...e.woman-bowing-medium-light-skin-tone.webp | Bin 0 -> 1830 bytes .../people.woman-bowing-medium-skin-tone.webp | Bin 0 -> 1752 bytes public/flair/img/people.woman-bowing.webp | Bin 0 -> 1932 bytes ...ple.woman-cartwheeling-dark-skin-tone.webp | Bin 0 -> 1512 bytes ...le.woman-cartwheeling-light-skin-tone.webp | Bin 0 -> 1738 bytes ...an-cartwheeling-medium-dark-skin-tone.webp | Bin 0 -> 1632 bytes ...n-cartwheeling-medium-light-skin-tone.webp | Bin 0 -> 1734 bytes ...e.woman-cartwheeling-medium-skin-tone.webp | Bin 0 -> 1652 bytes .../flair/img/people.woman-cartwheeling.webp | Bin 0 -> 1902 bytes .../people.woman-climbing-dark-skin-tone.webp | Bin 0 -> 1958 bytes ...people.woman-climbing-light-skin-tone.webp | Bin 0 -> 2094 bytes ....woman-climbing-medium-dark-skin-tone.webp | Bin 0 -> 2006 bytes ...woman-climbing-medium-light-skin-tone.webp | Bin 0 -> 2100 bytes ...eople.woman-climbing-medium-skin-tone.webp | Bin 0 -> 2026 bytes public/flair/img/people.woman-climbing.webp | Bin 0 -> 2140 bytes ...an-construction-worker-dark-skin-tone.webp | Bin 0 -> 1804 bytes ...n-construction-worker-light-skin-tone.webp | Bin 0 -> 1902 bytes ...truction-worker-medium-dark-skin-tone.webp | Bin 0 -> 1816 bytes ...ruction-worker-medium-light-skin-tone.webp | Bin 0 -> 1798 bytes ...-construction-worker-medium-skin-tone.webp | Bin 0 -> 1820 bytes .../img/people.woman-construction-worker.webp | Bin 0 -> 1926 bytes .../img/people.woman-cook-dark-skin-tone.webp | Bin 0 -> 1808 bytes .../people.woman-cook-light-skin-tone.webp | Bin 0 -> 1944 bytes ...ople.woman-cook-medium-dark-skin-tone.webp | Bin 0 -> 1894 bytes ...ple.woman-cook-medium-light-skin-tone.webp | Bin 0 -> 1906 bytes .../people.woman-cook-medium-skin-tone.webp | Bin 0 -> 1866 bytes public/flair/img/people.woman-cook.webp | Bin 0 -> 2096 bytes public/flair/img/people.woman-curly-hair.webp | Bin 0 -> 1662 bytes .../people.woman-dancing-dark-skin-tone.webp | Bin 0 -> 1852 bytes .../people.woman-dancing-light-skin-tone.webp | Bin 0 -> 2062 bytes ...e.woman-dancing-medium-dark-skin-tone.webp | Bin 0 -> 1916 bytes ....woman-dancing-medium-light-skin-tone.webp | Bin 0 -> 2050 bytes ...people.woman-dancing-medium-skin-tone.webp | Bin 0 -> 1962 bytes public/flair/img/people.woman-dancing.webp | Bin 0 -> 2118 bytes .../img/people.woman-dark-skin-tone-bald.webp | Bin 0 -> 1238 bytes .../people.woman-dark-skin-tone-beard.webp | Bin 0 -> 1072 bytes ...eople.woman-dark-skin-tone-blond-hair.webp | Bin 0 -> 1534 bytes ...eople.woman-dark-skin-tone-curly-hair.webp | Bin 0 -> 1338 bytes .../people.woman-dark-skin-tone-red-hair.webp | Bin 0 -> 1522 bytes ...eople.woman-dark-skin-tone-white-hair.webp | Bin 0 -> 1414 bytes .../img/people.woman-dark-skin-tone.webp | Bin 0 -> 1084 bytes ...people.woman-detective-dark-skin-tone.webp | Bin 0 -> 1574 bytes ...eople.woman-detective-light-skin-tone.webp | Bin 0 -> 1770 bytes ...woman-detective-medium-dark-skin-tone.webp | Bin 0 -> 1654 bytes ...oman-detective-medium-light-skin-tone.webp | Bin 0 -> 1744 bytes ...ople.woman-detective-medium-skin-tone.webp | Bin 0 -> 1660 bytes public/flair/img/people.woman-detective.webp | Bin 0 -> 1890 bytes .../img/people.woman-elf-dark-skin-tone.webp | Bin 0 -> 1724 bytes .../img/people.woman-elf-light-skin-tone.webp | Bin 0 -> 1774 bytes ...eople.woman-elf-medium-dark-skin-tone.webp | Bin 0 -> 1780 bytes ...ople.woman-elf-medium-light-skin-tone.webp | Bin 0 -> 1748 bytes .../people.woman-elf-medium-skin-tone.webp | Bin 0 -> 1762 bytes public/flair/img/people.woman-elf.webp | Bin 0 -> 1888 bytes ...ople.woman-facepalming-dark-skin-tone.webp | Bin 0 -> 1204 bytes ...ple.woman-facepalming-light-skin-tone.webp | Bin 0 -> 1388 bytes ...man-facepalming-medium-dark-skin-tone.webp | Bin 0 -> 1280 bytes ...an-facepalming-medium-light-skin-tone.webp | Bin 0 -> 1464 bytes ...le.woman-facepalming-medium-skin-tone.webp | Bin 0 -> 1316 bytes .../flair/img/people.woman-facepalming.webp | Bin 0 -> 1508 bytes ...e.woman-factory-worker-dark-skin-tone.webp | Bin 0 -> 2002 bytes ....woman-factory-worker-light-skin-tone.webp | Bin 0 -> 2134 bytes ...-factory-worker-medium-dark-skin-tone.webp | Bin 0 -> 2034 bytes ...factory-worker-medium-light-skin-tone.webp | Bin 0 -> 2086 bytes ...woman-factory-worker-medium-skin-tone.webp | Bin 0 -> 2042 bytes .../img/people.woman-factory-worker.webp | Bin 0 -> 2178 bytes .../people.woman-fairy-dark-skin-tone.webp | Bin 0 -> 2204 bytes .../people.woman-fairy-light-skin-tone.webp | Bin 0 -> 2252 bytes ...ple.woman-fairy-medium-dark-skin-tone.webp | Bin 0 -> 2252 bytes ...le.woman-fairy-medium-light-skin-tone.webp | Bin 0 -> 2226 bytes .../people.woman-fairy-medium-skin-tone.webp | Bin 0 -> 2238 bytes public/flair/img/people.woman-fairy.webp | Bin 0 -> 2356 bytes .../people.woman-farmer-dark-skin-tone.webp | Bin 0 -> 2216 bytes .../people.woman-farmer-light-skin-tone.webp | Bin 0 -> 2310 bytes ...le.woman-farmer-medium-dark-skin-tone.webp | Bin 0 -> 2246 bytes ...e.woman-farmer-medium-light-skin-tone.webp | Bin 0 -> 2174 bytes .../people.woman-farmer-medium-skin-tone.webp | Bin 0 -> 2232 bytes public/flair/img/people.woman-farmer.webp | Bin 0 -> 2362 bytes ...ple.woman-feeding-baby-dark-skin-tone.webp | Bin 0 -> 1540 bytes ...le.woman-feeding-baby-light-skin-tone.webp | Bin 0 -> 1674 bytes ...an-feeding-baby-medium-dark-skin-tone.webp | Bin 0 -> 1592 bytes ...n-feeding-baby-medium-light-skin-tone.webp | Bin 0 -> 1704 bytes ...e.woman-feeding-baby-medium-skin-tone.webp | Bin 0 -> 1606 bytes .../flair/img/people.woman-feeding-baby.webp | Bin 0 -> 1742 bytes ...ople.woman-firefighter-dark-skin-tone.webp | Bin 0 -> 1894 bytes ...ple.woman-firefighter-light-skin-tone.webp | Bin 0 -> 1952 bytes ...man-firefighter-medium-dark-skin-tone.webp | Bin 0 -> 1870 bytes ...an-firefighter-medium-light-skin-tone.webp | Bin 0 -> 1880 bytes ...le.woman-firefighter-medium-skin-tone.webp | Bin 0 -> 1866 bytes .../flair/img/people.woman-firefighter.webp | Bin 0 -> 1936 bytes .../people.woman-frowning-dark-skin-tone.webp | Bin 0 -> 1090 bytes ...people.woman-frowning-light-skin-tone.webp | Bin 0 -> 1270 bytes ....woman-frowning-medium-dark-skin-tone.webp | Bin 0 -> 1162 bytes ...woman-frowning-medium-light-skin-tone.webp | Bin 0 -> 1340 bytes ...eople.woman-frowning-medium-skin-tone.webp | Bin 0 -> 1202 bytes public/flair/img/people.woman-frowning.webp | Bin 0 -> 1382 bytes public/flair/img/people.woman-genie.webp | Bin 0 -> 2958 bytes ...ple.woman-gesturing-no-dark-skin-tone.webp | Bin 0 -> 1458 bytes ...le.woman-gesturing-no-light-skin-tone.webp | Bin 0 -> 1696 bytes ...an-gesturing-no-medium-dark-skin-tone.webp | Bin 0 -> 1544 bytes ...n-gesturing-no-medium-light-skin-tone.webp | Bin 0 -> 1764 bytes ...e.woman-gesturing-no-medium-skin-tone.webp | Bin 0 -> 1594 bytes .../flair/img/people.woman-gesturing-no.webp | Bin 0 -> 1814 bytes ...ple.woman-gesturing-ok-dark-skin-tone.webp | Bin 0 -> 1480 bytes ...le.woman-gesturing-ok-light-skin-tone.webp | Bin 0 -> 1764 bytes ...an-gesturing-ok-medium-dark-skin-tone.webp | Bin 0 -> 1562 bytes ...n-gesturing-ok-medium-light-skin-tone.webp | Bin 0 -> 1658 bytes ...e.woman-gesturing-ok-medium-skin-tone.webp | Bin 0 -> 1562 bytes .../flair/img/people.woman-gesturing-ok.webp | Bin 0 -> 1824 bytes ....woman-getting-haircut-dark-skin-tone.webp | Bin 0 -> 1686 bytes ...woman-getting-haircut-light-skin-tone.webp | Bin 0 -> 1860 bytes ...getting-haircut-medium-dark-skin-tone.webp | Bin 0 -> 1732 bytes ...etting-haircut-medium-light-skin-tone.webp | Bin 0 -> 1878 bytes ...oman-getting-haircut-medium-skin-tone.webp | Bin 0 -> 1762 bytes .../img/people.woman-getting-haircut.webp | Bin 0 -> 1958 bytes ....woman-getting-massage-dark-skin-tone.webp | Bin 0 -> 1354 bytes ...woman-getting-massage-light-skin-tone.webp | Bin 0 -> 1710 bytes ...getting-massage-medium-dark-skin-tone.webp | Bin 0 -> 1512 bytes ...etting-massage-medium-light-skin-tone.webp | Bin 0 -> 1572 bytes ...oman-getting-massage-medium-skin-tone.webp | Bin 0 -> 1506 bytes .../img/people.woman-getting-massage.webp | Bin 0 -> 1834 bytes .../people.woman-golfing-dark-skin-tone.webp | Bin 0 -> 1672 bytes .../people.woman-golfing-light-skin-tone.webp | Bin 0 -> 1730 bytes ...e.woman-golfing-medium-dark-skin-tone.webp | Bin 0 -> 1692 bytes ....woman-golfing-medium-light-skin-tone.webp | Bin 0 -> 1736 bytes ...people.woman-golfing-medium-skin-tone.webp | Bin 0 -> 1696 bytes public/flair/img/people.woman-golfing.webp | Bin 0 -> 1738 bytes .../people.woman-guard-dark-skin-tone.webp | Bin 0 -> 1236 bytes .../people.woman-guard-light-skin-tone.webp | Bin 0 -> 1332 bytes ...ple.woman-guard-medium-dark-skin-tone.webp | Bin 0 -> 1270 bytes ...le.woman-guard-medium-light-skin-tone.webp | Bin 0 -> 1386 bytes .../people.woman-guard-medium-skin-tone.webp | Bin 0 -> 1276 bytes public/flair/img/people.woman-guard.webp | Bin 0 -> 1372 bytes ...le.woman-health-worker-dark-skin-tone.webp | Bin 0 -> 1438 bytes ...e.woman-health-worker-light-skin-tone.webp | Bin 0 -> 1600 bytes ...n-health-worker-medium-dark-skin-tone.webp | Bin 0 -> 1516 bytes ...-health-worker-medium-light-skin-tone.webp | Bin 0 -> 1632 bytes ....woman-health-worker-medium-skin-tone.webp | Bin 0 -> 1544 bytes .../flair/img/people.woman-health-worker.webp | Bin 0 -> 1734 bytes ...oman-in-lotus-position-dark-skin-tone.webp | Bin 0 -> 1506 bytes ...man-in-lotus-position-light-skin-tone.webp | Bin 0 -> 1808 bytes ...-lotus-position-medium-dark-skin-tone.webp | Bin 0 -> 1634 bytes ...lotus-position-medium-light-skin-tone.webp | Bin 0 -> 1790 bytes ...an-in-lotus-position-medium-skin-tone.webp | Bin 0 -> 1676 bytes .../img/people.woman-in-lotus-position.webp | Bin 0 -> 1952 bytes ...n-in-manual-wheelchair-dark-skin-tone.webp | Bin 0 -> 1872 bytes ...heelchair-facing-right-dark-skin-tone.webp | Bin 0 -> 1854 bytes ...eelchair-facing-right-light-skin-tone.webp | Bin 0 -> 1942 bytes ...ir-facing-right-medium-dark-skin-tone.webp | Bin 0 -> 1880 bytes ...r-facing-right-medium-light-skin-tone.webp | Bin 0 -> 1976 bytes ...elchair-facing-right-medium-skin-tone.webp | Bin 0 -> 1912 bytes ...man-in-manual-wheelchair-facing-right.webp | Bin 0 -> 1978 bytes ...-in-manual-wheelchair-light-skin-tone.webp | Bin 0 -> 1968 bytes ...nual-wheelchair-medium-dark-skin-tone.webp | Bin 0 -> 1910 bytes ...ual-wheelchair-medium-light-skin-tone.webp | Bin 0 -> 2008 bytes ...in-manual-wheelchair-medium-skin-tone.webp | Bin 0 -> 1936 bytes .../people.woman-in-manual-wheelchair.webp | Bin 0 -> 2016 bytes ...n-motorized-wheelchair-dark-skin-tone.webp | Bin 0 -> 1714 bytes ...heelchair-facing-right-dark-skin-tone.webp | Bin 0 -> 1698 bytes ...eelchair-facing-right-light-skin-tone.webp | Bin 0 -> 1798 bytes ...ir-facing-right-medium-dark-skin-tone.webp | Bin 0 -> 1758 bytes ...r-facing-right-medium-light-skin-tone.webp | Bin 0 -> 1818 bytes ...elchair-facing-right-medium-skin-tone.webp | Bin 0 -> 1770 bytes ...-in-motorized-wheelchair-facing-right.webp | Bin 0 -> 1840 bytes ...-motorized-wheelchair-light-skin-tone.webp | Bin 0 -> 1810 bytes ...ized-wheelchair-medium-dark-skin-tone.webp | Bin 0 -> 1770 bytes ...zed-wheelchair-medium-light-skin-tone.webp | Bin 0 -> 1828 bytes ...motorized-wheelchair-medium-skin-tone.webp | Bin 0 -> 1780 bytes .../people.woman-in-motorized-wheelchair.webp | Bin 0 -> 1862 bytes ...e.woman-in-steamy-room-dark-skin-tone.webp | Bin 0 -> 1944 bytes ....woman-in-steamy-room-light-skin-tone.webp | Bin 0 -> 2000 bytes ...-in-steamy-room-medium-dark-skin-tone.webp | Bin 0 -> 1986 bytes ...in-steamy-room-medium-light-skin-tone.webp | Bin 0 -> 1886 bytes ...woman-in-steamy-room-medium-skin-tone.webp | Bin 0 -> 1958 bytes .../img/people.woman-in-steamy-room.webp | Bin 0 -> 2104 bytes ...people.woman-in-tuxedo-dark-skin-tone.webp | Bin 0 -> 1228 bytes ...eople.woman-in-tuxedo-light-skin-tone.webp | Bin 0 -> 1408 bytes ...woman-in-tuxedo-medium-dark-skin-tone.webp | Bin 0 -> 1308 bytes ...oman-in-tuxedo-medium-light-skin-tone.webp | Bin 0 -> 1468 bytes ...ople.woman-in-tuxedo-medium-skin-tone.webp | Bin 0 -> 1334 bytes public/flair/img/people.woman-in-tuxedo.webp | Bin 0 -> 1562 bytes .../people.woman-judge-dark-skin-tone.webp | Bin 0 -> 1656 bytes .../people.woman-judge-light-skin-tone.webp | Bin 0 -> 1864 bytes ...ple.woman-judge-medium-dark-skin-tone.webp | Bin 0 -> 1764 bytes ...le.woman-judge-medium-light-skin-tone.webp | Bin 0 -> 1860 bytes .../people.woman-judge-medium-skin-tone.webp | Bin 0 -> 1776 bytes public/flair/img/people.woman-judge.webp | Bin 0 -> 2026 bytes .../people.woman-juggling-dark-skin-tone.webp | Bin 0 -> 2142 bytes ...people.woman-juggling-light-skin-tone.webp | Bin 0 -> 2220 bytes ....woman-juggling-medium-dark-skin-tone.webp | Bin 0 -> 2170 bytes ...woman-juggling-medium-light-skin-tone.webp | Bin 0 -> 2196 bytes ...eople.woman-juggling-medium-skin-tone.webp | Bin 0 -> 2172 bytes public/flair/img/people.woman-juggling.webp | Bin 0 -> 2232 bytes .../people.woman-kneeling-dark-skin-tone.webp | Bin 0 -> 1450 bytes ...-kneeling-facing-right-dark-skin-tone.webp | Bin 0 -> 1466 bytes ...kneeling-facing-right-light-skin-tone.webp | Bin 0 -> 1568 bytes ...ng-facing-right-medium-dark-skin-tone.webp | Bin 0 -> 1502 bytes ...g-facing-right-medium-light-skin-tone.webp | Bin 0 -> 1632 bytes ...neeling-facing-right-medium-skin-tone.webp | Bin 0 -> 1534 bytes .../people.woman-kneeling-facing-right.webp | Bin 0 -> 1628 bytes ...people.woman-kneeling-light-skin-tone.webp | Bin 0 -> 1574 bytes ....woman-kneeling-medium-dark-skin-tone.webp | Bin 0 -> 1504 bytes ...woman-kneeling-medium-light-skin-tone.webp | Bin 0 -> 1636 bytes ...eople.woman-kneeling-medium-skin-tone.webp | Bin 0 -> 1540 bytes public/flair/img/people.woman-kneeling.webp | Bin 0 -> 1630 bytes ....woman-lifting-weights-dark-skin-tone.webp | Bin 0 -> 1954 bytes ...woman-lifting-weights-light-skin-tone.webp | Bin 0 -> 2138 bytes ...lifting-weights-medium-dark-skin-tone.webp | Bin 0 -> 2040 bytes ...ifting-weights-medium-light-skin-tone.webp | Bin 0 -> 2110 bytes ...oman-lifting-weights-medium-skin-tone.webp | Bin 0 -> 2048 bytes .../img/people.woman-lifting-weights.webp | Bin 0 -> 2266 bytes .../people.woman-light-skin-tone-bald.webp | Bin 0 -> 1506 bytes .../people.woman-light-skin-tone-beard.webp | Bin 0 -> 1264 bytes ...ople.woman-light-skin-tone-blond-hair.webp | Bin 0 -> 1522 bytes ...ople.woman-light-skin-tone-curly-hair.webp | Bin 0 -> 1534 bytes ...people.woman-light-skin-tone-red-hair.webp | Bin 0 -> 1720 bytes ...ople.woman-light-skin-tone-white-hair.webp | Bin 0 -> 1406 bytes .../img/people.woman-light-skin-tone.webp | Bin 0 -> 1300 bytes .../img/people.woman-mage-dark-skin-tone.webp | Bin 0 -> 2102 bytes .../people.woman-mage-light-skin-tone.webp | Bin 0 -> 2136 bytes ...ople.woman-mage-medium-dark-skin-tone.webp | Bin 0 -> 2140 bytes ...ple.woman-mage-medium-light-skin-tone.webp | Bin 0 -> 2128 bytes .../people.woman-mage-medium-skin-tone.webp | Bin 0 -> 2134 bytes public/flair/img/people.woman-mage.webp | Bin 0 -> 2212 bytes .../people.woman-mechanic-dark-skin-tone.webp | Bin 0 -> 1880 bytes ...people.woman-mechanic-light-skin-tone.webp | Bin 0 -> 2044 bytes ....woman-mechanic-medium-dark-skin-tone.webp | Bin 0 -> 1938 bytes ...woman-mechanic-medium-light-skin-tone.webp | Bin 0 -> 1994 bytes ...eople.woman-mechanic-medium-skin-tone.webp | Bin 0 -> 1928 bytes public/flair/img/people.woman-mechanic.webp | Bin 0 -> 2128 bytes ...ople.woman-medium-dark-skin-tone-bald.webp | Bin 0 -> 1366 bytes ...ple.woman-medium-dark-skin-tone-beard.webp | Bin 0 -> 1182 bytes ...oman-medium-dark-skin-tone-blond-hair.webp | Bin 0 -> 1514 bytes ...oman-medium-dark-skin-tone-curly-hair.webp | Bin 0 -> 1414 bytes ....woman-medium-dark-skin-tone-red-hair.webp | Bin 0 -> 1506 bytes ...oman-medium-dark-skin-tone-white-hair.webp | Bin 0 -> 1448 bytes .../people.woman-medium-dark-skin-tone.webp | Bin 0 -> 1212 bytes ...ple.woman-medium-light-skin-tone-bald.webp | Bin 0 -> 1446 bytes ...le.woman-medium-light-skin-tone-beard.webp | Bin 0 -> 1330 bytes ...man-medium-light-skin-tone-blond-hair.webp | Bin 0 -> 1472 bytes ...man-medium-light-skin-tone-curly-hair.webp | Bin 0 -> 1640 bytes ...woman-medium-light-skin-tone-red-hair.webp | Bin 0 -> 1644 bytes ...man-medium-light-skin-tone-white-hair.webp | Bin 0 -> 1384 bytes .../people.woman-medium-light-skin-tone.webp | Bin 0 -> 1358 bytes .../people.woman-medium-skin-tone-bald.webp | Bin 0 -> 1386 bytes .../people.woman-medium-skin-tone-beard.webp | Bin 0 -> 1198 bytes ...ple.woman-medium-skin-tone-blond-hair.webp | Bin 0 -> 1496 bytes ...ple.woman-medium-skin-tone-curly-hair.webp | Bin 0 -> 1472 bytes ...eople.woman-medium-skin-tone-red-hair.webp | Bin 0 -> 1568 bytes ...ple.woman-medium-skin-tone-white-hair.webp | Bin 0 -> 1426 bytes .../img/people.woman-medium-skin-tone.webp | Bin 0 -> 1218 bytes ....woman-mountain-biking-dark-skin-tone.webp | Bin 0 -> 1666 bytes ...woman-mountain-biking-light-skin-tone.webp | Bin 0 -> 1694 bytes ...mountain-biking-medium-dark-skin-tone.webp | Bin 0 -> 1680 bytes ...ountain-biking-medium-light-skin-tone.webp | Bin 0 -> 1698 bytes ...oman-mountain-biking-medium-skin-tone.webp | Bin 0 -> 1650 bytes .../img/people.woman-mountain-biking.webp | Bin 0 -> 1718 bytes ...le.woman-office-worker-dark-skin-tone.webp | Bin 0 -> 1254 bytes ...e.woman-office-worker-light-skin-tone.webp | Bin 0 -> 1396 bytes ...n-office-worker-medium-dark-skin-tone.webp | Bin 0 -> 1344 bytes ...-office-worker-medium-light-skin-tone.webp | Bin 0 -> 1444 bytes ....woman-office-worker-medium-skin-tone.webp | Bin 0 -> 1354 bytes .../flair/img/people.woman-office-worker.webp | Bin 0 -> 1560 bytes .../people.woman-pilot-dark-skin-tone.webp | Bin 0 -> 1486 bytes .../people.woman-pilot-light-skin-tone.webp | Bin 0 -> 1616 bytes ...ple.woman-pilot-medium-dark-skin-tone.webp | Bin 0 -> 1536 bytes ...le.woman-pilot-medium-light-skin-tone.webp | Bin 0 -> 1646 bytes .../people.woman-pilot-medium-skin-tone.webp | Bin 0 -> 1536 bytes public/flair/img/people.woman-pilot.webp | Bin 0 -> 1742 bytes ...woman-playing-handball-dark-skin-tone.webp | Bin 0 -> 1786 bytes ...oman-playing-handball-light-skin-tone.webp | Bin 0 -> 1972 bytes ...laying-handball-medium-dark-skin-tone.webp | Bin 0 -> 1906 bytes ...aying-handball-medium-light-skin-tone.webp | Bin 0 -> 1980 bytes ...man-playing-handball-medium-skin-tone.webp | Bin 0 -> 1914 bytes .../img/people.woman-playing-handball.webp | Bin 0 -> 2118 bytes ...man-playing-water-polo-dark-skin-tone.webp | Bin 0 -> 1896 bytes ...an-playing-water-polo-light-skin-tone.webp | Bin 0 -> 2098 bytes ...ying-water-polo-medium-dark-skin-tone.webp | Bin 0 -> 1960 bytes ...ing-water-polo-medium-light-skin-tone.webp | Bin 0 -> 2060 bytes ...n-playing-water-polo-medium-skin-tone.webp | Bin 0 -> 1988 bytes .../img/people.woman-playing-water-polo.webp | Bin 0 -> 2154 bytes ...e.woman-police-officer-dark-skin-tone.webp | Bin 0 -> 1660 bytes ....woman-police-officer-light-skin-tone.webp | Bin 0 -> 1872 bytes ...-police-officer-medium-dark-skin-tone.webp | Bin 0 -> 1724 bytes ...police-officer-medium-light-skin-tone.webp | Bin 0 -> 1808 bytes ...woman-police-officer-medium-skin-tone.webp | Bin 0 -> 1752 bytes .../img/people.woman-police-officer.webp | Bin 0 -> 1944 bytes .../people.woman-pouting-dark-skin-tone.webp | Bin 0 -> 1250 bytes .../people.woman-pouting-light-skin-tone.webp | Bin 0 -> 1486 bytes ...e.woman-pouting-medium-dark-skin-tone.webp | Bin 0 -> 1324 bytes ....woman-pouting-medium-light-skin-tone.webp | Bin 0 -> 1482 bytes ...people.woman-pouting-medium-skin-tone.webp | Bin 0 -> 1374 bytes public/flair/img/people.woman-pouting.webp | Bin 0 -> 1568 bytes ...ple.woman-raising-hand-dark-skin-tone.webp | Bin 0 -> 1398 bytes ...le.woman-raising-hand-light-skin-tone.webp | Bin 0 -> 1694 bytes ...an-raising-hand-medium-dark-skin-tone.webp | Bin 0 -> 1500 bytes ...n-raising-hand-medium-light-skin-tone.webp | Bin 0 -> 1630 bytes ...e.woman-raising-hand-medium-skin-tone.webp | Bin 0 -> 1520 bytes .../flair/img/people.woman-raising-hand.webp | Bin 0 -> 1800 bytes public/flair/img/people.woman-red-hair.webp | Bin 0 -> 1696 bytes ...ople.woman-rowing-boat-dark-skin-tone.webp | Bin 0 -> 1768 bytes ...ple.woman-rowing-boat-light-skin-tone.webp | Bin 0 -> 1862 bytes ...man-rowing-boat-medium-dark-skin-tone.webp | Bin 0 -> 1812 bytes ...an-rowing-boat-medium-light-skin-tone.webp | Bin 0 -> 1910 bytes ...le.woman-rowing-boat-medium-skin-tone.webp | Bin 0 -> 1830 bytes .../flair/img/people.woman-rowing-boat.webp | Bin 0 -> 1892 bytes .../people.woman-running-dark-skin-tone.webp | Bin 0 -> 1484 bytes ...n-running-facing-right-dark-skin-tone.webp | Bin 0 -> 1472 bytes ...-running-facing-right-light-skin-tone.webp | Bin 0 -> 1616 bytes ...ng-facing-right-medium-dark-skin-tone.webp | Bin 0 -> 1550 bytes ...g-facing-right-medium-light-skin-tone.webp | Bin 0 -> 1676 bytes ...running-facing-right-medium-skin-tone.webp | Bin 0 -> 1584 bytes .../people.woman-running-facing-right.webp | Bin 0 -> 1710 bytes .../people.woman-running-light-skin-tone.webp | Bin 0 -> 1630 bytes ...e.woman-running-medium-dark-skin-tone.webp | Bin 0 -> 1562 bytes ....woman-running-medium-light-skin-tone.webp | Bin 0 -> 1672 bytes ...people.woman-running-medium-skin-tone.webp | Bin 0 -> 1576 bytes public/flair/img/people.woman-running.webp | Bin 0 -> 1722 bytes ...people.woman-scientist-dark-skin-tone.webp | Bin 0 -> 1820 bytes ...eople.woman-scientist-light-skin-tone.webp | Bin 0 -> 1886 bytes ...woman-scientist-medium-dark-skin-tone.webp | Bin 0 -> 1872 bytes ...oman-scientist-medium-light-skin-tone.webp | Bin 0 -> 1942 bytes ...ople.woman-scientist-medium-skin-tone.webp | Bin 0 -> 1882 bytes public/flair/img/people.woman-scientist.webp | Bin 0 -> 2024 bytes ...people.woman-shrugging-dark-skin-tone.webp | Bin 0 -> 1400 bytes ...eople.woman-shrugging-light-skin-tone.webp | Bin 0 -> 1670 bytes ...woman-shrugging-medium-dark-skin-tone.webp | Bin 0 -> 1508 bytes ...oman-shrugging-medium-light-skin-tone.webp | Bin 0 -> 1678 bytes ...ople.woman-shrugging-medium-skin-tone.webp | Bin 0 -> 1536 bytes public/flair/img/people.woman-shrugging.webp | Bin 0 -> 1788 bytes .../people.woman-singer-dark-skin-tone.webp | Bin 0 -> 1638 bytes .../people.woman-singer-light-skin-tone.webp | Bin 0 -> 1878 bytes ...le.woman-singer-medium-dark-skin-tone.webp | Bin 0 -> 1714 bytes ...e.woman-singer-medium-light-skin-tone.webp | Bin 0 -> 1900 bytes .../people.woman-singer-medium-skin-tone.webp | Bin 0 -> 1752 bytes public/flair/img/people.woman-singer.webp | Bin 0 -> 1976 bytes .../people.woman-standing-dark-skin-tone.webp | Bin 0 -> 1262 bytes ...people.woman-standing-light-skin-tone.webp | Bin 0 -> 1418 bytes ....woman-standing-medium-dark-skin-tone.webp | Bin 0 -> 1320 bytes ...woman-standing-medium-light-skin-tone.webp | Bin 0 -> 1424 bytes ...eople.woman-standing-medium-skin-tone.webp | Bin 0 -> 1350 bytes public/flair/img/people.woman-standing.webp | Bin 0 -> 1484 bytes .../people.woman-student-dark-skin-tone.webp | Bin 0 -> 1400 bytes .../people.woman-student-light-skin-tone.webp | Bin 0 -> 1616 bytes ...e.woman-student-medium-dark-skin-tone.webp | Bin 0 -> 1488 bytes ....woman-student-medium-light-skin-tone.webp | Bin 0 -> 1644 bytes ...people.woman-student-medium-skin-tone.webp | Bin 0 -> 1512 bytes public/flair/img/people.woman-student.webp | Bin 0 -> 1734 bytes ...people.woman-superhero-dark-skin-tone.webp | Bin 0 -> 1770 bytes ...eople.woman-superhero-light-skin-tone.webp | Bin 0 -> 1794 bytes ...woman-superhero-medium-dark-skin-tone.webp | Bin 0 -> 1766 bytes ...oman-superhero-medium-light-skin-tone.webp | Bin 0 -> 1790 bytes ...ople.woman-superhero-medium-skin-tone.webp | Bin 0 -> 1772 bytes public/flair/img/people.woman-superhero.webp | Bin 0 -> 1794 bytes ...ple.woman-supervillain-dark-skin-tone.webp | Bin 0 -> 1956 bytes ...le.woman-supervillain-light-skin-tone.webp | Bin 0 -> 1990 bytes ...an-supervillain-medium-dark-skin-tone.webp | Bin 0 -> 1968 bytes ...n-supervillain-medium-light-skin-tone.webp | Bin 0 -> 2014 bytes ...e.woman-supervillain-medium-skin-tone.webp | Bin 0 -> 1976 bytes .../flair/img/people.woman-supervillain.webp | Bin 0 -> 1988 bytes .../people.woman-surfing-dark-skin-tone.webp | Bin 0 -> 2020 bytes .../people.woman-surfing-light-skin-tone.webp | Bin 0 -> 2116 bytes ...e.woman-surfing-medium-dark-skin-tone.webp | Bin 0 -> 2050 bytes ....woman-surfing-medium-light-skin-tone.webp | Bin 0 -> 2132 bytes ...people.woman-surfing-medium-skin-tone.webp | Bin 0 -> 2054 bytes public/flair/img/people.woman-surfing.webp | Bin 0 -> 2154 bytes .../people.woman-swimming-dark-skin-tone.webp | Bin 0 -> 1260 bytes ...people.woman-swimming-light-skin-tone.webp | Bin 0 -> 1426 bytes ....woman-swimming-medium-dark-skin-tone.webp | Bin 0 -> 1330 bytes ...woman-swimming-medium-light-skin-tone.webp | Bin 0 -> 1396 bytes ...eople.woman-swimming-medium-skin-tone.webp | Bin 0 -> 1354 bytes public/flair/img/people.woman-swimming.webp | Bin 0 -> 1484 bytes .../people.woman-teacher-dark-skin-tone.webp | Bin 0 -> 1414 bytes .../people.woman-teacher-light-skin-tone.webp | Bin 0 -> 1590 bytes ...e.woman-teacher-medium-dark-skin-tone.webp | Bin 0 -> 1476 bytes ....woman-teacher-medium-light-skin-tone.webp | Bin 0 -> 1562 bytes ...people.woman-teacher-medium-skin-tone.webp | Bin 0 -> 1488 bytes public/flair/img/people.woman-teacher.webp | Bin 0 -> 1648 bytes ...ple.woman-technologist-dark-skin-tone.webp | Bin 0 -> 1486 bytes ...le.woman-technologist-light-skin-tone.webp | Bin 0 -> 1664 bytes ...an-technologist-medium-dark-skin-tone.webp | Bin 0 -> 1536 bytes ...n-technologist-medium-light-skin-tone.webp | Bin 0 -> 1610 bytes ...e.woman-technologist-medium-skin-tone.webp | Bin 0 -> 1552 bytes .../flair/img/people.woman-technologist.webp | Bin 0 -> 1730 bytes ...ple.woman-tipping-hand-dark-skin-tone.webp | Bin 0 -> 1308 bytes ...le.woman-tipping-hand-light-skin-tone.webp | Bin 0 -> 1574 bytes ...an-tipping-hand-medium-dark-skin-tone.webp | Bin 0 -> 1414 bytes ...n-tipping-hand-medium-light-skin-tone.webp | Bin 0 -> 1556 bytes ...e.woman-tipping-hand-medium-skin-tone.webp | Bin 0 -> 1432 bytes .../flair/img/people.woman-tipping-hand.webp | Bin 0 -> 1694 bytes .../people.woman-vampire-dark-skin-tone.webp | Bin 0 -> 1452 bytes .../people.woman-vampire-light-skin-tone.webp | Bin 0 -> 1694 bytes ...e.woman-vampire-medium-dark-skin-tone.webp | Bin 0 -> 1550 bytes ....woman-vampire-medium-light-skin-tone.webp | Bin 0 -> 1666 bytes ...people.woman-vampire-medium-skin-tone.webp | Bin 0 -> 1562 bytes public/flair/img/people.woman-vampire.webp | Bin 0 -> 1808 bytes .../people.woman-walking-dark-skin-tone.webp | Bin 0 -> 1284 bytes ...n-walking-facing-right-dark-skin-tone.webp | Bin 0 -> 1260 bytes ...-walking-facing-right-light-skin-tone.webp | Bin 0 -> 1360 bytes ...ng-facing-right-medium-dark-skin-tone.webp | Bin 0 -> 1308 bytes ...g-facing-right-medium-light-skin-tone.webp | Bin 0 -> 1390 bytes ...walking-facing-right-medium-skin-tone.webp | Bin 0 -> 1330 bytes .../people.woman-walking-facing-right.webp | Bin 0 -> 1408 bytes .../people.woman-walking-light-skin-tone.webp | Bin 0 -> 1380 bytes ...e.woman-walking-medium-dark-skin-tone.webp | Bin 0 -> 1322 bytes ....woman-walking-medium-light-skin-tone.webp | Bin 0 -> 1406 bytes ...people.woman-walking-medium-skin-tone.webp | Bin 0 -> 1340 bytes public/flair/img/people.woman-walking.webp | Bin 0 -> 1422 bytes ...e.woman-wearing-turban-dark-skin-tone.webp | Bin 0 -> 1462 bytes ....woman-wearing-turban-light-skin-tone.webp | Bin 0 -> 1596 bytes ...-wearing-turban-medium-dark-skin-tone.webp | Bin 0 -> 1532 bytes ...wearing-turban-medium-light-skin-tone.webp | Bin 0 -> 1578 bytes ...woman-wearing-turban-medium-skin-tone.webp | Bin 0 -> 1540 bytes .../img/people.woman-wearing-turban.webp | Bin 0 -> 1734 bytes public/flair/img/people.woman-white-hair.webp | Bin 0 -> 1516 bytes ...people.woman-with-veil-dark-skin-tone.webp | Bin 0 -> 1578 bytes ...eople.woman-with-veil-light-skin-tone.webp | Bin 0 -> 1724 bytes ...woman-with-veil-medium-dark-skin-tone.webp | Bin 0 -> 1650 bytes ...oman-with-veil-medium-light-skin-tone.webp | Bin 0 -> 1682 bytes ...ople.woman-with-veil-medium-skin-tone.webp | Bin 0 -> 1628 bytes public/flair/img/people.woman-with-veil.webp | Bin 0 -> 1824 bytes ....woman-with-white-cane-dark-skin-tone.webp | Bin 0 -> 1668 bytes ...hite-cane-facing-right-dark-skin-tone.webp | Bin 0 -> 1622 bytes ...ite-cane-facing-right-light-skin-tone.webp | Bin 0 -> 1716 bytes ...ne-facing-right-medium-dark-skin-tone.webp | Bin 0 -> 1672 bytes ...e-facing-right-medium-light-skin-tone.webp | Bin 0 -> 1740 bytes ...te-cane-facing-right-medium-skin-tone.webp | Bin 0 -> 1686 bytes ...le.woman-with-white-cane-facing-right.webp | Bin 0 -> 1768 bytes ...woman-with-white-cane-light-skin-tone.webp | Bin 0 -> 1774 bytes ...with-white-cane-medium-dark-skin-tone.webp | Bin 0 -> 1728 bytes ...ith-white-cane-medium-light-skin-tone.webp | Bin 0 -> 1778 bytes ...oman-with-white-cane-medium-skin-tone.webp | Bin 0 -> 1736 bytes .../img/people.woman-with-white-cane.webp | Bin 0 -> 1816 bytes public/flair/img/people.woman-zombie.webp | Bin 0 -> 2124 bytes public/flair/img/people.woman.webp | Bin 0 -> 1496 bytes ...-hands-dark-skin-tone-light-skin-tone.webp | Bin 0 -> 2372 bytes ...-dark-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2280 bytes ...dark-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2390 bytes ...hands-dark-skin-tone-medium-skin-tone.webp | Bin 0 -> 2302 bytes ...le.women-holding-hands-dark-skin-tone.webp | Bin 0 -> 2210 bytes ...-hands-light-skin-tone-dark-skin-tone.webp | Bin 0 -> 2380 bytes ...light-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2428 bytes ...ight-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2498 bytes ...ands-light-skin-tone-medium-skin-tone.webp | Bin 0 -> 2438 bytes ...e.women-holding-hands-light-skin-tone.webp | Bin 0 -> 2478 bytes ...-medium-dark-skin-tone-dark-skin-tone.webp | Bin 0 -> 2290 bytes ...medium-dark-skin-tone-light-skin-tone.webp | Bin 0 -> 2440 bytes ...dark-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2444 bytes ...edium-dark-skin-tone-medium-skin-tone.webp | Bin 0 -> 2358 bytes ...n-holding-hands-medium-dark-skin-tone.webp | Bin 0 -> 2330 bytes ...medium-light-skin-tone-dark-skin-tone.webp | Bin 0 -> 2396 bytes ...edium-light-skin-tone-light-skin-tone.webp | Bin 0 -> 2496 bytes ...light-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2442 bytes ...dium-light-skin-tone-medium-skin-tone.webp | Bin 0 -> 2458 bytes ...-holding-hands-medium-light-skin-tone.webp | Bin 0 -> 2512 bytes ...hands-medium-skin-tone-dark-skin-tone.webp | Bin 0 -> 2318 bytes ...ands-medium-skin-tone-light-skin-tone.webp | Bin 0 -> 2452 bytes ...edium-skin-tone-medium-dark-skin-tone.webp | Bin 0 -> 2366 bytes ...dium-skin-tone-medium-light-skin-tone.webp | Bin 0 -> 2458 bytes ....women-holding-hands-medium-skin-tone.webp | Bin 0 -> 2376 bytes .../flair/img/people.women-holding-hands.webp | Bin 0 -> 2586 bytes .../img/people.women-with-bunny-ears.webp | Bin 0 -> 2994 bytes public/flair/img/people.women-wrestling.webp | Bin 0 -> 2646 bytes .../people.writing-hand-dark-skin-tone.webp | Bin 0 -> 1448 bytes .../people.writing-hand-light-skin-tone.webp | Bin 0 -> 1674 bytes ...le.writing-hand-medium-dark-skin-tone.webp | Bin 0 -> 1546 bytes ...e.writing-hand-medium-light-skin-tone.webp | Bin 0 -> 1604 bytes .../people.writing-hand-medium-skin-tone.webp | Bin 0 -> 1538 bytes public/flair/img/people.writing-hand.webp | Bin 0 -> 1812 bytes public/flair/img/people.zombie.webp | Bin 0 -> 2214 bytes public/flair/img/smileys.alien-monster.webp | Bin 0 -> 1318 bytes public/flair/img/smileys.alien.webp | Bin 0 -> 1458 bytes .../img/smileys.angry-face-with-horns.webp | Bin 0 -> 1650 bytes public/flair/img/smileys.angry-face.webp | Bin 0 -> 1774 bytes public/flair/img/smileys.anguished-face.webp | Bin 0 -> 1812 bytes .../img/smileys.anxious-face-with-sweat.webp | Bin 0 -> 1874 bytes public/flair/img/smileys.astonished-face.webp | Bin 0 -> 1852 bytes ...mileys.beaming-face-with-smiling-eyes.webp | Bin 0 -> 1884 bytes .../img/smileys.cat-with-tears-of-joy.webp | Bin 0 -> 2380 bytes .../flair/img/smileys.cat-with-wry-smile.webp | Bin 0 -> 2192 bytes public/flair/img/smileys.clown-face.webp | Bin 0 -> 2076 bytes public/flair/img/smileys.cold-face.webp | Bin 0 -> 2220 bytes public/flair/img/smileys.confounded-face.webp | Bin 0 -> 1868 bytes public/flair/img/smileys.confused-face.webp | Bin 0 -> 1734 bytes public/flair/img/smileys.cowboy-hat-face.webp | Bin 0 -> 1828 bytes public/flair/img/smileys.crying-cat.webp | Bin 0 -> 2222 bytes public/flair/img/smileys.crying-face.webp | Bin 0 -> 1924 bytes .../flair/img/smileys.disappointed-face.webp | Bin 0 -> 1698 bytes public/flair/img/smileys.disguised-face.webp | Bin 0 -> 2412 bytes public/flair/img/smileys.dizzy-face.webp | Bin 0 -> 1982 bytes .../flair/img/smileys.dotted-line-face.webp | Bin 0 -> 2234 bytes .../img/smileys.downcast-face-with-sweat.webp | Bin 0 -> 1812 bytes public/flair/img/smileys.drooling-face.webp | Bin 0 -> 1944 bytes public/flair/img/smileys.exploding-head.webp | Bin 0 -> 2634 bytes .../img/smileys.expressionless-face.webp | Bin 0 -> 1636 bytes .../img/smileys.face-blowing-a-kiss.webp | Bin 0 -> 1870 bytes public/flair/img/smileys.face-exhaling.webp | Bin 0 -> 1972 bytes .../img/smileys.face-holding-back-tears.webp | Bin 0 -> 2078 bytes public/flair/img/smileys.face-in-clouds.webp | Bin 0 -> 2468 bytes .../flair/img/smileys.face-savoring-food.webp | Bin 0 -> 1830 bytes .../img/smileys.face-screaming-in-fear.webp | Bin 0 -> 1962 bytes .../img/smileys.face-with-diagonal-mouth.webp | Bin 0 -> 1760 bytes .../smileys.face-with-hand-over-mouth.webp | Bin 0 -> 1872 bytes .../img/smileys.face-with-head-bandage.webp | Bin 0 -> 1904 bytes .../img/smileys.face-with-medical-mask.webp | Bin 0 -> 1826 bytes .../flair/img/smileys.face-with-monocle.webp | Bin 0 -> 2108 bytes ...ce-with-open-eyes-and-hand-over-mouth.webp | Bin 0 -> 1860 bytes .../img/smileys.face-with-open-mouth.webp | Bin 0 -> 1746 bytes .../img/smileys.face-with-peeking-eye.webp | Bin 0 -> 1956 bytes .../img/smileys.face-with-raised-eyebrow.webp | Bin 0 -> 1836 bytes .../img/smileys.face-with-rolling-eyes.webp | Bin 0 -> 1772 bytes .../img/smileys.face-with-spiral-eyes.webp | Bin 0 -> 1984 bytes .../smileys.face-with-steam-from-nose.webp | Bin 0 -> 2046 bytes .../smileys.face-with-symbols-on-mouth.webp | Bin 0 -> 2064 bytes .../img/smileys.face-with-tears-of-joy.webp | Bin 0 -> 2140 bytes .../img/smileys.face-with-thermometer.webp | Bin 0 -> 1934 bytes .../flair/img/smileys.face-with-tongue.webp | Bin 0 -> 1808 bytes .../flair/img/smileys.face-without-mouth.webp | Bin 0 -> 1616 bytes public/flair/img/smileys.fearful-face.webp | Bin 0 -> 1728 bytes public/flair/img/smileys.flushed-face.webp | Bin 0 -> 1956 bytes ...smileys.frowning-face-with-open-mouth.webp | Bin 0 -> 1736 bytes public/flair/img/smileys.frowning-face.webp | Bin 0 -> 1800 bytes public/flair/img/smileys.ghost.webp | Bin 0 -> 1612 bytes public/flair/img/smileys.goblin.webp | Bin 0 -> 2182 bytes public/flair/img/smileys.grimacing-face.webp | Bin 0 -> 1832 bytes ...mileys.grinning-cat-with-smiling-eyes.webp | Bin 0 -> 2280 bytes public/flair/img/smileys.grinning-cat.webp | Bin 0 -> 2208 bytes .../smileys.grinning-face-with-big-eyes.webp | Bin 0 -> 1908 bytes ...ileys.grinning-face-with-smiling-eyes.webp | Bin 0 -> 1898 bytes .../img/smileys.grinning-face-with-sweat.webp | Bin 0 -> 1998 bytes public/flair/img/smileys.grinning-face.webp | Bin 0 -> 1898 bytes .../img/smileys.grinning-squinting-face.webp | Bin 0 -> 1976 bytes .../smileys.head-shaking-horizontally.webp | Bin 0 -> 1746 bytes .../img/smileys.head-shaking-vertically.webp | Bin 0 -> 1932 bytes .../img/smileys.hear-no-evil-monkey.webp | Bin 0 -> 1630 bytes public/flair/img/smileys.hot-face.webp | Bin 0 -> 2054 bytes public/flair/img/smileys.hugging-face.webp | Bin 0 -> 2120 bytes public/flair/img/smileys.hushed-face.webp | Bin 0 -> 1800 bytes public/flair/img/smileys.kissing-cat.webp | Bin 0 -> 2158 bytes ...smileys.kissing-face-with-closed-eyes.webp | Bin 0 -> 1900 bytes ...mileys.kissing-face-with-smiling-eyes.webp | Bin 0 -> 1738 bytes public/flair/img/smileys.kissing-face.webp | Bin 0 -> 1724 bytes .../flair/img/smileys.loudly-crying-face.webp | Bin 0 -> 2172 bytes public/flair/img/smileys.lying-face.webp | Bin 0 -> 1828 bytes public/flair/img/smileys.melting-face.webp | Bin 0 -> 1902 bytes .../flair/img/smileys.money-mouth-face.webp | Bin 0 -> 2092 bytes public/flair/img/smileys.nauseated-face.webp | Bin 0 -> 1668 bytes public/flair/img/smileys.nerd-face.webp | Bin 0 -> 2232 bytes public/flair/img/smileys.neutral-face.webp | Bin 0 -> 1690 bytes public/flair/img/smileys.ogre.webp | Bin 0 -> 2306 bytes public/flair/img/smileys.partying-face.webp | Bin 0 -> 2472 bytes public/flair/img/smileys.pensive-face.webp | Bin 0 -> 1802 bytes .../flair/img/smileys.persevering-face.webp | Bin 0 -> 1898 bytes public/flair/img/smileys.pleading-face.webp | Bin 0 -> 2014 bytes public/flair/img/smileys.pouting-cat.webp | Bin 0 -> 2098 bytes public/flair/img/smileys.pouting-face.webp | Bin 0 -> 1752 bytes public/flair/img/smileys.relieved-face.webp | Bin 0 -> 1828 bytes public/flair/img/smileys.robot.webp | Bin 0 -> 1244 bytes ...smileys.rolling-on-the-floor-laughing.webp | Bin 0 -> 2186 bytes .../img/smileys.sad-but-relieved-face.webp | Bin 0 -> 1960 bytes public/flair/img/smileys.saluting-face.webp | Bin 0 -> 1920 bytes .../flair/img/smileys.see-no-evil-monkey.webp | Bin 0 -> 1614 bytes public/flair/img/smileys.shaking-face.webp | Bin 0 -> 2300 bytes public/flair/img/smileys.shushing-face.webp | Bin 0 -> 1836 bytes .../img/smileys.skull-and-crossbones.webp | Bin 0 -> 1894 bytes public/flair/img/smileys.skull.webp | Bin 0 -> 1348 bytes public/flair/img/smileys.sleeping-face.webp | Bin 0 -> 1988 bytes public/flair/img/smileys.sleepy-face.webp | Bin 0 -> 1922 bytes .../img/smileys.slightly-frowning-face.webp | Bin 0 -> 1728 bytes .../img/smileys.slightly-smiling-face.webp | Bin 0 -> 1732 bytes .../smileys.smiling-cat-with-heart-eyes.webp | Bin 0 -> 2268 bytes .../img/smileys.smiling-face-with-halo.webp | Bin 0 -> 2166 bytes .../smileys.smiling-face-with-heart-eyes.webp | Bin 0 -> 1924 bytes .../img/smileys.smiling-face-with-hearts.webp | Bin 0 -> 2082 bytes .../img/smileys.smiling-face-with-horns.webp | Bin 0 -> 1656 bytes ...mileys.smiling-face-with-smiling-eyes.webp | Bin 0 -> 1786 bytes .../smileys.smiling-face-with-sunglasses.webp | Bin 0 -> 1852 bytes .../img/smileys.smiling-face-with-tear.webp | Bin 0 -> 1836 bytes public/flair/img/smileys.smiling-face.webp | Bin 0 -> 1874 bytes public/flair/img/smileys.smirking-face.webp | Bin 0 -> 1816 bytes public/flair/img/smileys.sneezing-face.webp | Bin 0 -> 2026 bytes .../img/smileys.speak-no-evil-monkey.webp | Bin 0 -> 1592 bytes .../smileys.squinting-face-with-tongue.webp | Bin 0 -> 1886 bytes public/flair/img/smileys.star-struck.webp | Bin 0 -> 2066 bytes public/flair/img/smileys.thinking-face.webp | Bin 0 -> 2018 bytes public/flair/img/smileys.tired-face.webp | Bin 0 -> 1992 bytes public/flair/img/smileys.unamused-face.webp | Bin 0 -> 1818 bytes .../flair/img/smileys.upside-down-face.webp | Bin 0 -> 1714 bytes public/flair/img/smileys.weary-cat.webp | Bin 0 -> 2404 bytes public/flair/img/smileys.weary-face.webp | Bin 0 -> 1946 bytes .../img/smileys.winking-face-with-tongue.webp | Bin 0 -> 1896 bytes public/flair/img/smileys.winking-face.webp | Bin 0 -> 1846 bytes public/flair/img/smileys.woozy-face.webp | Bin 0 -> 1894 bytes public/flair/img/smileys.worried-face.webp | Bin 0 -> 1814 bytes public/flair/img/smileys.yawning-face.webp | Bin 0 -> 1990 bytes public/flair/img/smileys.zany-face.webp | Bin 0 -> 1960 bytes .../flair/img/smileys.zipper-mouth-face.webp | Bin 0 -> 2004 bytes .../img/symbols.a-button-blood-type.webp | Bin 0 -> 994 bytes .../img/symbols.ab-button-blood-type.webp | Bin 0 -> 1112 bytes public/flair/img/symbols.anger-symbol.webp | Bin 0 -> 2652 bytes public/flair/img/symbols.antenna-bars.webp | Bin 0 -> 800 bytes public/flair/img/symbols.aquarius.webp | Bin 0 -> 1792 bytes public/flair/img/symbols.aries.webp | Bin 0 -> 1614 bytes public/flair/img/symbols.asterisk.webp | Bin 0 -> 848 bytes public/flair/img/symbols.atm-sign.webp | Bin 0 -> 1104 bytes public/flair/img/symbols.atom-symbol.webp | Bin 0 -> 1350 bytes .../img/symbols.b-button-blood-type.webp | Bin 0 -> 946 bytes public/flair/img/symbols.baby-symbol.webp | Bin 0 -> 1198 bytes public/flair/img/symbols.back-arrow.webp | Bin 0 -> 848 bytes public/flair/img/symbols.baggage-claim.webp | Bin 0 -> 1138 bytes public/flair/img/symbols.beating-heart.webp | Bin 0 -> 2398 bytes public/flair/img/symbols.black-circle.webp | Bin 0 -> 996 bytes public/flair/img/symbols.black-flag.webp | Bin 0 -> 1148 bytes public/flair/img/symbols.black-heart.webp | Bin 0 -> 1054 bytes .../flair/img/symbols.black-large-square.webp | Bin 0 -> 316 bytes .../symbols.black-medium-small-square.webp | Bin 0 -> 240 bytes .../img/symbols.black-square-button.webp | Bin 0 -> 372 bytes public/flair/img/symbols.blue-heart.webp | Bin 0 -> 1474 bytes public/flair/img/symbols.bright-button.webp | Bin 0 -> 2622 bytes public/flair/img/symbols.broken-heart.webp | Bin 0 -> 1778 bytes public/flair/img/symbols.brown-heart.webp | Bin 0 -> 1328 bytes public/flair/img/symbols.cancer.webp | Bin 0 -> 1870 bytes public/flair/img/symbols.capricorn.webp | Bin 0 -> 1514 bytes public/flair/img/symbols.chequered-flag.webp | Bin 0 -> 1752 bytes .../flair/img/symbols.children-crossing.webp | Bin 0 -> 1704 bytes public/flair/img/symbols.cinema.webp | Bin 0 -> 968 bytes public/flair/img/symbols.circled-m.webp | Bin 0 -> 1680 bytes public/flair/img/symbols.cl-button.webp | Bin 0 -> 926 bytes .../symbols.clockwise-vertical-arrows.webp | Bin 0 -> 730 bytes public/flair/img/symbols.collision.webp | Bin 0 -> 2874 bytes .../symbols.combining-enclosing-keycap.webp | Bin 0 -> 382 bytes public/flair/img/symbols.cool-button.webp | Bin 0 -> 738 bytes public/flair/img/symbols.copyright.webp | Bin 0 -> 2234 bytes ...ymbols.counterclockwise-arrows-button.webp | Bin 0 -> 742 bytes .../flair/img/symbols.cross-mark-button.webp | Bin 0 -> 934 bytes public/flair/img/symbols.cross-mark.webp | Bin 0 -> 1702 bytes public/flair/img/symbols.crossed-flags.webp | Bin 0 -> 1994 bytes public/flair/img/symbols.curly-loop.webp | Bin 0 -> 1300 bytes .../flair/img/symbols.currency-exchange.webp | Bin 0 -> 2472 bytes public/flair/img/symbols.customs.webp | Bin 0 -> 1228 bytes public/flair/img/symbols.dashing-away.webp | Bin 0 -> 1882 bytes .../flair/img/symbols.diamond-with-a-dot.webp | Bin 0 -> 1686 bytes public/flair/img/symbols.digit-eight.webp | Bin 0 -> 936 bytes public/flair/img/symbols.digit-five.webp | Bin 0 -> 770 bytes public/flair/img/symbols.digit-four.webp | Bin 0 -> 612 bytes public/flair/img/symbols.digit-nine.webp | Bin 0 -> 836 bytes public/flair/img/symbols.digit-one.webp | Bin 0 -> 364 bytes public/flair/img/symbols.digit-seven.webp | Bin 0 -> 586 bytes public/flair/img/symbols.digit-six.webp | Bin 0 -> 832 bytes public/flair/img/symbols.digit-three.webp | Bin 0 -> 896 bytes public/flair/img/symbols.digit-two.webp | Bin 0 -> 758 bytes public/flair/img/symbols.digit-zero.webp | Bin 0 -> 802 bytes public/flair/img/symbols.dim-button.webp | Bin 0 -> 2396 bytes public/flair/img/symbols.divide.webp | Bin 0 -> 932 bytes public/flair/img/symbols.dizzy.webp | Bin 0 -> 2598 bytes .../flair/img/symbols.double-curly-loop.webp | Bin 0 -> 1570 bytes .../img/symbols.double-exclamation-mark.webp | Bin 0 -> 1436 bytes public/flair/img/symbols.down-arrow.webp | Bin 0 -> 534 bytes public/flair/img/symbols.down-left-arrow.webp | Bin 0 -> 568 bytes .../flair/img/symbols.down-right-arrow.webp | Bin 0 -> 548 bytes .../flair/img/symbols.downwards-button.webp | Bin 0 -> 708 bytes .../flair/img/symbols.eight-pointed-star.webp | Bin 0 -> 2272 bytes .../img/symbols.eight-spoked-asterisk.webp | Bin 0 -> 2038 bytes public/flair/img/symbols.eject-button.webp | Bin 0 -> 790 bytes public/flair/img/symbols.elevator.webp | Bin 0 -> 1230 bytes public/flair/img/symbols.end-arrow.webp | Bin 0 -> 766 bytes public/flair/img/symbols.esperanto-flag.webp | Bin 0 -> 808 bytes public/flair/img/symbols.esperanto.webp | Bin 0 -> 1342 bytes .../flair/img/symbols.exclamation-mark.webp | Bin 0 -> 858 bytes .../symbols.exclamation-question-mark.webp | Bin 0 -> 2030 bytes public/flair/img/symbols.extinction.webp | Bin 0 -> 1178 bytes .../img/symbols.eye-in-speech-bubble.webp | Bin 0 -> 1164 bytes .../flair/img/symbols.fast-down-button.webp | Bin 0 -> 858 bytes .../img/symbols.fast-forward-button.webp | Bin 0 -> 904 bytes .../img/symbols.fast-reverse-button.webp | Bin 0 -> 896 bytes public/flair/img/symbols.fast-up-button.webp | Bin 0 -> 884 bytes public/flair/img/symbols.female-sign.webp | Bin 0 -> 1796 bytes public/flair/img/symbols.fleur-de-lis.webp | Bin 0 -> 2292 bytes public/flair/img/symbols.free-button.webp | Bin 0 -> 736 bytes public/flair/img/symbols.gemini.webp | Bin 0 -> 1686 bytes public/flair/img/symbols.gnu-logo.webp | Bin 0 -> 2934 bytes public/flair/img/symbols.green-heart.webp | Bin 0 -> 1372 bytes public/flair/img/symbols.grey-heart.webp | Bin 0 -> 1174 bytes public/flair/img/symbols.growing-heart.webp | Bin 0 -> 2112 bytes .../flair/img/symbols.heart-decoration.webp | Bin 0 -> 808 bytes .../flair/img/symbols.heart-exclamation.webp | Bin 0 -> 1504 bytes public/flair/img/symbols.heart-on-fire.webp | Bin 0 -> 2120 bytes .../flair/img/symbols.heart-with-arrow.webp | Bin 0 -> 1804 bytes .../flair/img/symbols.heart-with-ribbon.webp | Bin 0 -> 1770 bytes .../flair/img/symbols.heavy-dollar-sign.webp | Bin 0 -> 1656 bytes .../flair/img/symbols.heavy-equals-sign.webp | Bin 0 -> 490 bytes public/flair/img/symbols.hole.webp | Bin 0 -> 972 bytes .../flair/img/symbols.hollow-red-circle.webp | Bin 0 -> 1924 bytes public/flair/img/symbols.hundred-points.webp | Bin 0 -> 3134 bytes public/flair/img/symbols.id-button.webp | Bin 0 -> 762 bytes public/flair/img/symbols.infinity.webp | Bin 0 -> 948 bytes public/flair/img/symbols.information.webp | Bin 0 -> 528 bytes .../img/symbols.input-latin-letters.webp | Bin 0 -> 734 bytes .../img/symbols.input-latin-lowercase.webp | Bin 0 -> 776 bytes .../img/symbols.input-latin-uppercase.webp | Bin 0 -> 812 bytes public/flair/img/symbols.input-numbers.webp | Bin 0 -> 790 bytes public/flair/img/symbols.input-symbols.webp | Bin 0 -> 832 bytes .../symbols.japanese-acceptable-button.webp | Bin 0 -> 1794 bytes .../symbols.japanese-application-button.webp | Bin 0 -> 962 bytes .../img/symbols.japanese-bargain-button.webp | Bin 0 -> 1936 bytes ...mbols.japanese-congratulations-button.webp | Bin 0 -> 1860 bytes .../img/symbols.japanese-discount-button.webp | Bin 0 -> 1114 bytes ...ymbols.japanese-free-of-charge-button.webp | Bin 0 -> 1196 bytes .../img/symbols.japanese-here-button.webp | Bin 0 -> 598 bytes ...ymbols.japanese-monthly-amount-button.webp | Bin 0 -> 902 bytes .../symbols.japanese-no-vacancy-button.webp | Bin 0 -> 1378 bytes ...ls.japanese-not-free-of-charge-button.webp | Bin 0 -> 1052 bytes ...ols.japanese-open-for-business-button.webp | Bin 0 -> 1104 bytes ...symbols.japanese-passing-grade-button.webp | Bin 0 -> 1036 bytes .../symbols.japanese-prohibited-button.webp | Bin 0 -> 1294 bytes .../img/symbols.japanese-reserved-button.webp | Bin 0 -> 958 bytes .../img/symbols.japanese-secret-button.webp | Bin 0 -> 1972 bytes ...ymbols.japanese-service-charge-button.webp | Bin 0 -> 656 bytes .../symbols.japanese-symbol-for-beginner.webp | Bin 0 -> 1202 bytes .../img/symbols.japanese-vacancy-button.webp | Bin 0 -> 728 bytes public/flair/img/symbols.keycap-10.webp | Bin 0 -> 648 bytes public/flair/img/symbols.keycap-asterisk.webp | Bin 0 -> 640 bytes .../flair/img/symbols.keycap-digit-eight.webp | Bin 0 -> 656 bytes .../flair/img/symbols.keycap-digit-five.webp | Bin 0 -> 630 bytes .../flair/img/symbols.keycap-digit-four.webp | Bin 0 -> 596 bytes .../flair/img/symbols.keycap-digit-nine.webp | Bin 0 -> 630 bytes .../flair/img/symbols.keycap-digit-one.webp | Bin 0 -> 512 bytes .../flair/img/symbols.keycap-digit-seven.webp | Bin 0 -> 578 bytes .../flair/img/symbols.keycap-digit-six.webp | Bin 0 -> 650 bytes .../flair/img/symbols.keycap-digit-three.webp | Bin 0 -> 640 bytes .../flair/img/symbols.keycap-digit-two.webp | Bin 0 -> 616 bytes .../flair/img/symbols.keycap-digit-zero.webp | Bin 0 -> 592 bytes .../flair/img/symbols.keycap-number-sign.webp | Bin 0 -> 700 bytes public/flair/img/symbols.kiss-mark.webp | Bin 0 -> 1786 bytes .../flair/img/symbols.large-blue-circle.webp | Bin 0 -> 1284 bytes .../flair/img/symbols.large-blue-diamond.webp | Bin 0 -> 1138 bytes .../flair/img/symbols.large-blue-square.webp | Bin 0 -> 436 bytes .../flair/img/symbols.large-brown-circle.webp | Bin 0 -> 1202 bytes .../flair/img/symbols.large-brown-square.webp | Bin 0 -> 376 bytes .../flair/img/symbols.large-green-circle.webp | Bin 0 -> 1238 bytes .../flair/img/symbols.large-green-square.webp | Bin 0 -> 392 bytes .../img/symbols.large-orange-circle.webp | Bin 0 -> 1442 bytes .../img/symbols.large-orange-diamond.webp | Bin 0 -> 1172 bytes .../img/symbols.large-orange-square.webp | Bin 0 -> 464 bytes .../img/symbols.large-purple-circle.webp | Bin 0 -> 1282 bytes .../img/symbols.large-purple-square.webp | Bin 0 -> 440 bytes .../flair/img/symbols.large-red-circle.webp | Bin 0 -> 1344 bytes .../flair/img/symbols.large-red-square.webp | Bin 0 -> 442 bytes .../img/symbols.large-yellow-circle.webp | Bin 0 -> 1466 bytes .../img/symbols.large-yellow-square.webp | Bin 0 -> 470 bytes .../flair/img/symbols.last-track-button.webp | Bin 0 -> 960 bytes .../img/symbols.left-arrow-curving-right.webp | Bin 0 -> 638 bytes public/flair/img/symbols.left-arrow.webp | Bin 0 -> 550 bytes public/flair/img/symbols.left-luggage.webp | Bin 0 -> 1152 bytes .../flair/img/symbols.left-right-arrow.webp | Bin 0 -> 626 bytes .../flair/img/symbols.left-speech-bubble.webp | Bin 0 -> 1032 bytes public/flair/img/symbols.leo.webp | Bin 0 -> 1682 bytes public/flair/img/symbols.libra.webp | Bin 0 -> 1484 bytes .../flair/img/symbols.lichess-4545-flag.webp | Bin 0 -> 920 bytes .../flair/img/symbols.light-blue-heart.webp | Bin 0 -> 1588 bytes .../flair/img/symbols.linux-tux-penguin.webp | Bin 0 -> 1564 bytes .../flair/img/symbols.litter-in-bin-sign.webp | Bin 0 -> 1228 bytes public/flair/img/symbols.love-letter.webp | Bin 0 -> 1018 bytes public/flair/img/symbols.male-sign.webp | Bin 0 -> 1858 bytes public/flair/img/symbols.medical-symbol.webp | Bin 0 -> 2052 bytes public/flair/img/symbols.mending-heart.webp | Bin 0 -> 1688 bytes public/flair/img/symbols.mens-room.webp | Bin 0 -> 874 bytes public/flair/img/symbols.minus.webp | Bin 0 -> 332 bytes .../flair/img/symbols.mobile-phone-off.webp | Bin 0 -> 1126 bytes public/flair/img/symbols.move-blunder.webp | Bin 0 -> 2519 bytes public/flair/img/symbols.move-brilliant.webp | Bin 0 -> 2011 bytes public/flair/img/symbols.move-dubious.webp | Bin 0 -> 2954 bytes public/flair/img/symbols.move-good.webp | Bin 0 -> 1802 bytes .../flair/img/symbols.move-interesting.webp | Bin 0 -> 2605 bytes public/flair/img/symbols.move-mistake.webp | Bin 0 -> 2095 bytes public/flair/img/symbols.multiply.webp | Bin 0 -> 1330 bytes public/flair/img/symbols.name-badge.webp | Bin 0 -> 1768 bytes public/flair/img/symbols.new-button.webp | Bin 0 -> 758 bytes .../flair/img/symbols.next-track-button.webp | Bin 0 -> 958 bytes public/flair/img/symbols.ng-button.webp | Bin 0 -> 732 bytes public/flair/img/symbols.no-bicycles.webp | Bin 0 -> 2048 bytes public/flair/img/symbols.no-entry.webp | Bin 0 -> 1382 bytes public/flair/img/symbols.no-littering.webp | Bin 0 -> 1898 bytes .../flair/img/symbols.no-mobile-phones.webp | Bin 0 -> 1776 bytes .../img/symbols.no-one-under-eighteen.webp | Bin 0 -> 1986 bytes public/flair/img/symbols.no-pedestrians.webp | Bin 0 -> 1872 bytes public/flair/img/symbols.no-smoking.webp | Bin 0 -> 1922 bytes .../flair/img/symbols.non-potable-water.webp | Bin 0 -> 1916 bytes public/flair/img/symbols.number-sign.webp | Bin 0 -> 1012 bytes .../img/symbols.o-button-blood-type.webp | Bin 0 -> 1044 bytes public/flair/img/symbols.ok-button.webp | Bin 0 -> 762 bytes public/flair/img/symbols.on-arrow.webp | Bin 0 -> 818 bytes public/flair/img/symbols.ophiuchus.webp | Bin 0 -> 1724 bytes public/flair/img/symbols.orange-heart.webp | Bin 0 -> 1574 bytes public/flair/img/symbols.p-button.webp | Bin 0 -> 584 bytes .../img/symbols.part-alternation-mark.webp | Bin 0 -> 1730 bytes .../flair/img/symbols.passport-control.webp | Bin 0 -> 1214 bytes public/flair/img/symbols.pause-button.webp | Bin 0 -> 714 bytes public/flair/img/symbols.peace-symbol.webp | Bin 0 -> 1240 bytes public/flair/img/symbols.pink-heart.webp | Bin 0 -> 1456 bytes public/flair/img/symbols.pirate-flag.webp | Bin 0 -> 1010 bytes public/flair/img/symbols.pisces.webp | Bin 0 -> 1508 bytes public/flair/img/symbols.play-button.webp | Bin 0 -> 702 bytes .../img/symbols.play-or-pause-button.webp | Bin 0 -> 864 bytes public/flair/img/symbols.plus.webp | Bin 0 -> 638 bytes public/flair/img/symbols.potable-water.webp | Bin 0 -> 1164 bytes public/flair/img/symbols.purple-heart.webp | Bin 0 -> 1438 bytes public/flair/img/symbols.puzzle-racer.webp | Bin 0 -> 8098 bytes public/flair/img/symbols.puzzle-storm.webp | Bin 0 -> 3560 bytes public/flair/img/symbols.puzzle-streak.webp | Bin 0 -> 4961 bytes public/flair/img/symbols.question-mark.webp | Bin 0 -> 1366 bytes public/flair/img/symbols.radio-button.webp | Bin 0 -> 1316 bytes public/flair/img/symbols.rainbow-flag.webp | Bin 0 -> 1294 bytes public/flair/img/symbols.record-button.webp | Bin 0 -> 758 bytes .../flair/img/symbols.recycling-symbol.webp | Bin 0 -> 2184 bytes public/flair/img/symbols.red-heart.webp | Bin 0 -> 1492 bytes .../symbols.red-triangle-pointed-down.webp | Bin 0 -> 682 bytes .../img/symbols.red-triangle-pointed-up.webp | Bin 0 -> 706 bytes public/flair/img/symbols.registered.webp | Bin 0 -> 2174 bytes public/flair/img/symbols.repeat-button.webp | Bin 0 -> 1152 bytes .../img/symbols.repeat-single-button.webp | Bin 0 -> 1240 bytes public/flair/img/symbols.restroom.webp | Bin 0 -> 1346 bytes public/flair/img/symbols.reverse-button.webp | Bin 0 -> 680 bytes .../flair/img/symbols.revolving-hearts.webp | Bin 0 -> 2356 bytes .../flair/img/symbols.right-anger-bubble.webp | Bin 0 -> 1684 bytes .../img/symbols.right-arrow-curving-down.webp | Bin 0 -> 636 bytes .../img/symbols.right-arrow-curving-left.webp | Bin 0 -> 656 bytes .../img/symbols.right-arrow-curving-up.webp | Bin 0 -> 664 bytes public/flair/img/symbols.right-arrow.webp | Bin 0 -> 534 bytes public/flair/img/symbols.sagittarius.webp | Bin 0 -> 1608 bytes public/flair/img/symbols.scorpio.webp | Bin 0 -> 1528 bytes .../img/symbols.shuffle-tracks-button.webp | Bin 0 -> 1190 bytes .../flair/img/symbols.small-blue-diamond.webp | Bin 0 -> 568 bytes .../img/symbols.small-orange-diamond.webp | Bin 0 -> 620 bytes public/flair/img/symbols.soon-arrow.webp | Bin 0 -> 868 bytes public/flair/img/symbols.sos-button.webp | Bin 0 -> 1262 bytes public/flair/img/symbols.sparkle.webp | Bin 0 -> 2486 bytes public/flair/img/symbols.sparkling-heart.webp | Bin 0 -> 1796 bytes public/flair/img/symbols.speech-balloon.webp | Bin 0 -> 1218 bytes public/flair/img/symbols.stop-button.webp | Bin 0 -> 638 bytes public/flair/img/symbols.taurus.webp | Bin 0 -> 1742 bytes public/flair/img/symbols.thought-balloon.webp | Bin 0 -> 1288 bytes public/flair/img/symbols.top-arrow.webp | Bin 0 -> 744 bytes public/flair/img/symbols.trade-mark.webp | Bin 0 -> 1022 bytes .../flair/img/symbols.transgender-flag.webp | Bin 0 -> 1300 bytes .../flair/img/symbols.transgender-symbol.webp | Bin 0 -> 2182 bytes public/flair/img/symbols.triangular-flag.webp | Bin 0 -> 1462 bytes public/flair/img/symbols.trident-emblem.webp | Bin 0 -> 2632 bytes public/flair/img/symbols.two-hearts.webp | Bin 0 -> 1670 bytes public/flair/img/symbols.up-arrow.webp | Bin 0 -> 536 bytes public/flair/img/symbols.up-button.webp | Bin 0 -> 732 bytes public/flair/img/symbols.up-down-arrow.webp | Bin 0 -> 636 bytes public/flair/img/symbols.up-left-arrow.webp | Bin 0 -> 552 bytes public/flair/img/symbols.up-right-arrow.webp | Bin 0 -> 558 bytes public/flair/img/symbols.upwards-button.webp | Bin 0 -> 692 bytes public/flair/img/symbols.vibration-mode.webp | Bin 0 -> 1940 bytes public/flair/img/symbols.vim-logo.webp | Bin 0 -> 2230 bytes public/flair/img/symbols.virgo.webp | Bin 0 -> 1514 bytes public/flair/img/symbols.vs-button.webp | Bin 0 -> 1024 bytes public/flair/img/symbols.water-closet.webp | Bin 0 -> 1090 bytes public/flair/img/symbols.wavy-dash.webp | Bin 0 -> 848 bytes .../flair/img/symbols.wheelchair-symbol.webp | Bin 0 -> 1272 bytes public/flair/img/symbols.white-circle.webp | Bin 0 -> 1096 bytes .../img/symbols.white-exclamation-mark.webp | Bin 0 -> 732 bytes public/flair/img/symbols.white-flag.webp | Bin 0 -> 1198 bytes public/flair/img/symbols.white-heart.webp | Bin 0 -> 1210 bytes .../flair/img/symbols.white-large-square.webp | Bin 0 -> 336 bytes .../symbols.white-medium-small-square.webp | Bin 0 -> 240 bytes .../img/symbols.white-question-mark.webp | Bin 0 -> 1158 bytes .../img/symbols.white-square-button.webp | Bin 0 -> 414 bytes public/flair/img/symbols.white-star.webp | Bin 0 -> 1600 bytes public/flair/img/symbols.wireless.webp | Bin 0 -> 1128 bytes public/flair/img/symbols.womens-room.webp | Bin 0 -> 908 bytes public/flair/img/symbols.yellow-heart.webp | Bin 0 -> 1616 bytes public/flair/img/symbols.zzz.webp | Bin 0 -> 1796 bytes .../img/travel-places.aerial-tramway.webp | Bin 0 -> 1808 bytes .../img/travel-places.airplane-arrival.webp | Bin 0 -> 2024 bytes .../img/travel-places.airplane-departure.webp | Bin 0 -> 1790 bytes public/flair/img/travel-places.airplane.webp | Bin 0 -> 2440 bytes public/flair/img/travel-places.ambulance.webp | Bin 0 -> 1634 bytes public/flair/img/travel-places.anchor.webp | Bin 0 -> 1886 bytes .../img/travel-places.articulated-lorry.webp | Bin 0 -> 1634 bytes .../img/travel-places.auto-rickshaw.webp | Bin 0 -> 2026 bytes .../flair/img/travel-places.automobile.webp | Bin 0 -> 1480 bytes public/flair/img/travel-places.bank.webp | Bin 0 -> 1666 bytes .../flair/img/travel-places.barber-pole.webp | Bin 0 -> 1440 bytes .../travel-places.beach-with-umbrella.webp | Bin 0 -> 2292 bytes public/flair/img/travel-places.bicycle.webp | Bin 0 -> 2454 bytes public/flair/img/travel-places.brick.webp | Bin 0 -> 1296 bytes .../img/travel-places.bridge-at-night.webp | Bin 0 -> 1448 bytes .../travel-places.building-construction.webp | Bin 0 -> 2788 bytes .../flair/img/travel-places.bullet-train.webp | Bin 0 -> 1330 bytes public/flair/img/travel-places.bus-stop.webp | Bin 0 -> 1726 bytes public/flair/img/travel-places.bus.webp | Bin 0 -> 1434 bytes public/flair/img/travel-places.camping.webp | Bin 0 -> 2108 bytes public/flair/img/travel-places.canoe.webp | Bin 0 -> 1470 bytes .../img/travel-places.carousel-horse.webp | Bin 0 -> 2328 bytes public/flair/img/travel-places.castle.webp | Bin 0 -> 1930 bytes public/flair/img/travel-places.church.webp | Bin 0 -> 1416 bytes .../flair/img/travel-places.circus-tent.webp | Bin 0 -> 2194 bytes .../img/travel-places.cityscape-at-dusk.webp | Bin 0 -> 1206 bytes public/flair/img/travel-places.cityscape.webp | Bin 0 -> 1006 bytes .../img/travel-places.classical-building.webp | Bin 0 -> 1366 bytes public/flair/img/travel-places.compass.webp | Bin 0 -> 2356 bytes .../flair/img/travel-places.construction.webp | Bin 0 -> 1846 bytes .../img/travel-places.convenience-store.webp | Bin 0 -> 1366 bytes .../img/travel-places.delivery-truck.webp | Bin 0 -> 1434 bytes .../img/travel-places.department-store.webp | Bin 0 -> 1370 bytes .../img/travel-places.derelict-house.webp | Bin 0 -> 1718 bytes .../img/travel-places.desert-island.webp | Bin 0 -> 2164 bytes public/flair/img/travel-places.desert.webp | Bin 0 -> 980 bytes .../flair/img/travel-places.earth-blue.webp | Bin 0 -> 3756 bytes public/flair/img/travel-places.factory.webp | Bin 0 -> 1804 bytes .../flair/img/travel-places.ferris-wheel.webp | Bin 0 -> 3470 bytes public/flair/img/travel-places.ferry.webp | Bin 0 -> 1652 bytes .../flair/img/travel-places.fire-engine.webp | Bin 0 -> 1798 bytes .../img/travel-places.flying-saucer.webp | Bin 0 -> 2224 bytes public/flair/img/travel-places.foggy.webp | Bin 0 -> 1208 bytes public/flair/img/travel-places.fountain.webp | Bin 0 -> 2200 bytes public/flair/img/travel-places.fuel-pump.webp | Bin 0 -> 1994 bytes .../travel-places.globe-showing-americas.webp | Bin 0 -> 1844 bytes ...l-places.globe-showing-asia-australia.webp | Bin 0 -> 1912 bytes ...el-places.globe-showing-europe-africa.webp | Bin 0 -> 2014 bytes .../travel-places.globe-with-meridians.webp | Bin 0 -> 3132 bytes .../flair/img/travel-places.helicopter.webp | Bin 0 -> 2114 bytes .../img/travel-places.high-speed-train.webp | Bin 0 -> 1246 bytes .../flair/img/travel-places.hindu-temple.webp | Bin 0 -> 1738 bytes ...ravel-places.horizontal-traffic-light.webp | Bin 0 -> 648 bytes public/flair/img/travel-places.hospital.webp | Bin 0 -> 1320 bytes .../flair/img/travel-places.hot-springs.webp | Bin 0 -> 2498 bytes public/flair/img/travel-places.hotel.webp | Bin 0 -> 1704 bytes .../img/travel-places.house-with-garden.webp | Bin 0 -> 1884 bytes public/flair/img/travel-places.house.webp | Bin 0 -> 1646 bytes public/flair/img/travel-places.houses.webp | Bin 0 -> 1712 bytes public/flair/img/travel-places.hut.webp | Bin 0 -> 1328 bytes .../img/travel-places.japanese-castle.webp | Bin 0 -> 2070 bytes .../travel-places.japanese-post-office.webp | Bin 0 -> 1074 bytes public/flair/img/travel-places.kaaba.webp | Bin 0 -> 1268 bytes .../flair/img/travel-places.kick-scooter.webp | Bin 0 -> 1802 bytes .../flair/img/travel-places.light-rail.webp | Bin 0 -> 1396 bytes .../flair/img/travel-places.locomotive.webp | Bin 0 -> 2262 bytes .../flair/img/travel-places.love-hotel.webp | Bin 0 -> 1762 bytes .../img/travel-places.manual-wheelchair.webp | Bin 0 -> 2364 bytes .../flair/img/travel-places.map-of-japan.webp | Bin 0 -> 882 bytes public/flair/img/travel-places.metro.webp | Bin 0 -> 1770 bytes public/flair/img/travel-places.minibus.webp | Bin 0 -> 1300 bytes public/flair/img/travel-places.moai.webp | Bin 0 -> 1252 bytes public/flair/img/travel-places.monorail.webp | Bin 0 -> 1394 bytes public/flair/img/travel-places.mosque.webp | Bin 0 -> 2238 bytes .../flair/img/travel-places.motor-boat.webp | Bin 0 -> 1356 bytes .../img/travel-places.motor-scooter.webp | Bin 0 -> 2196 bytes .../flair/img/travel-places.motorcycle.webp | Bin 0 -> 1964 bytes .../travel-places.motorized-wheelchair.webp | Bin 0 -> 1668 bytes public/flair/img/travel-places.motorway.webp | Bin 0 -> 1986 bytes .../flair/img/travel-places.mount-fuji.webp | Bin 0 -> 1670 bytes .../img/travel-places.mountain-cableway.webp | Bin 0 -> 1982 bytes .../img/travel-places.mountain-railway.webp | Bin 0 -> 1864 bytes public/flair/img/travel-places.mountain.webp | Bin 0 -> 1844 bytes .../img/travel-places.national-park.webp | Bin 0 -> 1470 bytes .../img/travel-places.night-with-stars.webp | Bin 0 -> 1388 bytes .../img/travel-places.office-building.webp | Bin 0 -> 1120 bytes public/flair/img/travel-places.oil-drum.webp | Bin 0 -> 1182 bytes .../travel-places.oncoming-automobile.webp | Bin 0 -> 1744 bytes .../flair/img/travel-places.oncoming-bus.webp | Bin 0 -> 1504 bytes .../travel-places.oncoming-police-car.webp | Bin 0 -> 1756 bytes .../img/travel-places.oncoming-taxi.webp | Bin 0 -> 1936 bytes public/flair/img/travel-places.parachute.webp | Bin 0 -> 2164 bytes .../img/travel-places.passenger-ship.webp | Bin 0 -> 1848 bytes .../flair/img/travel-places.pickup-truck.webp | Bin 0 -> 1398 bytes .../img/travel-places.playground-slide.webp | Bin 0 -> 2042 bytes .../img/travel-places.police-car-light.webp | Bin 0 -> 1768 bytes .../flair/img/travel-places.police-car.webp | Bin 0 -> 1404 bytes .../flair/img/travel-places.post-office.webp | Bin 0 -> 1500 bytes .../flair/img/travel-places.racing-car.webp | Bin 0 -> 1408 bytes .../flair/img/travel-places.railway-car.webp | Bin 0 -> 1296 bytes .../img/travel-places.railway-track.webp | Bin 0 -> 1368 bytes public/flair/img/travel-places.ring-buoy.webp | Bin 0 -> 2194 bytes public/flair/img/travel-places.rocket.webp | Bin 0 -> 2290 bytes .../img/travel-places.roller-coaster.webp | Bin 0 -> 2608 bytes .../flair/img/travel-places.roller-skate.webp | Bin 0 -> 2378 bytes public/flair/img/travel-places.sailboat.webp | Bin 0 -> 2178 bytes public/flair/img/travel-places.satellite.webp | Bin 0 -> 1690 bytes public/flair/img/travel-places.school.webp | Bin 0 -> 1650 bytes public/flair/img/travel-places.seat.webp | Bin 0 -> 1654 bytes .../img/travel-places.shinto-shrine.webp | Bin 0 -> 1612 bytes public/flair/img/travel-places.ship.webp | Bin 0 -> 1688 bytes .../flair/img/travel-places.skateboard.webp | Bin 0 -> 1370 bytes .../img/travel-places.small-airplane.webp | Bin 0 -> 2364 bytes .../travel-places.snow-capped-mountain.webp | Bin 0 -> 1976 bytes public/flair/img/travel-places.speedboat.webp | Bin 0 -> 1282 bytes .../travel-places.sport-utility-vehicle.webp | Bin 0 -> 1450 bytes public/flair/img/travel-places.stadium.webp | Bin 0 -> 2254 bytes public/flair/img/travel-places.station.webp | Bin 0 -> 1710 bytes .../img/travel-places.statue-of-liberty.webp | Bin 0 -> 2186 bytes public/flair/img/travel-places.stop-sign.webp | Bin 0 -> 844 bytes .../travel-places.sunrise-over-mountains.webp | Bin 0 -> 1080 bytes public/flair/img/travel-places.sunrise.webp | Bin 0 -> 1012 bytes public/flair/img/travel-places.sunset.webp | Bin 0 -> 1342 bytes .../img/travel-places.suspension-railway.webp | Bin 0 -> 2036 bytes public/flair/img/travel-places.synagogue.webp | Bin 0 -> 1734 bytes public/flair/img/travel-places.taxi.webp | Bin 0 -> 1712 bytes public/flair/img/travel-places.tent.webp | Bin 0 -> 1312 bytes .../flair/img/travel-places.tokyo-tower.webp | Bin 0 -> 1640 bytes public/flair/img/travel-places.tractor.webp | Bin 0 -> 2028 bytes public/flair/img/travel-places.train.webp | Bin 0 -> 1644 bytes public/flair/img/travel-places.tram-car.webp | Bin 0 -> 1692 bytes public/flair/img/travel-places.tram.webp | Bin 0 -> 1486 bytes .../flair/img/travel-places.trolleybus.webp | Bin 0 -> 1820 bytes .../travel-places.vertical-traffic-light.webp | Bin 0 -> 682 bytes public/flair/img/travel-places.volcano.webp | Bin 0 -> 2386 bytes public/flair/img/travel-places.wedding.webp | Bin 0 -> 2198 bytes public/flair/img/travel-places.wheel.webp | Bin 0 -> 3142 bytes .../flair/img/travel-places.wooden-ship.webp | Bin 0 -> 1854 bytes public/flair/img/travel-places.world-map.webp | Bin 0 -> 1958 bytes public/flair/index.html | 86 + public/flair/list.sh | 9 + public/flair/list.txt | 3435 +++++++++++++++++ public/images/board/newspaper.png | Bin 18359 -> 0 bytes public/images/board/newspaper.thumbnail.png | Bin 2744 -> 0 bytes public/images/board/svg/newspaper.svg | 25 + public/images/flags/ES-AR.png | Bin 0 -> 799 bytes public/images/flags/ES-AS.png | Bin 0 -> 969 bytes public/images/trophy/atomicwc23.png | Bin 0 -> 9058 bytes public/javascripts/study/tour-chapter.js | 6 +- public/javascripts/study/tour.js | 12 +- translation/dest/activity/hi-IN.xml | 10 +- translation/dest/activity/ta-IN.xml | 34 + translation/dest/arena/an-ES.xml | 1 + translation/dest/arena/ar-SA.xml | 15 +- translation/dest/arena/be-BY.xml | 2 + translation/dest/arena/bg-BG.xml | 2 +- translation/dest/arena/el-GR.xml | 4 +- translation/dest/arena/fa-IR.xml | 4 +- translation/dest/arena/gl-ES.xml | 2 +- translation/dest/arena/ka-GE.xml | 4 + translation/dest/arena/nl-NL.xml | 4 +- translation/dest/arena/ro-RO.xml | 2 +- translation/dest/arena/sv-SE.xml | 4 +- translation/dest/arena/ta-IN.xml | 61 +- translation/dest/arena/th-TH.xml | 5 +- translation/dest/arena/tp-TP.xml | 2 +- translation/dest/arena/vi-VN.xml | 8 +- translation/dest/broadcast/af-ZA.xml | 2 + translation/dest/broadcast/an-ES.xml | 4 + translation/dest/broadcast/ar-SA.xml | 18 +- translation/dest/broadcast/be-BY.xml | 9 + translation/dest/broadcast/bg-BG.xml | 2 +- translation/dest/broadcast/bs-BA.xml | 2 +- translation/dest/broadcast/ca-ES.xml | 17 +- translation/dest/broadcast/ckb-IR.xml | 6 +- translation/dest/broadcast/cs-CZ.xml | 2 +- translation/dest/broadcast/da-DK.xml | 17 +- translation/dest/broadcast/de-DE.xml | 17 +- translation/dest/broadcast/el-GR.xml | 4 +- translation/dest/broadcast/en-US.xml | 17 +- translation/dest/broadcast/eo-UY.xml | 2 +- translation/dest/broadcast/es-ES.xml | 17 +- translation/dest/broadcast/eu-ES.xml | 17 +- translation/dest/broadcast/fa-IR.xml | 4 +- translation/dest/broadcast/fi-FI.xml | 17 +- translation/dest/broadcast/fr-FR.xml | 17 +- translation/dest/broadcast/gl-ES.xml | 40 +- translation/dest/broadcast/gsw-CH.xml | 33 +- translation/dest/broadcast/he-IL.xml | 17 +- translation/dest/broadcast/hi-IN.xml | 2 +- translation/dest/broadcast/hr-HR.xml | 2 +- translation/dest/broadcast/hu-HU.xml | 2 +- translation/dest/broadcast/it-IT.xml | 17 +- translation/dest/broadcast/ja-JP.xml | 17 +- translation/dest/broadcast/kk-KZ.xml | 2 +- translation/dest/broadcast/kn-IN.xml | 2 +- translation/dest/broadcast/ko-KR.xml | 2 +- translation/dest/broadcast/lb-LU.xml | 4 +- translation/dest/broadcast/lt-LT.xml | 2 +- translation/dest/broadcast/nb-NO.xml | 17 +- translation/dest/broadcast/nl-NL.xml | 12 +- translation/dest/broadcast/nn-NO.xml | 17 +- translation/dest/broadcast/pl-PL.xml | 17 +- translation/dest/broadcast/pt-BR.xml | 17 +- translation/dest/broadcast/pt-PT.xml | 17 +- translation/dest/broadcast/ro-RO.xml | 2 +- translation/dest/broadcast/ru-RU.xml | 17 +- translation/dest/broadcast/sk-SK.xml | 2 +- translation/dest/broadcast/sl-SI.xml | 17 +- translation/dest/broadcast/sq-AL.xml | 17 +- translation/dest/broadcast/sv-SE.xml | 17 +- translation/dest/broadcast/th-TH.xml | 13 + translation/dest/broadcast/tr-TR.xml | 4 +- translation/dest/broadcast/uk-UA.xml | 17 +- translation/dest/broadcast/vi-VN.xml | 19 +- translation/dest/broadcast/zh-CN.xml | 2 +- translation/dest/challenge/an-ES.xml | 1 + translation/dest/challenge/br-FR.xml | 10 +- translation/dest/challenge/gl-ES.xml | 18 +- translation/dest/challenge/ms-MY.xml | 16 +- translation/dest/challenge/tp-TP.xml | 20 +- translation/dest/challenge/vi-VN.xml | 22 +- translation/dest/challenge/zh-TW.xml | 2 + translation/dest/class/gl-ES.xml | 2 +- translation/dest/class/gsw-CH.xml | 2 +- translation/dest/class/kn-IN.xml | 2 +- translation/dest/class/lb-LU.xml | 2 +- translation/dest/class/pt-PT.xml | 2 +- translation/dest/class/sk-SK.xml | 2 +- translation/dest/class/th-TH.xml | 20 + translation/dest/class/vi-VN.xml | 28 +- translation/dest/coach/ta-IN.xml | 23 + translation/dest/coach/vi-VN.xml | 6 +- translation/dest/contact/be-BY.xml | 3 + translation/dest/contact/gl-ES.xml | 2 +- translation/dest/contact/gsw-CH.xml | 4 +- translation/dest/contact/lb-LU.xml | 11 + translation/dest/contact/tp-TP.xml | 9 + translation/dest/contact/vi-VN.xml | 10 +- translation/dest/coordinates/vi-VN.xml | 2 +- translation/dest/dgt/ar-SA.xml | 48 +- translation/dest/dgt/cs-CZ.xml | 19 + translation/dest/dgt/de-DE.xml | 12 +- translation/dest/dgt/th-TH.xml | 5 +- translation/dest/dgt/tr-TR.xml | 23 + translation/dest/dgt/uk-UA.xml | 7 + translation/dest/dgt/vi-VN.xml | 4 +- translation/dest/emails/da-DK.xml | 4 +- translation/dest/emails/so-SO.xml | 22 +- translation/dest/emails/uk-UA.xml | 2 +- translation/dest/faq/an-ES.xml | 25 + translation/dest/faq/ar-SA.xml | 25 + translation/dest/faq/da-DK.xml | 14 +- translation/dest/faq/de-DE.xml | 2 +- translation/dest/faq/fi-FI.xml | 1 + translation/dest/faq/gsw-CH.xml | 18 +- translation/dest/faq/ko-KR.xml | 6 + translation/dest/faq/pt-PT.xml | 5 + translation/dest/faq/so-SO.xml | 3 +- translation/dest/faq/th-TH.xml | 18 + translation/dest/faq/tr-TR.xml | 9 + translation/dest/faq/vi-VN.xml | 10 +- translation/dest/lag/ko-KR.xml | 2 +- translation/dest/lag/sl-SI.xml | 4 +- translation/dest/learn/ar-SA.xml | 183 +- translation/dest/learn/da-DK.xml | 6 +- translation/dest/learn/nl-NL.xml | 2 +- translation/dest/learn/sl-SI.xml | 63 +- translation/dest/learn/so-SO.xml | 100 +- translation/dest/learn/vi-VN.xml | 8 +- translation/dest/oauthScope/ar-SA.xml | 58 +- translation/dest/oauthScope/be-BY.xml | 9 +- translation/dest/oauthScope/de-DE.xml | 50 +- translation/dest/oauthScope/gsw-CH.xml | 54 +- translation/dest/oauthScope/th-TH.xml | 11 +- translation/dest/oauthScope/vi-VN.xml | 54 +- translation/dest/oauthScope/zh-CN.xml | 4 +- translation/dest/onboarding/aa-ER.xml | 2 + translation/dest/onboarding/af-ZA.xml | 17 + translation/dest/onboarding/ak-GH.xml | 2 + translation/dest/onboarding/am-ET.xml | 2 + translation/dest/onboarding/an-ES.xml | 17 + translation/dest/onboarding/ar-SA.xml | 17 + translation/dest/onboarding/as-IN.xml | 2 + translation/dest/onboarding/ast-ES.xml | 2 + translation/dest/onboarding/av-DA.xml | 2 + translation/dest/onboarding/az-AZ.xml | 2 + translation/dest/onboarding/ba-RU.xml | 2 + translation/dest/onboarding/be-BY.xml | 4 + translation/dest/onboarding/bg-BG.xml | 2 + translation/dest/onboarding/bn-BD.xml | 2 + translation/dest/onboarding/br-FR.xml | 7 + translation/dest/onboarding/bs-BA.xml | 2 + translation/dest/onboarding/ca-ES.xml | 17 + translation/dest/onboarding/ce-CE.xml | 2 + translation/dest/onboarding/ceb-PH.xml | 2 + translation/dest/onboarding/ckb-IR.xml | 2 + translation/dest/onboarding/co-FR.xml | 2 + translation/dest/onboarding/cs-CZ.xml | 2 + translation/dest/onboarding/cv-CU.xml | 2 + translation/dest/onboarding/cy-GB.xml | 2 + translation/dest/onboarding/da-DK.xml | 17 + translation/dest/onboarding/de-DE.xml | 17 + translation/dest/onboarding/el-GR.xml | 2 + translation/dest/onboarding/en-US.xml | 17 + translation/dest/onboarding/eo-UY.xml | 17 + translation/dest/onboarding/es-ES.xml | 17 + translation/dest/onboarding/et-EE.xml | 2 + translation/dest/onboarding/eu-ES.xml | 2 + translation/dest/onboarding/fa-IR.xml | 17 + translation/dest/onboarding/fi-FI.xml | 15 + translation/dest/onboarding/fo-FO.xml | 2 + translation/dest/onboarding/fr-FR.xml | 17 + translation/dest/onboarding/frp-IT.xml | 2 + translation/dest/onboarding/fur-IT.xml | 2 + translation/dest/onboarding/fy-NL.xml | 2 + translation/dest/onboarding/ga-IE.xml | 2 + translation/dest/onboarding/gd-GB.xml | 2 + translation/dest/onboarding/gl-ES.xml | 17 + translation/dest/onboarding/gn-PY.xml | 2 + translation/dest/onboarding/gsw-CH.xml | 17 + translation/dest/onboarding/gu-IN.xml | 2 + translation/dest/onboarding/ha-HG.xml | 2 + translation/dest/onboarding/he-IL.xml | 17 + translation/dest/onboarding/hi-IN.xml | 14 + translation/dest/onboarding/hr-HR.xml | 2 + translation/dest/onboarding/hu-HU.xml | 2 + translation/dest/onboarding/hy-AM.xml | 2 + translation/dest/onboarding/ia-IA.xml | 2 + translation/dest/onboarding/id-ID.xml | 2 + translation/dest/onboarding/ig-NG.xml | 2 + translation/dest/onboarding/io-EN.xml | 2 + translation/dest/onboarding/is-IS.xml | 2 + translation/dest/onboarding/it-IT.xml | 17 + translation/dest/onboarding/ja-JP.xml | 16 + translation/dest/onboarding/jbo-EN.xml | 2 + translation/dest/onboarding/jv-ID.xml | 2 + translation/dest/onboarding/ka-GE.xml | 2 + translation/dest/onboarding/kaa-UZ.xml | 2 + translation/dest/onboarding/kab-DZ.xml | 2 + translation/dest/onboarding/kk-KZ.xml | 2 + translation/dest/onboarding/km-KH.xml | 2 + translation/dest/onboarding/kmr-TR.xml | 2 + translation/dest/onboarding/kn-IN.xml | 2 + translation/dest/onboarding/ko-KR.xml | 2 + translation/dest/onboarding/ky-KG.xml | 2 + translation/dest/onboarding/la-LA.xml | 2 + translation/dest/onboarding/lb-LU.xml | 16 + translation/dest/onboarding/lg-UG.xml | 2 + translation/dest/onboarding/lo-LA.xml | 2 + translation/dest/onboarding/lt-LT.xml | 2 + translation/dest/onboarding/lv-LV.xml | 2 + translation/dest/onboarding/mai-IN.xml | 2 + translation/dest/onboarding/mdf-RU.xml | 2 + translation/dest/onboarding/mg-MG.xml | 2 + translation/dest/onboarding/mi-NZ.xml | 2 + translation/dest/onboarding/mk-MK.xml | 2 + translation/dest/onboarding/ml-IN.xml | 2 + translation/dest/onboarding/mn-MN.xml | 2 + translation/dest/onboarding/mr-IN.xml | 6 + translation/dest/onboarding/ms-MY.xml | 2 + translation/dest/onboarding/mt-MT.xml | 2 + translation/dest/onboarding/my-MM.xml | 2 + translation/dest/onboarding/nb-NO.xml | 17 + translation/dest/onboarding/ne-NP.xml | 2 + translation/dest/onboarding/nl-NL.xml | 17 + translation/dest/onboarding/nn-NO.xml | 17 + translation/dest/onboarding/ns-ZA.xml | 2 + translation/dest/onboarding/ny-MW.xml | 2 + translation/dest/onboarding/oc-FR.xml | 2 + translation/dest/onboarding/om-ET.xml | 2 + translation/dest/onboarding/or-IN.xml | 2 + translation/dest/onboarding/os-SE.xml | 2 + translation/dest/onboarding/pa-IN.xml | 2 + translation/dest/onboarding/pi-IN.xml | 2 + translation/dest/onboarding/pl-PL.xml | 17 + translation/dest/onboarding/ps-AF.xml | 2 + translation/dest/onboarding/pt-BR.xml | 17 + translation/dest/onboarding/pt-PT.xml | 17 + translation/dest/onboarding/qu-PE.xml | 2 + translation/dest/onboarding/rn-BI.xml | 2 + translation/dest/onboarding/ro-RO.xml | 2 + translation/dest/onboarding/ru-RU.xml | 17 + translation/dest/onboarding/rw-RW.xml | 2 + translation/dest/onboarding/ry-UA.xml | 2 + translation/dest/onboarding/sa-IN.xml | 2 + translation/dest/onboarding/sc-IT.xml | 2 + translation/dest/onboarding/sco-GB.xml | 2 + translation/dest/onboarding/sd-PK.xml | 2 + translation/dest/onboarding/se-NO.xml | 2 + translation/dest/onboarding/si-LK.xml | 2 + translation/dest/onboarding/sk-SK.xml | 2 + translation/dest/onboarding/sl-SI.xml | 17 + translation/dest/onboarding/sn-ZW.xml | 2 + translation/dest/onboarding/so-SO.xml | 2 + translation/dest/onboarding/sq-AL.xml | 17 + translation/dest/onboarding/sr-SP.xml | 2 + translation/dest/onboarding/st-ZA.xml | 2 + translation/dest/onboarding/sv-SE.xml | 17 + translation/dest/onboarding/sw-KE.xml | 2 + translation/dest/onboarding/ta-IN.xml | 2 + translation/dest/onboarding/te-IN.xml | 2 + translation/dest/onboarding/tg-TJ.xml | 2 + translation/dest/onboarding/th-TH.xml | 17 + translation/dest/onboarding/ti-ER.xml | 2 + translation/dest/onboarding/tk-TM.xml | 2 + translation/dest/onboarding/tl-PH.xml | 2 + translation/dest/onboarding/tlh-AA.xml | 2 + translation/dest/onboarding/tn-ZA.xml | 2 + translation/dest/onboarding/tp-TP.xml | 17 + translation/dest/onboarding/tr-TR.xml | 2 + translation/dest/onboarding/tt-RU.xml | 2 + translation/dest/onboarding/ug-CN.xml | 2 + translation/dest/onboarding/uk-UA.xml | 17 + translation/dest/onboarding/ur-PK.xml | 2 + translation/dest/onboarding/uz-UZ.xml | 2 + translation/dest/onboarding/vi-VN.xml | 17 + translation/dest/onboarding/wo-SN.xml | 2 + translation/dest/onboarding/xh-ZA.xml | 2 + translation/dest/onboarding/yo-NG.xml | 2 + translation/dest/onboarding/zh-CN.xml | 7 + translation/dest/onboarding/zh-TW.xml | 2 + translation/dest/onboarding/zu-ZA.xml | 2 + translation/dest/patron/ar-SA.xml | 1 + translation/dest/patron/gsw-CH.xml | 16 +- translation/dest/patron/so-SO.xml | 4 +- translation/dest/patron/th-TH.xml | 1 + translation/dest/patron/vi-VN.xml | 15 +- translation/dest/perfStat/vi-VN.xml | 4 +- translation/dest/preferences/an-ES.xml | 4 + translation/dest/preferences/ar-SA.xml | 13 +- translation/dest/preferences/da-DK.xml | 2 +- translation/dest/preferences/de-DE.xml | 2 +- translation/dest/preferences/fa-IR.xml | 1 + translation/dest/preferences/he-IL.xml | 1 + translation/dest/preferences/it-IT.xml | 1 + translation/dest/preferences/lb-LU.xml | 2 +- translation/dest/preferences/nn-NO.xml | 1 + translation/dest/preferences/ru-RU.xml | 2 +- translation/dest/preferences/sl-SI.xml | 1 + translation/dest/preferences/sv-SE.xml | 2 +- translation/dest/preferences/vi-VN.xml | 6 +- translation/dest/puzzle/da-DK.xml | 30 +- translation/dest/puzzle/fa-IR.xml | 70 +- translation/dest/puzzle/so-SO.xml | 2 + translation/dest/puzzle/ta-IN.xml | 14 +- translation/dest/puzzle/vi-VN.xml | 4 +- translation/dest/puzzleTheme/da-DK.xml | 14 +- translation/dest/puzzleTheme/gl-ES.xml | 2 +- translation/dest/puzzleTheme/nn-NO.xml | 2 +- translation/dest/puzzleTheme/vi-VN.xml | 8 +- translation/dest/search/gsw-CH.xml | 4 +- translation/dest/search/hi-IN.xml | 6 +- translation/dest/search/sl-SI.xml | 2 +- translation/dest/settings/da-DK.xml | 4 +- translation/dest/site/af-ZA.xml | 35 +- translation/dest/site/an-ES.xml | 48 +- translation/dest/site/ar-SA.xml | 78 +- translation/dest/site/av-DA.xml | 7 +- translation/dest/site/az-AZ.xml | 3 - translation/dest/site/be-BY.xml | 8 +- translation/dest/site/bg-BG.xml | 5 +- translation/dest/site/bn-BD.xml | 7 +- translation/dest/site/br-FR.xml | 7 +- translation/dest/site/bs-BA.xml | 7 +- translation/dest/site/ca-ES.xml | 16 +- translation/dest/site/ckb-IR.xml | 16 +- translation/dest/site/co-FR.xml | 5 +- translation/dest/site/cs-CZ.xml | 7 +- translation/dest/site/cv-CU.xml | 2 - translation/dest/site/cy-GB.xml | 2 - translation/dest/site/da-DK.xml | 30 +- translation/dest/site/de-DE.xml | 36 +- translation/dest/site/el-GR.xml | 12 +- translation/dest/site/en-US.xml | 16 +- translation/dest/site/eo-UY.xml | 14 +- translation/dest/site/es-ES.xml | 16 +- translation/dest/site/et-EE.xml | 7 +- translation/dest/site/eu-ES.xml | 19 +- translation/dest/site/fa-IR.xml | 30 +- translation/dest/site/fi-FI.xml | 22 +- translation/dest/site/fo-FO.xml | 3 - translation/dest/site/fr-FR.xml | 17 +- translation/dest/site/fy-NL.xml | 1 - translation/dest/site/ga-IE.xml | 7 +- translation/dest/site/gl-ES.xml | 30 +- translation/dest/site/gsw-CH.xml | 22 +- translation/dest/site/gu-IN.xml | 16 +- translation/dest/site/he-IL.xml | 27 +- translation/dest/site/hi-IN.xml | 19 +- translation/dest/site/hr-HR.xml | 7 +- translation/dest/site/hu-HU.xml | 7 +- translation/dest/site/hy-AM.xml | 7 +- translation/dest/site/ia-IA.xml | 1 - translation/dest/site/id-ID.xml | 7 +- translation/dest/site/is-IS.xml | 2 - translation/dest/site/it-IT.xml | 17 +- translation/dest/site/ja-JP.xml | 16 +- translation/dest/site/ka-GE.xml | 7 +- translation/dest/site/kaa-UZ.xml | 1 - translation/dest/site/kk-KZ.xml | 7 +- translation/dest/site/kmr-TR.xml | 3 - translation/dest/site/kn-IN.xml | 7 +- translation/dest/site/ko-KR.xml | 7 +- translation/dest/site/la-LA.xml | 1 - translation/dest/site/lb-LU.xml | 64 +- translation/dest/site/lt-LT.xml | 7 +- translation/dest/site/lv-LV.xml | 7 +- translation/dest/site/mg-MG.xml | 1 - translation/dest/site/mk-MK.xml | 7 +- translation/dest/site/ml-IN.xml | 2 - translation/dest/site/mn-MN.xml | 2 - translation/dest/site/mr-IN.xml | 7 +- translation/dest/site/ms-MY.xml | 3 - translation/dest/site/nb-NO.xml | 16 +- translation/dest/site/ne-NP.xml | 2 - translation/dest/site/nl-NL.xml | 16 +- translation/dest/site/nn-NO.xml | 20 +- translation/dest/site/os-SE.xml | 1 - translation/dest/site/pa-IN.xml | 1 - translation/dest/site/pl-PL.xml | 20 +- translation/dest/site/pt-BR.xml | 18 +- translation/dest/site/pt-PT.xml | 17 +- translation/dest/site/ro-RO.xml | 12 +- translation/dest/site/ru-RU.xml | 18 +- translation/dest/site/ry-UA.xml | 6 +- translation/dest/site/sco-GB.xml | 1 - translation/dest/site/si-LK.xml | 1 - translation/dest/site/sk-SK.xml | 11 +- translation/dest/site/sl-SI.xml | 16 +- translation/dest/site/so-SO.xml | 359 +- translation/dest/site/sq-AL.xml | 14 +- translation/dest/site/sr-SP.xml | 3 - translation/dest/site/sv-SE.xml | 17 +- translation/dest/site/sw-KE.xml | 1 - translation/dest/site/ta-IN.xml | 4 +- translation/dest/site/te-IN.xml | 1 - translation/dest/site/th-TH.xml | 16 +- translation/dest/site/tk-TM.xml | 1 - translation/dest/site/tl-PH.xml | 7 +- translation/dest/site/tlh-AA.xml | 1 - translation/dest/site/tp-TP.xml | 11 +- translation/dest/site/tr-TR.xml | 15 +- translation/dest/site/tt-RU.xml | 3 - translation/dest/site/uk-UA.xml | 13 +- translation/dest/site/ur-PK.xml | 2 - translation/dest/site/uz-UZ.xml | 7 +- translation/dest/site/vi-VN.xml | 70 +- translation/dest/site/zh-CN.xml | 8 +- translation/dest/site/zh-TW.xml | 35 +- translation/dest/site/zu-ZA.xml | 1 - translation/dest/storm/ar-SA.xml | 18 +- translation/dest/storm/da-DK.xml | 10 +- translation/dest/storm/ta-IN.xml | 62 +- translation/dest/storm/vi-VN.xml | 2 +- translation/dest/streamer/be-BY.xml | 9 +- translation/dest/streamer/gsw-CH.xml | 10 +- translation/dest/streamer/ta-IN.xml | 52 +- translation/dest/streamer/th-TH.xml | 7 + translation/dest/streamer/vi-VN.xml | 8 +- translation/dest/study/da-DK.xml | 4 +- translation/dest/study/fi-FI.xml | 4 +- translation/dest/study/gsw-CH.xml | 12 +- translation/dest/study/ta-IN.xml | 70 +- translation/dest/study/vi-VN.xml | 10 +- translation/dest/swiss/ar-SA.xml | 30 +- translation/dest/swiss/gsw-CH.xml | 12 +- translation/dest/swiss/ta-IN.xml | 109 + translation/dest/swiss/tp-TP.xml | 4 + translation/dest/swiss/vi-VN.xml | 10 +- translation/dest/team/gsw-CH.xml | 4 +- translation/dest/team/lb-LU.xml | 3 + translation/dest/team/ta-IN.xml | 55 +- translation/dest/team/vi-VN.xml | 12 +- translation/dest/tfa/ar-SA.xml | 23 +- translation/dest/tfa/en-US.xml | 2 + translation/dest/tfa/gl-ES.xml | 1 + translation/dest/tfa/gsw-CH.xml | 20 +- translation/dest/tfa/he-IL.xml | 2 + translation/dest/tfa/it-IT.xml | 2 + translation/dest/tfa/nb-NO.xml | 2 + translation/dest/tfa/sv-SE.xml | 2 + translation/dest/tfa/ta-IN.xml | 3 + translation/dest/tfa/th-TH.xml | 2 + translation/dest/tfa/tr-TR.xml | 1 + translation/dest/tfa/vi-VN.xml | 2 +- translation/dest/timeago/af-ZA.xml | 9 + translation/dest/timeago/ar-SA.xml | 17 + translation/dest/timeago/ckb-IR.xml | 9 + translation/dest/timeago/da-DK.xml | 9 + translation/dest/timeago/de-DE.xml | 9 + translation/dest/timeago/el-GR.xml | 2 +- translation/dest/timeago/en-US.xml | 9 + translation/dest/timeago/es-ES.xml | 9 + translation/dest/timeago/fa-IR.xml | 9 + translation/dest/timeago/fr-FR.xml | 9 + translation/dest/timeago/gl-ES.xml | 9 + translation/dest/timeago/gsw-CH.xml | 9 + translation/dest/timeago/he-IL.xml | 13 + translation/dest/timeago/it-IT.xml | 1 + translation/dest/timeago/ja-JP.xml | 7 + translation/dest/timeago/lb-LU.xml | 8 + translation/dest/timeago/nb-NO.xml | 9 + translation/dest/timeago/nl-NL.xml | 9 + translation/dest/timeago/nn-NO.xml | 9 + translation/dest/timeago/pl-PL.xml | 13 + translation/dest/timeago/pt-BR.xml | 9 + translation/dest/timeago/pt-PT.xml | 11 +- translation/dest/timeago/ru-RU.xml | 13 + translation/dest/timeago/sl-SI.xml | 13 + translation/dest/timeago/so-SO.xml | 66 +- translation/dest/timeago/sv-SE.xml | 9 + translation/dest/timeago/th-TH.xml | 7 + translation/dest/timeago/tp-TP.xml | 9 + translation/dest/timeago/uk-UA.xml | 1 + translation/dest/timeago/vi-VN.xml | 7 + translation/dest/timeago/zh-CN.xml | 7 + translation/dest/tourname/ckb-IR.xml | 14 +- translation/dest/tourname/ta-IN.xml | 72 +- translation/dest/tourname/vi-VN.xml | 4 +- translation/dest/ublog/af-ZA.xml | 5 + translation/dest/ublog/ar-SA.xml | 9 +- translation/dest/ublog/ca-ES.xml | 8 + translation/dest/ublog/ckb-IR.xml | 2 +- translation/dest/ublog/da-DK.xml | 2 + translation/dest/ublog/de-DE.xml | 3 + translation/dest/ublog/el-GR.xml | 2 +- translation/dest/ublog/en-US.xml | 8 + translation/dest/ublog/eo-UY.xml | 1 + translation/dest/ublog/es-ES.xml | 8 + translation/dest/ublog/eu-ES.xml | 2 +- translation/dest/ublog/fa-IR.xml | 2 +- translation/dest/ublog/fi-FI.xml | 7 +- translation/dest/ublog/fr-FR.xml | 10 +- translation/dest/ublog/gl-ES.xml | 8 + translation/dest/ublog/gsw-CH.xml | 10 +- translation/dest/ublog/he-IL.xml | 8 + translation/dest/ublog/it-IT.xml | 9 + translation/dest/ublog/ja-JP.xml | 10 +- translation/dest/ublog/kk-KZ.xml | 2 +- translation/dest/ublog/lb-LU.xml | 9 +- translation/dest/ublog/nb-NO.xml | 10 +- translation/dest/ublog/nl-NL.xml | 2 +- translation/dest/ublog/nn-NO.xml | 8 + translation/dest/ublog/pl-PL.xml | 8 + translation/dest/ublog/pt-BR.xml | 7 + translation/dest/ublog/pt-PT.xml | 8 + translation/dest/ublog/ro-RO.xml | 2 +- translation/dest/ublog/ru-RU.xml | 10 +- translation/dest/ublog/sk-SK.xml | 10 +- translation/dest/ublog/sl-SI.xml | 8 + translation/dest/ublog/sq-AL.xml | 2 +- translation/dest/ublog/sv-SE.xml | 2 +- translation/dest/ublog/ta-IN.xml | 47 +- translation/dest/ublog/th-TH.xml | 10 +- translation/dest/ublog/tp-TP.xml | 1 + translation/dest/ublog/uk-UA.xml | 8 +- translation/dest/ublog/vi-VN.xml | 10 +- translation/dest/voiceCommands/ar-SA.xml | 22 +- translation/dest/voiceCommands/de-DE.xml | 8 +- translation/dest/voiceCommands/el-GR.xml | 5 +- translation/dest/voiceCommands/en-US.xml | 2 +- translation/dest/voiceCommands/gsw-CH.xml | 2 - translation/dest/voiceCommands/he-IL.xml | 22 +- translation/dest/voiceCommands/lb-LU.xml | 6 + translation/dest/voiceCommands/pt-BR.xml | 3 + translation/dest/voiceCommands/pt-PT.xml | 13 + translation/dest/voiceCommands/sv-SE.xml | 4 + translation/dest/voiceCommands/ta-IN.xml | 22 +- translation/dest/voiceCommands/tr-TR.xml | 13 +- translation/dest/voiceCommands/uk-UA.xml | 6 +- translation/source/arena.xml | 12 +- translation/source/broadcast.xml | 2 +- translation/source/challenge.xml | 4 +- translation/source/class.xml | 10 +- translation/source/coordinates.xml | 2 +- translation/source/onboarding.xml | 17 + translation/source/preferences.xml | 2 +- translation/source/site.xml | 20 +- translation/source/timeago.xml | 9 + translation/source/ublog.xml | 8 + ui/@types/lichess/index.d.ts | 45 +- ui/analyse/css/study/_modal.scss | 10 +- ui/analyse/css/study/panel/_multiboard.scss | 37 + ui/analyse/package.json | 2 +- ui/analyse/src/autoShape.ts | 3 +- ui/analyse/src/crazy/crazyView.ts | 6 +- ui/analyse/src/ctrl.ts | 49 +- ui/analyse/src/evalCache.ts | 103 +- ui/analyse/src/explorer/explorerConfig.ts | 47 +- ui/analyse/src/explorer/explorerView.ts | 215 +- ui/analyse/src/explorer/tablebaseView.ts | 16 +- ui/analyse/src/forecast/forecastView.ts | 100 +- ui/analyse/src/fork.ts | 19 +- ui/analyse/src/ground.ts | 13 +- ui/analyse/src/interfaces.ts | 11 +- ui/analyse/src/plugins/nvui.ts | 83 +- ui/analyse/src/practice/practiceCtrl.ts | 10 +- ui/analyse/src/practice/practiceView.ts | 4 +- ui/analyse/src/retrospect/retroView.ts | 56 +- ui/analyse/src/serverSideUnderboard.ts | 14 +- ui/analyse/src/socket.ts | 7 +- ui/analyse/src/start.ts | 2 +- ui/analyse/src/study/chapterEditForm.ts | 163 +- ui/analyse/src/study/chapterNewForm.ts | 473 +-- ui/analyse/src/study/commentForm.ts | 91 +- ui/analyse/src/study/description.ts | 80 +- .../src/study/gamebook/gamebookButtons.ts | 67 +- ui/analyse/src/study/gamebook/gamebookEdit.ts | 88 +- .../src/study/gamebook/gamebookPlayView.ts | 118 +- ui/analyse/src/study/interfaces.ts | 84 +- ui/analyse/src/study/inviteForm.ts | 14 +- ui/analyse/src/study/multiBoard.ts | 98 +- ui/analyse/src/study/nextChapter.ts | 9 +- ui/analyse/src/study/notif.ts | 33 +- ui/analyse/src/study/playerBars.ts | 33 +- .../src/study/practice/studyPracticeCtrl.ts | 151 +- .../src/study/practice/studyPracticeView.ts | 59 +- ui/analyse/src/study/relay/interfaces.ts | 5 - ui/analyse/src/study/relay/relayCtrl.ts | 15 +- .../src/study/relay/relayManagerView.ts | 64 +- ui/analyse/src/study/relay/relayTourView.ts | 87 +- ui/analyse/src/study/serverEval.ts | 11 +- ui/analyse/src/study/studyChapters.ts | 51 +- ui/analyse/src/study/studyComments.ts | 17 +- ui/analyse/src/study/studyCtrl.ts | 1094 +++--- ui/analyse/src/study/studyDeps.ts | 4 +- ui/analyse/src/study/studyForm.ts | 180 +- ui/analyse/src/study/studyGlyph.ts | 70 +- ui/analyse/src/study/studyMembers.ts | 336 +- ui/analyse/src/study/studySearch.ts | 42 +- ui/analyse/src/study/studyShare.ts | 148 +- ui/analyse/src/study/studyTags.ts | 135 +- ui/analyse/src/study/studyTour.ts | 4 +- ui/analyse/src/study/studyView.ts | 163 +- ui/analyse/src/study/studyXhr.ts | 8 +- ui/analyse/src/study/topics.ts | 40 +- ui/analyse/src/treeView/columnView.ts | 155 +- ui/analyse/src/treeView/common.ts | 5 +- ui/analyse/src/treeView/contextMenu.ts | 51 +- ui/analyse/src/treeView/inlineView.ts | 39 +- ui/analyse/src/treeView/treeView.ts | 43 +- ui/analyse/src/view/actionMenu.ts | 223 +- ui/analyse/src/view/clocks.ts | 8 +- ui/analyse/src/view/moveView.ts | 8 +- ui/analyse/src/view/roundTraining.ts | 18 +- ui/analyse/src/view/util.ts | 11 +- ui/analyse/src/view/view.ts | 192 +- ui/board/src/menu.ts | 14 +- ui/ceval/css/_settings.scss | 52 +- ui/ceval/package.json | 2 +- ui/ceval/src/ctrl.ts | 33 +- ui/ceval/src/engines/engines.ts | 44 +- ui/ceval/src/engines/externalEngine.ts | 4 +- ui/ceval/src/engines/simpleEngine.ts | 14 +- ui/ceval/src/engines/stockfishWebEngine.ts | 6 +- ui/ceval/src/engines/threadedEngine.ts | 6 +- ui/ceval/src/main.ts | 2 +- ui/ceval/src/types.ts | 14 +- ui/ceval/src/util.ts | 24 +- ui/ceval/src/view/main.ts | 157 +- ui/ceval/src/view/settings.ts | 259 +- ui/ceval/src/winningChances.ts | 16 +- ui/challenge/css/_page.scss | 62 +- ui/challenge/src/view.ts | 103 +- ui/chart/package.json | 4 +- ui/chart/src/acpl.ts | 15 +- ui/chart/src/common.ts | 10 +- ui/chart/src/game.ts | 4 +- ui/chart/src/interface.ts | 2 +- ui/chart/src/movetime.ts | 2 + ui/chart/src/ratingDistribution.ts | 3 +- ui/chart/src/resizePolyfill.ts | 5 + ui/chat/src/ctrl.ts | 2 +- ui/chat/src/discussion.ts | 28 +- ui/chat/src/interfaces.ts | 2 +- ui/chat/src/moderation.ts | 32 +- ui/chat/src/note.ts | 11 +- ui/chat/src/preset.ts | 13 +- ui/chat/src/spam.ts | 2 + ui/chat/src/view.ts | 28 +- ui/chess/src/promotion.ts | 23 +- ui/cli/src/main.ts | 4 +- ui/common/css/component/_dialog.scss | 1 + ui/common/css/component/_markdown.scss | 113 +- ui/common/css/component/_power-tip.scss | 4 +- ui/common/css/form/_emoji-picker.scss | 6 + ui/common/css/form/_form3.scss | 2 +- ui/common/css/vendor/chessground/_themes.scss | 2 +- ui/common/src/controls.ts | 35 +- ui/common/src/device.ts | 13 +- ui/common/src/dialog.ts | 18 +- ui/common/src/linkPopup.ts | 6 +- ui/common/src/mini-board.ts | 5 +- ui/common/src/notification.ts | 2 +- ui/common/src/snabbdom.ts | 31 +- ui/common/src/spinner.ts | 29 +- ui/common/src/userLink.ts | 23 +- ui/coordinateTrainer/src/side.ts | 46 +- ui/coordinateTrainer/src/view.ts | 138 +- ui/dasher/src/background.ts | 110 +- ui/dasher/src/board.ts | 69 +- ui/dasher/src/dasher.ts | 92 +- ui/dasher/src/langs.ts | 54 +- ui/dasher/src/links.ts | 37 +- ui/dasher/src/main.ts | 8 +- ui/dasher/src/piece.ts | 87 +- ui/dasher/src/ping.ts | 41 +- ui/dasher/src/sound.ts | 84 +- ui/dasher/src/theme.ts | 83 +- ui/dasher/src/util.ts | 46 +- ui/dasher/src/view.ts | 14 +- ui/dgt/package.json | 2 +- ui/dgt/src/config.ts | 16 +- ui/editor/css/_tools.scss | 11 + ui/editor/package.json | 2 +- ui/editor/src/chessground.ts | 4 +- ui/editor/src/ctrl.ts | 129 +- ui/editor/src/interfaces.ts | 1 + ui/editor/src/view.ts | 199 +- .../components/ratingDifferenceSliders.ts | 4 +- .../view/components/timePickerAndSliders.ts | 146 +- ui/gameSetup/src/view/localContent.ts | 64 +- ui/insight/src/axis.ts | 7 +- ui/insight/src/boards.ts | 59 +- ui/insight/src/chart.ts | 3 +- ui/insight/src/filters.ts | 7 +- ui/insight/src/info.ts | 17 +- ui/insight/src/view.ts | 18 +- ui/keyboardMove/src/main.ts | 5 +- ui/keyboardMove/src/plugins/keyboardMove.ts | 2 +- ui/libot/defs/bots.json | 8 +- ui/libot/src/ctrl.ts | 2 +- ui/libot/src/main.ts | 10 +- ui/libot/src/zfbot.ts | 2 +- ui/lobby/css/_feed.scss | 72 + ui/lobby/css/_layout.scss | 36 +- ui/lobby/css/_lobby.scss | 1 + ui/lobby/css/build/_lobby.scss | 1 + ui/lobby/src/ctrl.ts | 2 +- ui/lobby/src/disableDarkBoard.ts | 2 +- ui/lobby/src/view/correspondence.ts | 17 +- ui/lobby/src/view/playing.ts | 53 +- ui/lobby/src/view/pools.ts | 29 +- ui/lobby/src/view/realTime/chart.ts | 37 +- ui/lobby/src/view/realTime/filter.ts | 15 +- ui/lobby/src/view/realTime/list.ts | 44 +- ui/lobby/src/view/tabs.ts | 5 +- ui/localPlay/src/ctrl.ts | 2 +- ui/localPlay/src/main.ts | 2 +- ui/mod/src/teamAdmin.ts | 2 +- ui/msg/src/view/actions.ts | 6 +- ui/msg/src/view/contact.ts | 28 +- ui/msg/src/view/convo.ts | 56 +- ui/msg/src/view/enhance.ts | 2 +- ui/msg/src/view/interact.ts | 11 +- ui/msg/src/view/main.ts | 40 +- ui/msg/src/view/msgs.ts | 24 +- ui/msg/src/view/search.ts | 10 +- ui/notify/src/renderers.ts | 25 +- ui/notify/src/view.ts | 28 +- ui/nvui/package.json | 2 +- ui/nvui/src/chess.ts | 11 +- ui/nvui/src/notify.ts | 11 +- ui/nvui/src/setting.ts | 11 +- ui/opening/package.json | 6 +- ui/opening/src/chart.ts | 24 +- ui/puz/package.json | 2 +- ui/puz/src/view/history.ts | 51 +- ui/puz/src/view/util.ts | 10 +- ui/puzzle/package.json | 10 +- ui/puzzle/src/autoShape.ts | 18 +- ui/puzzle/src/control.ts | 28 +- ui/puzzle/src/ctrl.ts | 850 ++-- ui/puzzle/src/dashboard.ts | 5 +- ui/puzzle/src/interfaces.ts | 106 +- ui/puzzle/src/keyboard.ts | 10 +- ui/puzzle/src/main.ts | 6 +- ui/puzzle/src/moveTest.ts | 31 +- ui/puzzle/src/plugins/nvui.ts | 142 +- ui/puzzle/src/view/after.ts | 99 +- ui/puzzle/src/view/boardMenu.ts | 4 +- ui/puzzle/src/view/chessground.ts | 10 +- ui/puzzle/src/view/feedback.ts | 67 +- ui/puzzle/src/view/main.ts | 80 +- ui/puzzle/src/view/side.ts | 185 +- ui/puzzle/src/view/theme.ts | 133 +- ui/puzzle/src/view/tree.ts | 148 +- ui/racer/package.json | 2 +- ui/racer/src/view/board.ts | 18 +- ui/racer/src/view/main.ts | 117 +- ui/racer/src/view/race.ts | 16 +- ui/round/src/boot.ts | 2 +- ui/round/src/clock/clockView.ts | 44 +- ui/round/src/corresClock/corresClockCtrl.ts | 67 +- ui/round/src/corresClock/corresClockView.ts | 22 +- ui/round/src/crazy/crazyCtrl.ts | 2 +- ui/round/src/crazy/crazyView.ts | 6 +- ui/round/src/ctrl.ts | 6 +- ui/round/src/plugins/nvui.ts | 384 +- ui/round/src/tourStanding.ts | 67 +- ui/round/src/view/button.ts | 295 +- ui/round/src/view/expiration.ts | 7 +- ui/round/src/view/main.ts | 7 +- ui/round/src/view/replay.ts | 132 +- ui/round/src/view/table.ts | 39 +- ui/round/src/view/user.ts | 28 +- ui/simul/src/view/created.ts | 167 +- ui/simul/src/view/main.ts | 55 +- ui/simul/src/view/pairings.ts | 32 +- ui/simul/src/view/util.ts | 4 +- ui/site/css/_account.scss | 7 - ui/site/css/_dailyFeed.scss | 77 + ui/site/css/_importer.scss | 5 - ui/site/css/_linkPopup.scss | 3 +- ui/site/css/_video.scss | 5 - ui/site/css/build/_account.scss | 1 + ui/site/css/build/_dailyFeed.scss | 7 + ui/site/css/build/_team.scss | 1 + ui/site/css/build/dailyFeed.ltr.dark.scss | 3 + ui/site/css/build/dailyFeed.ltr.light.scss | 3 + ui/site/css/build/dailyFeed.ltr.transp.scss | 3 + ui/site/css/build/dailyFeed.rtl.dark.scss | 3 + ui/site/css/build/dailyFeed.rtl.light.scss | 3 + ui/site/css/build/dailyFeed.rtl.transp.scss | 3 + ui/site/css/team/_show.scss | 8 +- ui/site/css/ublog/_post.scss | 4 + ui/site/css/user/_list.scss | 3 +- ui/site/css/user/_rating.stats.scss | 4 + ui/site/package.json | 4 +- ui/site/src/account.ts | 36 +- ui/site/src/challengePage.ts | 13 +- ui/site/src/clas.ts | 2 +- ui/site/src/component/assets.ts | 36 +- ui/site/src/component/flairPicker.ts | 24 + ui/site/src/component/log.ts | 12 +- ui/site/src/component/mic.ts | 8 +- ui/site/src/component/serviceWorker.ts | 6 +- ui/site/src/component/socket.ts | 43 +- ui/site/src/component/sound.ts | 88 +- ui/site/src/component/storage.ts | 54 +- ui/site/src/component/timeago.ts | 23 +- ui/site/src/component/top-bar.ts | 3 +- ui/site/src/dailyFeed.ts | 5 + ui/site/src/diagnostic.ts | 31 +- ui/site/src/expandText.ts | 2 +- .../src/{emojiPicker.ts => flairPicker.ts} | 30 +- ui/site/src/forum.ts | 4 +- ui/site/src/login.ts | 2 +- ui/site/src/site.lichess.globals.ts | 25 +- ui/site/src/site.ts | 5 +- ui/site/src/team.ts | 5 + ui/site/src/userComplete.ts | 2 +- ui/storm/package.json | 2 +- ui/storm/src/view/end.ts | 46 +- ui/storm/src/view/main.ts | 47 +- ui/swiss/src/pagination.ts | 11 +- ui/swiss/src/search.ts | 11 +- ui/swiss/src/view/boards.ts | 11 +- ui/swiss/src/view/header.ts | 20 +- ui/swiss/src/view/main.ts | 282 +- ui/swiss/src/view/playerInfo.ts | 135 +- ui/swiss/src/view/podium.ts | 17 +- ui/swiss/src/view/standing.ts | 47 +- ui/swiss/src/view/util.ts | 4 +- ui/tournament/css/_leaderboard.scss | 3 +- ui/tournament/css/_team-info.scss | 4 + ui/tournament/src/interfaces.ts | 6 +- ui/tournament/src/pagination.ts | 11 +- ui/tournament/src/search.ts | 11 +- ui/tournament/src/view/arena.ts | 15 +- ui/tournament/src/view/battle.ts | 51 +- ui/tournament/src/view/button.ts | 54 +- ui/tournament/src/view/created.ts | 6 +- ui/tournament/src/view/finished.ts | 33 +- ui/tournament/src/view/header.ts | 36 +- ui/tournament/src/view/main.ts | 14 +- ui/tournament/src/view/playerInfo.ts | 130 +- ui/tournament/src/view/started.ts | 12 +- ui/tournament/src/view/table.ts | 92 +- ui/tournament/src/view/teamInfo.ts | 106 +- ui/tournament/src/view/util.ts | 4 +- ui/tournamentCalendar/src/view.ts | 25 +- ui/tournamentSchedule/src/view.ts | 24 +- ui/tree/src/tree.ts | 8 - ui/voice/src/move/moveCtrl.ts | 6 +- ui/voice/src/move/view.ts | 7 +- ui/voice/src/view.ts | 74 +- 4554 files changed, 17445 insertions(+), 12372 deletions(-) create mode 100644 .github/workflows/flair.yml create mode 100644 app/controllers/DailyFeed.scala create mode 100644 app/controllers/TeamApi.scala create mode 100644 app/templating/HtmlHelper.scala create mode 100644 app/views/dailyFeed.scala create mode 100755 bin/validate-flair create mode 100644 conf/team.routes create mode 100644 modules/blog/src/main/DailyFeed.scala create mode 100644 modules/i18n/src/main/LangForm.scala create mode 100644 modules/user/src/main/FlairApi.scala delete mode 100644 modules/user/src/main/UserFlair.scala create mode 100644 public/flair/README.md create mode 100644 public/flair/img/activity.1st-place-medal.webp create mode 100644 public/flair/img/activity.2024.webp create mode 100644 public/flair/img/activity.2nd-place-medal.webp create mode 100644 public/flair/img/activity.3rd-place-medal.webp create mode 100644 public/flair/img/activity.admission-tickets.webp create mode 100644 public/flair/img/activity.american-football.webp create mode 100644 public/flair/img/activity.artist-palette.webp create mode 100644 public/flair/img/activity.badminton.webp create mode 100644 public/flair/img/activity.balloon.webp create mode 100644 public/flair/img/activity.baseball.webp create mode 100644 public/flair/img/activity.basketball.webp create mode 100644 public/flair/img/activity.bowling.webp create mode 100644 public/flair/img/activity.boxing-glove.webp create mode 100644 public/flair/img/activity.carp-streamer.webp create mode 100644 public/flair/img/activity.chess-pawn.webp create mode 100644 public/flair/img/activity.chess.webp create mode 100644 public/flair/img/activity.christmas-tree.webp create mode 100644 public/flair/img/activity.club-suit.webp create mode 100644 public/flair/img/activity.confetti-ball.webp create mode 100644 public/flair/img/activity.cricket-game.webp create mode 100644 public/flair/img/activity.crystal-ball.webp create mode 100644 public/flair/img/activity.curling-stone.webp create mode 100644 public/flair/img/activity.diamond-suit.webp create mode 100644 public/flair/img/activity.direct-hit.webp create mode 100644 public/flair/img/activity.diving-mask.webp create mode 100644 public/flair/img/activity.field-hockey.webp create mode 100644 public/flair/img/activity.firecracker.webp create mode 100644 public/flair/img/activity.fireworks.webp create mode 100644 public/flair/img/activity.fishing-pole.webp create mode 100644 public/flair/img/activity.flag-in-hole.webp create mode 100644 public/flair/img/activity.flower-playing-cards.webp create mode 100644 public/flair/img/activity.flying-disc.webp create mode 100644 public/flair/img/activity.framed-picture.webp create mode 100644 public/flair/img/activity.game-die.webp create mode 100644 public/flair/img/activity.goal-net.webp create mode 100644 public/flair/img/activity.heart-suit.webp create mode 100644 public/flair/img/activity.ice-hockey.webp create mode 100644 public/flair/img/activity.ice-skate.webp create mode 100644 public/flair/img/activity.jack-o-lantern.webp create mode 100644 public/flair/img/activity.japanese-dolls.webp create mode 100644 public/flair/img/activity.joker.webp create mode 100644 public/flair/img/activity.joystick.webp create mode 100644 public/flair/img/activity.kite.webp create mode 100644 public/flair/img/activity.lacrosse.webp create mode 100644 public/flair/img/activity.lichess-berserk.webp create mode 100644 public/flair/img/activity.lichess-blitz.webp create mode 100644 public/flair/img/activity.lichess-bullet.webp create mode 100644 public/flair/img/activity.lichess-classical.webp create mode 100644 public/flair/img/activity.lichess-correspondence.webp create mode 100644 public/flair/img/activity.lichess-hogger.webp create mode 100644 public/flair/img/activity.lichess-horsey.webp create mode 100644 public/flair/img/activity.lichess-rapid.webp create mode 100644 public/flair/img/activity.lichess-ultrabullet.webp create mode 100644 public/flair/img/activity.lichess-variant-960.webp create mode 100644 public/flair/img/activity.lichess-variant-antichess.webp create mode 100644 public/flair/img/activity.lichess-variant-atomic.webp create mode 100644 public/flair/img/activity.lichess-variant-crazyhouse.webp create mode 100644 public/flair/img/activity.lichess-variant-horde.webp create mode 100644 public/flair/img/activity.lichess-variant-king-of-the-hill.webp create mode 100644 public/flair/img/activity.lichess-variant-racing-kings.webp create mode 100644 public/flair/img/activity.lichess-variant-three-check.webp create mode 100644 public/flair/img/activity.lichess.webp create mode 100644 public/flair/img/activity.magic-wand.webp create mode 100644 public/flair/img/activity.mahjong-red-dragon.webp create mode 100644 public/flair/img/activity.martial-arts-uniform.webp create mode 100644 public/flair/img/activity.military-medal.webp create mode 100644 public/flair/img/activity.mirror-ball.webp create mode 100644 public/flair/img/activity.moon-viewing-ceremony.webp create mode 100644 public/flair/img/activity.nesting-dolls.webp create mode 100644 public/flair/img/activity.party-popper.webp create mode 100644 public/flair/img/activity.performing-arts.webp create mode 100644 public/flair/img/activity.pinata.webp create mode 100644 public/flair/img/activity.pine-decoration.webp create mode 100644 public/flair/img/activity.ping-pong.webp create mode 100644 public/flair/img/activity.pistol.webp create mode 100644 public/flair/img/activity.pool-8-ball.webp create mode 100644 public/flair/img/activity.puzzle-piece.webp create mode 100644 public/flair/img/activity.red-envelope.webp create mode 100644 public/flair/img/activity.rugby-football.webp create mode 100644 public/flair/img/activity.running-shirt.webp create mode 100644 public/flair/img/activity.shogi-bigsby.webp create mode 100644 public/flair/img/activity.shogi-king.webp create mode 100644 public/flair/img/activity.skis.webp create mode 100644 public/flair/img/activity.sled.webp create mode 100644 public/flair/img/activity.slot-machine.webp create mode 100644 public/flair/img/activity.soccer-ball.webp create mode 100644 public/flair/img/activity.softball.webp create mode 100644 public/flair/img/activity.spade-suit.webp create mode 100644 public/flair/img/activity.sparkler.webp create mode 100644 public/flair/img/activity.sparkles.webp create mode 100644 public/flair/img/activity.sports-medal.webp create mode 100644 public/flair/img/activity.tanabata-tree.webp create mode 100644 public/flair/img/activity.tennis.webp create mode 100644 public/flair/img/activity.ticket.webp create mode 100644 public/flair/img/activity.trophy.webp create mode 100644 public/flair/img/activity.video-game.webp create mode 100644 public/flair/img/activity.volleyball.webp create mode 100644 public/flair/img/activity.wind-chime.webp create mode 100644 public/flair/img/activity.wrapped-gift.webp create mode 100644 public/flair/img/activity.xmas-lichess-horsey.webp create mode 100644 public/flair/img/activity.yo-yo.webp create mode 100644 public/flair/img/food-drink.amphora.webp create mode 100644 public/flair/img/food-drink.avocado.webp create mode 100644 public/flair/img/food-drink.baby-bottle.webp create mode 100644 public/flair/img/food-drink.bacon.webp create mode 100644 public/flair/img/food-drink.bagel.webp create mode 100644 public/flair/img/food-drink.baguette-bread.webp create mode 100644 public/flair/img/food-drink.banana.webp create mode 100644 public/flair/img/food-drink.beans.webp create mode 100644 public/flair/img/food-drink.beer-mug.webp create mode 100644 public/flair/img/food-drink.bell-pepper.webp create mode 100644 public/flair/img/food-drink.bento-box.webp create mode 100644 public/flair/img/food-drink.beverage-box.webp create mode 100644 public/flair/img/food-drink.birthday-cake.webp create mode 100644 public/flair/img/food-drink.blueberries.webp create mode 100644 public/flair/img/food-drink.bottle-with-popping-cork.webp create mode 100644 public/flair/img/food-drink.bowl-with-spoon.webp create mode 100644 public/flair/img/food-drink.bread.webp create mode 100644 public/flair/img/food-drink.broccoli.webp create mode 100644 public/flair/img/food-drink.brown-mushroom.webp create mode 100644 public/flair/img/food-drink.bubble-tea.webp create mode 100644 public/flair/img/food-drink.burrito.webp create mode 100644 public/flair/img/food-drink.butter.webp create mode 100644 public/flair/img/food-drink.candy.webp create mode 100644 public/flair/img/food-drink.canned-food.webp create mode 100644 public/flair/img/food-drink.carrot.webp create mode 100644 public/flair/img/food-drink.cheese-wedge.webp create mode 100644 public/flair/img/food-drink.cherries.webp create mode 100644 public/flair/img/food-drink.chestnut.webp create mode 100644 public/flair/img/food-drink.chocolate-bar.webp create mode 100644 public/flair/img/food-drink.chopsticks.webp create mode 100644 public/flair/img/food-drink.clinking-beer-mugs.webp create mode 100644 public/flair/img/food-drink.clinking-glasses.webp create mode 100644 public/flair/img/food-drink.cocktail-glass.webp create mode 100644 public/flair/img/food-drink.coconut.webp create mode 100644 public/flair/img/food-drink.cooked-rice.webp create mode 100644 public/flair/img/food-drink.cookie.webp create mode 100644 public/flair/img/food-drink.cooking.webp create mode 100644 public/flair/img/food-drink.croissant.webp create mode 100644 public/flair/img/food-drink.cucumber.webp create mode 100644 public/flair/img/food-drink.cup-with-straw.webp create mode 100644 public/flair/img/food-drink.cupcake.webp create mode 100644 public/flair/img/food-drink.curry-rice.webp create mode 100644 public/flair/img/food-drink.custard.webp create mode 100644 public/flair/img/food-drink.cut-of-meat.webp create mode 100644 public/flair/img/food-drink.dango.webp create mode 100644 public/flair/img/food-drink.doughnut.webp create mode 100644 public/flair/img/food-drink.dumpling.webp create mode 100644 public/flair/img/food-drink.ear-of-corn.webp create mode 100644 public/flair/img/food-drink.egg.webp create mode 100644 public/flair/img/food-drink.falafel.webp create mode 100644 public/flair/img/food-drink.fish-cake-with-swirl.webp create mode 100644 public/flair/img/food-drink.flatbread.webp create mode 100644 public/flair/img/food-drink.fondue.webp create mode 100644 public/flair/img/food-drink.fork-and-knife-with-plate.webp create mode 100644 public/flair/img/food-drink.fork-and-knife.webp create mode 100644 public/flair/img/food-drink.fortune-cookie.webp create mode 100644 public/flair/img/food-drink.french-fries.webp create mode 100644 public/flair/img/food-drink.fried-shrimp.webp create mode 100644 public/flair/img/food-drink.garlic.webp create mode 100644 public/flair/img/food-drink.ginger.webp create mode 100644 public/flair/img/food-drink.glass-of-milk.webp create mode 100644 public/flair/img/food-drink.grapes.webp create mode 100644 public/flair/img/food-drink.green-apple.webp create mode 100644 public/flair/img/food-drink.green-salad.webp create mode 100644 public/flair/img/food-drink.hamburger.webp create mode 100644 public/flair/img/food-drink.honey-pot.webp create mode 100644 public/flair/img/food-drink.hot-beverage.webp create mode 100644 public/flair/img/food-drink.hot-dog.webp create mode 100644 public/flair/img/food-drink.hot-pepper.webp create mode 100644 public/flair/img/food-drink.ice-cream.webp create mode 100644 public/flair/img/food-drink.ice.webp create mode 100644 public/flair/img/food-drink.jar.webp create mode 100644 public/flair/img/food-drink.kitchen-knife.webp create mode 100644 public/flair/img/food-drink.kiwi-fruit.webp create mode 100644 public/flair/img/food-drink.leafy-green.webp create mode 100644 public/flair/img/food-drink.lemon.webp create mode 100644 public/flair/img/food-drink.lime.webp create mode 100644 public/flair/img/food-drink.lollipop.webp create mode 100644 public/flair/img/food-drink.mango.webp create mode 100644 public/flair/img/food-drink.mate.webp create mode 100644 public/flair/img/food-drink.meat-on-bone.webp create mode 100644 public/flair/img/food-drink.melon.webp create mode 100644 public/flair/img/food-drink.moon-cake.webp create mode 100644 public/flair/img/food-drink.oden.webp create mode 100644 public/flair/img/food-drink.olive.webp create mode 100644 public/flair/img/food-drink.onion.webp create mode 100644 public/flair/img/food-drink.pancakes.webp create mode 100644 public/flair/img/food-drink.pea-pod.webp create mode 100644 public/flair/img/food-drink.peanuts.webp create mode 100644 public/flair/img/food-drink.pear.webp create mode 100644 public/flair/img/food-drink.pie.webp create mode 100644 public/flair/img/food-drink.pineapple.webp create mode 100644 public/flair/img/food-drink.pizza.webp create mode 100644 public/flair/img/food-drink.popcorn.webp create mode 100644 public/flair/img/food-drink.pot-of-food.webp create mode 100644 public/flair/img/food-drink.potato.webp create mode 100644 public/flair/img/food-drink.poultry-leg.webp create mode 100644 public/flair/img/food-drink.pouring-liquid.webp create mode 100644 public/flair/img/food-drink.pretzel.webp create mode 100644 public/flair/img/food-drink.red-apple.webp create mode 100644 public/flair/img/food-drink.rice-ball.webp create mode 100644 public/flair/img/food-drink.rice-cracker.webp create mode 100644 public/flair/img/food-drink.roasted-sweet-potato.webp create mode 100644 public/flair/img/food-drink.sake.webp create mode 100644 public/flair/img/food-drink.salt.webp create mode 100644 public/flair/img/food-drink.sandwich.webp create mode 100644 public/flair/img/food-drink.shallow-pan-of-food.webp create mode 100644 public/flair/img/food-drink.shaved-ice.webp create mode 100644 public/flair/img/food-drink.shortcake.webp create mode 100644 public/flair/img/food-drink.soft-ice-cream.webp create mode 100644 public/flair/img/food-drink.spaghetti.webp create mode 100644 public/flair/img/food-drink.spoon.webp create mode 100644 public/flair/img/food-drink.steaming-bowl.webp create mode 100644 public/flair/img/food-drink.strawberry.webp create mode 100644 public/flair/img/food-drink.stuffed-flatbread.webp create mode 100644 public/flair/img/food-drink.sushi.webp create mode 100644 public/flair/img/food-drink.taco.webp create mode 100644 public/flair/img/food-drink.takeout-box.webp create mode 100644 public/flair/img/food-drink.tamale.webp create mode 100644 public/flair/img/food-drink.tangerine.webp create mode 100644 public/flair/img/food-drink.teacup-without-handle.webp create mode 100644 public/flair/img/food-drink.teapot.webp create mode 100644 public/flair/img/food-drink.tomato.webp create mode 100644 public/flair/img/food-drink.tropical-drink.webp create mode 100644 public/flair/img/food-drink.tumbler-glass.webp create mode 100644 public/flair/img/food-drink.waffle.webp create mode 100644 public/flair/img/food-drink.watermelon.webp create mode 100644 public/flair/img/food-drink.wine-glass.webp create mode 100644 public/flair/img/nature.ant.webp create mode 100644 public/flair/img/nature.baby-chick.webp create mode 100644 public/flair/img/nature.badger.webp create mode 100644 public/flair/img/nature.bat.webp create mode 100644 public/flair/img/nature.bear.webp create mode 100644 public/flair/img/nature.beaver.webp create mode 100644 public/flair/img/nature.beetle.webp create mode 100644 public/flair/img/nature.bird.webp create mode 100644 public/flair/img/nature.bison.webp create mode 100644 public/flair/img/nature.black-bird.webp create mode 100644 public/flair/img/nature.black-cat.webp create mode 100644 public/flair/img/nature.blossom.webp create mode 100644 public/flair/img/nature.blowfish.webp create mode 100644 public/flair/img/nature.boar.webp create mode 100644 public/flair/img/nature.bouquet.webp create mode 100644 public/flair/img/nature.bug.webp create mode 100644 public/flair/img/nature.butterfly.webp create mode 100644 public/flair/img/nature.cactus.webp create mode 100644 public/flair/img/nature.camel.webp create mode 100644 public/flair/img/nature.cat-face.webp create mode 100644 public/flair/img/nature.cat.webp create mode 100644 public/flair/img/nature.cherry-blossom.webp create mode 100644 public/flair/img/nature.chicken.webp create mode 100644 public/flair/img/nature.chipmunk.webp create mode 100644 public/flair/img/nature.closed-umbrella.webp create mode 100644 public/flair/img/nature.cloud-with-lightning-and-rain.webp create mode 100644 public/flair/img/nature.cloud-with-lightning.webp create mode 100644 public/flair/img/nature.cloud-with-rain.webp create mode 100644 public/flair/img/nature.cloud-with-snow.webp create mode 100644 public/flair/img/nature.cloud.webp create mode 100644 public/flair/img/nature.cockroach.webp create mode 100644 public/flair/img/nature.comet.webp create mode 100644 public/flair/img/nature.coral.webp create mode 100644 public/flair/img/nature.cow-face.webp create mode 100644 public/flair/img/nature.cow.webp create mode 100644 public/flair/img/nature.crab.webp create mode 100644 public/flair/img/nature.crescent-moon.webp create mode 100644 public/flair/img/nature.cricket.webp create mode 100644 public/flair/img/nature.crocodile.webp create mode 100644 public/flair/img/nature.cyclone.webp create mode 100644 public/flair/img/nature.deciduous-tree.webp create mode 100644 public/flair/img/nature.deer.webp create mode 100644 public/flair/img/nature.dodo.webp create mode 100644 public/flair/img/nature.dog-face.webp create mode 100644 public/flair/img/nature.dog.webp create mode 100644 public/flair/img/nature.dolphin.webp create mode 100644 public/flair/img/nature.donkey.webp create mode 100644 public/flair/img/nature.dove.webp create mode 100644 public/flair/img/nature.dragon-face.webp create mode 100644 public/flair/img/nature.dragon.webp create mode 100644 public/flair/img/nature.droplet.webp create mode 100644 public/flair/img/nature.duck.webp create mode 100644 public/flair/img/nature.eagle.webp create mode 100644 public/flair/img/nature.elephant.webp create mode 100644 public/flair/img/nature.empty-nest.webp create mode 100644 public/flair/img/nature.evergreen-tree.webp create mode 100644 public/flair/img/nature.ewe.webp create mode 100644 public/flair/img/nature.fallen-leaf.webp create mode 100644 public/flair/img/nature.feather.webp create mode 100644 public/flair/img/nature.fire.webp create mode 100644 public/flair/img/nature.first-quarter-moon-face.webp create mode 100644 public/flair/img/nature.first-quarter-moon.webp create mode 100644 public/flair/img/nature.fish.webp create mode 100644 public/flair/img/nature.flamingo.webp create mode 100644 public/flair/img/nature.fly.webp create mode 100644 public/flair/img/nature.fog.webp create mode 100644 public/flair/img/nature.four-leaf-clover.webp create mode 100644 public/flair/img/nature.fox.webp create mode 100644 public/flair/img/nature.frog.webp create mode 100644 public/flair/img/nature.front-facing-baby-chick.webp create mode 100644 public/flair/img/nature.full-moon-face.webp create mode 100644 public/flair/img/nature.full-moon.webp create mode 100644 public/flair/img/nature.giraffe.webp create mode 100644 public/flair/img/nature.glowing-star.webp create mode 100644 public/flair/img/nature.goat.webp create mode 100644 public/flair/img/nature.goose.webp create mode 100644 public/flair/img/nature.gorilla.webp create mode 100644 public/flair/img/nature.guide-dog.webp create mode 100644 public/flair/img/nature.hamster.webp create mode 100644 public/flair/img/nature.hatching-chick.webp create mode 100644 public/flair/img/nature.hedgehog.webp create mode 100644 public/flair/img/nature.herb.webp create mode 100644 public/flair/img/nature.hibiscus.webp create mode 100644 public/flair/img/nature.high-voltage.webp create mode 100644 public/flair/img/nature.hippopotamus.webp create mode 100644 public/flair/img/nature.honeybee.webp create mode 100644 public/flair/img/nature.horse-face.webp create mode 100644 public/flair/img/nature.horse.webp create mode 100644 public/flair/img/nature.hyacinth.webp create mode 100644 public/flair/img/nature.jellyfish.webp create mode 100644 public/flair/img/nature.kangaroo.webp create mode 100644 public/flair/img/nature.koala.webp create mode 100644 public/flair/img/nature.lady-beetle.webp create mode 100644 public/flair/img/nature.last-quarter-moon-face.webp create mode 100644 public/flair/img/nature.last-quarter-moon.webp create mode 100644 public/flair/img/nature.leaf-fluttering-in-wind.webp create mode 100644 public/flair/img/nature.leopard.webp create mode 100644 public/flair/img/nature.lion.webp create mode 100644 public/flair/img/nature.lizard.webp create mode 100644 public/flair/img/nature.llama.webp create mode 100644 public/flair/img/nature.lobster.webp create mode 100644 public/flair/img/nature.lotus.webp create mode 100644 public/flair/img/nature.mammoth.webp create mode 100644 public/flair/img/nature.maple-leaf.webp create mode 100644 public/flair/img/nature.microbe.webp create mode 100644 public/flair/img/nature.milky-way.webp create mode 100644 public/flair/img/nature.monkey-face.webp create mode 100644 public/flair/img/nature.monkey.webp create mode 100644 public/flair/img/nature.moose.webp create mode 100644 public/flair/img/nature.mosquito.webp create mode 100644 public/flair/img/nature.mouse-face.webp create mode 100644 public/flair/img/nature.mouse.webp create mode 100644 public/flair/img/nature.mushroom.webp create mode 100644 public/flair/img/nature.nest-with-eggs.webp create mode 100644 public/flair/img/nature.new-moon-face.webp create mode 100644 public/flair/img/nature.new-moon.webp create mode 100644 public/flair/img/nature.octopus-howard.webp create mode 100644 public/flair/img/nature.octopus.webp create mode 100644 public/flair/img/nature.orangutan.webp create mode 100644 public/flair/img/nature.otter.webp create mode 100644 public/flair/img/nature.owl.webp create mode 100644 public/flair/img/nature.ox.webp create mode 100644 public/flair/img/nature.oyster.webp create mode 100644 public/flair/img/nature.palm-tree.webp create mode 100644 public/flair/img/nature.panda.webp create mode 100644 public/flair/img/nature.parrot.webp create mode 100644 public/flair/img/nature.paw-prints.webp create mode 100644 public/flair/img/nature.peacock.webp create mode 100644 public/flair/img/nature.penguin.webp create mode 100644 public/flair/img/nature.phoenix-bird.webp create mode 100644 public/flair/img/nature.pig-face.webp create mode 100644 public/flair/img/nature.pig-nose.webp create mode 100644 public/flair/img/nature.pig.webp create mode 100644 public/flair/img/nature.polar-bear.webp create mode 100644 public/flair/img/nature.poodle.webp create mode 100644 public/flair/img/nature.potted-plant.webp create mode 100644 public/flair/img/nature.rabbit-face.webp create mode 100644 public/flair/img/nature.rabbit.webp create mode 100644 public/flair/img/nature.raccoon.webp create mode 100644 public/flair/img/nature.rainbow.webp create mode 100644 public/flair/img/nature.ram.webp create mode 100644 public/flair/img/nature.rat.webp create mode 100644 public/flair/img/nature.rhinoceros.webp create mode 100644 public/flair/img/nature.ringed-planet.webp create mode 100644 public/flair/img/nature.rock.webp create mode 100644 public/flair/img/nature.rooster.webp create mode 100644 public/flair/img/nature.rose.webp create mode 100644 public/flair/img/nature.rosette.webp create mode 100644 public/flair/img/nature.rubber-duck.webp create mode 100644 public/flair/img/nature.sauropod.webp create mode 100644 public/flair/img/nature.scorpion.webp create mode 100644 public/flair/img/nature.seal.webp create mode 100644 public/flair/img/nature.seedling.webp create mode 100644 public/flair/img/nature.service-dog.webp create mode 100644 public/flair/img/nature.shamrock.webp create mode 100644 public/flair/img/nature.shark.webp create mode 100644 public/flair/img/nature.sheaf-of-rice.webp create mode 100644 public/flair/img/nature.shooting-star.webp create mode 100644 public/flair/img/nature.shrimp.webp create mode 100644 public/flair/img/nature.skunk.webp create mode 100644 public/flair/img/nature.sloth.webp create mode 100644 public/flair/img/nature.snail.webp create mode 100644 public/flair/img/nature.snake.webp create mode 100644 public/flair/img/nature.snowflake.webp create mode 100644 public/flair/img/nature.snowman-without-snow.webp create mode 100644 public/flair/img/nature.snowman.webp create mode 100644 public/flair/img/nature.spider-web.webp create mode 100644 public/flair/img/nature.spider.webp create mode 100644 public/flair/img/nature.spiral-shell.webp create mode 100644 public/flair/img/nature.spouting-whale.webp create mode 100644 public/flair/img/nature.squid.webp create mode 100644 public/flair/img/nature.star.webp create mode 100644 public/flair/img/nature.sun-behind-cloud.webp create mode 100644 public/flair/img/nature.sun-behind-large-cloud.webp create mode 100644 public/flair/img/nature.sun-behind-rain-cloud.webp create mode 100644 public/flair/img/nature.sun-behind-small-cloud.webp create mode 100644 public/flair/img/nature.sun-with-face.webp create mode 100644 public/flair/img/nature.sun.webp create mode 100644 public/flair/img/nature.sunflower.webp create mode 100644 public/flair/img/nature.swan.webp create mode 100644 public/flair/img/nature.t-rex.webp create mode 100644 public/flair/img/nature.tiger-face.webp create mode 100644 public/flair/img/nature.tiger.webp create mode 100644 public/flair/img/nature.tornado.webp create mode 100644 public/flair/img/nature.tropical-fish.webp create mode 100644 public/flair/img/nature.tulip.webp create mode 100644 public/flair/img/nature.turkey.webp create mode 100644 public/flair/img/nature.turtle.webp create mode 100644 public/flair/img/nature.two-hump-camel.webp create mode 100644 public/flair/img/nature.umbrella-on-ground.webp create mode 100644 public/flair/img/nature.umbrella-with-rain-drops.webp create mode 100644 public/flair/img/nature.umbrella.webp create mode 100644 public/flair/img/nature.unicorn.webp create mode 100644 public/flair/img/nature.waning-crescent-moon.webp create mode 100644 public/flair/img/nature.waning-gibbous-moon.webp create mode 100644 public/flair/img/nature.water-buffalo.webp create mode 100644 public/flair/img/nature.water-wave.webp create mode 100644 public/flair/img/nature.waxing-crescent-moon.webp create mode 100644 public/flair/img/nature.waxing-gibbous-moon.webp create mode 100644 public/flair/img/nature.whale.webp create mode 100644 public/flair/img/nature.white-flower.webp create mode 100644 public/flair/img/nature.wilted-flower.webp create mode 100644 public/flair/img/nature.wind-face.webp create mode 100644 public/flair/img/nature.wing.webp create mode 100644 public/flair/img/nature.wolf.webp create mode 100644 public/flair/img/nature.wood.webp create mode 100644 public/flair/img/nature.worm.webp create mode 100644 public/flair/img/nature.xmas-tree.webp create mode 100644 public/flair/img/nature.zebra.webp create mode 100644 public/flair/img/objects.abacus.webp create mode 100644 public/flair/img/objects.accordion.webp create mode 100644 public/flair/img/objects.adhesive-bandage.webp create mode 100644 public/flair/img/objects.alarm-clock.webp create mode 100644 public/flair/img/objects.alembic.webp create mode 100644 public/flair/img/objects.backpack.webp create mode 100644 public/flair/img/objects.balance-scale.webp create mode 100644 public/flair/img/objects.ballet-shoes.webp create mode 100644 public/flair/img/objects.ballot-box-with-ballot.webp create mode 100644 public/flair/img/objects.banjo.webp create mode 100644 public/flair/img/objects.bar-chart.webp create mode 100644 public/flair/img/objects.basket.webp create mode 100644 public/flair/img/objects.bathtub.webp create mode 100644 public/flair/img/objects.battery.webp create mode 100644 public/flair/img/objects.bed.webp create mode 100644 public/flair/img/objects.bell-with-slash.webp create mode 100644 public/flair/img/objects.bell.webp create mode 100644 public/flair/img/objects.bellhop-bell.webp create mode 100644 public/flair/img/objects.bikini.webp create mode 100644 public/flair/img/objects.billed-cap.webp create mode 100644 public/flair/img/objects.black-nib.webp create mode 100644 public/flair/img/objects.blue-book.webp create mode 100644 public/flair/img/objects.bookmark-tabs.webp create mode 100644 public/flair/img/objects.bookmark.webp create mode 100644 public/flair/img/objects.books.webp create mode 100644 public/flair/img/objects.boomerang.webp create mode 100644 public/flair/img/objects.bow-and-arrow.webp create mode 100644 public/flair/img/objects.briefcase.webp create mode 100644 public/flair/img/objects.briefs.webp create mode 100644 public/flair/img/objects.broken-chain.webp create mode 100644 public/flair/img/objects.broom.webp create mode 100644 public/flair/img/objects.bubbles.webp create mode 100644 public/flair/img/objects.bucket.webp create mode 100644 public/flair/img/objects.calendar.webp create mode 100644 public/flair/img/objects.camera-with-flash.webp create mode 100644 public/flair/img/objects.camera.webp create mode 100644 public/flair/img/objects.candle.webp create mode 100644 public/flair/img/objects.card-file-box.webp create mode 100644 public/flair/img/objects.card-index-dividers.webp create mode 100644 public/flair/img/objects.card-index.webp create mode 100644 public/flair/img/objects.carpentry-saw.webp create mode 100644 public/flair/img/objects.chains.webp create mode 100644 public/flair/img/objects.chair.webp create mode 100644 public/flair/img/objects.chart-decreasing.webp create mode 100644 public/flair/img/objects.chart-increasing-with-yen.webp create mode 100644 public/flair/img/objects.chart-increasing.webp create mode 100644 public/flair/img/objects.cigarette.webp create mode 100644 public/flair/img/objects.clamp.webp create mode 100644 public/flair/img/objects.clapper-board.webp create mode 100644 public/flair/img/objects.clipboard.webp create mode 100644 public/flair/img/objects.closed-book.webp create mode 100644 public/flair/img/objects.closed-mailbox-with-lowered-flag.webp create mode 100644 public/flair/img/objects.closed-mailbox-with-raised-flag.webp create mode 100644 public/flair/img/objects.clutch-bag.webp create mode 100644 public/flair/img/objects.coat.webp create mode 100644 public/flair/img/objects.coffin.webp create mode 100644 public/flair/img/objects.coin.webp create mode 100644 public/flair/img/objects.computer-disk.webp create mode 100644 public/flair/img/objects.computer-mouse.webp create mode 100644 public/flair/img/objects.control-knobs.webp create mode 100644 public/flair/img/objects.couch-and-lamp.webp create mode 100644 public/flair/img/objects.crayon.webp create mode 100644 public/flair/img/objects.credit-card.webp create mode 100644 public/flair/img/objects.crossed-swords.webp create mode 100644 public/flair/img/objects.crown.webp create mode 100644 public/flair/img/objects.crutch.webp create mode 100644 public/flair/img/objects.desktop-computer.webp create mode 100644 public/flair/img/objects.diya-lamp.webp create mode 100644 public/flair/img/objects.dollar-banknote.webp create mode 100644 public/flair/img/objects.door.webp create mode 100644 public/flair/img/objects.dress.webp create mode 100644 public/flair/img/objects.drum.webp create mode 100644 public/flair/img/objects.dvd.webp create mode 100644 public/flair/img/objects.e-mail.webp create mode 100644 public/flair/img/objects.eight-oclock.webp create mode 100644 public/flair/img/objects.eight-thirty.webp create mode 100644 public/flair/img/objects.electric-plug.webp create mode 100644 public/flair/img/objects.eleven-oclock.webp create mode 100644 public/flair/img/objects.eleven-thirty.webp create mode 100644 public/flair/img/objects.envelope-with-arrow.webp create mode 100644 public/flair/img/objects.envelope.webp create mode 100644 public/flair/img/objects.euro-banknote.webp create mode 100644 public/flair/img/objects.fax-machine.webp create mode 100644 public/flair/img/objects.file-cabinet.webp create mode 100644 public/flair/img/objects.file-folder.webp create mode 100644 public/flair/img/objects.film-frames.webp create mode 100644 public/flair/img/objects.film-projector.webp create mode 100644 public/flair/img/objects.fire-extinguisher.webp create mode 100644 public/flair/img/objects.five-oclock.webp create mode 100644 public/flair/img/objects.five-thirty.webp create mode 100644 public/flair/img/objects.flashlight.webp create mode 100644 public/flair/img/objects.flat-shoe.webp create mode 100644 public/flair/img/objects.floppy-disk.webp create mode 100644 public/flair/img/objects.flute.webp create mode 100644 public/flair/img/objects.folding-hand-fan.webp create mode 100644 public/flair/img/objects.fountain-pen.webp create mode 100644 public/flair/img/objects.four-oclock.webp create mode 100644 public/flair/img/objects.four-thirty.webp create mode 100644 public/flair/img/objects.funeral-urn.webp create mode 100644 public/flair/img/objects.gear.webp create mode 100644 public/flair/img/objects.gem-stone.webp create mode 100644 public/flair/img/objects.glasses.webp create mode 100644 public/flair/img/objects.gloves.webp create mode 100644 public/flair/img/objects.goggles.webp create mode 100644 public/flair/img/objects.graduation-cap.webp create mode 100644 public/flair/img/objects.green-book.webp create mode 100644 public/flair/img/objects.guitar.webp create mode 100644 public/flair/img/objects.hair-pick.webp create mode 100644 public/flair/img/objects.hammer-and-pick.webp create mode 100644 public/flair/img/objects.hammer-and-wrench.webp create mode 100644 public/flair/img/objects.hammer.webp create mode 100644 public/flair/img/objects.hamsa.webp create mode 100644 public/flair/img/objects.handbag.webp create mode 100644 public/flair/img/objects.headphone.webp create mode 100644 public/flair/img/objects.headstone.webp create mode 100644 public/flair/img/objects.high-heeled-shoe.webp create mode 100644 public/flair/img/objects.hiking-boot.webp create mode 100644 public/flair/img/objects.hook.webp create mode 100644 public/flair/img/objects.hourglass-done.webp create mode 100644 public/flair/img/objects.hourglass-not-done.webp create mode 100644 public/flair/img/objects.identification-card.webp create mode 100644 public/flair/img/objects.inbox-tray.webp create mode 100644 public/flair/img/objects.incoming-envelope.webp create mode 100644 public/flair/img/objects.jeans.webp create mode 100644 public/flair/img/objects.key.webp create mode 100644 public/flair/img/objects.keyboard.webp create mode 100644 public/flair/img/objects.kimono.webp create mode 100644 public/flair/img/objects.knot.webp create mode 100644 public/flair/img/objects.lab-coat.webp create mode 100644 public/flair/img/objects.label.webp create mode 100644 public/flair/img/objects.ladder.webp create mode 100644 public/flair/img/objects.laptop.webp create mode 100644 public/flair/img/objects.ledger.webp create mode 100644 public/flair/img/objects.level-slider.webp create mode 100644 public/flair/img/objects.light-bulb.webp create mode 100644 public/flair/img/objects.link.webp create mode 100644 public/flair/img/objects.linked-paperclips.webp create mode 100644 public/flair/img/objects.lipstick.webp create mode 100644 public/flair/img/objects.locked-with-key.webp create mode 100644 public/flair/img/objects.locked-with-pen.webp create mode 100644 public/flair/img/objects.locked.webp create mode 100644 public/flair/img/objects.long-drum.webp create mode 100644 public/flair/img/objects.lotion-bottle.webp create mode 100644 public/flair/img/objects.loudspeaker.webp create mode 100644 public/flair/img/objects.low-battery.webp create mode 100644 public/flair/img/objects.luggage.webp create mode 100644 public/flair/img/objects.magnet.webp create mode 100644 public/flair/img/objects.magnifying-glass-tilted-left.webp create mode 100644 public/flair/img/objects.magnifying-glass-tilted-right.webp create mode 100644 public/flair/img/objects.mans-shoe.webp create mode 100644 public/flair/img/objects.mantelpiece-clock.webp create mode 100644 public/flair/img/objects.maracas.webp create mode 100644 public/flair/img/objects.megaphone.webp create mode 100644 public/flair/img/objects.memo.webp create mode 100644 public/flair/img/objects.microphone.webp create mode 100644 public/flair/img/objects.microscope.webp create mode 100644 public/flair/img/objects.mirror.webp create mode 100644 public/flair/img/objects.mobile-phone-with-arrow.webp create mode 100644 public/flair/img/objects.mobile-phone.webp create mode 100644 public/flair/img/objects.money-bag.webp create mode 100644 public/flair/img/objects.money-with-wings.webp create mode 100644 public/flair/img/objects.mouse-trap.webp create mode 100644 public/flair/img/objects.movie-camera.webp create mode 100644 public/flair/img/objects.musical-keyboard.webp create mode 100644 public/flair/img/objects.musical-note.webp create mode 100644 public/flair/img/objects.musical-notes.webp create mode 100644 public/flair/img/objects.musical-score.webp create mode 100644 public/flair/img/objects.muted-speaker.webp create mode 100644 public/flair/img/objects.nazar-amulet.webp create mode 100644 public/flair/img/objects.necktie.webp create mode 100644 public/flair/img/objects.newspaper.webp create mode 100644 public/flair/img/objects.nine-oclock.webp create mode 100644 public/flair/img/objects.nine-thirty.webp create mode 100644 public/flair/img/objects.notebook-with-decorative-cover.webp create mode 100644 public/flair/img/objects.notebook.webp create mode 100644 public/flair/img/objects.nut-and-bolt.webp create mode 100644 public/flair/img/objects.old-key.webp create mode 100644 public/flair/img/objects.one-oclock.webp create mode 100644 public/flair/img/objects.one-piece-swimsuit.webp create mode 100644 public/flair/img/objects.one-thirty.webp create mode 100644 public/flair/img/objects.open-book.webp create mode 100644 public/flair/img/objects.open-file-folder.webp create mode 100644 public/flair/img/objects.open-mailbox-with-lowered-flag.webp create mode 100644 public/flair/img/objects.open-mailbox-with-raised-flag.webp create mode 100644 public/flair/img/objects.optical-disk.webp create mode 100644 public/flair/img/objects.orange-book.webp create mode 100644 public/flair/img/objects.outbox-tray.webp create mode 100644 public/flair/img/objects.package.webp create mode 100644 public/flair/img/objects.page-facing-up.webp create mode 100644 public/flair/img/objects.page-with-curl.webp create mode 100644 public/flair/img/objects.pager.webp create mode 100644 public/flair/img/objects.paintbrush.webp create mode 100644 public/flair/img/objects.paperclip.webp create mode 100644 public/flair/img/objects.pen.webp create mode 100644 public/flair/img/objects.pencil.webp create mode 100644 public/flair/img/objects.petri-dish.webp create mode 100644 public/flair/img/objects.pick.webp create mode 100644 public/flair/img/objects.pill.webp create mode 100644 public/flair/img/objects.placard.webp create mode 100644 public/flair/img/objects.plunger.webp create mode 100644 public/flair/img/objects.postal-horn.webp create mode 100644 public/flair/img/objects.postbox.webp create mode 100644 public/flair/img/objects.pound-banknote.webp create mode 100644 public/flair/img/objects.prayer-beads.webp create mode 100644 public/flair/img/objects.printer.webp create mode 100644 public/flair/img/objects.purse.webp create mode 100644 public/flair/img/objects.pushpin.webp create mode 100644 public/flair/img/objects.radio.webp create mode 100644 public/flair/img/objects.razor.webp create mode 100644 public/flair/img/objects.receipt.webp create mode 100644 public/flair/img/objects.red-paper-lantern.webp create mode 100644 public/flair/img/objects.reminder-ribbon.webp create mode 100644 public/flair/img/objects.rescue-workers-helmet.webp create mode 100644 public/flair/img/objects.ribbon.webp create mode 100644 public/flair/img/objects.ring.webp create mode 100644 public/flair/img/objects.roll-of-paper.webp create mode 100644 public/flair/img/objects.rolled-up-newspaper.webp create mode 100644 public/flair/img/objects.round-pushpin.webp create mode 100644 public/flair/img/objects.running-shoe.webp create mode 100644 public/flair/img/objects.safety-pin.webp create mode 100644 public/flair/img/objects.safety-vest.webp create mode 100644 public/flair/img/objects.sari.webp create mode 100644 public/flair/img/objects.satellite-antenna.webp create mode 100644 public/flair/img/objects.saxophone.webp create mode 100644 public/flair/img/objects.scarf.webp create mode 100644 public/flair/img/objects.scissors.webp create mode 100644 public/flair/img/objects.screwdriver.webp create mode 100644 public/flair/img/objects.scroll.webp create mode 100644 public/flair/img/objects.seven-oclock.webp create mode 100644 public/flair/img/objects.seven-thirty.webp create mode 100644 public/flair/img/objects.sewing-needle.webp create mode 100644 public/flair/img/objects.shield.webp create mode 100644 public/flair/img/objects.shopping-bags.webp create mode 100644 public/flair/img/objects.shopping-cart.webp create mode 100644 public/flair/img/objects.shorts.webp create mode 100644 public/flair/img/objects.shower.webp create mode 100644 public/flair/img/objects.six-oclock.webp create mode 100644 public/flair/img/objects.six-thirty.webp create mode 100644 public/flair/img/objects.soap.webp create mode 100644 public/flair/img/objects.socks.webp create mode 100644 public/flair/img/objects.speaker-high-volume.webp create mode 100644 public/flair/img/objects.speaker-low-volume.webp create mode 100644 public/flair/img/objects.speaker-medium-volume.webp create mode 100644 public/flair/img/objects.spiral-calendar.webp create mode 100644 public/flair/img/objects.spiral-notepad.webp create mode 100644 public/flair/img/objects.sponge.webp create mode 100644 public/flair/img/objects.stethoscope.webp create mode 100644 public/flair/img/objects.stopwatch.webp create mode 100644 public/flair/img/objects.straight-ruler.webp create mode 100644 public/flair/img/objects.studio-microphone.webp create mode 100644 public/flair/img/objects.sunglasses.webp create mode 100644 public/flair/img/objects.syringe.webp create mode 100644 public/flair/img/objects.t-shirt.webp create mode 100644 public/flair/img/objects.tear-off-calendar.webp create mode 100644 public/flair/img/objects.teddy-bear.webp create mode 100644 public/flair/img/objects.telephone-receiver.webp create mode 100644 public/flair/img/objects.telephone.webp create mode 100644 public/flair/img/objects.telescope.webp create mode 100644 public/flair/img/objects.television.webp create mode 100644 public/flair/img/objects.ten-oclock.webp create mode 100644 public/flair/img/objects.ten-thirty.webp create mode 100644 public/flair/img/objects.test-tube.webp create mode 100644 public/flair/img/objects.thermometer.webp create mode 100644 public/flair/img/objects.thong-sandal.webp create mode 100644 public/flair/img/objects.thread.webp create mode 100644 public/flair/img/objects.three-oclock.webp create mode 100644 public/flair/img/objects.three-thirty.webp create mode 100644 public/flair/img/objects.timer-clock.webp create mode 100644 public/flair/img/objects.toilet.webp create mode 100644 public/flair/img/objects.toolbox.webp create mode 100644 public/flair/img/objects.toothbrush.webp create mode 100644 public/flair/img/objects.top-hat.webp create mode 100644 public/flair/img/objects.trackball.webp create mode 100644 public/flair/img/objects.triangular-ruler.webp create mode 100644 public/flair/img/objects.trumpet.webp create mode 100644 public/flair/img/objects.twelve-oclock.webp create mode 100644 public/flair/img/objects.twelve-thirty.webp create mode 100644 public/flair/img/objects.two-oclocktime.webp create mode 100644 public/flair/img/objects.two-thirty.webp create mode 100644 public/flair/img/objects.unlocked.webp create mode 100644 public/flair/img/objects.video-camera.webp create mode 100644 public/flair/img/objects.videocassette.webp create mode 100644 public/flair/img/objects.violin.webp create mode 100644 public/flair/img/objects.wastebasket.webp create mode 100644 public/flair/img/objects.watch.webp create mode 100644 public/flair/img/objects.white-cane.webp create mode 100644 public/flair/img/objects.window.webp create mode 100644 public/flair/img/objects.womans-boot.webp create mode 100644 public/flair/img/objects.womans-clothes.webp create mode 100644 public/flair/img/objects.womans-hat.webp create mode 100644 public/flair/img/objects.womans-sandal.webp create mode 100644 public/flair/img/objects.wrench.webp create mode 100644 public/flair/img/objects.x-ray.webp create mode 100644 public/flair/img/objects.xmas-bell.webp create mode 100644 public/flair/img/objects.xmas-candle.webp create mode 100644 public/flair/img/objects.xmas-hat.webp create mode 100644 public/flair/img/objects.yarn.webp create mode 100644 public/flair/img/objects.yen-banknote.webp create mode 100644 public/flair/img/people.anatomical-heart.webp create mode 100644 public/flair/img/people.artist-dark-skin-tone.webp create mode 100644 public/flair/img/people.artist-light-skin-tone.webp create mode 100644 public/flair/img/people.artist-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.artist-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.artist-medium-skin-tone.webp create mode 100644 public/flair/img/people.artist.webp create mode 100644 public/flair/img/people.astronaut-dark-skin-tone.webp create mode 100644 public/flair/img/people.astronaut-light-skin-tone.webp create mode 100644 public/flair/img/people.astronaut-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.astronaut-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.astronaut-medium-skin-tone.webp create mode 100644 public/flair/img/people.astronaut.webp create mode 100644 public/flair/img/people.baby-angel-dark-skin-tone.webp create mode 100644 public/flair/img/people.baby-angel-light-skin-tone.webp create mode 100644 public/flair/img/people.baby-angel-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.baby-angel-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.baby-angel-medium-skin-tone.webp create mode 100644 public/flair/img/people.baby-angel.webp create mode 100644 public/flair/img/people.baby-dark-skin-tone.webp create mode 100644 public/flair/img/people.baby-light-skin-tone.webp create mode 100644 public/flair/img/people.baby-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.baby-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.baby-medium-skin-tone.webp create mode 100644 public/flair/img/people.baby.webp create mode 100644 public/flair/img/people.backhand-index-pointing-down-dark-skin-tone.webp create mode 100644 public/flair/img/people.backhand-index-pointing-down-light-skin-tone.webp create mode 100644 public/flair/img/people.backhand-index-pointing-down-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.backhand-index-pointing-down-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.backhand-index-pointing-down-medium-skin-tone.webp create mode 100644 public/flair/img/people.backhand-index-pointing-down.webp create mode 100644 public/flair/img/people.backhand-index-pointing-left-dark-skin-tone.webp create mode 100644 public/flair/img/people.backhand-index-pointing-left-light-skin-tone.webp create mode 100644 public/flair/img/people.backhand-index-pointing-left-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.backhand-index-pointing-left-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.backhand-index-pointing-left-medium-skin-tone.webp create mode 100644 public/flair/img/people.backhand-index-pointing-left.webp create mode 100644 public/flair/img/people.backhand-index-pointing-right-dark-skin-tone.webp create mode 100644 public/flair/img/people.backhand-index-pointing-right-light-skin-tone.webp create mode 100644 public/flair/img/people.backhand-index-pointing-right-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.backhand-index-pointing-right-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.backhand-index-pointing-right-medium-skin-tone.webp create mode 100644 public/flair/img/people.backhand-index-pointing-right.webp create mode 100644 public/flair/img/people.backhand-index-pointing-up-dark-skin-tone.webp create mode 100644 public/flair/img/people.backhand-index-pointing-up-light-skin-tone.webp create mode 100644 public/flair/img/people.backhand-index-pointing-up-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.backhand-index-pointing-up-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.backhand-index-pointing-up-medium-skin-tone.webp create mode 100644 public/flair/img/people.backhand-index-pointing-up.webp create mode 100644 public/flair/img/people.bald.webp create mode 100644 public/flair/img/people.biting-lip.webp create mode 100644 public/flair/img/people.bone.webp create mode 100644 public/flair/img/people.boy-dark-skin-tone.webp create mode 100644 public/flair/img/people.boy-light-skin-tone.webp create mode 100644 public/flair/img/people.boy-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.boy-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.boy-medium-skin-tone.webp create mode 100644 public/flair/img/people.boy.webp create mode 100644 public/flair/img/people.brain.webp create mode 100644 public/flair/img/people.breast-feeding-dark-skin-tone.webp create mode 100644 public/flair/img/people.breast-feeding-light-skin-tone.webp create mode 100644 public/flair/img/people.breast-feeding-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.breast-feeding-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.breast-feeding-medium-skin-tone.webp create mode 100644 public/flair/img/people.breast-feeding.webp create mode 100644 public/flair/img/people.bust-in-silhouette.webp create mode 100644 public/flair/img/people.busts-in-silhouette.webp create mode 100644 public/flair/img/people.call-me-hand-dark-skin-tone.webp create mode 100644 public/flair/img/people.call-me-hand-light-skin-tone.webp create mode 100644 public/flair/img/people.call-me-hand-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.call-me-hand-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.call-me-hand-medium-skin-tone.webp create mode 100644 public/flair/img/people.call-me-hand.webp create mode 100644 public/flair/img/people.child-dark-skin-tone.webp create mode 100644 public/flair/img/people.child-light-skin-tone.webp create mode 100644 public/flair/img/people.child-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.child-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.child-medium-skin-tone.webp create mode 100644 public/flair/img/people.child.webp create mode 100644 public/flair/img/people.clapping-hands-dark-skin-tone.webp create mode 100644 public/flair/img/people.clapping-hands-light-skin-tone.webp create mode 100644 public/flair/img/people.clapping-hands-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.clapping-hands-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.clapping-hands-medium-skin-tone.webp create mode 100644 public/flair/img/people.clapping-hands.webp create mode 100644 public/flair/img/people.construction-worker-dark-skin-tone.webp create mode 100644 public/flair/img/people.construction-worker-light-skin-tone.webp create mode 100644 public/flair/img/people.construction-worker-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.construction-worker-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.construction-worker-medium-skin-tone.webp create mode 100644 public/flair/img/people.construction-worker.webp create mode 100644 public/flair/img/people.cook-dark-skin-tone.webp create mode 100644 public/flair/img/people.cook-light-skin-tone.webp create mode 100644 public/flair/img/people.cook-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.cook-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.cook-medium-skin-tone.webp create mode 100644 public/flair/img/people.cook.webp create mode 100644 public/flair/img/people.couple-with-heart-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-dark-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-dark-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-dark-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-dark-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-light-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-light-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-light-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-light-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-medium-dark-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-medium-dark-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-medium-dark-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-medium-dark-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-medium-light-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-medium-light-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-medium-light-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-medium-light-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-medium-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-medium-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-medium-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-medium-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man-medium-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-man-man.webp create mode 100644 public/flair/img/people.couple-with-heart-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-medium-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-person-person-dark-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-person-person-dark-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-person-person-dark-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-person-person-dark-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-person-person-light-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-person-person-light-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-person-person-light-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-person-person-light-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-person-person-medium-dark-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-person-person-medium-dark-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-person-person-medium-dark-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-person-person-medium-dark-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-person-person-medium-light-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-person-person-medium-light-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-person-person-medium-light-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-person-person-medium-light-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-person-person-medium-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-person-person-medium-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-person-person-medium-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-person-person-medium-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-dark-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-dark-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-dark-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-dark-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-light-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-light-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-light-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-light-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-medium-dark-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-medium-dark-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-medium-dark-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-medium-dark-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-medium-light-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-medium-light-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-medium-light-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-medium-light-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-medium-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-medium-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-medium-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-medium-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man-medium-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-man.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-dark-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-dark-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-dark-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-dark-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-light-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-light-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-light-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-light-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-medium-dark-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-medium-dark-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-medium-dark-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-medium-dark-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-medium-light-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-medium-light-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-medium-light-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-medium-light-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-medium-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-medium-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-medium-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-medium-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman-medium-skin-tone.webp create mode 100644 public/flair/img/people.couple-with-heart-woman-woman.webp create mode 100644 public/flair/img/people.couple-with-heart.webp create mode 100644 public/flair/img/people.crossed-fingers-dark-skin-tone.webp create mode 100644 public/flair/img/people.crossed-fingers-light-skin-tone.webp create mode 100644 public/flair/img/people.crossed-fingers-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.crossed-fingers-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.crossed-fingers-medium-skin-tone.webp create mode 100644 public/flair/img/people.crossed-fingers.webp create mode 100644 public/flair/img/people.curly-hair.webp create mode 100644 public/flair/img/people.dark-skin-tone.webp create mode 100644 public/flair/img/people.deaf-man-dark-skin-tone.webp create mode 100644 public/flair/img/people.deaf-man-light-skin-tone.webp create mode 100644 public/flair/img/people.deaf-man-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.deaf-man-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.deaf-man-medium-skin-tone.webp create mode 100644 public/flair/img/people.deaf-man.webp create mode 100644 public/flair/img/people.deaf-person-dark-skin-tone.webp create mode 100644 public/flair/img/people.deaf-person-light-skin-tone.webp create mode 100644 public/flair/img/people.deaf-person-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.deaf-person-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.deaf-person-medium-skin-tone.webp create mode 100644 public/flair/img/people.deaf-person.webp create mode 100644 public/flair/img/people.deaf-woman-dark-skin-tone.webp create mode 100644 public/flair/img/people.deaf-woman-light-skin-tone.webp create mode 100644 public/flair/img/people.deaf-woman-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.deaf-woman-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.deaf-woman-medium-skin-tone.webp create mode 100644 public/flair/img/people.deaf-woman.webp create mode 100644 public/flair/img/people.detective-dark-skin-tone.webp create mode 100644 public/flair/img/people.detective-light-skin-tone.webp create mode 100644 public/flair/img/people.detective-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.detective-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.detective-medium-skin-tone.webp create mode 100644 public/flair/img/people.detective.webp create mode 100644 public/flair/img/people.dna.webp create mode 100644 public/flair/img/people.drop-of-blood.webp create mode 100644 public/flair/img/people.ear-dark-skin-tone.webp create mode 100644 public/flair/img/people.ear-light-skin-tone.webp create mode 100644 public/flair/img/people.ear-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.ear-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.ear-medium-skin-tone.webp create mode 100644 public/flair/img/people.ear-with-hearing-aid-dark-skin-tone.webp create mode 100644 public/flair/img/people.ear-with-hearing-aid-light-skin-tone.webp create mode 100644 public/flair/img/people.ear-with-hearing-aid-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.ear-with-hearing-aid-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.ear-with-hearing-aid-medium-skin-tone.webp create mode 100644 public/flair/img/people.ear-with-hearing-aid.webp create mode 100644 public/flair/img/people.ear.webp create mode 100644 public/flair/img/people.elf-dark-skin-tone.webp create mode 100644 public/flair/img/people.elf-light-skin-tone.webp create mode 100644 public/flair/img/people.elf-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.elf-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.elf-medium-skin-tone.webp create mode 100644 public/flair/img/people.elf.webp create mode 100644 public/flair/img/people.eye.webp create mode 100644 public/flair/img/people.eyes.webp create mode 100644 public/flair/img/people.factory-worker-dark-skin-tone.webp create mode 100644 public/flair/img/people.factory-worker-light-skin-tone.webp create mode 100644 public/flair/img/people.factory-worker-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.factory-worker-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.factory-worker-medium-skin-tone.webp create mode 100644 public/flair/img/people.factory-worker.webp create mode 100644 public/flair/img/people.fairy-dark-skin-tone.webp create mode 100644 public/flair/img/people.fairy-light-skin-tone.webp create mode 100644 public/flair/img/people.fairy-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.fairy-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.fairy-medium-skin-tone.webp create mode 100644 public/flair/img/people.fairy.webp create mode 100644 public/flair/img/people.family-adult-adult-child-child.webp create mode 100644 public/flair/img/people.family-adult-adult-child.webp create mode 100644 public/flair/img/people.family-adult-child-child.webp create mode 100644 public/flair/img/people.family-adult-child.webp create mode 100644 public/flair/img/people.family-man-boy-boy.webp create mode 100644 public/flair/img/people.family-man-boy.webp create mode 100644 public/flair/img/people.family-man-girl-boy.webp create mode 100644 public/flair/img/people.family-man-girl-girl.webp create mode 100644 public/flair/img/people.family-man-girl.webp create mode 100644 public/flair/img/people.family-man-man-boy-boy.webp create mode 100644 public/flair/img/people.family-man-man-boy.webp create mode 100644 public/flair/img/people.family-man-man-girl-boy.webp create mode 100644 public/flair/img/people.family-man-man-girl-girl.webp create mode 100644 public/flair/img/people.family-man-man-girl.webp create mode 100644 public/flair/img/people.family-man-woman-boy-boy.webp create mode 100644 public/flair/img/people.family-man-woman-boy.webp create mode 100644 public/flair/img/people.family-man-woman-girl-boy.webp create mode 100644 public/flair/img/people.family-man-woman-girl-girl.webp create mode 100644 public/flair/img/people.family-man-woman-girl.webp create mode 100644 public/flair/img/people.family-woman-boy-boy.webp create mode 100644 public/flair/img/people.family-woman-boy.webp create mode 100644 public/flair/img/people.family-woman-girl-boy.webp create mode 100644 public/flair/img/people.family-woman-girl-girl.webp create mode 100644 public/flair/img/people.family-woman-girl.webp create mode 100644 public/flair/img/people.family-woman-woman-boy-boy.webp create mode 100644 public/flair/img/people.family-woman-woman-boy.webp create mode 100644 public/flair/img/people.family-woman-woman-girl-boy.webp create mode 100644 public/flair/img/people.family-woman-woman-girl-girl.webp create mode 100644 public/flair/img/people.family-woman-woman-girl.webp create mode 100644 public/flair/img/people.family.webp create mode 100644 public/flair/img/people.farmer-dark-skin-tone.webp create mode 100644 public/flair/img/people.farmer-light-skin-tone.webp create mode 100644 public/flair/img/people.farmer-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.farmer-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.farmer-medium-skin-tone.webp create mode 100644 public/flair/img/people.farmer.webp create mode 100644 public/flair/img/people.firefighter-dark-skin-tone.webp create mode 100644 public/flair/img/people.firefighter-light-skin-tone.webp create mode 100644 public/flair/img/people.firefighter-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.firefighter-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.firefighter-medium-skin-tone.webp create mode 100644 public/flair/img/people.firefighter.webp create mode 100644 public/flair/img/people.flexed-biceps-dark-skin-tone.webp create mode 100644 public/flair/img/people.flexed-biceps-light-skin-tone.webp create mode 100644 public/flair/img/people.flexed-biceps-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.flexed-biceps-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.flexed-biceps-medium-skin-tone.webp create mode 100644 public/flair/img/people.flexed-biceps.webp create mode 100644 public/flair/img/people.folded-hands-dark-skin-tone.webp create mode 100644 public/flair/img/people.folded-hands-light-skin-tone.webp create mode 100644 public/flair/img/people.folded-hands-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.folded-hands-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.folded-hands-medium-skin-tone.webp create mode 100644 public/flair/img/people.folded-hands.webp create mode 100644 public/flair/img/people.foot-dark-skin-tone.webp create mode 100644 public/flair/img/people.foot-light-skin-tone.webp create mode 100644 public/flair/img/people.foot-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.foot-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.foot-medium-skin-tone.webp create mode 100644 public/flair/img/people.foot.webp create mode 100644 public/flair/img/people.footprints.webp create mode 100644 public/flair/img/people.genie.webp create mode 100644 public/flair/img/people.girl-dark-skin-tone.webp create mode 100644 public/flair/img/people.girl-light-skin-tone.webp create mode 100644 public/flair/img/people.girl-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.girl-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.girl-medium-skin-tone.webp create mode 100644 public/flair/img/people.girl.webp create mode 100644 public/flair/img/people.guard-dark-skin-tone.webp create mode 100644 public/flair/img/people.guard-light-skin-tone.webp create mode 100644 public/flair/img/people.guard-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.guard-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.guard-medium-skin-tone.webp create mode 100644 public/flair/img/people.guard.webp create mode 100644 public/flair/img/people.hand-with-fingers-splayed-dark-skin-tone.webp create mode 100644 public/flair/img/people.hand-with-fingers-splayed-light-skin-tone.webp create mode 100644 public/flair/img/people.hand-with-fingers-splayed-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.hand-with-fingers-splayed-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.hand-with-fingers-splayed-medium-skin-tone.webp create mode 100644 public/flair/img/people.hand-with-fingers-splayed.webp create mode 100644 public/flair/img/people.hand-with-index-finger-and-thumb-crossed-dark-skin-tone.webp create mode 100644 public/flair/img/people.hand-with-index-finger-and-thumb-crossed-light-skin-tone.webp create mode 100644 public/flair/img/people.hand-with-index-finger-and-thumb-crossed-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.hand-with-index-finger-and-thumb-crossed-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.hand-with-index-finger-and-thumb-crossed-medium-skin-tone.webp create mode 100644 public/flair/img/people.hand-with-index-finger-and-thumb-crossed.webp create mode 100644 public/flair/img/people.handshake-dark-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.handshake-dark-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.handshake-dark-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.handshake-dark-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.handshake-dark-skin-tone.webp create mode 100644 public/flair/img/people.handshake-light-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.handshake-light-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.handshake-light-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.handshake-light-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.handshake-light-skin-tone.webp create mode 100644 public/flair/img/people.handshake-medium-dark-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.handshake-medium-dark-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.handshake-medium-dark-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.handshake-medium-dark-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.handshake-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.handshake-medium-light-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.handshake-medium-light-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.handshake-medium-light-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.handshake-medium-light-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.handshake-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.handshake-medium-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.handshake-medium-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.handshake-medium-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.handshake-medium-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.handshake-medium-skin-tone.webp create mode 100644 public/flair/img/people.handshake.webp create mode 100644 public/flair/img/people.health-worker-dark-skin-tone.webp create mode 100644 public/flair/img/people.health-worker-light-skin-tone.webp create mode 100644 public/flair/img/people.health-worker-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.health-worker-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.health-worker-medium-skin-tone.webp create mode 100644 public/flair/img/people.health-worker.webp create mode 100644 public/flair/img/people.heart-hands-dark-skin-tone.webp create mode 100644 public/flair/img/people.heart-hands-light-skin-tone.webp create mode 100644 public/flair/img/people.heart-hands-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.heart-hands-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.heart-hands-medium-skin-tone.webp create mode 100644 public/flair/img/people.heart-hands.webp create mode 100644 public/flair/img/people.horse-racing-dark-skin-tone.webp create mode 100644 public/flair/img/people.horse-racing-light-skin-tone.webp create mode 100644 public/flair/img/people.horse-racing-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.horse-racing-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.horse-racing-medium-skin-tone.webp create mode 100644 public/flair/img/people.horse-racing.webp create mode 100644 public/flair/img/people.index-pointing-at-the-viewer-dark-skin-tone.webp create mode 100644 public/flair/img/people.index-pointing-at-the-viewer-light-skin-tone.webp create mode 100644 public/flair/img/people.index-pointing-at-the-viewer-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.index-pointing-at-the-viewer-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.index-pointing-at-the-viewer-medium-skin-tone.webp create mode 100644 public/flair/img/people.index-pointing-at-the-viewer.webp create mode 100644 public/flair/img/people.index-pointing-up-dark-skin-tone.webp create mode 100644 public/flair/img/people.index-pointing-up-light-skin-tone.webp create mode 100644 public/flair/img/people.index-pointing-up-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.index-pointing-up-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.index-pointing-up-medium-skin-tone.webp create mode 100644 public/flair/img/people.index-pointing-up.webp create mode 100644 public/flair/img/people.judge-dark-skin-tone.webp create mode 100644 public/flair/img/people.judge-light-skin-tone.webp create mode 100644 public/flair/img/people.judge-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.judge-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.judge-medium-skin-tone.webp create mode 100644 public/flair/img/people.judge.webp create mode 100644 public/flair/img/people.kiss-dark-skin-tone.webp create mode 100644 public/flair/img/people.kiss-light-skin-tone.webp create mode 100644 public/flair/img/people.kiss-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.kiss-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.kiss-medium-skin-tone.webp create mode 100644 public/flair/img/people.kiss.webp create mode 100644 public/flair/img/people.left-facing-fist-dark-skin-tone.webp create mode 100644 public/flair/img/people.left-facing-fist-light-skin-tone.webp create mode 100644 public/flair/img/people.left-facing-fist-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.left-facing-fist-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.left-facing-fist-medium-skin-tone.webp create mode 100644 public/flair/img/people.left-facing-fist.webp create mode 100644 public/flair/img/people.leftwards-hand-dark-skin-tone.webp create mode 100644 public/flair/img/people.leftwards-hand-light-skin-tone.webp create mode 100644 public/flair/img/people.leftwards-hand-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.leftwards-hand-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.leftwards-hand-medium-skin-tone.webp create mode 100644 public/flair/img/people.leftwards-hand.webp create mode 100644 public/flair/img/people.leftwards-pushing-hand-dark-skin-tone.webp create mode 100644 public/flair/img/people.leftwards-pushing-hand-light-skin-tone.webp create mode 100644 public/flair/img/people.leftwards-pushing-hand-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.leftwards-pushing-hand-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.leftwards-pushing-hand-medium-skin-tone.webp create mode 100644 public/flair/img/people.leftwards-pushing-hand.webp create mode 100644 public/flair/img/people.leg-dark-skin-tone.webp create mode 100644 public/flair/img/people.leg-light-skin-tone.webp create mode 100644 public/flair/img/people.leg-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.leg-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.leg-medium-skin-tone.webp create mode 100644 public/flair/img/people.leg.webp create mode 100644 public/flair/img/people.light-skin-tone.webp create mode 100644 public/flair/img/people.love-you-gesture-dark-skin-tone.webp create mode 100644 public/flair/img/people.love-you-gesture-light-skin-tone.webp create mode 100644 public/flair/img/people.love-you-gesture-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.love-you-gesture-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.love-you-gesture-medium-skin-tone.webp create mode 100644 public/flair/img/people.love-you-gesture.webp create mode 100644 public/flair/img/people.lungs.webp create mode 100644 public/flair/img/people.mage-dark-skin-tone.webp create mode 100644 public/flair/img/people.mage-light-skin-tone.webp create mode 100644 public/flair/img/people.mage-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.mage-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.mage-medium-skin-tone.webp create mode 100644 public/flair/img/people.mage.webp create mode 100644 public/flair/img/people.man-artist-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-artist-light-skin-tone.webp create mode 100644 public/flair/img/people.man-artist-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-artist-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-artist-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-artist.webp create mode 100644 public/flair/img/people.man-astronaut-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-astronaut-light-skin-tone.webp create mode 100644 public/flair/img/people.man-astronaut-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-astronaut-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-astronaut-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-astronaut.webp create mode 100644 public/flair/img/people.man-bald.webp create mode 100644 public/flair/img/people.man-beard.webp create mode 100644 public/flair/img/people.man-biking-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-biking-light-skin-tone.webp create mode 100644 public/flair/img/people.man-biking-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-biking-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-biking-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-biking.webp create mode 100644 public/flair/img/people.man-blond-hair.webp create mode 100644 public/flair/img/people.man-bouncing-ball-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-bouncing-ball-light-skin-tone.webp create mode 100644 public/flair/img/people.man-bouncing-ball-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-bouncing-ball-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-bouncing-ball-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-bouncing-ball.webp create mode 100644 public/flair/img/people.man-bowing-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-bowing-light-skin-tone.webp create mode 100644 public/flair/img/people.man-bowing-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-bowing-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-bowing-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-bowing.webp create mode 100644 public/flair/img/people.man-cartwheeling-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-cartwheeling-light-skin-tone.webp create mode 100644 public/flair/img/people.man-cartwheeling-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-cartwheeling-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-cartwheeling-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-cartwheeling.webp create mode 100644 public/flair/img/people.man-climbing-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-climbing-light-skin-tone.webp create mode 100644 public/flair/img/people.man-climbing-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-climbing-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-climbing-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-climbing.webp create mode 100644 public/flair/img/people.man-construction-worker-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-construction-worker-light-skin-tone.webp create mode 100644 public/flair/img/people.man-construction-worker-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-construction-worker-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-construction-worker-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-construction-worker.webp create mode 100644 public/flair/img/people.man-cook-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-cook-light-skin-tone.webp create mode 100644 public/flair/img/people.man-cook-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-cook-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-cook-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-cook.webp create mode 100644 public/flair/img/people.man-curly-hair.webp create mode 100644 public/flair/img/people.man-dancing-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-dancing-light-skin-tone.webp create mode 100644 public/flair/img/people.man-dancing-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-dancing-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-dancing-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-dancing.webp create mode 100644 public/flair/img/people.man-dark-skin-tone-bald.webp create mode 100644 public/flair/img/people.man-dark-skin-tone-beard.webp create mode 100644 public/flair/img/people.man-dark-skin-tone-blond-hair.webp create mode 100644 public/flair/img/people.man-dark-skin-tone-curly-hair.webp create mode 100644 public/flair/img/people.man-dark-skin-tone-red-hair.webp create mode 100644 public/flair/img/people.man-dark-skin-tone-white-hair.webp create mode 100644 public/flair/img/people.man-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-detective-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-detective-light-skin-tone.webp create mode 100644 public/flair/img/people.man-detective-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-detective-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-detective-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-detective.webp create mode 100644 public/flair/img/people.man-elf-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-elf-light-skin-tone.webp create mode 100644 public/flair/img/people.man-elf-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-elf-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-elf-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-elf.webp create mode 100644 public/flair/img/people.man-facepalming-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-facepalming-light-skin-tone.webp create mode 100644 public/flair/img/people.man-facepalming-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-facepalming-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-facepalming-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-facepalming.webp create mode 100644 public/flair/img/people.man-factory-worker-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-factory-worker-light-skin-tone.webp create mode 100644 public/flair/img/people.man-factory-worker-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-factory-worker-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-factory-worker-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-factory-worker.webp create mode 100644 public/flair/img/people.man-fairy-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-fairy-light-skin-tone.webp create mode 100644 public/flair/img/people.man-fairy-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-fairy-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-fairy-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-fairy.webp create mode 100644 public/flair/img/people.man-farmer-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-farmer-light-skin-tone.webp create mode 100644 public/flair/img/people.man-farmer-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-farmer-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-farmer-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-farmer.webp create mode 100644 public/flair/img/people.man-feeding-baby-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-feeding-baby-light-skin-tone.webp create mode 100644 public/flair/img/people.man-feeding-baby-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-feeding-baby-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-feeding-baby-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-feeding-baby.webp create mode 100644 public/flair/img/people.man-firefighter-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-firefighter-light-skin-tone.webp create mode 100644 public/flair/img/people.man-firefighter-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-firefighter-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-firefighter-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-firefighter.webp create mode 100644 public/flair/img/people.man-frowning-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-frowning-light-skin-tone.webp create mode 100644 public/flair/img/people.man-frowning-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-frowning-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-frowning-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-frowning.webp create mode 100644 public/flair/img/people.man-genie.webp create mode 100644 public/flair/img/people.man-gesturing-no-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-gesturing-no-light-skin-tone.webp create mode 100644 public/flair/img/people.man-gesturing-no-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-gesturing-no-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-gesturing-no-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-gesturing-no.webp create mode 100644 public/flair/img/people.man-gesturing-ok-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-gesturing-ok-light-skin-tone.webp create mode 100644 public/flair/img/people.man-gesturing-ok-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-gesturing-ok-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-gesturing-ok-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-gesturing-ok.webp create mode 100644 public/flair/img/people.man-getting-haircut-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-getting-haircut-light-skin-tone.webp create mode 100644 public/flair/img/people.man-getting-haircut-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-getting-haircut-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-getting-haircut-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-getting-haircut.webp create mode 100644 public/flair/img/people.man-getting-massage-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-getting-massage-light-skin-tone.webp create mode 100644 public/flair/img/people.man-getting-massage-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-getting-massage-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-getting-massage-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-getting-massage.webp create mode 100644 public/flair/img/people.man-golfing-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-golfing-light-skin-tone.webp create mode 100644 public/flair/img/people.man-golfing-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-golfing-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-golfing-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-golfing.webp create mode 100644 public/flair/img/people.man-guard-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-guard-light-skin-tone.webp create mode 100644 public/flair/img/people.man-guard-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-guard-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-guard-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-guard.webp create mode 100644 public/flair/img/people.man-health-worker-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-health-worker-light-skin-tone.webp create mode 100644 public/flair/img/people.man-health-worker-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-health-worker-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-health-worker-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-health-worker.webp create mode 100644 public/flair/img/people.man-in-lotus-position-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-in-lotus-position-light-skin-tone.webp create mode 100644 public/flair/img/people.man-in-lotus-position-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-in-lotus-position-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-in-lotus-position-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-in-lotus-position.webp create mode 100644 public/flair/img/people.man-in-manual-wheelchair-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-in-manual-wheelchair-facing-right-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-in-manual-wheelchair-facing-right-light-skin-tone.webp create mode 100644 public/flair/img/people.man-in-manual-wheelchair-facing-right-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-in-manual-wheelchair-facing-right-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-in-manual-wheelchair-facing-right-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-in-manual-wheelchair-facing-right.webp create mode 100644 public/flair/img/people.man-in-manual-wheelchair-light-skin-tone.webp create mode 100644 public/flair/img/people.man-in-manual-wheelchair-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-in-manual-wheelchair-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-in-manual-wheelchair-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-in-manual-wheelchair.webp create mode 100644 public/flair/img/people.man-in-motorized-wheelchair-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-in-motorized-wheelchair-facing-right-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-in-motorized-wheelchair-facing-right-light-skin-tone.webp create mode 100644 public/flair/img/people.man-in-motorized-wheelchair-facing-right-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-in-motorized-wheelchair-facing-right-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-in-motorized-wheelchair-facing-right-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-in-motorized-wheelchair-facing-right.webp create mode 100644 public/flair/img/people.man-in-motorized-wheelchair-light-skin-tone.webp create mode 100644 public/flair/img/people.man-in-motorized-wheelchair-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-in-motorized-wheelchair-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-in-motorized-wheelchair-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-in-motorized-wheelchair.webp create mode 100644 public/flair/img/people.man-in-steamy-room-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-in-steamy-room-light-skin-tone.webp create mode 100644 public/flair/img/people.man-in-steamy-room-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-in-steamy-room-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-in-steamy-room-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-in-steamy-room.webp create mode 100644 public/flair/img/people.man-in-tuxedo-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-in-tuxedo-light-skin-tone.webp create mode 100644 public/flair/img/people.man-in-tuxedo-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-in-tuxedo-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-in-tuxedo-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-in-tuxedo.webp create mode 100644 public/flair/img/people.man-judge-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-judge-light-skin-tone.webp create mode 100644 public/flair/img/people.man-judge-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-judge-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-judge-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-judge.webp create mode 100644 public/flair/img/people.man-juggling-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-juggling-light-skin-tone.webp create mode 100644 public/flair/img/people.man-juggling-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-juggling-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-juggling-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-juggling.webp create mode 100644 public/flair/img/people.man-kneeling-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-kneeling-facing-right-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-kneeling-facing-right-light-skin-tone.webp create mode 100644 public/flair/img/people.man-kneeling-facing-right-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-kneeling-facing-right-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-kneeling-facing-right-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-kneeling-facing-right.webp create mode 100644 public/flair/img/people.man-kneeling-light-skin-tone.webp create mode 100644 public/flair/img/people.man-kneeling-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-kneeling-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-kneeling-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-kneeling.webp create mode 100644 public/flair/img/people.man-lifting-weights-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-lifting-weights-light-skin-tone.webp create mode 100644 public/flair/img/people.man-lifting-weights-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-lifting-weights-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-lifting-weights-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-lifting-weights.webp create mode 100644 public/flair/img/people.man-light-skin-tone-bald.webp create mode 100644 public/flair/img/people.man-light-skin-tone-beard.webp create mode 100644 public/flair/img/people.man-light-skin-tone-blond-hair.webp create mode 100644 public/flair/img/people.man-light-skin-tone-curly-hair.webp create mode 100644 public/flair/img/people.man-light-skin-tone-red-hair.webp create mode 100644 public/flair/img/people.man-light-skin-tone-white-hair.webp create mode 100644 public/flair/img/people.man-light-skin-tone.webp create mode 100644 public/flair/img/people.man-mage-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-mage-light-skin-tone.webp create mode 100644 public/flair/img/people.man-mage-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-mage-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-mage-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-mage.webp create mode 100644 public/flair/img/people.man-mechanic-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-mechanic-light-skin-tone.webp create mode 100644 public/flair/img/people.man-mechanic-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-mechanic-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-mechanic-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-mechanic.webp create mode 100644 public/flair/img/people.man-medium-dark-skin-tone-bald.webp create mode 100644 public/flair/img/people.man-medium-dark-skin-tone-beard.webp create mode 100644 public/flair/img/people.man-medium-dark-skin-tone-blond-hair.webp create mode 100644 public/flair/img/people.man-medium-dark-skin-tone-curly-hair.webp create mode 100644 public/flair/img/people.man-medium-dark-skin-tone-red-hair.webp create mode 100644 public/flair/img/people.man-medium-dark-skin-tone-white-hair.webp create mode 100644 public/flair/img/people.man-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-medium-light-skin-tone-bald.webp create mode 100644 public/flair/img/people.man-medium-light-skin-tone-beard.webp create mode 100644 public/flair/img/people.man-medium-light-skin-tone-blond-hair.webp create mode 100644 public/flair/img/people.man-medium-light-skin-tone-curly-hair.webp create mode 100644 public/flair/img/people.man-medium-light-skin-tone-red-hair.webp create mode 100644 public/flair/img/people.man-medium-light-skin-tone-white-hair.webp create mode 100644 public/flair/img/people.man-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-medium-skin-tone-bald.webp create mode 100644 public/flair/img/people.man-medium-skin-tone-beard.webp create mode 100644 public/flair/img/people.man-medium-skin-tone-blond-hair.webp create mode 100644 public/flair/img/people.man-medium-skin-tone-curly-hair.webp create mode 100644 public/flair/img/people.man-medium-skin-tone-red-hair.webp create mode 100644 public/flair/img/people.man-medium-skin-tone-white-hair.webp create mode 100644 public/flair/img/people.man-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-mountain-biking-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-mountain-biking-light-skin-tone.webp create mode 100644 public/flair/img/people.man-mountain-biking-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-mountain-biking-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-mountain-biking-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-mountain-biking.webp create mode 100644 public/flair/img/people.man-office-worker-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-office-worker-light-skin-tone.webp create mode 100644 public/flair/img/people.man-office-worker-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-office-worker-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-office-worker-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-office-worker.webp create mode 100644 public/flair/img/people.man-pilot-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-pilot-light-skin-tone.webp create mode 100644 public/flair/img/people.man-pilot-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-pilot-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-pilot-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-pilot.webp create mode 100644 public/flair/img/people.man-playing-handball-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-playing-handball-light-skin-tone.webp create mode 100644 public/flair/img/people.man-playing-handball-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-playing-handball-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-playing-handball-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-playing-handball.webp create mode 100644 public/flair/img/people.man-playing-water-polo-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-playing-water-polo-light-skin-tone.webp create mode 100644 public/flair/img/people.man-playing-water-polo-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-playing-water-polo-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-playing-water-polo-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-playing-water-polo.webp create mode 100644 public/flair/img/people.man-police-officer-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-police-officer-light-skin-tone.webp create mode 100644 public/flair/img/people.man-police-officer-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-police-officer-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-police-officer-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-police-officer.webp create mode 100644 public/flair/img/people.man-pouting-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-pouting-light-skin-tone.webp create mode 100644 public/flair/img/people.man-pouting-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-pouting-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-pouting-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-pouting.webp create mode 100644 public/flair/img/people.man-raising-hand-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-raising-hand-light-skin-tone.webp create mode 100644 public/flair/img/people.man-raising-hand-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-raising-hand-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-raising-hand-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-raising-hand.webp create mode 100644 public/flair/img/people.man-red-hair.webp create mode 100644 public/flair/img/people.man-rowing-boat-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-rowing-boat-light-skin-tone.webp create mode 100644 public/flair/img/people.man-rowing-boat-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-rowing-boat-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-rowing-boat-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-rowing-boat.webp create mode 100644 public/flair/img/people.man-running-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-running-facing-right-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-running-facing-right-light-skin-tone.webp create mode 100644 public/flair/img/people.man-running-facing-right-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-running-facing-right-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-running-facing-right-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-running-facing-right.webp create mode 100644 public/flair/img/people.man-running-light-skin-tone.webp create mode 100644 public/flair/img/people.man-running-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-running-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-running-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-running.webp create mode 100644 public/flair/img/people.man-scientist-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-scientist-light-skin-tone.webp create mode 100644 public/flair/img/people.man-scientist-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-scientist-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-scientist-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-scientist.webp create mode 100644 public/flair/img/people.man-shrugging-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-shrugging-light-skin-tone.webp create mode 100644 public/flair/img/people.man-shrugging-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-shrugging-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-shrugging-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-shrugging.webp create mode 100644 public/flair/img/people.man-singer-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-singer-light-skin-tone.webp create mode 100644 public/flair/img/people.man-singer-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-singer-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-singer-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-singer.webp create mode 100644 public/flair/img/people.man-standing-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-standing-light-skin-tone.webp create mode 100644 public/flair/img/people.man-standing-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-standing-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-standing-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-standing.webp create mode 100644 public/flair/img/people.man-student-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-student-light-skin-tone.webp create mode 100644 public/flair/img/people.man-student-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-student-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-student-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-student.webp create mode 100644 public/flair/img/people.man-superhero-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-superhero-light-skin-tone.webp create mode 100644 public/flair/img/people.man-superhero-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-superhero-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-superhero-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-superhero.webp create mode 100644 public/flair/img/people.man-supervillain-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-supervillain-light-skin-tone.webp create mode 100644 public/flair/img/people.man-supervillain-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-supervillain-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-supervillain-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-supervillain.webp create mode 100644 public/flair/img/people.man-surfing-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-surfing-light-skin-tone.webp create mode 100644 public/flair/img/people.man-surfing-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-surfing-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-surfing-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-surfing.webp create mode 100644 public/flair/img/people.man-swimming-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-swimming-light-skin-tone.webp create mode 100644 public/flair/img/people.man-swimming-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-swimming-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-swimming-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-swimming.webp create mode 100644 public/flair/img/people.man-teacher-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-teacher-light-skin-tone.webp create mode 100644 public/flair/img/people.man-teacher-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-teacher-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-teacher-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-teacher.webp create mode 100644 public/flair/img/people.man-technologist-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-technologist-light-skin-tone.webp create mode 100644 public/flair/img/people.man-technologist-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-technologist-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-technologist-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-technologist.webp create mode 100644 public/flair/img/people.man-tipping-hand-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-tipping-hand-light-skin-tone.webp create mode 100644 public/flair/img/people.man-tipping-hand-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-tipping-hand-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-tipping-hand-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-tipping-hand.webp create mode 100644 public/flair/img/people.man-vampire-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-vampire-light-skin-tone.webp create mode 100644 public/flair/img/people.man-vampire-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-vampire-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-vampire-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-vampire.webp create mode 100644 public/flair/img/people.man-walking-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-walking-facing-right-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-walking-facing-right-light-skin-tone.webp create mode 100644 public/flair/img/people.man-walking-facing-right-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-walking-facing-right-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-walking-facing-right-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-walking-facing-right.webp create mode 100644 public/flair/img/people.man-walking-light-skin-tone.webp create mode 100644 public/flair/img/people.man-walking-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-walking-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-walking-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-walking.webp create mode 100644 public/flair/img/people.man-wearing-turban-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-wearing-turban-light-skin-tone.webp create mode 100644 public/flair/img/people.man-wearing-turban-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-wearing-turban-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-wearing-turban-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-wearing-turban.webp create mode 100644 public/flair/img/people.man-white-hair.webp create mode 100644 public/flair/img/people.man-with-veil-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-with-veil-light-skin-tone.webp create mode 100644 public/flair/img/people.man-with-veil-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-with-veil-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-with-veil-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-with-veil.webp create mode 100644 public/flair/img/people.man-with-white-cane-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-with-white-cane-facing-right-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-with-white-cane-facing-right-light-skin-tone.webp create mode 100644 public/flair/img/people.man-with-white-cane-facing-right-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-with-white-cane-facing-right-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-with-white-cane-facing-right-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-with-white-cane-facing-right.webp create mode 100644 public/flair/img/people.man-with-white-cane-light-skin-tone.webp create mode 100644 public/flair/img/people.man-with-white-cane-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.man-with-white-cane-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.man-with-white-cane-medium-skin-tone.webp create mode 100644 public/flair/img/people.man-with-white-cane.webp create mode 100644 public/flair/img/people.man-zombie.webp create mode 100644 public/flair/img/people.man.webp create mode 100644 public/flair/img/people.mechanic-dark-skin-tone.webp create mode 100644 public/flair/img/people.mechanic-light-skin-tone.webp create mode 100644 public/flair/img/people.mechanic-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.mechanic-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.mechanic-medium-skin-tone.webp create mode 100644 public/flair/img/people.mechanic.webp create mode 100644 public/flair/img/people.mechanical-arm.webp create mode 100644 public/flair/img/people.mechanical-leg.webp create mode 100644 public/flair/img/people.medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.medium-light-skin-tone.webp create mode 100644 public/flair/img/people.medium-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-dark-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-dark-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-dark-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-dark-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-dark-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-light-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-light-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-light-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-light-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-light-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-medium-dark-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-medium-dark-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-medium-dark-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-medium-dark-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-medium-light-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-medium-light-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-medium-light-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-medium-light-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-medium-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-medium-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-medium-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-medium-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands-medium-skin-tone.webp create mode 100644 public/flair/img/people.men-holding-hands.webp create mode 100644 public/flair/img/people.men-with-bunny-ears.webp create mode 100644 public/flair/img/people.men-wrestling.webp create mode 100644 public/flair/img/people.mermaid-dark-skin-tone.webp create mode 100644 public/flair/img/people.mermaid-light-skin-tone.webp create mode 100644 public/flair/img/people.mermaid-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.mermaid-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.mermaid-medium-skin-tone.webp create mode 100644 public/flair/img/people.mermaid.webp create mode 100644 public/flair/img/people.merman-dark-skin-tone.webp create mode 100644 public/flair/img/people.merman-light-skin-tone.webp create mode 100644 public/flair/img/people.merman-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.merman-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.merman-medium-skin-tone.webp create mode 100644 public/flair/img/people.merman.webp create mode 100644 public/flair/img/people.merperson-dark-skin-tone.webp create mode 100644 public/flair/img/people.merperson-light-skin-tone.webp create mode 100644 public/flair/img/people.merperson-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.merperson-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.merperson-medium-skin-tone.webp create mode 100644 public/flair/img/people.merperson.webp create mode 100644 public/flair/img/people.mouth.webp create mode 100644 public/flair/img/people.mrs-claus-dark-skin-tone.webp create mode 100644 public/flair/img/people.mrs-claus-light-skin-tone.webp create mode 100644 public/flair/img/people.mrs-claus-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.mrs-claus-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.mrs-claus-medium-skin-tone.webp create mode 100644 public/flair/img/people.mrs-claus.webp create mode 100644 public/flair/img/people.mx-claus-dark-skin-tone.webp create mode 100644 public/flair/img/people.mx-claus-light-skin-tone.webp create mode 100644 public/flair/img/people.mx-claus-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.mx-claus-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.mx-claus-medium-skin-tone.webp create mode 100644 public/flair/img/people.mx-claus.webp create mode 100644 public/flair/img/people.nail-polish-dark-skin-tone.webp create mode 100644 public/flair/img/people.nail-polish-light-skin-tone.webp create mode 100644 public/flair/img/people.nail-polish-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.nail-polish-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.nail-polish-medium-skin-tone.webp create mode 100644 public/flair/img/people.nail-polish.webp create mode 100644 public/flair/img/people.ninja-dark-skin-tone.webp create mode 100644 public/flair/img/people.ninja-light-skin-tone.webp create mode 100644 public/flair/img/people.ninja-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.ninja-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.ninja-medium-skin-tone.webp create mode 100644 public/flair/img/people.ninja.webp create mode 100644 public/flair/img/people.nose-dark-skin-tone.webp create mode 100644 public/flair/img/people.nose-light-skin-tone.webp create mode 100644 public/flair/img/people.nose-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.nose-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.nose-medium-skin-tone.webp create mode 100644 public/flair/img/people.nose.webp create mode 100644 public/flair/img/people.office-worker-dark-skin-tone.webp create mode 100644 public/flair/img/people.office-worker-light-skin-tone.webp create mode 100644 public/flair/img/people.office-worker-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.office-worker-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.office-worker-medium-skin-tone.webp create mode 100644 public/flair/img/people.office-worker.webp create mode 100644 public/flair/img/people.ok-hand-dark-skin-tone.webp create mode 100644 public/flair/img/people.ok-hand-light-skin-tone.webp create mode 100644 public/flair/img/people.ok-hand-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.ok-hand-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.ok-hand-medium-skin-tone.webp create mode 100644 public/flair/img/people.ok-hand.webp create mode 100644 public/flair/img/people.old-man-dark-skin-tone.webp create mode 100644 public/flair/img/people.old-man-light-skin-tone.webp create mode 100644 public/flair/img/people.old-man-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.old-man-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.old-man-medium-skin-tone.webp create mode 100644 public/flair/img/people.old-man.webp create mode 100644 public/flair/img/people.old-woman-dark-skin-tone.webp create mode 100644 public/flair/img/people.old-woman-light-skin-tone.webp create mode 100644 public/flair/img/people.old-woman-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.old-woman-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.old-woman-medium-skin-tone.webp create mode 100644 public/flair/img/people.old-woman.webp create mode 100644 public/flair/img/people.older-person-dark-skin-tone.webp create mode 100644 public/flair/img/people.older-person-light-skin-tone.webp create mode 100644 public/flair/img/people.older-person-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.older-person-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.older-person-medium-skin-tone.webp create mode 100644 public/flair/img/people.older-person.webp create mode 100644 public/flair/img/people.oncoming-fist-dark-skin-tone.webp create mode 100644 public/flair/img/people.oncoming-fist-light-skin-tone.webp create mode 100644 public/flair/img/people.oncoming-fist-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.oncoming-fist-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.oncoming-fist-medium-skin-tone.webp create mode 100644 public/flair/img/people.oncoming-fist.webp create mode 100644 public/flair/img/people.open-hands-dark-skin-tone.webp create mode 100644 public/flair/img/people.open-hands-light-skin-tone.webp create mode 100644 public/flair/img/people.open-hands-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.open-hands-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.open-hands-medium-skin-tone.webp create mode 100644 public/flair/img/people.open-hands.webp create mode 100644 public/flair/img/people.palm-down-hand-dark-skin-tone.webp create mode 100644 public/flair/img/people.palm-down-hand-light-skin-tone.webp create mode 100644 public/flair/img/people.palm-down-hand-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.palm-down-hand-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.palm-down-hand-medium-skin-tone.webp create mode 100644 public/flair/img/people.palm-down-hand.webp create mode 100644 public/flair/img/people.palm-up-hand-dark-skin-tone.webp create mode 100644 public/flair/img/people.palm-up-hand-light-skin-tone.webp create mode 100644 public/flair/img/people.palm-up-hand-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.palm-up-hand-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.palm-up-hand-medium-skin-tone.webp create mode 100644 public/flair/img/people.palm-up-hand.webp create mode 100644 public/flair/img/people.palms-up-together-dark-skin-tone.webp create mode 100644 public/flair/img/people.palms-up-together-light-skin-tone.webp create mode 100644 public/flair/img/people.palms-up-together-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.palms-up-together-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.palms-up-together-medium-skin-tone.webp create mode 100644 public/flair/img/people.palms-up-together.webp create mode 100644 public/flair/img/people.people-holding-hands-dark-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-dark-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-dark-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-dark-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-dark-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-light-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-light-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-light-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-light-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-light-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-medium-dark-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-medium-dark-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-medium-dark-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-medium-dark-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-medium-light-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-medium-light-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-medium-light-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-medium-light-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-medium-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-medium-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-medium-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-medium-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands-medium-skin-tone.webp create mode 100644 public/flair/img/people.people-holding-hands.webp create mode 100644 public/flair/img/people.people-hugging.webp create mode 100644 public/flair/img/people.people-with-bunny-ears.webp create mode 100644 public/flair/img/people.people-wrestling.webp create mode 100644 public/flair/img/people.person-bald.webp create mode 100644 public/flair/img/people.person-beard.webp create mode 100644 public/flair/img/people.person-biking-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-biking-light-skin-tone.webp create mode 100644 public/flair/img/people.person-biking-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-biking-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-biking-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-biking.webp create mode 100644 public/flair/img/people.person-blond-hair.webp create mode 100644 public/flair/img/people.person-bouncing-ball-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-bouncing-ball-light-skin-tone.webp create mode 100644 public/flair/img/people.person-bouncing-ball-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-bouncing-ball-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-bouncing-ball-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-bouncing-ball.webp create mode 100644 public/flair/img/people.person-bowing-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-bowing-light-skin-tone.webp create mode 100644 public/flair/img/people.person-bowing-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-bowing-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-bowing-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-bowing.webp create mode 100644 public/flair/img/people.person-cartwheeling-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-cartwheeling-light-skin-tone.webp create mode 100644 public/flair/img/people.person-cartwheeling-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-cartwheeling-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-cartwheeling-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-cartwheeling.webp create mode 100644 public/flair/img/people.person-climbing-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-climbing-light-skin-tone.webp create mode 100644 public/flair/img/people.person-climbing-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-climbing-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-climbing-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-climbing.webp create mode 100644 public/flair/img/people.person-curly-hair.webp create mode 100644 public/flair/img/people.person-dark-skin-tone-bald.webp create mode 100644 public/flair/img/people.person-dark-skin-tone-beard.webp create mode 100644 public/flair/img/people.person-dark-skin-tone-blond-hair.webp create mode 100644 public/flair/img/people.person-dark-skin-tone-curly-hair.webp create mode 100644 public/flair/img/people.person-dark-skin-tone-red-hair.webp create mode 100644 public/flair/img/people.person-dark-skin-tone-white-hair.webp create mode 100644 public/flair/img/people.person-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-facepalming-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-facepalming-light-skin-tone.webp create mode 100644 public/flair/img/people.person-facepalming-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-facepalming-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-facepalming-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-facepalming.webp create mode 100644 public/flair/img/people.person-feeding-baby-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-feeding-baby-light-skin-tone.webp create mode 100644 public/flair/img/people.person-feeding-baby-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-feeding-baby-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-feeding-baby-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-feeding-baby.webp create mode 100644 public/flair/img/people.person-fencing.webp create mode 100644 public/flair/img/people.person-frowning-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-frowning-light-skin-tone.webp create mode 100644 public/flair/img/people.person-frowning-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-frowning-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-frowning-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-frowning.webp create mode 100644 public/flair/img/people.person-gesturing-no-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-gesturing-no-light-skin-tone.webp create mode 100644 public/flair/img/people.person-gesturing-no-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-gesturing-no-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-gesturing-no-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-gesturing-no.webp create mode 100644 public/flair/img/people.person-gesturing-ok-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-gesturing-ok-light-skin-tone.webp create mode 100644 public/flair/img/people.person-gesturing-ok-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-gesturing-ok-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-gesturing-ok-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-gesturing-ok.webp create mode 100644 public/flair/img/people.person-getting-haircut-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-getting-haircut-light-skin-tone.webp create mode 100644 public/flair/img/people.person-getting-haircut-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-getting-haircut-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-getting-haircut-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-getting-haircut.webp create mode 100644 public/flair/img/people.person-getting-massage-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-getting-massage-light-skin-tone.webp create mode 100644 public/flair/img/people.person-getting-massage-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-getting-massage-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-getting-massage-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-getting-massage.webp create mode 100644 public/flair/img/people.person-golfing-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-golfing-light-skin-tone.webp create mode 100644 public/flair/img/people.person-golfing-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-golfing-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-golfing-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-golfing.webp create mode 100644 public/flair/img/people.person-in-bed-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-in-bed-light-skin-tone.webp create mode 100644 public/flair/img/people.person-in-bed-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-in-bed-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-in-bed-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-in-bed.webp create mode 100644 public/flair/img/people.person-in-lotus-position-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-in-lotus-position-light-skin-tone.webp create mode 100644 public/flair/img/people.person-in-lotus-position-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-in-lotus-position-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-in-lotus-position-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-in-lotus-position.webp create mode 100644 public/flair/img/people.person-in-manual-wheelchair-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-in-manual-wheelchair-facing-right-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-in-manual-wheelchair-facing-right-light-skin-tone.webp create mode 100644 public/flair/img/people.person-in-manual-wheelchair-facing-right-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-in-manual-wheelchair-facing-right-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-in-manual-wheelchair-facing-right-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-in-manual-wheelchair-facing-right.webp create mode 100644 public/flair/img/people.person-in-manual-wheelchair-light-skin-tone.webp create mode 100644 public/flair/img/people.person-in-manual-wheelchair-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-in-manual-wheelchair-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-in-manual-wheelchair-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-in-manual-wheelchair.webp create mode 100644 public/flair/img/people.person-in-motorized-wheelchair-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-in-motorized-wheelchair-facing-right-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-in-motorized-wheelchair-facing-right-light-skin-tone.webp create mode 100644 public/flair/img/people.person-in-motorized-wheelchair-facing-right-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-in-motorized-wheelchair-facing-right-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-in-motorized-wheelchair-facing-right-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-in-motorized-wheelchair-facing-right.webp create mode 100644 public/flair/img/people.person-in-motorized-wheelchair-light-skin-tone.webp create mode 100644 public/flair/img/people.person-in-motorized-wheelchair-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-in-motorized-wheelchair-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-in-motorized-wheelchair-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-in-motorized-wheelchair.webp create mode 100644 public/flair/img/people.person-in-steamy-room-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-in-steamy-room-light-skin-tone.webp create mode 100644 public/flair/img/people.person-in-steamy-room-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-in-steamy-room-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-in-steamy-room-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-in-steamy-room.webp create mode 100644 public/flair/img/people.person-in-suit-levitating-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-in-suit-levitating-light-skin-tone.webp create mode 100644 public/flair/img/people.person-in-suit-levitating-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-in-suit-levitating-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-in-suit-levitating-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-in-suit-levitating.webp create mode 100644 public/flair/img/people.person-in-tuxedo-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-in-tuxedo-light-skin-tone.webp create mode 100644 public/flair/img/people.person-in-tuxedo-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-in-tuxedo-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-in-tuxedo-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-in-tuxedo.webp create mode 100644 public/flair/img/people.person-juggling-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-juggling-light-skin-tone.webp create mode 100644 public/flair/img/people.person-juggling-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-juggling-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-juggling-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-juggling.webp create mode 100644 public/flair/img/people.person-kneeling-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-kneeling-facing-right-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-kneeling-facing-right-light-skin-tone.webp create mode 100644 public/flair/img/people.person-kneeling-facing-right-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-kneeling-facing-right-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-kneeling-facing-right-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-kneeling-facing-right.webp create mode 100644 public/flair/img/people.person-kneeling-light-skin-tone.webp create mode 100644 public/flair/img/people.person-kneeling-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-kneeling-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-kneeling-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-kneeling.webp create mode 100644 public/flair/img/people.person-lifting-weights-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-lifting-weights-light-skin-tone.webp create mode 100644 public/flair/img/people.person-lifting-weights-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-lifting-weights-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-lifting-weights-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-lifting-weights.webp create mode 100644 public/flair/img/people.person-light-skin-tone-bald.webp create mode 100644 public/flair/img/people.person-light-skin-tone-beard.webp create mode 100644 public/flair/img/people.person-light-skin-tone-blond-hair.webp create mode 100644 public/flair/img/people.person-light-skin-tone-curly-hair.webp create mode 100644 public/flair/img/people.person-light-skin-tone-red-hair.webp create mode 100644 public/flair/img/people.person-light-skin-tone-white-hair.webp create mode 100644 public/flair/img/people.person-light-skin-tone.webp create mode 100644 public/flair/img/people.person-medium-dark-skin-tone-bald.webp create mode 100644 public/flair/img/people.person-medium-dark-skin-tone-beard.webp create mode 100644 public/flair/img/people.person-medium-dark-skin-tone-blond-hair.webp create mode 100644 public/flair/img/people.person-medium-dark-skin-tone-curly-hair.webp create mode 100644 public/flair/img/people.person-medium-dark-skin-tone-red-hair.webp create mode 100644 public/flair/img/people.person-medium-dark-skin-tone-white-hair.webp create mode 100644 public/flair/img/people.person-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-medium-light-skin-tone-bald.webp create mode 100644 public/flair/img/people.person-medium-light-skin-tone-beard.webp create mode 100644 public/flair/img/people.person-medium-light-skin-tone-blond-hair.webp create mode 100644 public/flair/img/people.person-medium-light-skin-tone-curly-hair.webp create mode 100644 public/flair/img/people.person-medium-light-skin-tone-red-hair.webp create mode 100644 public/flair/img/people.person-medium-light-skin-tone-white-hair.webp create mode 100644 public/flair/img/people.person-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-medium-skin-tone-bald.webp create mode 100644 public/flair/img/people.person-medium-skin-tone-beard.webp create mode 100644 public/flair/img/people.person-medium-skin-tone-blond-hair.webp create mode 100644 public/flair/img/people.person-medium-skin-tone-curly-hair.webp create mode 100644 public/flair/img/people.person-medium-skin-tone-red-hair.webp create mode 100644 public/flair/img/people.person-medium-skin-tone-white-hair.webp create mode 100644 public/flair/img/people.person-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-mountain-biking-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-mountain-biking-light-skin-tone.webp create mode 100644 public/flair/img/people.person-mountain-biking-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-mountain-biking-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-mountain-biking-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-mountain-biking.webp create mode 100644 public/flair/img/people.person-playing-handball-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-playing-handball-light-skin-tone.webp create mode 100644 public/flair/img/people.person-playing-handball-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-playing-handball-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-playing-handball-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-playing-handball.webp create mode 100644 public/flair/img/people.person-playing-water-polo-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-playing-water-polo-light-skin-tone.webp create mode 100644 public/flair/img/people.person-playing-water-polo-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-playing-water-polo-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-playing-water-polo-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-playing-water-polo.webp create mode 100644 public/flair/img/people.person-pouting-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-pouting-light-skin-tone.webp create mode 100644 public/flair/img/people.person-pouting-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-pouting-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-pouting-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-pouting.webp create mode 100644 public/flair/img/people.person-raising-hand-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-raising-hand-light-skin-tone.webp create mode 100644 public/flair/img/people.person-raising-hand-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-raising-hand-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-raising-hand-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-raising-hand.webp create mode 100644 public/flair/img/people.person-red-hair.webp create mode 100644 public/flair/img/people.person-rowing-boat-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-rowing-boat-light-skin-tone.webp create mode 100644 public/flair/img/people.person-rowing-boat-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-rowing-boat-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-rowing-boat-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-rowing-boat.webp create mode 100644 public/flair/img/people.person-running-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-running-facing-right-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-running-facing-right-light-skin-tonet.webp create mode 100644 public/flair/img/people.person-running-facing-right-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-running-facing-right-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-running-facing-right-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-running-facing-right.webp create mode 100644 public/flair/img/people.person-running-light-skin-tone.webp create mode 100644 public/flair/img/people.person-running-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-running-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-running-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-running.webp create mode 100644 public/flair/img/people.person-shrugging-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-shrugging-light-skin-tone.webp create mode 100644 public/flair/img/people.person-shrugging-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-shrugging-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-shrugging-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-shrugging.webp create mode 100644 public/flair/img/people.person-standing-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-standing-light-skin-tone.webp create mode 100644 public/flair/img/people.person-standing-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-standing-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-standing-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-standing.webp create mode 100644 public/flair/img/people.person-surfing-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-surfing-light-skin-tone.webp create mode 100644 public/flair/img/people.person-surfing-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-surfing-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-surfing-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-surfing.webp create mode 100644 public/flair/img/people.person-swimming-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-swimming-light-skin-tone.webp create mode 100644 public/flair/img/people.person-swimming-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-swimming-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-swimming-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-swimming.webp create mode 100644 public/flair/img/people.person-taking-bath-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-taking-bath-light-skin-tone.webp create mode 100644 public/flair/img/people.person-taking-bath-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-taking-bath-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-taking-bath-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-taking-bath.webp create mode 100644 public/flair/img/people.person-tipping-hand-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-tipping-hand-light-skin-tone.webp create mode 100644 public/flair/img/people.person-tipping-hand-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-tipping-hand-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-tipping-hand-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-tipping-hand.webp create mode 100644 public/flair/img/people.person-walking-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-walking-facing-right-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-walking-facing-right-light-skin-tone.webp create mode 100644 public/flair/img/people.person-walking-facing-right-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-walking-facing-right-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-walking-facing-right-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-walking-facing-right.webp create mode 100644 public/flair/img/people.person-walking-light-skin-tone.webp create mode 100644 public/flair/img/people.person-walking-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-walking-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-walking-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-walking.webp create mode 100644 public/flair/img/people.person-wearing-turban-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-wearing-turban-light-skin-tone.webp create mode 100644 public/flair/img/people.person-wearing-turban-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-wearing-turban-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-wearing-turban-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-wearing-turban.webp create mode 100644 public/flair/img/people.person-white-hair.webp create mode 100644 public/flair/img/people.person-with-crown-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-with-crown-light-skin-tone.webp create mode 100644 public/flair/img/people.person-with-crown-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-with-crown-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-with-crown-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-with-crown.webp create mode 100644 public/flair/img/people.person-with-headscarf-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-with-headscarf-light-skin-tone.webp create mode 100644 public/flair/img/people.person-with-headscarf-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-with-headscarf-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-with-headscarf-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-with-headscarf.webp create mode 100644 public/flair/img/people.person-with-skullcap-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-with-skullcap-light-skin-tone.webp create mode 100644 public/flair/img/people.person-with-skullcap-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-with-skullcap-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-with-skullcap-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-with-skullcap.webp create mode 100644 public/flair/img/people.person-with-veil-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-with-veil-light-skin-tone.webp create mode 100644 public/flair/img/people.person-with-veil-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-with-veil-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-with-veil-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-with-veil.webp create mode 100644 public/flair/img/people.person-with-white-cane-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-with-white-cane-facing-right-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-with-white-cane-facing-right-light-skin-tone.webp create mode 100644 public/flair/img/people.person-with-white-cane-facing-right-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-with-white-cane-facing-right-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-with-white-cane-facing-right-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-with-white-cane-facing-right.webp create mode 100644 public/flair/img/people.person-with-white-cane-light-skin-tone.webp create mode 100644 public/flair/img/people.person-with-white-cane-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.person-with-white-cane-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.person-with-white-cane-medium-skin-tone.webp create mode 100644 public/flair/img/people.person-with-white-cane.webp create mode 100644 public/flair/img/people.person.webp create mode 100644 public/flair/img/people.pilot-dark-skin-tone.webp create mode 100644 public/flair/img/people.pilot-light-skin-tone.webp create mode 100644 public/flair/img/people.pilot-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.pilot-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.pilot-medium-skin-tone.webp create mode 100644 public/flair/img/people.pilot.webp create mode 100644 public/flair/img/people.pinched-fingers-dark-skin-tone.webp create mode 100644 public/flair/img/people.pinched-fingers-light-skin-tone.webp create mode 100644 public/flair/img/people.pinched-fingers-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.pinched-fingers-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.pinched-fingers-medium-skin-tone.webp create mode 100644 public/flair/img/people.pinched-fingers.webp create mode 100644 public/flair/img/people.police-officer-dark-skin-tone.webp create mode 100644 public/flair/img/people.police-officer-light-skin-tone.webp create mode 100644 public/flair/img/people.police-officer-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.police-officer-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.police-officer-medium-skin-tone.webp create mode 100644 public/flair/img/people.police-officer.webp create mode 100644 public/flair/img/people.pregnant-man-dark-skin-tone.webp create mode 100644 public/flair/img/people.pregnant-man-light-skin-tone.webp create mode 100644 public/flair/img/people.pregnant-man-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.pregnant-man-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.pregnant-man-medium-skin-tone.webp create mode 100644 public/flair/img/people.pregnant-man.webp create mode 100644 public/flair/img/people.pregnant-person-dark-skin-tone.webp create mode 100644 public/flair/img/people.pregnant-person-light-skin-tone.webp create mode 100644 public/flair/img/people.pregnant-person-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.pregnant-person-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.pregnant-person-medium-skin-tone.webp create mode 100644 public/flair/img/people.pregnant-person.webp create mode 100644 public/flair/img/people.pregnant-woman-dark-skin-tone.webp create mode 100644 public/flair/img/people.pregnant-woman-light-skin-tone.webp create mode 100644 public/flair/img/people.pregnant-woman-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.pregnant-woman-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.pregnant-woman-medium-skin-tone.webp create mode 100644 public/flair/img/people.pregnant-woman.webp create mode 100644 public/flair/img/people.prince-dark-skin-tone.webp create mode 100644 public/flair/img/people.prince-light-skin-tone.webp create mode 100644 public/flair/img/people.prince-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.prince-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.prince-medium-skin-tone.webp create mode 100644 public/flair/img/people.prince.webp create mode 100644 public/flair/img/people.princess-dark-skin-tone.webp create mode 100644 public/flair/img/people.princess-light-skin-tone.webp create mode 100644 public/flair/img/people.princess-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.princess-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.princess-medium-skin-tone.webp create mode 100644 public/flair/img/people.princess.webp create mode 100644 public/flair/img/people.raised-back-of-hand-dark-skin-tone.webp create mode 100644 public/flair/img/people.raised-back-of-hand-light-skin-tone.webp create mode 100644 public/flair/img/people.raised-back-of-hand-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.raised-back-of-hand-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.raised-back-of-hand-medium-skin-tone.webp create mode 100644 public/flair/img/people.raised-back-of-hand.webp create mode 100644 public/flair/img/people.raised-fist-dark-skin-tone.webp create mode 100644 public/flair/img/people.raised-fist-light-skin-tone.webp create mode 100644 public/flair/img/people.raised-fist-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.raised-fist-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.raised-fist-medium-skin-tone.webp create mode 100644 public/flair/img/people.raised-fist.webp create mode 100644 public/flair/img/people.raised-hand-dark-skin-tone.webp create mode 100644 public/flair/img/people.raised-hand-light-skin-tone.webp create mode 100644 public/flair/img/people.raised-hand-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.raised-hand-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.raised-hand-medium-skin-tone.webp create mode 100644 public/flair/img/people.raised-hand.webp create mode 100644 public/flair/img/people.raising-hands-dark-skin-tone.webp create mode 100644 public/flair/img/people.raising-hands-light-skin-tone.webp create mode 100644 public/flair/img/people.raising-hands-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.raising-hands-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.raising-hands-medium-skin-tone.webp create mode 100644 public/flair/img/people.raising-hands.webp create mode 100644 public/flair/img/people.red-hair.webp create mode 100644 public/flair/img/people.right-facing-fist-dark-skin-tone.webp create mode 100644 public/flair/img/people.right-facing-fist-light-skin-tone.webp create mode 100644 public/flair/img/people.right-facing-fist-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.right-facing-fist-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.right-facing-fist-medium-skin-tone.webp create mode 100644 public/flair/img/people.right-facing-fist.webp create mode 100644 public/flair/img/people.rightwards-hand-dark-skin-tone.webp create mode 100644 public/flair/img/people.rightwards-hand-light-skin-tone.webp create mode 100644 public/flair/img/people.rightwards-hand-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.rightwards-hand-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.rightwards-hand-medium-skin-tone.webp create mode 100644 public/flair/img/people.rightwards-hand.webp create mode 100644 public/flair/img/people.rightwards-pushing-hand-dark-skin-tone.webp create mode 100644 public/flair/img/people.rightwards-pushing-hand-light-skin-tone.webp create mode 100644 public/flair/img/people.rightwards-pushing-hand-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.rightwards-pushing-hand-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.rightwards-pushing-hand-medium-skin-tone.webp create mode 100644 public/flair/img/people.rightwards-pushing-hand.webp create mode 100644 public/flair/img/people.santa-claus-dark-skin-tone.webp create mode 100644 public/flair/img/people.santa-claus-light-skin-tone.webp create mode 100644 public/flair/img/people.santa-claus-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.santa-claus-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.santa-claus-medium-skin-tone.webp create mode 100644 public/flair/img/people.santa-claus.webp create mode 100644 public/flair/img/people.scientist-dark-skin-tone.webp create mode 100644 public/flair/img/people.scientist-light-skin-tone.webp create mode 100644 public/flair/img/people.scientist-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.scientist-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.scientist-medium-skin-tone.webp create mode 100644 public/flair/img/people.scientist.webp create mode 100644 public/flair/img/people.selfie-dark-skin-tone.webp create mode 100644 public/flair/img/people.selfie-light-skin-tone.webp create mode 100644 public/flair/img/people.selfie-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.selfie-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.selfie-medium-skin-tone.webp create mode 100644 public/flair/img/people.selfie.webp create mode 100644 public/flair/img/people.sign-of-the-horns-dark-skin-tone.webp create mode 100644 public/flair/img/people.sign-of-the-horns-light-skin-tone.webp create mode 100644 public/flair/img/people.sign-of-the-horns-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.sign-of-the-horns-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.sign-of-the-horns-medium-skin-tone.webp create mode 100644 public/flair/img/people.sign-of-the-horns.webp create mode 100644 public/flair/img/people.singer-dark-skin-tone.webp create mode 100644 public/flair/img/people.singer-light-skin-tone.webp create mode 100644 public/flair/img/people.singer-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.singer-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.singer-medium-skin-tone.webp create mode 100644 public/flair/img/people.singer.webp create mode 100644 public/flair/img/people.skier.webp create mode 100644 public/flair/img/people.snowboarder-dark-skin-tone.webp create mode 100644 public/flair/img/people.snowboarder-light-skin-tone.webp create mode 100644 public/flair/img/people.snowboarder-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.snowboarder-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.snowboarder-medium-skin-tone.webp create mode 100644 public/flair/img/people.snowboarder.webp create mode 100644 public/flair/img/people.speaking-head.webp create mode 100644 public/flair/img/people.student-dark-skin-tone.webp create mode 100644 public/flair/img/people.student-light-skin-tone.webp create mode 100644 public/flair/img/people.student-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.student-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.student-medium-skin-tone.webp create mode 100644 public/flair/img/people.student.webp create mode 100644 public/flair/img/people.superhero-dark-skin-tone.webp create mode 100644 public/flair/img/people.superhero-light-skin-tone.webp create mode 100644 public/flair/img/people.superhero-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.superhero-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.superhero-medium-skin-tone.webp create mode 100644 public/flair/img/people.superhero.webp create mode 100644 public/flair/img/people.supervillain-dark-skin-tone.webp create mode 100644 public/flair/img/people.supervillain-light-skin-tone.webp create mode 100644 public/flair/img/people.supervillain-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.supervillain-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.supervillain-medium-skin-tone.webp create mode 100644 public/flair/img/people.supervillain.webp create mode 100644 public/flair/img/people.teacher-dark-skin-tone.webp create mode 100644 public/flair/img/people.teacher-light-skin-tone.webp create mode 100644 public/flair/img/people.teacher-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.teacher-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.teacher-medium-skin-tone.webp create mode 100644 public/flair/img/people.teacher.webp create mode 100644 public/flair/img/people.technologist-dark-skin-tone.webp create mode 100644 public/flair/img/people.technologist-light-skin-tone.webp create mode 100644 public/flair/img/people.technologist-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.technologist-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.technologist-medium-skin-tone.webp create mode 100644 public/flair/img/people.technologist.webp create mode 100644 public/flair/img/people.thumbs-up-dark-skin-tone.webp create mode 100644 public/flair/img/people.thumbs-up-light-skin-tone.webp create mode 100644 public/flair/img/people.thumbs-up-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.thumbs-up-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.thumbs-up-medium-skin-tone.webp create mode 100644 public/flair/img/people.thumbs-up.webp create mode 100644 public/flair/img/people.tongue.webp create mode 100644 public/flair/img/people.tooth.webp create mode 100644 public/flair/img/people.troll.webp create mode 100644 public/flair/img/people.vampire-dark-skin-tone.webp create mode 100644 public/flair/img/people.vampire-light-skin-tone.webp create mode 100644 public/flair/img/people.vampire-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.vampire-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.vampire-medium-skin-tone.webp create mode 100644 public/flair/img/people.vampire.webp create mode 100644 public/flair/img/people.victory-hand-dark-skin-tone.webp create mode 100644 public/flair/img/people.victory-hand-light-skin-tone.webp create mode 100644 public/flair/img/people.victory-hand-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.victory-hand-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.victory-hand-medium-skin-tone.webp create mode 100644 public/flair/img/people.victory-hand.webp create mode 100644 public/flair/img/people.vulcan-salute-dark-skin-tone.webp create mode 100644 public/flair/img/people.vulcan-salute-light-skin-tone.webp create mode 100644 public/flair/img/people.vulcan-salute-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.vulcan-salute-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.vulcan-salute-medium-skin-tone.webp create mode 100644 public/flair/img/people.vulcan-salute.webp create mode 100644 public/flair/img/people.waving-hand-dark-skin-tone.webp create mode 100644 public/flair/img/people.waving-hand-light-skin-tone.webp create mode 100644 public/flair/img/people.waving-hand-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.waving-hand-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.waving-hand-medium-skin-tone.webp create mode 100644 public/flair/img/people.waving-hand.webp create mode 100644 public/flair/img/people.white-hair.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-dark-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-dark-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-dark-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-dark-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-light-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-light-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-light-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-light-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-medium-dark-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-medium-dark-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-medium-dark-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-medium-dark-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-medium-light-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-medium-light-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-medium-light-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-medium-light-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-medium-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-medium-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-medium-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-medium-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-and-man-holding-hands.webp create mode 100644 public/flair/img/people.woman-artist-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-artist-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-artist-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-artist-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-artist-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-artist.webp create mode 100644 public/flair/img/people.woman-astronaut-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-astronaut-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-astronaut-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-astronaut-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-astronaut-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-astronaut.webp create mode 100644 public/flair/img/people.woman-bald.webp create mode 100644 public/flair/img/people.woman-beard.webp create mode 100644 public/flair/img/people.woman-biking-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-biking-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-biking-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-biking-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-biking-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-biking.webp create mode 100644 public/flair/img/people.woman-blond-hair.webp create mode 100644 public/flair/img/people.woman-bouncing-ball-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-bouncing-ball-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-bouncing-ball-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-bouncing-ball-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-bouncing-ball-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-bouncing-ball.webp create mode 100644 public/flair/img/people.woman-bowing-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-bowing-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-bowing-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-bowing-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-bowing-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-bowing.webp create mode 100644 public/flair/img/people.woman-cartwheeling-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-cartwheeling-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-cartwheeling-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-cartwheeling-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-cartwheeling-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-cartwheeling.webp create mode 100644 public/flair/img/people.woman-climbing-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-climbing-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-climbing-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-climbing-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-climbing-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-climbing.webp create mode 100644 public/flair/img/people.woman-construction-worker-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-construction-worker-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-construction-worker-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-construction-worker-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-construction-worker-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-construction-worker.webp create mode 100644 public/flair/img/people.woman-cook-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-cook-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-cook-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-cook-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-cook-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-cook.webp create mode 100644 public/flair/img/people.woman-curly-hair.webp create mode 100644 public/flair/img/people.woman-dancing-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-dancing-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-dancing-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-dancing-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-dancing-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-dancing.webp create mode 100644 public/flair/img/people.woman-dark-skin-tone-bald.webp create mode 100644 public/flair/img/people.woman-dark-skin-tone-beard.webp create mode 100644 public/flair/img/people.woman-dark-skin-tone-blond-hair.webp create mode 100644 public/flair/img/people.woman-dark-skin-tone-curly-hair.webp create mode 100644 public/flair/img/people.woman-dark-skin-tone-red-hair.webp create mode 100644 public/flair/img/people.woman-dark-skin-tone-white-hair.webp create mode 100644 public/flair/img/people.woman-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-detective-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-detective-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-detective-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-detective-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-detective-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-detective.webp create mode 100644 public/flair/img/people.woman-elf-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-elf-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-elf-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-elf-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-elf-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-elf.webp create mode 100644 public/flair/img/people.woman-facepalming-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-facepalming-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-facepalming-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-facepalming-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-facepalming-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-facepalming.webp create mode 100644 public/flair/img/people.woman-factory-worker-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-factory-worker-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-factory-worker-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-factory-worker-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-factory-worker-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-factory-worker.webp create mode 100644 public/flair/img/people.woman-fairy-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-fairy-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-fairy-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-fairy-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-fairy-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-fairy.webp create mode 100644 public/flair/img/people.woman-farmer-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-farmer-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-farmer-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-farmer-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-farmer-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-farmer.webp create mode 100644 public/flair/img/people.woman-feeding-baby-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-feeding-baby-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-feeding-baby-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-feeding-baby-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-feeding-baby-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-feeding-baby.webp create mode 100644 public/flair/img/people.woman-firefighter-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-firefighter-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-firefighter-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-firefighter-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-firefighter-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-firefighter.webp create mode 100644 public/flair/img/people.woman-frowning-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-frowning-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-frowning-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-frowning-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-frowning-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-frowning.webp create mode 100644 public/flair/img/people.woman-genie.webp create mode 100644 public/flair/img/people.woman-gesturing-no-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-gesturing-no-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-gesturing-no-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-gesturing-no-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-gesturing-no-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-gesturing-no.webp create mode 100644 public/flair/img/people.woman-gesturing-ok-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-gesturing-ok-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-gesturing-ok-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-gesturing-ok-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-gesturing-ok-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-gesturing-ok.webp create mode 100644 public/flair/img/people.woman-getting-haircut-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-getting-haircut-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-getting-haircut-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-getting-haircut-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-getting-haircut-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-getting-haircut.webp create mode 100644 public/flair/img/people.woman-getting-massage-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-getting-massage-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-getting-massage-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-getting-massage-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-getting-massage-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-getting-massage.webp create mode 100644 public/flair/img/people.woman-golfing-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-golfing-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-golfing-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-golfing-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-golfing-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-golfing.webp create mode 100644 public/flair/img/people.woman-guard-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-guard-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-guard-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-guard-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-guard-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-guard.webp create mode 100644 public/flair/img/people.woman-health-worker-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-health-worker-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-health-worker-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-health-worker-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-health-worker-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-health-worker.webp create mode 100644 public/flair/img/people.woman-in-lotus-position-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-lotus-position-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-lotus-position-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-lotus-position-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-lotus-position-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-lotus-position.webp create mode 100644 public/flair/img/people.woman-in-manual-wheelchair-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-manual-wheelchair-facing-right-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-manual-wheelchair-facing-right-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-manual-wheelchair-facing-right-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-manual-wheelchair-facing-right-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-manual-wheelchair-facing-right-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-manual-wheelchair-facing-right.webp create mode 100644 public/flair/img/people.woman-in-manual-wheelchair-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-manual-wheelchair-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-manual-wheelchair-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-manual-wheelchair-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-manual-wheelchair.webp create mode 100644 public/flair/img/people.woman-in-motorized-wheelchair-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-motorized-wheelchair-facing-right-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-motorized-wheelchair-facing-right-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-motorized-wheelchair-facing-right-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-motorized-wheelchair-facing-right-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-motorized-wheelchair-facing-right-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-motorized-wheelchair-facing-right.webp create mode 100644 public/flair/img/people.woman-in-motorized-wheelchair-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-motorized-wheelchair-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-motorized-wheelchair-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-motorized-wheelchair-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-motorized-wheelchair.webp create mode 100644 public/flair/img/people.woman-in-steamy-room-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-steamy-room-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-steamy-room-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-steamy-room-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-steamy-room-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-steamy-room.webp create mode 100644 public/flair/img/people.woman-in-tuxedo-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-tuxedo-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-tuxedo-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-tuxedo-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-tuxedo-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-in-tuxedo.webp create mode 100644 public/flair/img/people.woman-judge-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-judge-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-judge-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-judge-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-judge-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-judge.webp create mode 100644 public/flair/img/people.woman-juggling-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-juggling-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-juggling-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-juggling-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-juggling-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-juggling.webp create mode 100644 public/flair/img/people.woman-kneeling-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-kneeling-facing-right-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-kneeling-facing-right-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-kneeling-facing-right-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-kneeling-facing-right-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-kneeling-facing-right-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-kneeling-facing-right.webp create mode 100644 public/flair/img/people.woman-kneeling-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-kneeling-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-kneeling-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-kneeling-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-kneeling.webp create mode 100644 public/flair/img/people.woman-lifting-weights-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-lifting-weights-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-lifting-weights-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-lifting-weights-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-lifting-weights-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-lifting-weights.webp create mode 100644 public/flair/img/people.woman-light-skin-tone-bald.webp create mode 100644 public/flair/img/people.woman-light-skin-tone-beard.webp create mode 100644 public/flair/img/people.woman-light-skin-tone-blond-hair.webp create mode 100644 public/flair/img/people.woman-light-skin-tone-curly-hair.webp create mode 100644 public/flair/img/people.woman-light-skin-tone-red-hair.webp create mode 100644 public/flair/img/people.woman-light-skin-tone-white-hair.webp create mode 100644 public/flair/img/people.woman-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-mage-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-mage-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-mage-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-mage-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-mage-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-mage.webp create mode 100644 public/flair/img/people.woman-mechanic-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-mechanic-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-mechanic-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-mechanic-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-mechanic-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-mechanic.webp create mode 100644 public/flair/img/people.woman-medium-dark-skin-tone-bald.webp create mode 100644 public/flair/img/people.woman-medium-dark-skin-tone-beard.webp create mode 100644 public/flair/img/people.woman-medium-dark-skin-tone-blond-hair.webp create mode 100644 public/flair/img/people.woman-medium-dark-skin-tone-curly-hair.webp create mode 100644 public/flair/img/people.woman-medium-dark-skin-tone-red-hair.webp create mode 100644 public/flair/img/people.woman-medium-dark-skin-tone-white-hair.webp create mode 100644 public/flair/img/people.woman-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-medium-light-skin-tone-bald.webp create mode 100644 public/flair/img/people.woman-medium-light-skin-tone-beard.webp create mode 100644 public/flair/img/people.woman-medium-light-skin-tone-blond-hair.webp create mode 100644 public/flair/img/people.woman-medium-light-skin-tone-curly-hair.webp create mode 100644 public/flair/img/people.woman-medium-light-skin-tone-red-hair.webp create mode 100644 public/flair/img/people.woman-medium-light-skin-tone-white-hair.webp create mode 100644 public/flair/img/people.woman-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-medium-skin-tone-bald.webp create mode 100644 public/flair/img/people.woman-medium-skin-tone-beard.webp create mode 100644 public/flair/img/people.woman-medium-skin-tone-blond-hair.webp create mode 100644 public/flair/img/people.woman-medium-skin-tone-curly-hair.webp create mode 100644 public/flair/img/people.woman-medium-skin-tone-red-hair.webp create mode 100644 public/flair/img/people.woman-medium-skin-tone-white-hair.webp create mode 100644 public/flair/img/people.woman-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-mountain-biking-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-mountain-biking-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-mountain-biking-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-mountain-biking-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-mountain-biking-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-mountain-biking.webp create mode 100644 public/flair/img/people.woman-office-worker-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-office-worker-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-office-worker-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-office-worker-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-office-worker-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-office-worker.webp create mode 100644 public/flair/img/people.woman-pilot-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-pilot-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-pilot-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-pilot-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-pilot-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-pilot.webp create mode 100644 public/flair/img/people.woman-playing-handball-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-playing-handball-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-playing-handball-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-playing-handball-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-playing-handball-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-playing-handball.webp create mode 100644 public/flair/img/people.woman-playing-water-polo-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-playing-water-polo-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-playing-water-polo-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-playing-water-polo-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-playing-water-polo-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-playing-water-polo.webp create mode 100644 public/flair/img/people.woman-police-officer-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-police-officer-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-police-officer-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-police-officer-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-police-officer-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-police-officer.webp create mode 100644 public/flair/img/people.woman-pouting-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-pouting-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-pouting-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-pouting-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-pouting-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-pouting.webp create mode 100644 public/flair/img/people.woman-raising-hand-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-raising-hand-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-raising-hand-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-raising-hand-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-raising-hand-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-raising-hand.webp create mode 100644 public/flair/img/people.woman-red-hair.webp create mode 100644 public/flair/img/people.woman-rowing-boat-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-rowing-boat-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-rowing-boat-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-rowing-boat-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-rowing-boat-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-rowing-boat.webp create mode 100644 public/flair/img/people.woman-running-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-running-facing-right-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-running-facing-right-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-running-facing-right-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-running-facing-right-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-running-facing-right-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-running-facing-right.webp create mode 100644 public/flair/img/people.woman-running-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-running-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-running-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-running-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-running.webp create mode 100644 public/flair/img/people.woman-scientist-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-scientist-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-scientist-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-scientist-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-scientist-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-scientist.webp create mode 100644 public/flair/img/people.woman-shrugging-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-shrugging-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-shrugging-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-shrugging-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-shrugging-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-shrugging.webp create mode 100644 public/flair/img/people.woman-singer-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-singer-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-singer-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-singer-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-singer-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-singer.webp create mode 100644 public/flair/img/people.woman-standing-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-standing-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-standing-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-standing-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-standing-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-standing.webp create mode 100644 public/flair/img/people.woman-student-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-student-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-student-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-student-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-student-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-student.webp create mode 100644 public/flair/img/people.woman-superhero-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-superhero-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-superhero-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-superhero-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-superhero-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-superhero.webp create mode 100644 public/flair/img/people.woman-supervillain-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-supervillain-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-supervillain-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-supervillain-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-supervillain-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-supervillain.webp create mode 100644 public/flair/img/people.woman-surfing-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-surfing-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-surfing-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-surfing-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-surfing-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-surfing.webp create mode 100644 public/flair/img/people.woman-swimming-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-swimming-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-swimming-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-swimming-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-swimming-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-swimming.webp create mode 100644 public/flair/img/people.woman-teacher-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-teacher-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-teacher-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-teacher-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-teacher-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-teacher.webp create mode 100644 public/flair/img/people.woman-technologist-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-technologist-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-technologist-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-technologist-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-technologist-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-technologist.webp create mode 100644 public/flair/img/people.woman-tipping-hand-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-tipping-hand-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-tipping-hand-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-tipping-hand-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-tipping-hand-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-tipping-hand.webp create mode 100644 public/flair/img/people.woman-vampire-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-vampire-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-vampire-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-vampire-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-vampire-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-vampire.webp create mode 100644 public/flair/img/people.woman-walking-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-walking-facing-right-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-walking-facing-right-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-walking-facing-right-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-walking-facing-right-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-walking-facing-right-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-walking-facing-right.webp create mode 100644 public/flair/img/people.woman-walking-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-walking-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-walking-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-walking-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-walking.webp create mode 100644 public/flair/img/people.woman-wearing-turban-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-wearing-turban-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-wearing-turban-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-wearing-turban-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-wearing-turban-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-wearing-turban.webp create mode 100644 public/flair/img/people.woman-white-hair.webp create mode 100644 public/flair/img/people.woman-with-veil-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-with-veil-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-with-veil-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-with-veil-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-with-veil-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-with-veil.webp create mode 100644 public/flair/img/people.woman-with-white-cane-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-with-white-cane-facing-right-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-with-white-cane-facing-right-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-with-white-cane-facing-right-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-with-white-cane-facing-right-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-with-white-cane-facing-right-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-with-white-cane-facing-right.webp create mode 100644 public/flair/img/people.woman-with-white-cane-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-with-white-cane-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.woman-with-white-cane-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.woman-with-white-cane-medium-skin-tone.webp create mode 100644 public/flair/img/people.woman-with-white-cane.webp create mode 100644 public/flair/img/people.woman-zombie.webp create mode 100644 public/flair/img/people.woman.webp create mode 100644 public/flair/img/people.women-holding-hands-dark-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-dark-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-dark-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-dark-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-dark-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-light-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-light-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-light-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-light-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-light-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-medium-dark-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-medium-dark-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-medium-dark-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-medium-dark-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-medium-light-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-medium-light-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-medium-light-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-medium-light-skin-tone-medium-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-medium-skin-tone-dark-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-medium-skin-tone-light-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-medium-skin-tone-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-medium-skin-tone-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands-medium-skin-tone.webp create mode 100644 public/flair/img/people.women-holding-hands.webp create mode 100644 public/flair/img/people.women-with-bunny-ears.webp create mode 100644 public/flair/img/people.women-wrestling.webp create mode 100644 public/flair/img/people.writing-hand-dark-skin-tone.webp create mode 100644 public/flair/img/people.writing-hand-light-skin-tone.webp create mode 100644 public/flair/img/people.writing-hand-medium-dark-skin-tone.webp create mode 100644 public/flair/img/people.writing-hand-medium-light-skin-tone.webp create mode 100644 public/flair/img/people.writing-hand-medium-skin-tone.webp create mode 100644 public/flair/img/people.writing-hand.webp create mode 100644 public/flair/img/people.zombie.webp create mode 100644 public/flair/img/smileys.alien-monster.webp create mode 100644 public/flair/img/smileys.alien.webp create mode 100644 public/flair/img/smileys.angry-face-with-horns.webp create mode 100644 public/flair/img/smileys.angry-face.webp create mode 100644 public/flair/img/smileys.anguished-face.webp create mode 100644 public/flair/img/smileys.anxious-face-with-sweat.webp create mode 100644 public/flair/img/smileys.astonished-face.webp create mode 100644 public/flair/img/smileys.beaming-face-with-smiling-eyes.webp create mode 100644 public/flair/img/smileys.cat-with-tears-of-joy.webp create mode 100644 public/flair/img/smileys.cat-with-wry-smile.webp create mode 100644 public/flair/img/smileys.clown-face.webp create mode 100644 public/flair/img/smileys.cold-face.webp create mode 100644 public/flair/img/smileys.confounded-face.webp create mode 100644 public/flair/img/smileys.confused-face.webp create mode 100644 public/flair/img/smileys.cowboy-hat-face.webp create mode 100644 public/flair/img/smileys.crying-cat.webp create mode 100644 public/flair/img/smileys.crying-face.webp create mode 100644 public/flair/img/smileys.disappointed-face.webp create mode 100644 public/flair/img/smileys.disguised-face.webp create mode 100644 public/flair/img/smileys.dizzy-face.webp create mode 100644 public/flair/img/smileys.dotted-line-face.webp create mode 100644 public/flair/img/smileys.downcast-face-with-sweat.webp create mode 100644 public/flair/img/smileys.drooling-face.webp create mode 100644 public/flair/img/smileys.exploding-head.webp create mode 100644 public/flair/img/smileys.expressionless-face.webp create mode 100644 public/flair/img/smileys.face-blowing-a-kiss.webp create mode 100644 public/flair/img/smileys.face-exhaling.webp create mode 100644 public/flair/img/smileys.face-holding-back-tears.webp create mode 100644 public/flair/img/smileys.face-in-clouds.webp create mode 100644 public/flair/img/smileys.face-savoring-food.webp create mode 100644 public/flair/img/smileys.face-screaming-in-fear.webp create mode 100644 public/flair/img/smileys.face-with-diagonal-mouth.webp create mode 100644 public/flair/img/smileys.face-with-hand-over-mouth.webp create mode 100644 public/flair/img/smileys.face-with-head-bandage.webp create mode 100644 public/flair/img/smileys.face-with-medical-mask.webp create mode 100644 public/flair/img/smileys.face-with-monocle.webp create mode 100644 public/flair/img/smileys.face-with-open-eyes-and-hand-over-mouth.webp create mode 100644 public/flair/img/smileys.face-with-open-mouth.webp create mode 100644 public/flair/img/smileys.face-with-peeking-eye.webp create mode 100644 public/flair/img/smileys.face-with-raised-eyebrow.webp create mode 100644 public/flair/img/smileys.face-with-rolling-eyes.webp create mode 100644 public/flair/img/smileys.face-with-spiral-eyes.webp create mode 100644 public/flair/img/smileys.face-with-steam-from-nose.webp create mode 100644 public/flair/img/smileys.face-with-symbols-on-mouth.webp create mode 100644 public/flair/img/smileys.face-with-tears-of-joy.webp create mode 100644 public/flair/img/smileys.face-with-thermometer.webp create mode 100644 public/flair/img/smileys.face-with-tongue.webp create mode 100644 public/flair/img/smileys.face-without-mouth.webp create mode 100644 public/flair/img/smileys.fearful-face.webp create mode 100644 public/flair/img/smileys.flushed-face.webp create mode 100644 public/flair/img/smileys.frowning-face-with-open-mouth.webp create mode 100644 public/flair/img/smileys.frowning-face.webp create mode 100644 public/flair/img/smileys.ghost.webp create mode 100644 public/flair/img/smileys.goblin.webp create mode 100644 public/flair/img/smileys.grimacing-face.webp create mode 100644 public/flair/img/smileys.grinning-cat-with-smiling-eyes.webp create mode 100644 public/flair/img/smileys.grinning-cat.webp create mode 100644 public/flair/img/smileys.grinning-face-with-big-eyes.webp create mode 100644 public/flair/img/smileys.grinning-face-with-smiling-eyes.webp create mode 100644 public/flair/img/smileys.grinning-face-with-sweat.webp create mode 100644 public/flair/img/smileys.grinning-face.webp create mode 100644 public/flair/img/smileys.grinning-squinting-face.webp create mode 100644 public/flair/img/smileys.head-shaking-horizontally.webp create mode 100644 public/flair/img/smileys.head-shaking-vertically.webp create mode 100644 public/flair/img/smileys.hear-no-evil-monkey.webp create mode 100644 public/flair/img/smileys.hot-face.webp create mode 100644 public/flair/img/smileys.hugging-face.webp create mode 100644 public/flair/img/smileys.hushed-face.webp create mode 100644 public/flair/img/smileys.kissing-cat.webp create mode 100644 public/flair/img/smileys.kissing-face-with-closed-eyes.webp create mode 100644 public/flair/img/smileys.kissing-face-with-smiling-eyes.webp create mode 100644 public/flair/img/smileys.kissing-face.webp create mode 100644 public/flair/img/smileys.loudly-crying-face.webp create mode 100644 public/flair/img/smileys.lying-face.webp create mode 100644 public/flair/img/smileys.melting-face.webp create mode 100644 public/flair/img/smileys.money-mouth-face.webp create mode 100644 public/flair/img/smileys.nauseated-face.webp create mode 100644 public/flair/img/smileys.nerd-face.webp create mode 100644 public/flair/img/smileys.neutral-face.webp create mode 100644 public/flair/img/smileys.ogre.webp create mode 100644 public/flair/img/smileys.partying-face.webp create mode 100644 public/flair/img/smileys.pensive-face.webp create mode 100644 public/flair/img/smileys.persevering-face.webp create mode 100644 public/flair/img/smileys.pleading-face.webp create mode 100644 public/flair/img/smileys.pouting-cat.webp create mode 100644 public/flair/img/smileys.pouting-face.webp create mode 100644 public/flair/img/smileys.relieved-face.webp create mode 100644 public/flair/img/smileys.robot.webp create mode 100644 public/flair/img/smileys.rolling-on-the-floor-laughing.webp create mode 100644 public/flair/img/smileys.sad-but-relieved-face.webp create mode 100644 public/flair/img/smileys.saluting-face.webp create mode 100644 public/flair/img/smileys.see-no-evil-monkey.webp create mode 100644 public/flair/img/smileys.shaking-face.webp create mode 100644 public/flair/img/smileys.shushing-face.webp create mode 100644 public/flair/img/smileys.skull-and-crossbones.webp create mode 100644 public/flair/img/smileys.skull.webp create mode 100644 public/flair/img/smileys.sleeping-face.webp create mode 100644 public/flair/img/smileys.sleepy-face.webp create mode 100644 public/flair/img/smileys.slightly-frowning-face.webp create mode 100644 public/flair/img/smileys.slightly-smiling-face.webp create mode 100644 public/flair/img/smileys.smiling-cat-with-heart-eyes.webp create mode 100644 public/flair/img/smileys.smiling-face-with-halo.webp create mode 100644 public/flair/img/smileys.smiling-face-with-heart-eyes.webp create mode 100644 public/flair/img/smileys.smiling-face-with-hearts.webp create mode 100644 public/flair/img/smileys.smiling-face-with-horns.webp create mode 100644 public/flair/img/smileys.smiling-face-with-smiling-eyes.webp create mode 100644 public/flair/img/smileys.smiling-face-with-sunglasses.webp create mode 100644 public/flair/img/smileys.smiling-face-with-tear.webp create mode 100644 public/flair/img/smileys.smiling-face.webp create mode 100644 public/flair/img/smileys.smirking-face.webp create mode 100644 public/flair/img/smileys.sneezing-face.webp create mode 100644 public/flair/img/smileys.speak-no-evil-monkey.webp create mode 100644 public/flair/img/smileys.squinting-face-with-tongue.webp create mode 100644 public/flair/img/smileys.star-struck.webp create mode 100644 public/flair/img/smileys.thinking-face.webp create mode 100644 public/flair/img/smileys.tired-face.webp create mode 100644 public/flair/img/smileys.unamused-face.webp create mode 100644 public/flair/img/smileys.upside-down-face.webp create mode 100644 public/flair/img/smileys.weary-cat.webp create mode 100644 public/flair/img/smileys.weary-face.webp create mode 100644 public/flair/img/smileys.winking-face-with-tongue.webp create mode 100644 public/flair/img/smileys.winking-face.webp create mode 100644 public/flair/img/smileys.woozy-face.webp create mode 100644 public/flair/img/smileys.worried-face.webp create mode 100644 public/flair/img/smileys.yawning-face.webp create mode 100644 public/flair/img/smileys.zany-face.webp create mode 100644 public/flair/img/smileys.zipper-mouth-face.webp create mode 100644 public/flair/img/symbols.a-button-blood-type.webp create mode 100644 public/flair/img/symbols.ab-button-blood-type.webp create mode 100644 public/flair/img/symbols.anger-symbol.webp create mode 100644 public/flair/img/symbols.antenna-bars.webp create mode 100644 public/flair/img/symbols.aquarius.webp create mode 100644 public/flair/img/symbols.aries.webp create mode 100644 public/flair/img/symbols.asterisk.webp create mode 100644 public/flair/img/symbols.atm-sign.webp create mode 100644 public/flair/img/symbols.atom-symbol.webp create mode 100644 public/flair/img/symbols.b-button-blood-type.webp create mode 100644 public/flair/img/symbols.baby-symbol.webp create mode 100644 public/flair/img/symbols.back-arrow.webp create mode 100644 public/flair/img/symbols.baggage-claim.webp create mode 100644 public/flair/img/symbols.beating-heart.webp create mode 100644 public/flair/img/symbols.black-circle.webp create mode 100644 public/flair/img/symbols.black-flag.webp create mode 100644 public/flair/img/symbols.black-heart.webp create mode 100644 public/flair/img/symbols.black-large-square.webp create mode 100644 public/flair/img/symbols.black-medium-small-square.webp create mode 100644 public/flair/img/symbols.black-square-button.webp create mode 100644 public/flair/img/symbols.blue-heart.webp create mode 100644 public/flair/img/symbols.bright-button.webp create mode 100644 public/flair/img/symbols.broken-heart.webp create mode 100644 public/flair/img/symbols.brown-heart.webp create mode 100644 public/flair/img/symbols.cancer.webp create mode 100644 public/flair/img/symbols.capricorn.webp create mode 100644 public/flair/img/symbols.chequered-flag.webp create mode 100644 public/flair/img/symbols.children-crossing.webp create mode 100644 public/flair/img/symbols.cinema.webp create mode 100644 public/flair/img/symbols.circled-m.webp create mode 100644 public/flair/img/symbols.cl-button.webp create mode 100644 public/flair/img/symbols.clockwise-vertical-arrows.webp create mode 100644 public/flair/img/symbols.collision.webp create mode 100644 public/flair/img/symbols.combining-enclosing-keycap.webp create mode 100644 public/flair/img/symbols.cool-button.webp create mode 100644 public/flair/img/symbols.copyright.webp create mode 100644 public/flair/img/symbols.counterclockwise-arrows-button.webp create mode 100644 public/flair/img/symbols.cross-mark-button.webp create mode 100644 public/flair/img/symbols.cross-mark.webp create mode 100644 public/flair/img/symbols.crossed-flags.webp create mode 100644 public/flair/img/symbols.curly-loop.webp create mode 100644 public/flair/img/symbols.currency-exchange.webp create mode 100644 public/flair/img/symbols.customs.webp create mode 100644 public/flair/img/symbols.dashing-away.webp create mode 100644 public/flair/img/symbols.diamond-with-a-dot.webp create mode 100644 public/flair/img/symbols.digit-eight.webp create mode 100644 public/flair/img/symbols.digit-five.webp create mode 100644 public/flair/img/symbols.digit-four.webp create mode 100644 public/flair/img/symbols.digit-nine.webp create mode 100644 public/flair/img/symbols.digit-one.webp create mode 100644 public/flair/img/symbols.digit-seven.webp create mode 100644 public/flair/img/symbols.digit-six.webp create mode 100644 public/flair/img/symbols.digit-three.webp create mode 100644 public/flair/img/symbols.digit-two.webp create mode 100644 public/flair/img/symbols.digit-zero.webp create mode 100644 public/flair/img/symbols.dim-button.webp create mode 100644 public/flair/img/symbols.divide.webp create mode 100644 public/flair/img/symbols.dizzy.webp create mode 100644 public/flair/img/symbols.double-curly-loop.webp create mode 100644 public/flair/img/symbols.double-exclamation-mark.webp create mode 100644 public/flair/img/symbols.down-arrow.webp create mode 100644 public/flair/img/symbols.down-left-arrow.webp create mode 100644 public/flair/img/symbols.down-right-arrow.webp create mode 100644 public/flair/img/symbols.downwards-button.webp create mode 100644 public/flair/img/symbols.eight-pointed-star.webp create mode 100644 public/flair/img/symbols.eight-spoked-asterisk.webp create mode 100644 public/flair/img/symbols.eject-button.webp create mode 100644 public/flair/img/symbols.elevator.webp create mode 100644 public/flair/img/symbols.end-arrow.webp create mode 100644 public/flair/img/symbols.esperanto-flag.webp create mode 100644 public/flair/img/symbols.esperanto.webp create mode 100644 public/flair/img/symbols.exclamation-mark.webp create mode 100644 public/flair/img/symbols.exclamation-question-mark.webp create mode 100644 public/flair/img/symbols.extinction.webp create mode 100644 public/flair/img/symbols.eye-in-speech-bubble.webp create mode 100644 public/flair/img/symbols.fast-down-button.webp create mode 100644 public/flair/img/symbols.fast-forward-button.webp create mode 100644 public/flair/img/symbols.fast-reverse-button.webp create mode 100644 public/flair/img/symbols.fast-up-button.webp create mode 100644 public/flair/img/symbols.female-sign.webp create mode 100644 public/flair/img/symbols.fleur-de-lis.webp create mode 100644 public/flair/img/symbols.free-button.webp create mode 100644 public/flair/img/symbols.gemini.webp create mode 100644 public/flair/img/symbols.gnu-logo.webp create mode 100644 public/flair/img/symbols.green-heart.webp create mode 100644 public/flair/img/symbols.grey-heart.webp create mode 100644 public/flair/img/symbols.growing-heart.webp create mode 100644 public/flair/img/symbols.heart-decoration.webp create mode 100644 public/flair/img/symbols.heart-exclamation.webp create mode 100644 public/flair/img/symbols.heart-on-fire.webp create mode 100644 public/flair/img/symbols.heart-with-arrow.webp create mode 100644 public/flair/img/symbols.heart-with-ribbon.webp create mode 100644 public/flair/img/symbols.heavy-dollar-sign.webp create mode 100644 public/flair/img/symbols.heavy-equals-sign.webp create mode 100644 public/flair/img/symbols.hole.webp create mode 100644 public/flair/img/symbols.hollow-red-circle.webp create mode 100644 public/flair/img/symbols.hundred-points.webp create mode 100644 public/flair/img/symbols.id-button.webp create mode 100644 public/flair/img/symbols.infinity.webp create mode 100644 public/flair/img/symbols.information.webp create mode 100644 public/flair/img/symbols.input-latin-letters.webp create mode 100644 public/flair/img/symbols.input-latin-lowercase.webp create mode 100644 public/flair/img/symbols.input-latin-uppercase.webp create mode 100644 public/flair/img/symbols.input-numbers.webp create mode 100644 public/flair/img/symbols.input-symbols.webp create mode 100644 public/flair/img/symbols.japanese-acceptable-button.webp create mode 100644 public/flair/img/symbols.japanese-application-button.webp create mode 100644 public/flair/img/symbols.japanese-bargain-button.webp create mode 100644 public/flair/img/symbols.japanese-congratulations-button.webp create mode 100644 public/flair/img/symbols.japanese-discount-button.webp create mode 100644 public/flair/img/symbols.japanese-free-of-charge-button.webp create mode 100644 public/flair/img/symbols.japanese-here-button.webp create mode 100644 public/flair/img/symbols.japanese-monthly-amount-button.webp create mode 100644 public/flair/img/symbols.japanese-no-vacancy-button.webp create mode 100644 public/flair/img/symbols.japanese-not-free-of-charge-button.webp create mode 100644 public/flair/img/symbols.japanese-open-for-business-button.webp create mode 100644 public/flair/img/symbols.japanese-passing-grade-button.webp create mode 100644 public/flair/img/symbols.japanese-prohibited-button.webp create mode 100644 public/flair/img/symbols.japanese-reserved-button.webp create mode 100644 public/flair/img/symbols.japanese-secret-button.webp create mode 100644 public/flair/img/symbols.japanese-service-charge-button.webp create mode 100644 public/flair/img/symbols.japanese-symbol-for-beginner.webp create mode 100644 public/flair/img/symbols.japanese-vacancy-button.webp create mode 100644 public/flair/img/symbols.keycap-10.webp create mode 100644 public/flair/img/symbols.keycap-asterisk.webp create mode 100644 public/flair/img/symbols.keycap-digit-eight.webp create mode 100644 public/flair/img/symbols.keycap-digit-five.webp create mode 100644 public/flair/img/symbols.keycap-digit-four.webp create mode 100644 public/flair/img/symbols.keycap-digit-nine.webp create mode 100644 public/flair/img/symbols.keycap-digit-one.webp create mode 100644 public/flair/img/symbols.keycap-digit-seven.webp create mode 100644 public/flair/img/symbols.keycap-digit-six.webp create mode 100644 public/flair/img/symbols.keycap-digit-three.webp create mode 100644 public/flair/img/symbols.keycap-digit-two.webp create mode 100644 public/flair/img/symbols.keycap-digit-zero.webp create mode 100644 public/flair/img/symbols.keycap-number-sign.webp create mode 100644 public/flair/img/symbols.kiss-mark.webp create mode 100644 public/flair/img/symbols.large-blue-circle.webp create mode 100644 public/flair/img/symbols.large-blue-diamond.webp create mode 100644 public/flair/img/symbols.large-blue-square.webp create mode 100644 public/flair/img/symbols.large-brown-circle.webp create mode 100644 public/flair/img/symbols.large-brown-square.webp create mode 100644 public/flair/img/symbols.large-green-circle.webp create mode 100644 public/flair/img/symbols.large-green-square.webp create mode 100644 public/flair/img/symbols.large-orange-circle.webp create mode 100644 public/flair/img/symbols.large-orange-diamond.webp create mode 100644 public/flair/img/symbols.large-orange-square.webp create mode 100644 public/flair/img/symbols.large-purple-circle.webp create mode 100644 public/flair/img/symbols.large-purple-square.webp create mode 100644 public/flair/img/symbols.large-red-circle.webp create mode 100644 public/flair/img/symbols.large-red-square.webp create mode 100644 public/flair/img/symbols.large-yellow-circle.webp create mode 100644 public/flair/img/symbols.large-yellow-square.webp create mode 100644 public/flair/img/symbols.last-track-button.webp create mode 100644 public/flair/img/symbols.left-arrow-curving-right.webp create mode 100644 public/flair/img/symbols.left-arrow.webp create mode 100644 public/flair/img/symbols.left-luggage.webp create mode 100644 public/flair/img/symbols.left-right-arrow.webp create mode 100644 public/flair/img/symbols.left-speech-bubble.webp create mode 100644 public/flair/img/symbols.leo.webp create mode 100644 public/flair/img/symbols.libra.webp create mode 100644 public/flair/img/symbols.lichess-4545-flag.webp create mode 100644 public/flair/img/symbols.light-blue-heart.webp create mode 100644 public/flair/img/symbols.linux-tux-penguin.webp create mode 100644 public/flair/img/symbols.litter-in-bin-sign.webp create mode 100644 public/flair/img/symbols.love-letter.webp create mode 100644 public/flair/img/symbols.male-sign.webp create mode 100644 public/flair/img/symbols.medical-symbol.webp create mode 100644 public/flair/img/symbols.mending-heart.webp create mode 100644 public/flair/img/symbols.mens-room.webp create mode 100644 public/flair/img/symbols.minus.webp create mode 100644 public/flair/img/symbols.mobile-phone-off.webp create mode 100644 public/flair/img/symbols.move-blunder.webp create mode 100644 public/flair/img/symbols.move-brilliant.webp create mode 100644 public/flair/img/symbols.move-dubious.webp create mode 100644 public/flair/img/symbols.move-good.webp create mode 100644 public/flair/img/symbols.move-interesting.webp create mode 100644 public/flair/img/symbols.move-mistake.webp create mode 100644 public/flair/img/symbols.multiply.webp create mode 100644 public/flair/img/symbols.name-badge.webp create mode 100644 public/flair/img/symbols.new-button.webp create mode 100644 public/flair/img/symbols.next-track-button.webp create mode 100644 public/flair/img/symbols.ng-button.webp create mode 100644 public/flair/img/symbols.no-bicycles.webp create mode 100644 public/flair/img/symbols.no-entry.webp create mode 100644 public/flair/img/symbols.no-littering.webp create mode 100644 public/flair/img/symbols.no-mobile-phones.webp create mode 100644 public/flair/img/symbols.no-one-under-eighteen.webp create mode 100644 public/flair/img/symbols.no-pedestrians.webp create mode 100644 public/flair/img/symbols.no-smoking.webp create mode 100644 public/flair/img/symbols.non-potable-water.webp create mode 100644 public/flair/img/symbols.number-sign.webp create mode 100644 public/flair/img/symbols.o-button-blood-type.webp create mode 100644 public/flair/img/symbols.ok-button.webp create mode 100644 public/flair/img/symbols.on-arrow.webp create mode 100644 public/flair/img/symbols.ophiuchus.webp create mode 100644 public/flair/img/symbols.orange-heart.webp create mode 100644 public/flair/img/symbols.p-button.webp create mode 100644 public/flair/img/symbols.part-alternation-mark.webp create mode 100644 public/flair/img/symbols.passport-control.webp create mode 100644 public/flair/img/symbols.pause-button.webp create mode 100644 public/flair/img/symbols.peace-symbol.webp create mode 100644 public/flair/img/symbols.pink-heart.webp create mode 100644 public/flair/img/symbols.pirate-flag.webp create mode 100644 public/flair/img/symbols.pisces.webp create mode 100644 public/flair/img/symbols.play-button.webp create mode 100644 public/flair/img/symbols.play-or-pause-button.webp create mode 100644 public/flair/img/symbols.plus.webp create mode 100644 public/flair/img/symbols.potable-water.webp create mode 100644 public/flair/img/symbols.purple-heart.webp create mode 100644 public/flair/img/symbols.puzzle-racer.webp create mode 100644 public/flair/img/symbols.puzzle-storm.webp create mode 100644 public/flair/img/symbols.puzzle-streak.webp create mode 100644 public/flair/img/symbols.question-mark.webp create mode 100644 public/flair/img/symbols.radio-button.webp create mode 100644 public/flair/img/symbols.rainbow-flag.webp create mode 100644 public/flair/img/symbols.record-button.webp create mode 100644 public/flair/img/symbols.recycling-symbol.webp create mode 100644 public/flair/img/symbols.red-heart.webp create mode 100644 public/flair/img/symbols.red-triangle-pointed-down.webp create mode 100644 public/flair/img/symbols.red-triangle-pointed-up.webp create mode 100644 public/flair/img/symbols.registered.webp create mode 100644 public/flair/img/symbols.repeat-button.webp create mode 100644 public/flair/img/symbols.repeat-single-button.webp create mode 100644 public/flair/img/symbols.restroom.webp create mode 100644 public/flair/img/symbols.reverse-button.webp create mode 100644 public/flair/img/symbols.revolving-hearts.webp create mode 100644 public/flair/img/symbols.right-anger-bubble.webp create mode 100644 public/flair/img/symbols.right-arrow-curving-down.webp create mode 100644 public/flair/img/symbols.right-arrow-curving-left.webp create mode 100644 public/flair/img/symbols.right-arrow-curving-up.webp create mode 100644 public/flair/img/symbols.right-arrow.webp create mode 100644 public/flair/img/symbols.sagittarius.webp create mode 100644 public/flair/img/symbols.scorpio.webp create mode 100644 public/flair/img/symbols.shuffle-tracks-button.webp create mode 100644 public/flair/img/symbols.small-blue-diamond.webp create mode 100644 public/flair/img/symbols.small-orange-diamond.webp create mode 100644 public/flair/img/symbols.soon-arrow.webp create mode 100644 public/flair/img/symbols.sos-button.webp create mode 100644 public/flair/img/symbols.sparkle.webp create mode 100644 public/flair/img/symbols.sparkling-heart.webp create mode 100644 public/flair/img/symbols.speech-balloon.webp create mode 100644 public/flair/img/symbols.stop-button.webp create mode 100644 public/flair/img/symbols.taurus.webp create mode 100644 public/flair/img/symbols.thought-balloon.webp create mode 100644 public/flair/img/symbols.top-arrow.webp create mode 100644 public/flair/img/symbols.trade-mark.webp create mode 100644 public/flair/img/symbols.transgender-flag.webp create mode 100644 public/flair/img/symbols.transgender-symbol.webp create mode 100644 public/flair/img/symbols.triangular-flag.webp create mode 100644 public/flair/img/symbols.trident-emblem.webp create mode 100644 public/flair/img/symbols.two-hearts.webp create mode 100644 public/flair/img/symbols.up-arrow.webp create mode 100644 public/flair/img/symbols.up-button.webp create mode 100644 public/flair/img/symbols.up-down-arrow.webp create mode 100644 public/flair/img/symbols.up-left-arrow.webp create mode 100644 public/flair/img/symbols.up-right-arrow.webp create mode 100644 public/flair/img/symbols.upwards-button.webp create mode 100644 public/flair/img/symbols.vibration-mode.webp create mode 100644 public/flair/img/symbols.vim-logo.webp create mode 100644 public/flair/img/symbols.virgo.webp create mode 100644 public/flair/img/symbols.vs-button.webp create mode 100644 public/flair/img/symbols.water-closet.webp create mode 100644 public/flair/img/symbols.wavy-dash.webp create mode 100644 public/flair/img/symbols.wheelchair-symbol.webp create mode 100644 public/flair/img/symbols.white-circle.webp create mode 100644 public/flair/img/symbols.white-exclamation-mark.webp create mode 100644 public/flair/img/symbols.white-flag.webp create mode 100644 public/flair/img/symbols.white-heart.webp create mode 100644 public/flair/img/symbols.white-large-square.webp create mode 100644 public/flair/img/symbols.white-medium-small-square.webp create mode 100644 public/flair/img/symbols.white-question-mark.webp create mode 100644 public/flair/img/symbols.white-square-button.webp create mode 100644 public/flair/img/symbols.white-star.webp create mode 100644 public/flair/img/symbols.wireless.webp create mode 100644 public/flair/img/symbols.womens-room.webp create mode 100644 public/flair/img/symbols.yellow-heart.webp create mode 100644 public/flair/img/symbols.zzz.webp create mode 100644 public/flair/img/travel-places.aerial-tramway.webp create mode 100644 public/flair/img/travel-places.airplane-arrival.webp create mode 100644 public/flair/img/travel-places.airplane-departure.webp create mode 100644 public/flair/img/travel-places.airplane.webp create mode 100644 public/flair/img/travel-places.ambulance.webp create mode 100644 public/flair/img/travel-places.anchor.webp create mode 100644 public/flair/img/travel-places.articulated-lorry.webp create mode 100644 public/flair/img/travel-places.auto-rickshaw.webp create mode 100644 public/flair/img/travel-places.automobile.webp create mode 100644 public/flair/img/travel-places.bank.webp create mode 100644 public/flair/img/travel-places.barber-pole.webp create mode 100644 public/flair/img/travel-places.beach-with-umbrella.webp create mode 100644 public/flair/img/travel-places.bicycle.webp create mode 100644 public/flair/img/travel-places.brick.webp create mode 100644 public/flair/img/travel-places.bridge-at-night.webp create mode 100644 public/flair/img/travel-places.building-construction.webp create mode 100644 public/flair/img/travel-places.bullet-train.webp create mode 100644 public/flair/img/travel-places.bus-stop.webp create mode 100644 public/flair/img/travel-places.bus.webp create mode 100644 public/flair/img/travel-places.camping.webp create mode 100644 public/flair/img/travel-places.canoe.webp create mode 100644 public/flair/img/travel-places.carousel-horse.webp create mode 100644 public/flair/img/travel-places.castle.webp create mode 100644 public/flair/img/travel-places.church.webp create mode 100644 public/flair/img/travel-places.circus-tent.webp create mode 100644 public/flair/img/travel-places.cityscape-at-dusk.webp create mode 100644 public/flair/img/travel-places.cityscape.webp create mode 100644 public/flair/img/travel-places.classical-building.webp create mode 100644 public/flair/img/travel-places.compass.webp create mode 100644 public/flair/img/travel-places.construction.webp create mode 100644 public/flair/img/travel-places.convenience-store.webp create mode 100644 public/flair/img/travel-places.delivery-truck.webp create mode 100644 public/flair/img/travel-places.department-store.webp create mode 100644 public/flair/img/travel-places.derelict-house.webp create mode 100644 public/flair/img/travel-places.desert-island.webp create mode 100644 public/flair/img/travel-places.desert.webp create mode 100644 public/flair/img/travel-places.earth-blue.webp create mode 100644 public/flair/img/travel-places.factory.webp create mode 100644 public/flair/img/travel-places.ferris-wheel.webp create mode 100644 public/flair/img/travel-places.ferry.webp create mode 100644 public/flair/img/travel-places.fire-engine.webp create mode 100644 public/flair/img/travel-places.flying-saucer.webp create mode 100644 public/flair/img/travel-places.foggy.webp create mode 100644 public/flair/img/travel-places.fountain.webp create mode 100644 public/flair/img/travel-places.fuel-pump.webp create mode 100644 public/flair/img/travel-places.globe-showing-americas.webp create mode 100644 public/flair/img/travel-places.globe-showing-asia-australia.webp create mode 100644 public/flair/img/travel-places.globe-showing-europe-africa.webp create mode 100644 public/flair/img/travel-places.globe-with-meridians.webp create mode 100644 public/flair/img/travel-places.helicopter.webp create mode 100644 public/flair/img/travel-places.high-speed-train.webp create mode 100644 public/flair/img/travel-places.hindu-temple.webp create mode 100644 public/flair/img/travel-places.horizontal-traffic-light.webp create mode 100644 public/flair/img/travel-places.hospital.webp create mode 100644 public/flair/img/travel-places.hot-springs.webp create mode 100644 public/flair/img/travel-places.hotel.webp create mode 100644 public/flair/img/travel-places.house-with-garden.webp create mode 100644 public/flair/img/travel-places.house.webp create mode 100644 public/flair/img/travel-places.houses.webp create mode 100644 public/flair/img/travel-places.hut.webp create mode 100644 public/flair/img/travel-places.japanese-castle.webp create mode 100644 public/flair/img/travel-places.japanese-post-office.webp create mode 100644 public/flair/img/travel-places.kaaba.webp create mode 100644 public/flair/img/travel-places.kick-scooter.webp create mode 100644 public/flair/img/travel-places.light-rail.webp create mode 100644 public/flair/img/travel-places.locomotive.webp create mode 100644 public/flair/img/travel-places.love-hotel.webp create mode 100644 public/flair/img/travel-places.manual-wheelchair.webp create mode 100644 public/flair/img/travel-places.map-of-japan.webp create mode 100644 public/flair/img/travel-places.metro.webp create mode 100644 public/flair/img/travel-places.minibus.webp create mode 100644 public/flair/img/travel-places.moai.webp create mode 100644 public/flair/img/travel-places.monorail.webp create mode 100644 public/flair/img/travel-places.mosque.webp create mode 100644 public/flair/img/travel-places.motor-boat.webp create mode 100644 public/flair/img/travel-places.motor-scooter.webp create mode 100644 public/flair/img/travel-places.motorcycle.webp create mode 100644 public/flair/img/travel-places.motorized-wheelchair.webp create mode 100644 public/flair/img/travel-places.motorway.webp create mode 100644 public/flair/img/travel-places.mount-fuji.webp create mode 100644 public/flair/img/travel-places.mountain-cableway.webp create mode 100644 public/flair/img/travel-places.mountain-railway.webp create mode 100644 public/flair/img/travel-places.mountain.webp create mode 100644 public/flair/img/travel-places.national-park.webp create mode 100644 public/flair/img/travel-places.night-with-stars.webp create mode 100644 public/flair/img/travel-places.office-building.webp create mode 100644 public/flair/img/travel-places.oil-drum.webp create mode 100644 public/flair/img/travel-places.oncoming-automobile.webp create mode 100644 public/flair/img/travel-places.oncoming-bus.webp create mode 100644 public/flair/img/travel-places.oncoming-police-car.webp create mode 100644 public/flair/img/travel-places.oncoming-taxi.webp create mode 100644 public/flair/img/travel-places.parachute.webp create mode 100644 public/flair/img/travel-places.passenger-ship.webp create mode 100644 public/flair/img/travel-places.pickup-truck.webp create mode 100644 public/flair/img/travel-places.playground-slide.webp create mode 100644 public/flair/img/travel-places.police-car-light.webp create mode 100644 public/flair/img/travel-places.police-car.webp create mode 100644 public/flair/img/travel-places.post-office.webp create mode 100644 public/flair/img/travel-places.racing-car.webp create mode 100644 public/flair/img/travel-places.railway-car.webp create mode 100644 public/flair/img/travel-places.railway-track.webp create mode 100644 public/flair/img/travel-places.ring-buoy.webp create mode 100644 public/flair/img/travel-places.rocket.webp create mode 100644 public/flair/img/travel-places.roller-coaster.webp create mode 100644 public/flair/img/travel-places.roller-skate.webp create mode 100644 public/flair/img/travel-places.sailboat.webp create mode 100644 public/flair/img/travel-places.satellite.webp create mode 100644 public/flair/img/travel-places.school.webp create mode 100644 public/flair/img/travel-places.seat.webp create mode 100644 public/flair/img/travel-places.shinto-shrine.webp create mode 100644 public/flair/img/travel-places.ship.webp create mode 100644 public/flair/img/travel-places.skateboard.webp create mode 100644 public/flair/img/travel-places.small-airplane.webp create mode 100644 public/flair/img/travel-places.snow-capped-mountain.webp create mode 100644 public/flair/img/travel-places.speedboat.webp create mode 100644 public/flair/img/travel-places.sport-utility-vehicle.webp create mode 100644 public/flair/img/travel-places.stadium.webp create mode 100644 public/flair/img/travel-places.station.webp create mode 100644 public/flair/img/travel-places.statue-of-liberty.webp create mode 100644 public/flair/img/travel-places.stop-sign.webp create mode 100644 public/flair/img/travel-places.sunrise-over-mountains.webp create mode 100644 public/flair/img/travel-places.sunrise.webp create mode 100644 public/flair/img/travel-places.sunset.webp create mode 100644 public/flair/img/travel-places.suspension-railway.webp create mode 100644 public/flair/img/travel-places.synagogue.webp create mode 100644 public/flair/img/travel-places.taxi.webp create mode 100644 public/flair/img/travel-places.tent.webp create mode 100644 public/flair/img/travel-places.tokyo-tower.webp create mode 100644 public/flair/img/travel-places.tractor.webp create mode 100644 public/flair/img/travel-places.train.webp create mode 100644 public/flair/img/travel-places.tram-car.webp create mode 100644 public/flair/img/travel-places.tram.webp create mode 100644 public/flair/img/travel-places.trolleybus.webp create mode 100644 public/flair/img/travel-places.vertical-traffic-light.webp create mode 100644 public/flair/img/travel-places.volcano.webp create mode 100644 public/flair/img/travel-places.wedding.webp create mode 100644 public/flair/img/travel-places.wheel.webp create mode 100644 public/flair/img/travel-places.wooden-ship.webp create mode 100644 public/flair/img/travel-places.world-map.webp create mode 100644 public/flair/index.html create mode 100755 public/flair/list.sh create mode 100644 public/flair/list.txt delete mode 100644 public/images/board/newspaper.png delete mode 100644 public/images/board/newspaper.thumbnail.png create mode 100644 public/images/board/svg/newspaper.svg create mode 100644 public/images/flags/ES-AR.png create mode 100644 public/images/flags/ES-AS.png create mode 100644 public/images/trophy/atomicwc23.png create mode 100644 translation/dest/onboarding/aa-ER.xml create mode 100644 translation/dest/onboarding/af-ZA.xml create mode 100644 translation/dest/onboarding/ak-GH.xml create mode 100644 translation/dest/onboarding/am-ET.xml create mode 100644 translation/dest/onboarding/an-ES.xml create mode 100644 translation/dest/onboarding/ar-SA.xml create mode 100644 translation/dest/onboarding/as-IN.xml create mode 100644 translation/dest/onboarding/ast-ES.xml create mode 100644 translation/dest/onboarding/av-DA.xml create mode 100644 translation/dest/onboarding/az-AZ.xml create mode 100644 translation/dest/onboarding/ba-RU.xml create mode 100644 translation/dest/onboarding/be-BY.xml create mode 100644 translation/dest/onboarding/bg-BG.xml create mode 100644 translation/dest/onboarding/bn-BD.xml create mode 100644 translation/dest/onboarding/br-FR.xml create mode 100644 translation/dest/onboarding/bs-BA.xml create mode 100644 translation/dest/onboarding/ca-ES.xml create mode 100644 translation/dest/onboarding/ce-CE.xml create mode 100644 translation/dest/onboarding/ceb-PH.xml create mode 100644 translation/dest/onboarding/ckb-IR.xml create mode 100644 translation/dest/onboarding/co-FR.xml create mode 100644 translation/dest/onboarding/cs-CZ.xml create mode 100644 translation/dest/onboarding/cv-CU.xml create mode 100644 translation/dest/onboarding/cy-GB.xml create mode 100644 translation/dest/onboarding/da-DK.xml create mode 100644 translation/dest/onboarding/de-DE.xml create mode 100644 translation/dest/onboarding/el-GR.xml create mode 100644 translation/dest/onboarding/en-US.xml create mode 100644 translation/dest/onboarding/eo-UY.xml create mode 100644 translation/dest/onboarding/es-ES.xml create mode 100644 translation/dest/onboarding/et-EE.xml create mode 100644 translation/dest/onboarding/eu-ES.xml create mode 100644 translation/dest/onboarding/fa-IR.xml create mode 100644 translation/dest/onboarding/fi-FI.xml create mode 100644 translation/dest/onboarding/fo-FO.xml create mode 100644 translation/dest/onboarding/fr-FR.xml create mode 100644 translation/dest/onboarding/frp-IT.xml create mode 100644 translation/dest/onboarding/fur-IT.xml create mode 100644 translation/dest/onboarding/fy-NL.xml create mode 100644 translation/dest/onboarding/ga-IE.xml create mode 100644 translation/dest/onboarding/gd-GB.xml create mode 100644 translation/dest/onboarding/gl-ES.xml create mode 100644 translation/dest/onboarding/gn-PY.xml create mode 100644 translation/dest/onboarding/gsw-CH.xml create mode 100644 translation/dest/onboarding/gu-IN.xml create mode 100644 translation/dest/onboarding/ha-HG.xml create mode 100644 translation/dest/onboarding/he-IL.xml create mode 100644 translation/dest/onboarding/hi-IN.xml create mode 100644 translation/dest/onboarding/hr-HR.xml create mode 100644 translation/dest/onboarding/hu-HU.xml create mode 100644 translation/dest/onboarding/hy-AM.xml create mode 100644 translation/dest/onboarding/ia-IA.xml create mode 100644 translation/dest/onboarding/id-ID.xml create mode 100644 translation/dest/onboarding/ig-NG.xml create mode 100644 translation/dest/onboarding/io-EN.xml create mode 100644 translation/dest/onboarding/is-IS.xml create mode 100644 translation/dest/onboarding/it-IT.xml create mode 100644 translation/dest/onboarding/ja-JP.xml create mode 100644 translation/dest/onboarding/jbo-EN.xml create mode 100644 translation/dest/onboarding/jv-ID.xml create mode 100644 translation/dest/onboarding/ka-GE.xml create mode 100644 translation/dest/onboarding/kaa-UZ.xml create mode 100644 translation/dest/onboarding/kab-DZ.xml create mode 100644 translation/dest/onboarding/kk-KZ.xml create mode 100644 translation/dest/onboarding/km-KH.xml create mode 100644 translation/dest/onboarding/kmr-TR.xml create mode 100644 translation/dest/onboarding/kn-IN.xml create mode 100644 translation/dest/onboarding/ko-KR.xml create mode 100644 translation/dest/onboarding/ky-KG.xml create mode 100644 translation/dest/onboarding/la-LA.xml create mode 100644 translation/dest/onboarding/lb-LU.xml create mode 100644 translation/dest/onboarding/lg-UG.xml create mode 100644 translation/dest/onboarding/lo-LA.xml create mode 100644 translation/dest/onboarding/lt-LT.xml create mode 100644 translation/dest/onboarding/lv-LV.xml create mode 100644 translation/dest/onboarding/mai-IN.xml create mode 100644 translation/dest/onboarding/mdf-RU.xml create mode 100644 translation/dest/onboarding/mg-MG.xml create mode 100644 translation/dest/onboarding/mi-NZ.xml create mode 100644 translation/dest/onboarding/mk-MK.xml create mode 100644 translation/dest/onboarding/ml-IN.xml create mode 100644 translation/dest/onboarding/mn-MN.xml create mode 100644 translation/dest/onboarding/mr-IN.xml create mode 100644 translation/dest/onboarding/ms-MY.xml create mode 100644 translation/dest/onboarding/mt-MT.xml create mode 100644 translation/dest/onboarding/my-MM.xml create mode 100644 translation/dest/onboarding/nb-NO.xml create mode 100644 translation/dest/onboarding/ne-NP.xml create mode 100644 translation/dest/onboarding/nl-NL.xml create mode 100644 translation/dest/onboarding/nn-NO.xml create mode 100644 translation/dest/onboarding/ns-ZA.xml create mode 100644 translation/dest/onboarding/ny-MW.xml create mode 100644 translation/dest/onboarding/oc-FR.xml create mode 100644 translation/dest/onboarding/om-ET.xml create mode 100644 translation/dest/onboarding/or-IN.xml create mode 100644 translation/dest/onboarding/os-SE.xml create mode 100644 translation/dest/onboarding/pa-IN.xml create mode 100644 translation/dest/onboarding/pi-IN.xml create mode 100644 translation/dest/onboarding/pl-PL.xml create mode 100644 translation/dest/onboarding/ps-AF.xml create mode 100644 translation/dest/onboarding/pt-BR.xml create mode 100644 translation/dest/onboarding/pt-PT.xml create mode 100644 translation/dest/onboarding/qu-PE.xml create mode 100644 translation/dest/onboarding/rn-BI.xml create mode 100644 translation/dest/onboarding/ro-RO.xml create mode 100644 translation/dest/onboarding/ru-RU.xml create mode 100644 translation/dest/onboarding/rw-RW.xml create mode 100644 translation/dest/onboarding/ry-UA.xml create mode 100644 translation/dest/onboarding/sa-IN.xml create mode 100644 translation/dest/onboarding/sc-IT.xml create mode 100644 translation/dest/onboarding/sco-GB.xml create mode 100644 translation/dest/onboarding/sd-PK.xml create mode 100644 translation/dest/onboarding/se-NO.xml create mode 100644 translation/dest/onboarding/si-LK.xml create mode 100644 translation/dest/onboarding/sk-SK.xml create mode 100644 translation/dest/onboarding/sl-SI.xml create mode 100644 translation/dest/onboarding/sn-ZW.xml create mode 100644 translation/dest/onboarding/so-SO.xml create mode 100644 translation/dest/onboarding/sq-AL.xml create mode 100644 translation/dest/onboarding/sr-SP.xml create mode 100644 translation/dest/onboarding/st-ZA.xml create mode 100644 translation/dest/onboarding/sv-SE.xml create mode 100644 translation/dest/onboarding/sw-KE.xml create mode 100644 translation/dest/onboarding/ta-IN.xml create mode 100644 translation/dest/onboarding/te-IN.xml create mode 100644 translation/dest/onboarding/tg-TJ.xml create mode 100644 translation/dest/onboarding/th-TH.xml create mode 100644 translation/dest/onboarding/ti-ER.xml create mode 100644 translation/dest/onboarding/tk-TM.xml create mode 100644 translation/dest/onboarding/tl-PH.xml create mode 100644 translation/dest/onboarding/tlh-AA.xml create mode 100644 translation/dest/onboarding/tn-ZA.xml create mode 100644 translation/dest/onboarding/tp-TP.xml create mode 100644 translation/dest/onboarding/tr-TR.xml create mode 100644 translation/dest/onboarding/tt-RU.xml create mode 100644 translation/dest/onboarding/ug-CN.xml create mode 100644 translation/dest/onboarding/uk-UA.xml create mode 100644 translation/dest/onboarding/ur-PK.xml create mode 100644 translation/dest/onboarding/uz-UZ.xml create mode 100644 translation/dest/onboarding/vi-VN.xml create mode 100644 translation/dest/onboarding/wo-SN.xml create mode 100644 translation/dest/onboarding/xh-ZA.xml create mode 100644 translation/dest/onboarding/yo-NG.xml create mode 100644 translation/dest/onboarding/zh-CN.xml create mode 100644 translation/dest/onboarding/zh-TW.xml create mode 100644 translation/dest/onboarding/zu-ZA.xml create mode 100644 translation/source/onboarding.xml create mode 100644 ui/chart/src/resizePolyfill.ts create mode 100644 ui/common/css/form/_emoji-picker.scss create mode 100644 ui/lobby/css/_feed.scss create mode 100644 ui/site/css/_dailyFeed.scss create mode 100644 ui/site/css/build/_dailyFeed.scss create mode 100644 ui/site/css/build/dailyFeed.ltr.dark.scss create mode 100644 ui/site/css/build/dailyFeed.ltr.light.scss create mode 100644 ui/site/css/build/dailyFeed.ltr.transp.scss create mode 100644 ui/site/css/build/dailyFeed.rtl.dark.scss create mode 100644 ui/site/css/build/dailyFeed.rtl.light.scss create mode 100644 ui/site/css/build/dailyFeed.rtl.transp.scss create mode 100644 ui/site/src/component/flairPicker.ts create mode 100644 ui/site/src/dailyFeed.ts rename ui/site/src/{emojiPicker.ts => flairPicker.ts} (87%) diff --git a/.github/workflows/assets.yml b/.github/workflows/assets.yml index 6020034c1352e..84a11b474adf6 100644 --- a/.github/workflows/assets.yml +++ b/.github/workflows/assets.yml @@ -47,10 +47,9 @@ jobs: - run: ./ui/build --no-install -p - run: cd ui && pnpm run test && cd - - run: mkdir assets && mv public assets/ && cp bin/download-lifat LICENSE COPYING.md README.md assets/ && git log -n 1 --pretty=oneline > assets/commit.txt - - run: cd assets && tar -cvpJf ../assets.tar.xz . && cd - - env: - XZ_OPT: '-0' - - uses: actions/upload-artifact@v3 + - run: cd assets && tar --zstd -cvpf ../assets.tar.zst . && cd - + - uses: actions/upload-artifact@v4 with: name: lila-assets - path: assets.tar.xz + path: assets.tar.zst + compression-level: 0 # already compressed diff --git a/.github/workflows/flair.yml b/.github/workflows/flair.yml new file mode 100644 index 0000000000000..67725139814e1 --- /dev/null +++ b/.github/workflows/flair.yml @@ -0,0 +1,16 @@ +name: Validate Flair + +on: + push: + paths: + - 'public/flair/**' + pull_request: + paths: + - 'public/flair/**' + +jobs: + validate-flair: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: ./bin/validate-flair public/flair/img/ diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml index 0c8525b655f2e..473ae8e3ea2ca 100644 --- a/.github/workflows/server.yml +++ b/.github/workflows/server.yml @@ -33,7 +33,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: temurin java-version: 21 @@ -41,10 +41,11 @@ jobs: - run: TZ=UTC git log -1 --date=iso-strict-local --pretty='format:app.version.commit = "%H"%napp.version.date = "%ad"%napp.version.message = """%s"""%n' | tee conf/version.conf - run: ./lila -Depoll=true "test;stage" - run: cp LICENSE COPYING.md README.md target/universal/stage && git log -n 1 --pretty=oneline > target/universal/stage/commit.txt - - run: cd target/universal/stage && tar -cvpJf ../../../lila-3.0.tar.xz . && cd - + - run: cd target/universal/stage && tar --zstd -cvpf ../../../lila-3.0.tar.zst . && cd - env: - XZ_OPT: '-0' - - uses: actions/upload-artifact@v3 + ZSTD_LEVEL: 1 # most files are already zipped + - uses: actions/upload-artifact@v4 with: name: lila-server - path: lila-3.0.tar.xz + path: lila-3.0.tar.zst + compression-level: 0 # already compressed diff --git a/app/LilaComponents.scala b/app/LilaComponents.scala index 122180b331246..9f4331c319a25 100644 --- a/app/LilaComponents.scala +++ b/app/LilaComponents.scala @@ -100,6 +100,7 @@ final class LilaComponents( lazy val appealC: appeal.Appeal = wire[appeal.Appeal] lazy val auth: Auth = wire[Auth] lazy val blog: Blog = wire[Blog] + lazy val dailyFeed: DailyFeed = wire[DailyFeed] lazy val playApi: PlayApi = wire[PlayApi] lazy val challenge: Challenge = wire[Challenge] lazy val coach: Coach = wire[Coach] @@ -146,7 +147,8 @@ final class LilaComponents( lazy val simul: Simul = wire[Simul] lazy val streamer: Streamer = wire[Streamer] lazy val study: Study = wire[Study] - lazy val team: Team = wire[Team] + lazy val teamC: team.Team = wire[team.Team] + lazy val teamApi: TeamApi = wire[TeamApi] lazy val timeline: Timeline = wire[Timeline] lazy val tournament: Tournament = wire[Tournament] lazy val tournamentCrud: TournamentCrud = wire[TournamentCrud] @@ -168,6 +170,7 @@ final class LilaComponents( private val appealRouter: _root_.router.appeal.Routes = wire[_root_.router.appeal.Routes] private val reportRouter: _root_.router.report.Routes = wire[_root_.router.report.Routes] private val clasRouter: _root_.router.clas.Routes = wire[_root_.router.clas.Routes] + private val teamRouter: _root_.router.team.Routes = wire[_root_.router.team.Routes] val router: Router = wire[_root_.router.router.Routes] if configuration.get[Boolean]("kamon.enabled") then diff --git a/app/controllers/Account.scala b/app/controllers/Account.scala index a8718020859e2..632d5b810112a 100644 --- a/app/controllers/Account.scala +++ b/app/controllers/Account.scala @@ -115,14 +115,8 @@ final class Account( .urgentGames(me) .map: _.take((getInt("nb") | 9) atMost 50) - .flatMap: povs => - if ctx.isMobileOauth then - povs.traverse: pov => - env.round.roundSocket.statusIfPresent(pov.gameId) flatMap: - env.round.mobile.json(pov.game, pov.fullId.anyId, _) - else - fuccess: - povs.filterNot(_.game.isTournament) map env.api.lobbyApi.nowPlaying + .map: + _.filterNot(_.game.isTournament) map env.api.lobbyApi.nowPlaying .map: povs => Ok(Json.obj("nowPlaying" -> JsArray(povs))) diff --git a/app/controllers/Api.scala b/app/controllers/Api.scala index 653e0e312652f..89b7163d93e21 100644 --- a/app/controllers/Api.scala +++ b/app/controllers/Api.scala @@ -22,12 +22,13 @@ final class Api( import Api.* import env.api.{ userApi, gameApi } - private lazy val apiStatusJson = Json.obj( + private lazy val apiStatusJson = Json.obj: "api" -> Json.obj( "current" -> Mobile.Api.currentVersion.value, "olds" -> Json.arr() ) - ) + + private given lila.hub.LightTeam.Api = env.team.lightTeamApi val status = Anon: val appVersion = get("v") @@ -106,14 +107,6 @@ final class Api( key = "user_games.api.global" ) - private def UserGamesRateLimit(cost: Int, req: RequestHeader)(run: => Fu[ApiResult]) = - val ip = req.ipAddress - def limited = fuccess(ApiResult.Limited) - UserGamesRateLimitPerIP(ip, limited, cost = cost): - UserGamesRateLimitPerUA(HTTPRequest.userAgent(req), limited, cost = cost, msg = ip.value): - UserGamesRateLimitGlobal("-", limited, cost = cost, msg = ip.value): - run - private def gameFlagsFromRequest(using RequestHeader) = lila.api.GameApi.WithFlags( analysis = getBool("with_analysis"), @@ -124,25 +117,6 @@ final class Api( token = get("token") ) - // for mobile app - def userGames(name: UserStr) = MobileApiRequest: - val page = (getInt("page") | 1) atLeast 1 atMost 200 - val nb = MaxPerPage((getInt("nb") | 10) atLeast 1 atMost 100) - val cost = page * nb.value + 10 - UserGamesRateLimit(cost, req): - lila.mon.api.userGames.increment(cost.toLong) - env.user.repo byId name flatMapz { user => - gameApi.byUser( - user = user, - rated = getBoolOpt("rated"), - playing = getBoolOpt("playing"), - analysed = getBoolOpt("analysed"), - withFlags = gameFlagsFromRequest, - nb = nb, - page = page - ) map some - } map toApiResult - def game(id: GameId) = ApiRequest: gameApi.one(id, gameFlagsFromRequest) map toApiResult @@ -175,7 +149,6 @@ final class Api( env.tournament.jsonView( tour = tour, page = page.some, - getTeamName = env.team.getTeamName.apply, playerInfoExt = none, socketVersion = none, partial = false, @@ -372,18 +345,18 @@ final class Api( } def perfStat(username: UserStr, perfKey: lila.rating.Perf.Key) = ApiRequest: - env.perfStat.api.data(username, perfKey) map { + env.perfStat.api.data(username, perfKey) map: _.fold[ApiResult](ApiResult.NoData) { data => ApiResult.Data(env.perfStat.jsonView(data)) } - } + + def mobileGames = Scoped(_.Web.Mobile) { _ ?=> _ ?=> + val ids = get("ids").so(_.split(',').take(50).toList) map GameId.take + ids.nonEmpty.so: + env.round.roundSocket.getMany(ids).flatMap(env.round.mobile.online).map(JsonOk) + } def ApiRequest(js: Context ?=> Fu[ApiResult]) = Anon: js map toHttp - def MobileApiRequest(js: RequestHeader ?=> Fu[ApiResult]) = Anon: - if lila.security.Mobile.Api.requested(req) - then js map toHttp - else NotFound - def toApiResult(json: Option[JsValue]): ApiResult = json.fold[ApiResult](ApiResult.NoData)(ApiResult.Data.apply) def toApiResult(json: Seq[JsValue]): ApiResult = ApiResult.Data(JsArray(json)) diff --git a/app/controllers/Appeal.scala b/app/controllers/Appeal.scala index 719c40be2ef4d..e9e56938f6df0 100644 --- a/app/controllers/Appeal.scala +++ b/app/controllers/Appeal.scala @@ -31,7 +31,12 @@ final class Appeal(env: Env, reportC: => report.Report, prismicC: => Prismic, us )(using Context)(using me: Me): Fu[Frag] = env.appeal.api.byId(me) flatMap { case None => renderAsync: - env.playban.api.currentBan(me).dmap(_.isDefined) map { html.appeal.tree(me, _) } + for + playban <- env.playban.api.currentBan(me).dmap(_.isDefined) + // if no blog, consider it's visible because even if it is not, for now the user + // has not been negatively impacted + ublogIsVisible <- env.ublog.api.isBlogVisible(me.userId).dmap(_.getOrElse(true)) + yield html.appeal.tree(me, playban, ublogIsVisible) case Some(a) => renderPage(html.appeal.discussion(a, me, err | userForm)) } diff --git a/app/controllers/Blog.scala b/app/controllers/Blog.scala index 00f4b7b58ac2b..4fbcbed19603b 100644 --- a/app/controllers/Blog.scala +++ b/app/controllers/Blog.scala @@ -68,9 +68,8 @@ final class Blog( _.refreshAfterWrite(30.minutes) .buildAsyncFuture: _ => blogApi.masterContext.flatMap: prismic => - blogApi.recent(prismic.api, 1, MaxPerPage(50), none) mapz { docs => + blogApi.recent(prismic.api, 1, MaxPerPage(50), none) mapz: docs => views.html.blog.atom(docs)(using prismic).render - } def atom = Anon: atomCache.getUnit.map: xml => diff --git a/app/controllers/Challenge.scala b/app/controllers/Challenge.scala index bc8d444329b98..13c0233b41232 100644 --- a/app/controllers/Challenge.scala +++ b/app/controllers/Challenge.scala @@ -8,6 +8,7 @@ import lila.app.{ given, * } import lila.challenge.{ Challenge as ChallengeModel } import lila.challenge.Challenge.{ Id as ChallengeId } import lila.common.{ Bearer, IpAddress, Template, Preload } +import lila.common.config.Max import lila.game.{ AnonCookie, Pov } import lila.oauth.{ OAuthScope, EndpointScopes } import lila.setup.ApiConfig @@ -59,9 +60,13 @@ final class Challenge( html = val color = get("color") flatMap chess.Color.fromName if mine then - error match - case Some(e) => BadRequest.page(html.challenge.mine(c, json, e.some, color)) - case None => Ok.page(html.challenge.mine(c, json, none, color)) + ctx.userId + .so(env.game.gameRepo.recentChallengersOf(_, Max(10))) + .flatMap(env.user.lightUserApi.asyncManyFallback) + .flatMap: friends => + error match + case Some(e) => BadRequest.page(html.challenge.mine(c, json, friends, e.some, color)) + case None => Ok.page(html.challenge.mine(c, json, friends, none, color)) else Ok.pageAsync: c.challengerUserId.so(env.user.api.withPerf(_, c.perfType)) map: @@ -113,9 +118,8 @@ final class Challenge( case None => tryRematch case Some(c) if c.accepted => tryRematch case Some(c) => - api.accept(c, none) map { + api.accept(c, none) map: _.fold(err => BadRequest(jsonError(err)), _ => jsonOkResult) - } } } diff --git a/app/controllers/DailyFeed.scala b/app/controllers/DailyFeed.scala new file mode 100644 index 0000000000000..d9b56cee4de1d --- /dev/null +++ b/app/controllers/DailyFeed.scala @@ -0,0 +1,61 @@ +package controllers + +import play.api.mvc.* +import views.* +import java.time.LocalDate + +import lila.app.{ given, * } +import lila.common.config.Max +import lila.blog.DailyFeed.Update + +final class DailyFeed(env: Env) extends LilaController(env): + + def api = env.blog.dailyFeed + + def index = Open: + for + updates <- api.recent + page <- renderPage(html.dailyFeed.index(updates)) + yield Ok(page) + + def createForm = Secure(_.DailyFeed) { _ ?=> _ ?=> + Ok.pageAsync(html.dailyFeed.create(api.form(none))) + } + + def create = SecureBody(_.DailyFeed) { _ ?=> _ ?=> + api + .form(none) + .bindFromRequest() + .fold( + err => BadRequest.pageAsync(html.dailyFeed.create(err)), + data => + val up = data toUpdate none + api.set(up) inject Redirect(routes.DailyFeed.edit(up.id)).flashSuccess + ) + } + + def edit(id: String) = Secure(_.DailyFeed) { _ ?=> _ ?=> + Found(api.get(id)): up => + Ok.pageAsync(html.dailyFeed.edit(api.form(up.some), up)) + } + + def update(id: String) = SecureBody(_.DailyFeed) { _ ?=> _ ?=> + Found(api.get(id)): from => + api + .form(from.some) + .bindFromRequest() + .fold( + err => BadRequest.pageAsync(html.dailyFeed.edit(err, from)), + data => + api.set(data toUpdate from.id.some) inject Redirect(routes.DailyFeed.edit(from.id)).flashSuccess + ) + } + + def delete(id: String) = Secure(_.DailyFeed) { _ ?=> _ ?=> + Found(api.get(id)): up => + api.delete(up.id) inject Redirect(routes.DailyFeed.index).flashSuccess + } + + def atom = Anon: + api.recentPublished map: ups => + Ok(html.dailyFeed.atom(ups)) as XML diff --git a/app/controllers/Dev.scala b/app/controllers/Dev.scala index aecb23c38d2eb..2c07c4e968514 100644 --- a/app/controllers/Dev.scala +++ b/app/controllers/Dev.scala @@ -33,7 +33,10 @@ final class Dev(env: Env) extends LilaController(env): env.tutor.nbAnalysisSetting, env.tutor.parallelismSetting, env.firefoxOriginTrial, - env.credentiallessUaRegex + env.credentiallessUaRegex, + env.relay.proxyDomainRegex, + env.relay.proxyHostPort, + env.relay.proxyCredentials ) def settings = Secure(_.Settings) { _ ?=> _ ?=> diff --git a/app/controllers/Game.scala b/app/controllers/Game.scala index 7a1e5347db7d9..66d6a6da8400f 100644 --- a/app/controllers/Game.scala +++ b/app/controllers/Game.scala @@ -28,10 +28,10 @@ final class Game(env: Env, apiC: => Api) extends LilaController(env): else Redirect(routes.Round.watcher(game.id, game.naturalOrientation.name)) } - def exportOne(id: GameAnyId) = Anon: + def exportOne(id: GameAnyId) = AnonOrScoped(): exportGame(id.gameId) - private[controllers] def exportGame(gameId: GameId)(using req: RequestHeader): Fu[Result] = + private[controllers] def exportGame(gameId: GameId)(using Context): Fu[Result] = env.round.proxyRepo.gameIfPresent(gameId) orElse env.game.gameRepo.game(gameId) flatMap { case None => NotFound case Some(game) => @@ -54,17 +54,17 @@ final class Game(env: Env, apiC: => Api) extends LilaController(env): private def handleExport(username: UserStr)(using ctx: Context) = env.user.repo byId username flatMap { - _.filter(u => u.enabled.yes || ctx.me.exists(_ is u) || isGrantedOpt(_.GamesModView)) so { user => + _.filter(u => u.enabled.yes || ctx.is(u) || isGrantedOpt(_.GamesModView)) so { user => val format = GameApiV2.Format byRequest req import lila.rating.{ Perf, PerfType } WithVs: vs => env.security.ipTrust - .throttle(MaxPerSecond(ctx.me match - case Some(m) if m is lila.user.User.explorerId => env.apiExplorerGamesPerSecond.get() - case Some(m) if m is user.id => 60 - case Some(_) if ctx.isOAuth => 30 // bonus for oauth logged in only (not for CSRF) - case _ => 25 - )) + .throttle(MaxPerSecond: + if ctx is lila.user.User.explorerId then env.apiExplorerGamesPerSecond.get() + else if ctx is user then 60 + else if ctx.isOAuth then 30 // bonus for oauth logged in only (not for CSRF) + else 25 + ) .flatMap: perSecond => val finished = getBoolOpt("finished") | true val config = GameApiV2.ByUserConfig( @@ -87,7 +87,7 @@ final class Game(env: Env, apiC: => Api) extends LilaController(env): ongoing = getBool("ongoing") || !finished, finished = finished ) - if ctx.me.exists(_ is lila.user.User.explorerId) then + if ctx.is(lila.user.User.explorerId) then Ok.chunked(env.api.gameApiV2.exportByUser(config)) .pipe(noProxyBuffer) .as(gameContentType(config)) diff --git a/app/controllers/GameMod.scala b/app/controllers/GameMod.scala index 8b6bdcd709cfa..39e809ab04624 100644 --- a/app/controllers/GameMod.scala +++ b/app/controllers/GameMod.scala @@ -77,7 +77,7 @@ final class GameMod(env: Env)(using akka.stream.Materializer) extends LilaContro }.parallel >> env.fishnet.awaiter(games.map(_.id), 2 minutes) } inject NoContent - private def downloadPgn(user: lila.user.User, gameIds: Seq[GameId]) = + private def downloadPgn(user: lila.user.User, gameIds: Seq[GameId])(using Option[Me]) = Ok.chunked { env.api.gameApiV2.exportByIds( GameApiV2.ByIdsConfig( diff --git a/app/controllers/LilaController.scala b/app/controllers/LilaController.scala index 2432a1c904e90..70943d98505cc 100644 --- a/app/controllers/LilaController.scala +++ b/app/controllers/LilaController.scala @@ -238,7 +238,7 @@ abstract private[controllers] class LilaController(val env: Env) def handleScopedFail(accepted: EndpointScopes, e: OAuthServer.AuthError)(using RequestHeader) = e match case e @ lila.oauth.OAuthServer.MissingScope(available) => OAuthServer.responseHeaders(accepted, available): - Forbidden(jsonError(e.message)) + forbiddenJson(e.message) case e => OAuthServer.responseHeaders(accepted, TokenScopes(Nil)): Unauthorized(jsonError(e.message)) @@ -331,6 +331,8 @@ abstract private[controllers] class LilaController(val env: Env) .flatMap: f(using _) + given (using req: RequestHeader): lila.chat.AllMessages = lila.chat.AllMessages(HTTPRequest.isLitools(req)) + /* We roll our own action, as we don't want to compose play Actions. */ private def action[A](parser: BodyParser[A])(handler: Request[A] ?=> Fu[Result]): EssentialAction = new: import play.api.libs.streams.Accumulator diff --git a/app/controllers/Mod.scala b/app/controllers/Mod.scala index c13a130ea9c61..8b35beceab7e8 100644 --- a/app/controllers/Mod.scala +++ b/app/controllers/Mod.scala @@ -195,13 +195,16 @@ final class Mod( } } - def createNameCloseVote(username: UserStr) = SendToZulip(username, env.irc.api.nameCloseVote) - def askUsertableCheck(username: UserStr) = SendToZulip(username, env.irc.api.usertableCheck) + def createNameCloseVote(username: UserStr) = Secure(_.SendToZulip) { _ ?=> me ?=> + env.report.api.inquiries ofModId me.id map { + _.filter(_.reason == lila.report.Reason.Username).map(_.bestAtom.simplifiedText) + } flatMap: reason => + env.user.repo byId username orNotFound { env.irc.api.nameCloseVote(_, reason) inject NoContent } - private def SendToZulip(username: UserStr, method: UserModel => Me ?=> Funit) = - Secure(_.SendToZulip) { _ ?=> _ ?=> - env.user.repo byId username orNotFound { method(_) inject NoContent } - } + } + def askUsertableCheck(username: UserStr) = Secure(_.SendToZulip) { _ ?=> _ ?=> + env.user.repo byId username orNotFound { env.irc.api.usertableCheck(_) inject NoContent } + } def table = Secure(_.Admin) { ctx ?=> _ ?=> Ok.pageAsync: diff --git a/app/controllers/Pref.scala b/app/controllers/Pref.scala index 5e98b7e28dafe..a501bccca67f8 100644 --- a/app/controllers/Pref.scala +++ b/app/controllers/Pref.scala @@ -84,7 +84,7 @@ final class Pref(env: Env) extends LilaController(env): change.form .bindFromRequest() .fold( - form => fuccess(BadRequest(form.errors mkString "\n")), + form => fuccess(BadRequest(form.errors.flatMap(_.messages) mkString "\n")), v => ctx.me .so(api.setPref(_, change.update(v))) diff --git a/app/controllers/Push.scala b/app/controllers/Push.scala index 60322c46590a7..7fc585422061d 100644 --- a/app/controllers/Push.scala +++ b/app/controllers/Push.scala @@ -5,11 +5,11 @@ import lila.push.WebSubscription final class Push(env: Env) extends LilaController(env): - def mobileRegister(platform: String, deviceId: String) = Auth { ctx ?=> me ?=> + def mobileRegister(platform: String, deviceId: String) = AuthOrScoped(_.Web.Mobile) { ctx ?=> me ?=> env.push.registerDevice(me, platform, deviceId) inject NoContent } - def mobileUnregister = Auth { ctx ?=> me ?=> + def mobileUnregister = AuthOrScoped(_.Web.Mobile) { ctx ?=> me ?=> env.push.unregisterDevices(me) inject NoContent } diff --git a/app/controllers/RelayRound.scala b/app/controllers/RelayRound.scala index 521a5080c663c..38ba974a47de3 100644 --- a/app/controllers/RelayRound.scala +++ b/app/controllers/RelayRound.scala @@ -9,6 +9,8 @@ import lila.common.HTTPRequest import lila.relay.{ RelayRound as RoundModel, RelayRoundForm, RelayTour as TourModel } import chess.format.pgn.PgnStr import views.* +import lila.common.config.{ Max, MaxPerSecond } +import play.api.libs.json.Json final class RelayRound( env: Env, @@ -42,12 +44,11 @@ final class RelayRound( ), setup => rateLimitCreation(whenRateLimited): - env.relay.api.create(setup, tour) flatMap { round => + env.relay.api.create(setup, tour) flatMap: rt => negotiate( - Redirect(routes.RelayRound.show(tour.slug, round.slug, round.id.value)), - JsonOk(env.relay.jsonView.withUrl(round withTour tour)) + Redirect(routes.RelayRound.show(tour.slug, rt.relay.slug, rt.relay.id)), + JsonOk(env.relay.jsonView.myRound(rt)) ) - } ) } @@ -101,18 +102,28 @@ final class RelayRound( else env.study.api byIdWithChapter rt.round.studyId sc orNotFound { doShow(rt, _) } , - json = Found(env.relay.api.byIdWithTour(id)): rt => - Found(env.study.studyRepo.byId(rt.round.studyId)): study => - studyC.CanView(study)( - env.study.chapterRepo orderedMetadataByStudy rt.round.studyId map { games => - JsonOk(env.relay.jsonView.withUrlAndGames(rt, games)) - } - )(studyC.privateUnauthorizedJson, studyC.privateForbiddenJson) + json = doApiShow(id) ) + def apiShow(ts: String, rs: String, id: RelayRoundId) = AnonOrScoped(_.Study.Read): + doApiShow(id) + + private def doApiShow(id: RelayRoundId)(using Context): Fu[Result] = + Found(env.relay.api.byIdWithTour(id)): rt => + Found(env.study.studyRepo.byId(rt.round.studyId)): study => + studyC.CanView(study)( + env.study.chapterRepo orderedMetadataByStudy rt.round.studyId map: games => + JsonOk(env.relay.jsonView.withUrlAndGames(rt withStudy study, games)) + )(studyC.privateUnauthorizedJson, studyC.privateForbiddenJson) + def pgn(ts: String, rs: String, id: StudyId) = studyC.pgn(id) def apiPgn = studyC.apiPgn + def apiMyRounds = Scoped(_.Study.Read) { ctx ?=> _ ?=> + val source = env.relay.api.myRounds(MaxPerSecond(20), getIntAs[Max]("nb")).map(env.relay.jsonView.myRound) + apiC.GlobalConcurrencyLimitPerIP.download(ctx.ip)(source)(apiC.sourceToNdJson) + } + def stream(id: RelayRoundId) = AnonOrScoped(): ctx ?=> Found(env.relay.api.byIdWithStudy(id)): rt => studyC.CanView(rt.study) { @@ -127,10 +138,16 @@ final class RelayRound( def push(id: RelayRoundId) = ScopedBody(parse.tolerantText)(Seq(_.Study.Write)) { ctx ?=> me ?=> env.relay.api - .byIdAndContributor(id) + .byIdWithStudy(id) .flatMap: - case None => notFoundJson() - case Some(rt) => env.relay.push(rt, PgnStr(ctx.body.body)) inject jsonOkResult + case None => notFoundJson() + case Some(rt) if !rt.study.canContribute(me) => forbiddenJson() + case Some(rt) => + env.relay + .push(rt.withTour, PgnStr(ctx.body.body)) + .map: + case Right(moves) => JsonOk(Json.obj("moves" -> moves)) + case Left(e) => JsonBadRequest(e.message) } private def WithRoundAndTour(@nowarn ts: String, @nowarn rs: String, id: RelayRoundId)( diff --git a/app/controllers/Round.scala b/app/controllers/Round.scala index 407de73009743..ed6c7d47192b2 100644 --- a/app/controllers/Round.scala +++ b/app/controllers/Round.scala @@ -10,7 +10,7 @@ import lila.common.{ Preload, HTTPRequest } import lila.common.Json.given import lila.game.{ Game as GameModel, PgnDump, Pov } import lila.tournament.{ Tournament as Tour } -import lila.user.{ User as UserModel, UserFlairApi } +import lila.user.{ User as UserModel, FlairApi } final class Round( env: Env, @@ -23,7 +23,7 @@ final class Round( ) extends LilaController(env) with TheftPrevention: - private given UserFlairApi = env.user.flairApi + private given FlairApi = env.user.flairApi private def renderPlayer(pov: Pov)(using ctx: Context): Fu[Result] = pov.game.playableByAi so env.fishnet.player(pov.game) @@ -141,13 +141,12 @@ final class Round( if pov.game.replayable then analyseC.replay(pov, userTv = userTv) else if HTTPRequest.isHuman(ctx.req) then for - users <- env.user.api.gamePlayers(pov.game.userIdPair, pov.game.perfType) - ((((tour, simul), chat), crosstable), bookmarked) <- env.tournament.api.gameView - .watcher(pov.game) zip - (pov.game.simulId so env.simul.repo.find) zip - getWatcherChat(pov.game) zip - (ctx.noBlind so env.game.crosstableApi.withMatchup(pov.game)) zip - env.bookmark.api.exists(pov.game, ctx.me) + users <- env.user.api.gamePlayers(pov.game.userIdPair, pov.game.perfType) + tour <- env.tournament.api.gameView.watcher(pov.game) + simul <- pov.game.simulId so env.simul.repo.find + chat <- getWatcherChat(pov.game) + crosstable <- ctx.noBlind so env.game.crosstableApi.withMatchup(pov.game) + bookmarked <- env.bookmark.api.exists(pov.game, ctx.me) tv = userTv.map: u => lila.round.OnTv.User(u.id) data <- env.api.roundApi.watcher(pov, users, tour, tv) diff --git a/app/controllers/Setup.scala b/app/controllers/Setup.scala index 9fd72ca8c6c48..71a3a342ddefe 100644 --- a/app/controllers/Setup.scala +++ b/app/controllers/Setup.scala @@ -78,7 +78,7 @@ final class Setup( val message = lila.challenge.ChallengeDenied.translated(denied) negotiate( // 403 tells setupCtrl.ts to close the setup modal - Forbidden(jsonError(message)), // TODO test + forbiddenJson(message), // TODO test BadRequest(jsonError(message)) ) case None => diff --git a/app/controllers/Streamer.scala b/app/controllers/Streamer.scala index 32b898d3c088c..4d6fe9e6a7201 100644 --- a/app/controllers/Streamer.scala +++ b/app/controllers/Streamer.scala @@ -24,10 +24,10 @@ final class Streamer(env: Env, apiC: => Api) extends LilaController(env): page <- renderPage(html.streamer.index(live, pager, requests)) yield Ok(page) - def featured = Anon: + def featured = Anon: ctx ?=> env.streamer.liveStreamApi.all.map: streams => val max = env.streamer.homepageMaxSetting.get() - val featured = streams.homepage(max, req, none) withTitles env.user.lightUserApi + val featured = streams.homepage(max, ctx.acceptLanguages) withTitles env.user.lightUserApi JsonOk: featured.live.streams.map: s => Json.obj( diff --git a/app/controllers/Study.scala b/app/controllers/Study.scala index b0638c8345011..c5ede8e8fd788 100644 --- a/app/controllers/Study.scala +++ b/app/controllers/Study.scala @@ -24,7 +24,7 @@ final class Study( prismicC: Prismic ) extends LilaController(env): - private given lila.user.UserFlairApi = env.user.flairApi + private given lila.user.FlairApi = env.user.flairApi def search(text: String, page: Int) = OpenBody: Reasonable(page): @@ -539,7 +539,7 @@ final class Study( ) def privateForbiddenText = Forbidden("This study is now private") - def privateForbiddenJson = Forbidden(jsonError("This study is now private")) + def privateForbiddenJson = forbiddenJson("This study is now private") def privateForbiddenFu(study: StudyModel)(using Context) = negotiate( Forbidden.page(html.site.message.privateStudy(study)), privateForbiddenJson diff --git a/app/controllers/Swiss.scala b/app/controllers/Swiss.scala index 92d2095eec56b..7265b3c1b84bb 100644 --- a/app/controllers/Swiss.scala +++ b/app/controllers/Swiss.scala @@ -6,10 +6,12 @@ import play.api.mvc.* import scala.util.chaining.* import views.* +import controllers.team.routes.{ Team as teamRoutes } import lila.app.{ given, * } import lila.common.HTTPRequest import lila.swiss.Swiss.ChatFor import lila.swiss.{ Swiss as SwissModel, SwissForm } +import lila.hub.LightTeam final class Swiss( env: Env, @@ -27,14 +29,15 @@ final class Swiss( for teamIds <- ctx.userId.so(env.team.cached.teamIdsList) swiss <- env.swiss.feature.get(teamIds) + _ <- env.team.lightTeamApi.preload(swiss.teamIds) page <- renderPage(html.swiss.home(swiss)) yield Ok(page) def show(id: SwissId) = Open: - env.swiss.cache.swissCache.byId(id) flatMap { swissOption => + cachedSwissAndTeam(id).flatMap: swissOption => val page = getInt("page").filter(0.<) negotiate( - html = swissOption.fold(swissNotFound): swiss => + html = swissOption.fold(swissNotFound): (swiss, team) => for verdicts <- env.swiss.api.verdicts(swiss) version <- env.swiss.version(swiss.id) @@ -56,16 +59,15 @@ final class Swiss( _.copy(locked = !env.api.chatFreshness.of(swiss)) streamers <- streamerCache get swiss.id isLocalMod <- ctx.me.so { env.team.api.hasPerm(swiss.teamId, _, _.Comm) } - page <- renderPage(html.swiss.show(swiss, verdicts, json, chat, streamers, isLocalMod)) + page <- renderPage(html.swiss.show(swiss, team, verdicts, json, chat, streamers, isLocalMod)) yield Ok(page), - json = swissOption.fold[Fu[Result]](notFoundJson("No such Swiss tournament")): swiss => + json = swissOption.fold[Fu[Result]](notFoundJson("No such Swiss tournament")): (swiss, team) => for isInTeam <- ctx.me.so(isUserInTheTeam(swiss.teamId)(_)) verdicts <- env.swiss.api.verdicts(swiss) socketVersion <- getBool("socketVersion").soFu(env.swiss version swiss.id) playerInfo <- getUserStr("playerInfo").so: u => env.swiss.api.playerInfo(swiss, u.id) - page = getInt("page").filter(0.<) json <- env.swiss.json( swiss = swiss, me = ctx.me, @@ -77,25 +79,26 @@ final class Swiss( ) yield JsonOk(json) ) - } def apiShow(id: SwissId) = Anon: - env.swiss.cache.swissCache byId id flatMap { + env.swiss.cache.swissCache byId id flatMap: case Some(swiss) => env.swiss.json.api(swiss) map JsonOk case _ => notFoundJson() - } private def isUserInTheTeam(teamId: lila.team.TeamId)(user: UserId) = env.team.cached.teamIds(user).dmap(_ contains teamId) + private def cachedSwissAndTeam(id: SwissId): Fu[Option[(SwissModel, LightTeam)]] = + env.swiss.cache.swissCache.byId(id) flatMap: + _.so: swiss => + env.team.lightTeam(swiss.teamId).map2(swiss -> _) + def round(id: SwissId, round: Int) = Open: - Found(env.swiss.cache.swissCache byId id): swiss => - (round > 0 && round <= swiss.round.value).option(lila.swiss.SwissRoundNumber(round)) so { r => + Found(cachedSwissAndTeam(id)): (swiss, team) => + (round > 0 && round <= swiss.round.value).option(lila.swiss.SwissRoundNumber(round)) so: r => val page = getInt("page").filter(0.<) - env.swiss.roundPager(swiss, r, page | 0) flatMap { pager => - Ok.page(html.swiss.show.round(swiss, r, pager)) - } - } + env.swiss.roundPager(swiss, r, page | 0) flatMap: pager => + Ok.page(html.swiss.show.round(swiss, r, team, pager)) private def CheckTeamLeader(teamId: TeamId)(f: => Fu[Result])(using ctx: Context): Fu[Result] = ctx.me so { env.team.api.isGranted(teamId, _, _.Tour) } elseNotFound f @@ -115,10 +118,9 @@ final class Swiss( .fold( err => BadRequest.page(html.swiss.form.create(err, teamId)), data => - tourC.rateLimitCreation(isPrivate = true, Redirect(routes.Team.show(teamId))): - env.swiss.api.create(data, teamId) map { swiss => + tourC.rateLimitCreation(isPrivate = true, Redirect(teamRoutes.show(teamId))): + env.swiss.api.create(data, teamId) map: swiss => Redirect(routes.Swiss.show(swiss.id)) - } ) } @@ -218,7 +220,7 @@ final class Swiss( def terminate(id: SwissId) = Auth { _ ?=> me ?=> WithEditableSwiss(id): swiss => - env.swiss.api kill swiss inject Redirect(routes.Team.show(swiss.teamId)) + env.swiss.api kill swiss inject Redirect(teamRoutes.show(swiss.teamId)) } def standing(id: SwissId, page: Int) = Anon: diff --git a/app/controllers/Team.scala b/app/controllers/Team.scala index eef74407ec116..324d4722e5110 100644 --- a/app/controllers/Team.scala +++ b/app/controllers/Team.scala @@ -1,4 +1,5 @@ package controllers +package team import play.api.data.{ Forms, Form } import play.api.data.Forms.* @@ -8,16 +9,11 @@ import views.* import lila.app.{ given, * } import lila.common.{ config, HTTPRequest, IpAddress } -import lila.memo.RateLimit import lila.team.{ Requesting, Team as TeamModel, TeamMember } import lila.user.{ User as UserModel } -import Api.ApiResult import lila.team.TeamSecurity -final class Team( - env: Env, - apiC: => Api -) extends LilaController(env): +final class Team(env: Env, apiC: => Api) extends LilaController(env): private def forms = env.team.forms private def api = env.team.api @@ -26,15 +22,13 @@ final class Team( def all(page: Int) = Open: Reasonable(page): Ok.pageAsync: - paginator popularTeams page map { + paginator popularTeams page map: html.team.list.all(_) - } def home(page: Int) = Open: - ctx.me.so(api.hasTeams(_)) map { + ctx.me.so(api.hasTeams(_)) map: if _ then Redirect(routes.Team.mine) else Redirect(routes.Team.all(page)) - } def show(id: TeamId, page: Int, mod: Boolean) = Open: Reasonable(page): @@ -44,17 +38,15 @@ final class Team( Reasonable(page, config.Max(50)): Found(api teamEnabled id): team => val canSee = - fuccess(team.publicMembers || isGrantedOpt(_.ManageTeam)) >>| ctx.userId.so { + fuccess(team.publicMembers || isGrantedOpt(_.ManageTeam)) >>| ctx.userId.so: api.belongsTo(team.id, _) - } - canSee flatMap { + canSee.flatMap: if _ then Ok.pageAsync: paginator.teamMembersWithDate(team, page) map { html.team.members(team, _) } else authorizationFailed - } def search(text: String, page: Int) = OpenBody: Reasonable(page): @@ -62,9 +54,8 @@ final class Team( if text.trim.isEmpty then paginator popularTeams page map { html.team.list.all(_) } else - env.teamSearch(text, page).flatMap(_.mapFutureList(env.team.memberRepo.addMyLeadership)) map { + env.teamSearch(text, page).flatMap(_.mapFutureList(env.team.memberRepo.addMyLeadership)) map: html.team.list.search(text, _) - } private def renderTeam(team: TeamModel, page: Int, asMod: Boolean)(using ctx: Context) = for team <- api.withLeaders(team) @@ -99,26 +90,10 @@ final class Team( (isGrantedOpt(_.ModerateForum) && asMod) } - def users(teamId: TeamId) = AnonOrScoped(_.Team.Read): ctx ?=> - Found(api teamEnabled teamId): team => - val canView: Fu[Boolean] = - if team.publicMembers then fuccess(true) - else ctx.me.so(api.belongsTo(team.id, _)) - canView.map: - if _ then - apiC.jsonDownload( - env.team - .memberStream(team, config.MaxPerSecond(20)) - .map: (user, joinedAt) => - env.api.userApi.one(user, joinedAt.some) - ) - else Unauthorized - def tournaments(teamId: TeamId) = Open: FoundPage(api teamEnabled teamId): team => - env.teamInfo.tournaments(team, 30, 30) map { + env.teamInfo.tournaments(team, 30, 30) map: html.team.tournaments.page(team, _) - } private def renderEdit(team: TeamModel, form: Form[?])(using me: Me, ctx: PageContext) = for member <- env.team.memberRepo.get(team.id, me) @@ -156,27 +131,6 @@ final class Team( Redirect(routes.Team.show(team.id)).flashSuccess } - private val ApiKickRateLimitPerIP = lila.memo.RateLimit.composite[IpAddress]( - key = "team.kick.api.ip", - enforce = env.net.rateLimit.value - )( - ("fast", 10, 2.minutes), - ("slow", 50, 1.day) - ) - private val kickLimitReportOnce = lila.memo.OnceEvery[UserId](10.minutes) - - def kickUser(teamId: TeamId, username: UserStr) = Scoped(_.Team.Lead) { ctx ?=> me ?=> - WithOwnedTeamEnabledApi(teamId, _.Kick): team => - def limited = - if kickLimitReportOnce(username.id) then - lila - .log("security") - .warn(s"API team.kick limited team:${teamId} user:${me.username} ip:${req.ipAddress}") - fuccess(ApiResult.Limited) - ApiKickRateLimitPerIP(req.ipAddress, limited, cost = if me.isVerified || me.isApiHog then 0 else 1): - api.kick(team, username.id) inject ApiResult.Done - } - def blocklist(id: TeamId) = AuthBody { ctx ?=> me ?=> WithOwnedTeamEnabled(id, _.Kick): team => forms.blocklist.bindFromRequest().value so { api.blocklist.set(team, _) } inject @@ -269,6 +223,17 @@ final class Team( ) } + private def LimitPerWeek[A <: Result](a: => Fu[A])(using ctx: Context, me: Me): Fu[Result] = + api.countCreatedRecently(me) flatMap { count => + val allow = + isGrantedOpt(_.ManageTeam) || + (isGrantedOpt(_.Verified) && count < 100) || + (isGrantedOpt(_.Teacher) && count < 10) || + count < 3 + if allow then a + else Forbidden.page(views.html.site.message.teamCreateLimit) + } + def form = Auth { ctx ?=> me ?=> LimitPerWeek: forms.anyCaptcha.flatMap: captcha => @@ -460,7 +425,7 @@ final class Team( def pmAllSubmit(id: TeamId) = AuthOrScopedBody(_.Team.Lead) { ctx ?=> me ?=> WithOwnedTeamEnabled(id, _.PmAll): team => - import RateLimit.Result + import lila.memo.RateLimit.Result forms.pmAll .bindFromRequest() .fold( @@ -504,82 +469,6 @@ final class Team( ) } - // API - - def apiAll(page: Int) = Anon: - import env.team.jsonView.given - import lila.common.paginator.PaginatorJson.given - JsonOk: - for - pager <- paginator popularTeamsWithPublicLeaders page - _ <- env.user.lightUserApi.preloadMany(pager.currentPageResults.flatMap(_.publicLeaders)) - yield pager - - def apiShow(id: TeamId) = Open: - JsonOptionOk: - api teamEnabled id flatMapz { team => - for - joined <- ctx.userId.so { api.belongsTo(id, _) } - requested <- ctx.userId.ifFalse(joined).so { env.team.requestRepo.exists(id, _) } - withLeaders <- env.team.memberRepo.addPublicLeaderIds(team) - _ <- env.user.lightUserApi.preloadMany(withLeaders.publicLeaders) - yield some: - import env.team.jsonView.given - Json.toJsObject(withLeaders) ++ Json - .obj( - "joined" -> joined, - "requested" -> requested - ) - } - - def apiSearch(text: String, page: Int) = Anon: - import env.team.jsonView.given - import lila.common.paginator.PaginatorJson.given - JsonOk: - if text.trim.isEmpty - then paginator popularTeamsWithPublicLeaders page - else env.teamSearch(text, page).flatMap(_.mapFutureList(env.team.memberRepo.addPublicLeaderIds)) - - def apiTeamsOf(username: UserStr) = AnonOrScoped(): ctx ?=> - import env.team.jsonView.given - JsonOk: - for - ids <- api.joinedTeamIdsOfUserAsSeenBy(username) - teams <- api.teamsByIds(ids) - teams <- env.team.memberRepo.addPublicLeaderIds(teams) - _ <- env.user.lightUserApi.preloadMany(teams.flatMap(_.publicLeaders)) - yield teams - - def apiRequests(teamId: TeamId) = Scoped(_.Team.Read) { ctx ?=> me ?=> - WithOwnedTeamEnabledApi(teamId, _.Request): team => - import env.team.jsonView.requestWithUserWrites - val reqs = - if getBool("declined") then api.declinedRequestsWithUsers(team) - else api.requestsWithUsers(team) - reqs map Json.toJson map ApiResult.Data.apply - - } - - def apiRequestProcess(teamId: TeamId, userId: UserStr, decision: String) = Scoped(_.Team.Lead) { - _ ?=> me ?=> - WithOwnedTeamEnabledApi(teamId, _.Request): team => - api request lila.team.TeamRequest.makeId(team.id, userId.id) flatMap { - case None => fuccess(ApiResult.ClientError("No such team join request")) - case Some(req) => api.processRequest(team, req, decision) inject ApiResult.Done - } - } - - private def LimitPerWeek[A <: Result](a: => Fu[A])(using ctx: Context, me: Me): Fu[Result] = - api.countCreatedRecently(me) flatMap { count => - val allow = - isGrantedOpt(_.ManageTeam) || - (isGrantedOpt(_.Verified) && count < 100) || - (isGrantedOpt(_.Teacher) && count < 10) || - count < 3 - if allow then a - else Forbidden.page(views.html.site.message.teamCreateLimit) - } - private def WithOwnedTeam(teamId: TeamId, perm: TeamSecurity.Permission.Selector)( f: TeamModel => Fu[Result] )(using Context): Fu[Result] = @@ -597,16 +486,3 @@ final class Team( WithOwnedTeam(teamId, perm): team => if team.enabled || isGrantedOpt(_.ManageTeam) then f(team) else notFound - - private def WithOwnedTeamEnabledApi(teamId: TeamId, perm: TeamSecurity.Permission.Selector)( - f: TeamModel => Fu[ApiResult] - )(using me: Me): Fu[Result] = - api teamEnabled teamId flatMap { - case Some(team) => - api - .isGranted(team.id, me.value, perm) - .flatMap: - if _ then f(team) - else fuccess(ApiResult.ClientError("Insufficient team permissions")) - case None => fuccess(ApiResult.NoData) - } map apiC.toHttp diff --git a/app/controllers/TeamApi.scala b/app/controllers/TeamApi.scala new file mode 100644 index 0000000000000..82dbe2cac914f --- /dev/null +++ b/app/controllers/TeamApi.scala @@ -0,0 +1,125 @@ +package controllers + +import play.api.libs.json.* +import play.api.mvc.* + +import lila.app.{ given, * } +import lila.common.{ config, HTTPRequest, IpAddress } +import lila.team.{ Team as TeamModel } +import Api.ApiResult +import lila.team.TeamSecurity + +final class TeamApi(env: Env, apiC: => Api) extends LilaController(env): + + private def api = env.team.api + private def paginator = env.team.paginator + + private val ApiKickRateLimitPerIP = lila.memo.RateLimit.composite[IpAddress]( + key = "team.kick.api.ip", + enforce = env.net.rateLimit.value + )( + ("fast", 10, 2.minutes), + ("slow", 50, 1.day) + ) + + def all(page: Int) = Anon: + import env.team.jsonView.given + import lila.common.paginator.PaginatorJson.given + JsonOk: + for + pager <- paginator popularTeamsWithPublicLeaders page + _ <- env.user.lightUserApi.preloadMany(pager.currentPageResults.flatMap(_.publicLeaders)) + yield pager + + def show(id: TeamId) = Open: + JsonOptionOk: + api teamEnabled id flatMapz: team => + for + joined <- ctx.userId.so { api.belongsTo(id, _) } + requested <- ctx.userId.ifFalse(joined).so { env.team.requestRepo.exists(id, _) } + withLeaders <- env.team.memberRepo.addPublicLeaderIds(team) + _ <- env.user.lightUserApi.preloadMany(withLeaders.publicLeaders) + yield some: + import env.team.jsonView.given + Json.toJsObject(withLeaders) ++ Json + .obj( + "joined" -> joined, + "requested" -> requested + ) + + def users(teamId: TeamId) = AnonOrScoped(_.Team.Read): ctx ?=> + Found(api teamEnabled teamId): team => + val canView: Fu[Boolean] = + if team.publicMembers then fuccess(true) + else ctx.me.so(api.belongsTo(team.id, _)) + canView.map: + if _ then + apiC.jsonDownload( + env.team + .memberStream(team, config.MaxPerSecond(20)) + .map: (user, joinedAt) => + env.api.userApi.one(user, joinedAt.some) + ) + else Unauthorized + + def search(text: String, page: Int) = Anon: + import env.team.jsonView.given + import lila.common.paginator.PaginatorJson.given + JsonOk: + if text.trim.isEmpty + then paginator popularTeamsWithPublicLeaders page + else env.teamSearch(text, page).flatMap(_.mapFutureList(env.team.memberRepo.addPublicLeaderIds)) + + def teamsOf(username: UserStr) = AnonOrScoped(): ctx ?=> + import env.team.jsonView.given + JsonOk: + for + ids <- api.joinedTeamIdsOfUserAsSeenBy(username) + teams <- api.teamsByIds(ids) + teams <- env.team.memberRepo.addPublicLeaderIds(teams) + _ <- env.user.lightUserApi.preloadMany(teams.flatMap(_.publicLeaders)) + yield teams + + def requests(teamId: TeamId) = Scoped(_.Team.Read) { ctx ?=> me ?=> + WithOwnedTeamEnabled(teamId, _.Request): team => + import env.team.jsonView.requestWithUserWrites + val reqs = + if getBool("declined") then api.declinedRequestsWithUsers(team) + else api.requestsWithUsers(team) + reqs map Json.toJson map ApiResult.Data.apply + } + + def requestProcess(teamId: TeamId, userId: UserStr, decision: String) = Scoped(_.Team.Lead) { _ ?=> me ?=> + WithOwnedTeamEnabled(teamId, _.Request): team => + api request lila.team.TeamRequest.makeId(team.id, userId.id) flatMap { + case None => fuccess(ApiResult.ClientError("No such team join request")) + case Some(req) => api.processRequest(team, req, decision) inject ApiResult.Done + } + } + + private val kickLimitReportOnce = lila.memo.OnceEvery[UserId](10.minutes) + + def kickUser(teamId: TeamId, username: UserStr) = Scoped(_.Team.Lead) { ctx ?=> me ?=> + WithOwnedTeamEnabled(teamId, _.Kick): team => + def limited = + if kickLimitReportOnce(username.id) then + lila + .log("security") + .warn(s"API team.kick limited team:${teamId} user:${me.username} ip:${req.ipAddress}") + fuccess(ApiResult.Limited) + ApiKickRateLimitPerIP(req.ipAddress, limited, cost = if me.isVerified || me.isApiHog then 0 else 1): + api.kick(team, username.id) inject ApiResult.Done + } + + private def WithOwnedTeamEnabled(teamId: TeamId, perm: TeamSecurity.Permission.Selector)( + f: TeamModel => Fu[ApiResult] + )(using me: Me): Fu[Result] = + api teamEnabled teamId flatMap { + case Some(team) => + api + .isGranted(team.id, me.value, perm) + .flatMap: + if _ then f(team) + else fuccess(ApiResult.ClientError("Insufficient team permissions")) + case None => fuccess(ApiResult.NoData) + } map apiC.toHttp diff --git a/app/controllers/Timeline.scala b/app/controllers/Timeline.scala index d0e39c37c81ee..da4547a3b0124 100644 --- a/app/controllers/Timeline.scala +++ b/app/controllers/Timeline.scala @@ -21,22 +21,28 @@ final class Timeline(env: Env) extends LilaController(env): yield html.timeline.entries(entries) else for - entries <- env.timeline.entryApi.moreUserEntries(me, Max(30)) + entries <- env.timeline.entryApi.moreUserEntries(me, Max(30), since = getTimestamp("since")) _ <- env.user.lightUserApi.preloadMany(entries.flatMap(_.userIds)) yield html.timeline.more(entries) , - json = for + json = // Must be empty if nb is not given, because old versions of the // mobile app that do not send nb are vulnerable to XSS in // timeline entries. - entries <- env.timeline.entryApi - .moreUserEntries(me, Max(getInt("nb") | 0 atMost env.apiTimelineSetting.get())) - users <- env.user.lightUserApi.asyncManyFallback(entries.flatMap(_.userIds).distinct) - userMap = users.mapBy(_.id) - yield Ok(Json.obj("entries" -> entries, "users" -> Json.toJsObject(userMap))) + apiOutput(max = getIntAs[Max]("nb").fold(Max(0))(_ atMost env.apiTimelineSetting.get())) ) } + def api = Scoped() { + apiOutput(getIntAs[Max]("nb").fold(Max(15))(_ atMost Max(30))) + } + + private def apiOutput(max: Max)(using ctx: Context, me: Me) = for + entries <- env.timeline.entryApi.moreUserEntries(me, max, since = getTimestamp("since")) + users <- env.user.lightUserApi.asyncManyFallback(entries.flatMap(_.userIds).distinct) + userMap = users.mapBy(_.id) + yield Ok(Json.obj("entries" -> entries, "users" -> Json.toJsObject(userMap))) + def unsub(channel: String) = Auth { ctx ?=> me ?=> env.timeline.unsubApi.set(channel, me, ~get("unsub") == "on") inject NoContent } diff --git a/app/controllers/Tournament.scala b/app/controllers/Tournament.scala index f3fce34bead2d..cbf06d3f31b61 100644 --- a/app/controllers/Tournament.scala +++ b/app/controllers/Tournament.scala @@ -19,7 +19,8 @@ final class Tournament(env: Env, apiC: => Api)(using akka.stream.Materializer) e private def jsonView = env.tournament.jsonView private def forms = env.tournament.forms private def cachedTour(id: TourId) = env.tournament.cached.tourCache.byId(id) - private given lila.user.UserFlairApi = env.user.flairApi + private given lila.user.FlairApi = env.user.flairApi + private given lila.hub.LightTeam.Api = env.team.lightTeamApi private def tournamentNotFound(using Context) = NotFound.page(html.tournament.bits.notFound()) @@ -80,7 +81,6 @@ final class Tournament(env: Env, apiC: => Api)(using akka.stream.Materializer) e json <- jsonView( tour = tour, page = page, - getTeamName = env.team.getTeamName.apply, playerInfoExt = none, socketVersion = version.some, partial = false, @@ -106,7 +106,6 @@ final class Tournament(env: Env, apiC: => Api)(using akka.stream.Materializer) e json <- jsonView( tour = tour, page = page, - getTeamName = env.team.getTeamName.apply, playerInfoExt = playerInfoExt, socketVersion = socketVersion, partial = partial, @@ -138,7 +137,7 @@ final class Tournament(env: Env, apiC: => Api)(using akka.stream.Materializer) e def teamInfo(tourId: TourId, teamId: TeamId) = Open: Found(cachedTour(tourId)): tour => - Found(env.team.teamRepo mini teamId): team => + Found(env.team lightTeam teamId): team => negotiate( FoundPage(api.teamBattleTeamInfo(tour, teamId)): views.html.tournament.teamBattle.teamInfo(tour, team, _) @@ -267,7 +266,6 @@ final class Tournament(env: Env, apiC: => Api)(using akka.stream.Materializer) e json = jsonView( tour, none, - env.team.getTeamName.apply, none, none, partial = false, @@ -291,7 +289,6 @@ final class Tournament(env: Env, apiC: => Api)(using akka.stream.Materializer) e jsonView( tour, none, - env.team.getTeamName.apply, none, none, partial = false, @@ -356,7 +353,6 @@ final class Tournament(env: Env, apiC: => Api)(using akka.stream.Materializer) e jsonView( tour, none, - env.team.getTeamName.apply, none, none, partial = false, @@ -443,11 +439,10 @@ final class Tournament(env: Env, apiC: => Api)(using akka.stream.Materializer) e def battleTeams(id: TourId) = Open: cachedTour(id).flatMap: - _.filter(_.isTeamBattle) so { tour => - env.tournament.cached.battle.teamStanding.get(tour.id) flatMap { standing => - Ok.page(views.html.tournament.teamBattle.standing(tour, standing)) - } - } + _.filter(_.isTeamBattle) so: tour => + env.tournament.cached.battle.teamStanding.get(tour.id) flatMap: standing => + env.team.cached.preloadMany(standing.map(_.teamId)) >> + Ok.page(views.html.tournament.teamBattle.standing(tour, standing)) private def WithEditableTournament(id: TourId)( f: Tour => Fu[Result] @@ -461,14 +456,11 @@ final class Tournament(env: Env, apiC: => Api)(using akka.stream.Materializer) e _.refreshAfterWrite(15.seconds) .maximumSize(256) .buildAsyncFuture: tourId => - repo.isUnfinished(tourId) flatMapz { - env.streamer.liveStreamApi.all.flatMap { + repo.isUnfinished(tourId) flatMapz: + env.streamer.liveStreamApi.all.flatMap: _.streams - .map: stream => + .traverse: stream => env.tournament.hasUser(tourId, stream.streamer.userId).dmap(_ option stream.streamer.userId) - .parallel .dmap(_.flatten) - } - } private given GetMyTeamIds = me => env.team.cached.teamIdsList(me.userId) diff --git a/app/controllers/Ublog.scala b/app/controllers/Ublog.scala index 1745c58eb6714..3ece241718f9d 100644 --- a/app/controllers/Ublog.scala +++ b/app/controllers/Ublog.scala @@ -1,11 +1,13 @@ package controllers import play.api.i18n.Lang +import play.api.data.Form +import play.api.data.Forms.* import views.* import lila.app.{ given, * } import lila.common.config -import lila.i18n.{ I18nLangPicker, LangList } +import lila.i18n.{ I18nLangPicker, LangList, Language } import lila.report.Suspect import lila.ublog.{ UblogBlog, UblogPost } import lila.user.{ User as UserModel } @@ -164,9 +166,8 @@ final class Ublog(env: Env) extends LilaController(env): def like(id: UblogPostId, v: Boolean) = Auth { ctx ?=> _ ?=> NoBot: NotForKids: - env.ublog.rank.like(id, v) map { likes => + env.ublog.rank.like(id, v) map: likes => Ok(likes.value) - } } def redirect(id: UblogPostId) = Open: @@ -189,6 +190,23 @@ final class Ublog(env: Env) extends LilaController(env): ) } + def rankAdjust(postId: String) = SecureBody(_.ModerateBlog) { ctx ?=> me ?=> + Found(env.ublog.api.getPost(UblogPostId(postId))): post => + Form: + single: + "value" -> optional(number) + .bindFromRequest() + .fold( + _ => Redirect(urlOfPost(post)).flashFailure, + rankAdjustDays => + for + _ <- env.ublog.api.setRankAdjust(post.id, ~rankAdjustDays) + _ <- env.mod.logApi.ublogRankAdjust(post.created.by, post.id, ~rankAdjustDays) + _ <- env.ublog.rank.recomputePostRank(post) + yield Redirect(urlOfPost(post)).flashSuccess + ) + } + private val ImageRateLimitPerIp = lila.memo.RateLimit.composite[lila.common.IpAddress]( key = "ublog.image.ip" )( @@ -220,9 +238,9 @@ final class Ublog(env: Env) extends LilaController(env): env.ublog.paginator.liveByFollowed(me, page) map html.ublog.index.friends } - def communityLang(language: String, page: Int = 1) = Open: + def communityLang(langStr: String, page: Int = 1) = Open: import I18nLangPicker.ByHref - I18nLangPicker.byHref(language, ctx.req) match + I18nLangPicker.byHref(langStr, ctx.req) match case ByHref.NotFound => Redirect(routes.Ublog.communityAll(page)) case ByHref.Redir(code) => Redirect(routes.Ublog.communityLang(code, page)) case ByHref.Refused(lang) => communityIndex(lang.some, page) @@ -233,24 +251,19 @@ final class Ublog(env: Env) extends LilaController(env): def communityAll(page: Int) = Open: communityIndex(none, page) - def communityIndex(l: Option[Lang], page: Int)(using ctx: Context) = + private def communityIndex(l: Option[Lang], page: Int)(using ctx: Context) = NotForKids: Reasonable(page, config.Max(100)): pageHit Ok.pageAsync: - env.ublog.paginator.liveByCommunity(l, page) map { - html.ublog.index.community(l, _) - } - - def communityLangBC(code: String) = Anon: - val l = LangList.popularNoRegion.find(_.code == code) - Redirect: - l.fold(routes.Ublog.communityAll())(l => routes.Ublog.communityLang(l.language)) + val language = l.map(Language.apply) + env.ublog.paginator.liveByCommunity(language, page) map: + html.ublog.index.community(language, _) def communityAtom(language: String) = Anon: val l = LangList.popularNoRegion.find(l => l.language == language || l.code == language) env.ublog.paginator - .liveByCommunity(l, page = 1) + .liveByCommunity(l.map(Language.apply), page = 1) .map: posts => Ok(html.ublog.atom.community(language, posts.currentPageResults)) as XML @@ -258,9 +271,8 @@ final class Ublog(env: Env) extends LilaController(env): NotForKids: Reasonable(page, config.Max(100)): Ok.pageAsync: - env.ublog.paginator.liveByLiked(page) map { + env.ublog.paginator.liveByLiked(page) map: html.ublog.index.liked(_) - } } def topics = Open: @@ -272,12 +284,10 @@ final class Ublog(env: Env) extends LilaController(env): def topic(str: String, page: Int, byDate: Boolean) = Open: NotForKids: Reasonable(page, config.Max(100)): - lila.ublog.UblogTopic.fromUrl(str) so { top => + lila.ublog.UblogTopic.fromUrl(str) so: top => Ok.pageAsync: - env.ublog.paginator.liveByTopic(top, page, byDate) map { + env.ublog.paginator.liveByTopic(top, page, byDate) map: html.ublog.index.topic(top, _, byDate) - } - } def userAtom(username: UserStr) = Anon: env.user.repo @@ -288,9 +298,8 @@ final class Ublog(env: Env) extends LilaController(env): env.ublog.api .getUserBlog(user) .flatMap: blog => - (isBlogVisible(user, blog) so env.ublog.paginator.byUser(user, true, 1)) map { posts => + (isBlogVisible(user, blog) so env.ublog.paginator.byUser(user, true, 1)) map: posts => Ok(html.ublog.atom.user(user, posts.currentPageResults)) as XML - } private def isBlogVisible(user: UserModel, blog: UblogBlog) = user.enabled.yes && blog.visible diff --git a/app/controllers/User.scala b/app/controllers/User.scala index 423be7dec716a..5b43471e46edc 100644 --- a/app/controllers/User.scala +++ b/app/controllers/User.scala @@ -122,11 +122,10 @@ final class User( if HTTPRequest.isSynchronousHttp(ctx.req) then for info <- env.userInfo(u, nbs, withUblog = false) - _ <- env.team.cached.nameCache preloadMany info.teamIds + _ <- env.team.cached.lightCache preloadMany info.teamIds social <- env.socialInfo(u) searchForm = (filters.current == GameFilter.Search) option - GameFilterMenu - .searchForm(userGameSearch, filters.current) + GameFilterMenu.searchForm(userGameSearch, filters.current) page <- renderPage: html.user.show.page.games(info, pag, filters, searchForm, social, notes) yield Ok(page) diff --git a/app/controllers/Video.scala b/app/controllers/Video.scala index b2c3b3d27045c..debf9ec83a5f3 100644 --- a/app/controllers/Video.scala +++ b/app/controllers/Video.scala @@ -37,7 +37,7 @@ final class Video(env: Env) extends LilaController(env): def show(id: String) = Open: WithUserControl: control => - api.video.find(id) flatMap { + api.video.find(id) flatMap: case None => NotFound.page(html.video.bits.notFound(control)) case Some(video) => api.video.similar(ctx.me, video, 9) zip @@ -46,7 +46,6 @@ final class Video(env: Env) extends LilaController(env): } flatMap { (similar, _) => Ok.page(html.video.show(video, similar, control)) } - } def author(author: String) = Open: WithUserControl: control => diff --git a/app/http/ContentSecurityPolicy.scala b/app/http/ContentSecurityPolicy.scala index 85dfb27f1fd48..76e82f8f016b7 100644 --- a/app/http/ContentSecurityPolicy.scala +++ b/app/http/ContentSecurityPolicy.scala @@ -85,6 +85,12 @@ case class ContentSecurityPolicy( def withWikiBooks = copy(connectSrc = "en.wikibooks.org" :: connectSrc) + // for extensions to use their cloud eval API + // https://www.chessdb.cn/cloudbook_api_en.html + def withChessDbCn = copy(connectSrc = "chessdb.cn" :: connectSrc) + + def withExternalAnalysisApis = withWikiBooks.withChessDbCn + def withLilaHttp = copy(connectSrc = "http.lichess.org" :: connectSrc) def withInlineIconFont = copy(fontSrc = "data:" :: fontSrc) diff --git a/app/http/CtrlErrors.scala b/app/http/CtrlErrors.scala index 28fb57d720276..2d7312bf1d33e 100644 --- a/app/http/CtrlErrors.scala +++ b/app/http/CtrlErrors.scala @@ -15,6 +15,9 @@ trait CtrlErrors extends ControllerHelpers: def notFoundJson(msg: String = "Not found"): Result = NotFound(jsonError(msg)) as JSON def notFoundText(msg: String = "Not found"): Result = Results.NotFound(msg) + def forbiddenJson(msg: String = "You can't do that"): Result = Forbidden(jsonError(msg)) as JSON + def forbiddenText(msg: String = "You can't do that"): Result = Results.Forbidden(msg) + private val jsonGlobalErrorRenamer: Reads[JsObject] = import play.api.libs.json.* __.json update ( diff --git a/app/http/KeyPages.scala b/app/http/KeyPages.scala index c6fcabfe36283..d9a206877b09c 100644 --- a/app/http/KeyPages.scala +++ b/app/http/KeyPages.scala @@ -28,7 +28,7 @@ final class KeyPages(val env: Env)(using Executor) .flatMap(env.tournament.featuring.homepage.get) .recoverDefault, swiss = env.swiss.feature.onHomepage.getUnit.getIfPresent, - events = env.event.api.promoteTo(ctx.req).recoverDefault, + events = env.event.api.promoteTo(ctx.acceptLanguages).recoverDefault, simuls = env.simul.allCreatedFeaturable.get {}.recoverDefault, streamerSpots = env.streamer.homepageMaxSetting.get() ) diff --git a/app/http/RequestContext.scala b/app/http/RequestContext.scala index efb072decd348..ce4d620505909 100644 --- a/app/http/RequestContext.scala +++ b/app/http/RequestContext.scala @@ -85,7 +85,6 @@ trait RequestContext(using Executor): case Some(Right(d)) if !env.net.isProd => d.copy(me = d.me.map: _.addRole(lila.security.Permission.Beta.dbKey) - .addRole(lila.security.Permission.Prismic.dbKey) ).some case Some(Right(d)) => d.some case _ => none diff --git a/app/http/RequestGetter.scala b/app/http/RequestGetter.scala index 6a4df6f6bd770..dcb1899495b54 100644 --- a/app/http/RequestGetter.scala +++ b/app/http/RequestGetter.scala @@ -34,7 +34,7 @@ trait RequestGetter: protected def getLong(name: String)(using RequestHeader) = get(name).flatMap(_.toLongOption) - protected def getTimestamp(name: String)(using RequestHeader) = + protected def getTimestamp(name: String)(using RequestHeader): Option[Instant] = getLong(name) map millisToInstant protected def getBool(name: String)(using RequestHeader): Boolean = diff --git a/app/http/ResponseBuilder.scala b/app/http/ResponseBuilder.scala index 3de0941095cfb..f89b8bccec85e 100644 --- a/app/http/ResponseBuilder.scala +++ b/app/http/ResponseBuilder.scala @@ -56,8 +56,9 @@ trait ResponseBuilder(using Executor) json = TooManyRequests(jsonError(msg)) ) - val jsonOkBody = Json.obj("ok" -> true) - val jsonOkResult = JsonOk(jsonOkBody) + val jsonOkBody = Json.obj("ok" -> true) + val jsonOkResult = JsonOk(jsonOkBody) + def jsonOkMsg(msg: String) = JsonOk(Json.obj("ok" -> msg)) def JsonOk(body: JsValue): Result = Ok(body) as JSON def JsonOk[A: Writes](body: A): Result = Ok(Json toJson body) as JSON @@ -101,16 +102,14 @@ trait ResponseBuilder(using Executor) Unauthorized(jsonError("Login required")) ) - private val forbiddenJsonResult = Forbidden(jsonError("Authorization failed")) - def authorizationFailed(using ctx: Context): Fu[Result] = if HTTPRequest.isSynchronousHttp(ctx.req) then Forbidden.page(views.html.site.message.authFailed) else fuccess: render: - case Accepts.Json() => forbiddenJsonResult - case _ => Results.Forbidden("Authorization failed") + case Accepts.Json() => forbiddenJson() + case _ => forbiddenText() def serverError(msg: String)(using ctx: Context): Fu[Result] = negotiate( @@ -120,12 +119,12 @@ trait ResponseBuilder(using Executor) def notForBotAccounts(using Context) = negotiate( Forbidden.page(views.html.site.message.noBot), - Forbidden(jsonError("This API endpoint is not for Bot accounts.")) + forbiddenJson("This API endpoint is not for Bot accounts.") ) def notForLameAccounts(using Context, Me) = negotiate( Forbidden.page(views.html.site.message.noLame), - Forbidden(jsonError("The access to this resource is restricted.")) + forbiddenJson("The access to this resource is restricted.") ) def playbanJsonError(ban: lila.playban.TempBan) = diff --git a/app/mashup/Preload.scala b/app/mashup/Preload.scala index ac4a7bc71b818..e64ec227b59b0 100644 --- a/app/mashup/Preload.scala +++ b/app/mashup/Preload.scala @@ -31,6 +31,7 @@ final class Preload( roundProxy: lila.round.GameProxyRepo, simulIsFeaturable: SimulIsFeaturable, lastPostCache: lila.blog.LastPostCache, + getLastUpdates: lila.blog.DailyFeed.GetLastUpdates, lastPostsCache: AsyncLoadingCache[Unit, List[UblogPost.PreviewPost]], msgApi: lila.msg.MsgApi, relayApi: lila.relay.RelayApi, @@ -74,7 +75,7 @@ final class Preload( tourWinners.all.dmap(_.top).mon(_.lobby segment "tourWinners") zip (ctx.noBot so dailyPuzzle()).mon(_.lobby segment "puzzle") zip (ctx.kid.no so liveStreamApi.all - .dmap(_.homepage(streamerSpots, ctx.req, ctx.me.flatMap(_.lang)) withTitles lightUserApi) + .dmap(_.homepage(streamerSpots, ctx.acceptLanguages) withTitles lightUserApi) .mon(_.lobby segment "streams")) zip (ctx.userId so playbanApi.currentBan).mon(_.lobby segment "playban") zip (ctx.blind so ctx.me so roundProxy.urgentGames) zip @@ -105,7 +106,8 @@ final class Preload( currentGame, simulIsFeaturable, blindGames, - lastPostCache.apply, + lastPostCache.apply.filterNot(_.isOld).filter(_.forKids || ctx.kid.no), + getLastUpdates(), ublogPosts, withPerfs, hasUnreadLichessMessage = lichessMsg @@ -149,6 +151,7 @@ object Preload: isFeaturable: Simul => Boolean, blindGames: List[Pov], lastPost: Option[lila.blog.MiniPost], + lastUpdates: List[lila.blog.DailyFeed.Update], ublogPosts: List[UblogPost.PreviewPost], me: Option[User.WithPerfs], hasUnreadLichessMessage: Boolean diff --git a/app/mashup/UserInfo.scala b/app/mashup/UserInfo.scala index f4935e41fef97..b15c135de422f 100644 --- a/app/mashup/UserInfo.scala +++ b/app/mashup/UserInfo.scala @@ -124,4 +124,4 @@ object UserInfo: (user.count.rated >= 10).so(insightShare.grant(user)) ).mapN(UserInfo(nbs, _, _, _, _, _, _, _, _, _, _, _, _, _)) - def preloadTeams(info: UserInfo) = teamCache.nameCache.preloadMany(info.teamIds) + def preloadTeams(info: UserInfo) = teamCache.lightCache.preloadMany(info.teamIds) diff --git a/app/router.scala b/app/router.scala index 73a0fa99551f8..2f4c9d5cd263d 100644 --- a/app/router.scala +++ b/app/router.scala @@ -70,6 +70,7 @@ object ReverseRouterConversions: given Conversion[UserId, UserStr] = _ into UserStr given Conversion[ForumCategId, String] = _.value given Conversion[ForumTopicId, String] = _.value + given Conversion[lila.i18n.Language, String] = _.value given challengeIdConv: Conversion[Challenge.Id, String] = _.value given appealIdConv: Conversion[Appeal.Id, String] = _.value given reportIdConv: Conversion[Report.Id, String] = _.value @@ -79,3 +80,4 @@ object ReverseRouterConversions: given relayTourIdConv: Conversion[lila.relay.RelayTour.Id, String] = _.value given perfKeyConv: Conversion[Perf.Key, String] = _.value given puzzleKeyConv: Conversion[PuzzleTheme.Key, String] = _.value + given localDateConv: Conversion[java.time.LocalDate, String] = _.toString diff --git a/app/templating/AssetHelper.scala b/app/templating/AssetHelper.scala index 18bc75c781832..c81587e74484c 100644 --- a/app/templating/AssetHelper.scala +++ b/app/templating/AssetHelper.scala @@ -24,11 +24,16 @@ trait AssetHelper extends HasEnv: def assetVersion = AssetVersion.current + // bump flairs version if a flair is changed only (not added or removed) + val flairVersion = "______2" + def assetUrl(path: String): String = s"$assetBaseUrl/assets/_$assetVersion/$path" def staticAssetUrl(path: String): String = s"$assetBaseUrl/assets/$path" def cdnUrl(path: String) = s"$assetBaseUrl$path" + def flairSrc(flair: Flair) = staticAssetUrl(s"$flairVersion/flair/img/$flair.webp") + def cssTag(name: String)(using ctx: Context): Frag = cssTagWithDirAndTheme(name, isRTL, ctx.pref.currentBg) @@ -41,9 +46,8 @@ trait AssetHelper extends HasEnv: else cssTagWithDirAndSimpleTheme(name, isRTL, theme) private def cssTagWithDirAndSimpleTheme(name: String, isRTL: Boolean, theme: String): Tag = - cssAt( + cssAt: s"css/$name.${if isRTL then "rtl" else "ltr"}.$theme.${if minifiedAssets then "min" else "dev"}.css" - ) def cssTagNoTheme(name: String): Frag = cssAt(s"css/$name.${if minifiedAssets then "min" else "dev"}.css") @@ -63,16 +67,16 @@ if (window.matchMedia('(prefers-color-scheme: dark)').media === 'not all') def jsModule(name: String): Frag = script(tpe := "module", src := assetUrl(s"compiled/$name${minifiedAssets so ".min"}.js")) def jsModuleInit(name: String)(using PageContext) = - frag(jsModule(name), embedJsUnsafeLoadThen(s"lichess.loadEsm('$name')")) + frag(jsModule(name), embedJsUnsafeLoadThen(s"lichess.asset.loadEsm('$name')")) def jsModuleInit(name: String, text: String)(using PageContext) = - frag(jsModule(name), embedJsUnsafeLoadThen(s"lichess.loadEsm('$name',{init:$text})")) + frag(jsModule(name), embedJsUnsafeLoadThen(s"lichess.asset.loadEsm('$name',{init:$text})")) def jsModuleInit(name: String, json: JsValue)(using PageContext): Frag = jsModuleInit(name, safeJsonValue(json)) def jsModuleInit(name: String, text: String, nonce: lila.api.Nonce) = - frag(jsModule(name), embedJsUnsafeLoadThen(s"lichess.loadEsm('$name',{init:$text})", nonce)) + frag(jsModule(name), embedJsUnsafeLoadThen(s"lichess.asset.loadEsm('$name',{init:$text})", nonce)) def jsModuleInit(name: String, json: JsValue, nonce: lila.api.Nonce) = frag( jsModule(name), - embedJsUnsafeLoadThen(s"lichess.loadEsm('$name',{init:${safeJsonValue(json)}})", nonce) + embedJsUnsafeLoadThen(s"lichess.asset.loadEsm('$name',{init:${safeJsonValue(json)}})", nonce) ) def analyseInit(mode: String, json: JsValue)(using ctx: PageContext) = jsModuleInit("analysisBoard", Json.obj("mode" -> mode, "cfg" -> json)) diff --git a/app/templating/DateHelper.scala b/app/templating/DateHelper.scala index 6fc503a17e66a..86581731e413c 100644 --- a/app/templating/DateHelper.scala +++ b/app/templating/DateHelper.scala @@ -35,10 +35,10 @@ trait DateHelper: _ => DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(lang.toLocale) ) - def showInstantUTC(instant: Instant)(using Lang): String = + def showInstant(instant: Instant)(using Lang): String = dateTimeFormatter print instant - def showDate(instant: Instant)(using lang: Lang): String = + def showDate(instant: Instant)(using Lang): String = showDate(instant.date) def showDate(date: LocalDate)(using lang: Lang): String = @@ -81,8 +81,8 @@ trait DateHelper: def secondsFromNow(seconds: Int, alwaysRelative: Boolean = false): Tag = momentFromNow(nowInstant plusSeconds seconds, alwaysRelative) - def momentFromNowServer(instant: Instant): Frag = - timeTag(title := f"${showEnglishInstant(instant)} UTC")(momentFromNowServerText(instant)) + def momentFromNowServer(instant: Instant)(using Lang): Frag = + timeTag(title := f"${showInstant(instant)} UTC")(momentFromNowServerText(instant)) def momentFromNowServerText(instant: Instant, inFuture: Boolean = false): String = val (dateSec, nowSec) = (instant.toMillis / 1000, nowSeconds) @@ -101,3 +101,6 @@ trait DateHelper: else if months == 0 then s"${pluralize("week", weeks)}$preposition" else if years == 0 then s"${pluralize("month", months)}$preposition" else s"${pluralize("year", years)}$preposition" + + def timeRemaining(instant: Instant, once: Boolean = false): Tag = + timeTag(cls := s"timeago remaining${once so " once"}", datetimeAttr := isoDateTime(instant))(nbsp) diff --git a/app/templating/Environment.scala b/app/templating/Environment.scala index 7d997b3581920..d844c210758d8 100644 --- a/app/templating/Environment.scala +++ b/app/templating/Environment.scala @@ -20,7 +20,8 @@ object Environment with TeamHelper with TournamentHelper with FlashHelper - with ChessgroundHelper: + with ChessgroundHelper + with HtmlHelper: export lila.Lila.{ id as _, *, given } export lila.api.Context.{ *, given } @@ -59,7 +60,3 @@ object Environment env.report.scoreThresholdsSetting.get().mid, env.report.scoreThresholdsSetting.get().high ) - - val spinner: Frag = raw( - """
""" - ) diff --git a/app/templating/FormHelper.scala b/app/templating/FormHelper.scala index de8d445850713..f960c13067f1a 100644 --- a/app/templating/FormHelper.scala +++ b/app/templating/FormHelper.scala @@ -7,6 +7,8 @@ import play.api.i18n.Lang import lila.app.ui.ScalatagsTemplate.{ *, given } import lila.i18n.I18nKey import lila.common.licon +import scalatags.generic.TypedTag +import scalatags.text.Builder trait FormHelper: self: I18nHelper => @@ -32,13 +34,12 @@ trait FormHelper: val postForm = form(method := "post") val submitButton = button(tpe := "submit") - def markdownAvailable(using Lang) = - trans.markdownAvailable( + def markdownAvailable(using Lang): Frag = + trans.markdownAvailable: a( href := "https://guides.github.com/features/mastering-markdown/", targetBlank )("Markdown") - ) def checkboxes[V]( field: play.api.data.Field, @@ -252,16 +253,42 @@ trait FormHelper: field: Field, withTime: Boolean = true, utc: Boolean = false, - minDate: Option[String] = Some("today") + minDate: Option[String] = Some("today"), + dateFormat: Option[String] = None ): Tag = input(field, klass = s"flatpickr${if utc then " flatpickr-utc" else ""}")( dataEnableTime := withTime, dataTime24h := withTime, + dateFormat.map(df => data("date-format") := df), dataMinDate := minDate.map: case "today" if utc => "yesterday" case d => d ) + private val exceptEmojis = data("except-emojis") := lila.user.FlairApi.adminFlairs.mkString(" ") + def flairPickerGroup(field: Field, current: Option[Flair], label: Frag)(view: Frag)(using Context): Tag = + form3.group(field, trans.flair(), half = true): f => + flairPicker(f, current, label)(view) + + def flairPicker(field: Field, current: Option[Flair], label: Frag)(view: Frag)(using ctx: Context): Frag = + frag( + details(cls := "form-control emoji-details")( + summary(cls := "button button-metal button-no-upper")( + label, + ":", + nbsp, + view + ), + hidden(field, current.map(_.value)), + div( + cls := "flair-picker", + (!ctx.me.exists(_.isAdmin)).option(exceptEmojis) + ) + ), + current.isDefined option p: + button(cls := "button button-red button-thin button-empty text emoji-remove")(trans.delete()) + ) + object file: def image(name: String): Frag = st.input(tpe := "file", st.name := name, accept := "image/png, image/jpeg") diff --git a/app/templating/HtmlHelper.scala b/app/templating/HtmlHelper.scala new file mode 100644 index 0000000000000..fc2d12fe0389a --- /dev/null +++ b/app/templating/HtmlHelper.scala @@ -0,0 +1,15 @@ +package lila.app +package templating + +import lila.app.ui.ScalatagsTemplate.* + +trait HtmlHelper: + + def renderCache[A](ttl: FiniteDuration)(toFrag: A => Frag): A => Frag = + val cache = lila.memo.CacheApi.scaffeineNoScheduler + .expireAfterWrite(1 minute) + .build[A, String]() + from => raw(cache.get(from, from => toFrag(from).render)) + + val spinner: Frag = raw: + """
""" diff --git a/app/templating/I18nHelper.scala b/app/templating/I18nHelper.scala index 866091aea5187..4d3477c291003 100644 --- a/app/templating/I18nHelper.scala +++ b/app/templating/I18nHelper.scala @@ -33,6 +33,6 @@ trait I18nHelper: if ctx.isAuth || ctx.lang.language == "en" then path else - val code = lila.i18n.fixJavaLanguageCode(ctx.lang) + val code = lila.i18n.fixJavaLanguage(ctx.lang) if path == "/" then s"/$code" else s"/$code$path" diff --git a/app/templating/SetupHelper.scala b/app/templating/SetupHelper.scala index 15548b570e4ec..7d0f9455fdc6b 100644 --- a/app/templating/SetupHelper.scala +++ b/app/templating/SetupHelper.scala @@ -259,7 +259,7 @@ trait SetupHelper: (Pref.SubmitMove.CORRESPONDENCE, trans.correspondence.txt()), (Pref.SubmitMove.CLASSICAL, trans.classical.txt()), (Pref.SubmitMove.RAPID, trans.rapid.txt()), - (Pref.SubmitMove.BLITZ, "Blitz") + (Pref.SubmitMove.BLITZ, trans.blitz.txt()) ) def confirmResignChoices(using Lang) = diff --git a/app/templating/TeamHelper.scala b/app/templating/TeamHelper.scala index 34c857cc761b1..722b1c9179ff2 100644 --- a/app/templating/TeamHelper.scala +++ b/app/templating/TeamHelper.scala @@ -1,34 +1,45 @@ package lila.app - package templating import scalatags.Text.all.Tag import controllers.routes +import controllers.team.routes.{ Team as teamRoutes } import lila.app.ui.ScalatagsTemplate.{ *, given } -import lila.hub.LightTeam.TeamName +import lila.hub.LightTeam +import lila.team.Team trait TeamHelper: - self: HasEnv with RouterHelper => + self: HasEnv with RouterHelper with AssetHelper => def isMyTeamSync(teamId: TeamId)(using ctx: Context): Boolean = - ctx.userId.so { env.team.api.syncBelongsTo(teamId, _) } + ctx.userId.exists { env.team.api.syncBelongsTo(teamId, _) } - def teamIdToName(id: TeamId): TeamName = env.team.getTeamName(id).getOrElse(id.value) + def teamIdToLight(id: TeamId): LightTeam = + env.team.lightTeamSync(id).getOrElse(LightTeam(id, id.value, none)) def teamLink(id: TeamId, withIcon: Boolean = true): Tag = - teamLink(id, teamIdToName(id), withIcon) + teamLink(teamIdToLight(id), withIcon) - def teamLink(id: TeamId, name: Frag, withIcon: Boolean): Tag = + def teamLink(team: LightTeam, withIcon: Boolean): Tag = a( - href := routes.Team.show(id), + href := teamRoutes.show(team.id), dataIcon := withIcon.option(lila.common.licon.Group), cls := withIcon option "text" - )(name) + )(team.name, teamFlair(team)) + + def teamLink(team: Team, withIcon: Boolean): Tag = teamLink(team.light, withIcon) + + def teamFlair(team: Team): Option[Tag] = team.flair.map(teamFlair) + def teamFlair(team: LightTeam): Option[Tag] = team.flair.map(teamFlair) + + def teamFlair(flair: Flair): Tag = + img(cls := "uflair", src := staticAssetUrl(s"$flairVersion/flair/img/$flair.webp")) def teamForumUrl(id: TeamId) = routes.ForumCateg.show("team-" + id) - lazy val variantTeamLinks: Map[chess.variant.Variant.LilaKey, (lila.team.Team.Mini, Frag)] = - lila.team.Team.variants.view.mapValues { team => - (team, teamLink(team.id, team.name, true)) - }.toMap + lazy val variantTeamLinks: Map[chess.variant.Variant.LilaKey, (LightTeam, Frag)] = + lila.team.Team.variants.view + .mapValues: team => + (team, teamLink(team, true)) + .toMap diff --git a/app/templating/UserHelper.scala b/app/templating/UserHelper.scala index 3d5a1cda2f77e..043bfcad41ab6 100644 --- a/app/templating/UserHelper.scala +++ b/app/templating/UserHelper.scala @@ -10,7 +10,7 @@ import lila.common.licon import lila.common.LightUser import lila.i18n.{ I18nKey, I18nKeys as trans } import lila.rating.{ Perf, PerfType } -import lila.user.{ User, UserPerfs, UserFlairApi } +import lila.user.{ User, UserPerfs, FlairApi } trait UserHelper extends HasEnv: self: I18nHelper with StringHelper with NumberHelper with DateHelper with AssetHelper => @@ -145,6 +145,23 @@ trait UserHelper extends HasEnv: modIcon = false ) + def lightUserSpan( + user: LightUser, + cssClass: Option[String] = None, + withOnline: Boolean = true, + withTitle: Boolean = true, + params: String = "" + )(using Lang): Tag = + span( + cls := userClass(user.id, cssClass, withOnline), + dataHref := userUrl(user.name) + )( + withOnline so lineIcon(user.isPatron), + titleTag(user.title), + user.name, + user.flair.map(userFlair) + ) + def titleTag(title: Option[UserTitle]): Option[Frag] = title.map: t => frag(userTitleTag(t), nbsp) @@ -158,7 +175,7 @@ trait UserHelper extends HasEnv: withOnline: Boolean, truncate: Option[Int], title: Option[UserTitle], - flair: Option[UserFlair], + flair: Option[Flair], params: String, modIcon: Boolean )(using Lang): Tag = @@ -227,14 +244,9 @@ trait UserHelper extends HasEnv: name ) - def userFlair(user: User): Option[Tag] = - user.flair.map(userFlair) + def userFlair(user: User): Option[Tag] = user.flair.map(userFlair) - def userFlair(flair: UserFlair): Tag = - img( - cls := "uflair", - src := staticAssetUrl(s"flair/img/$flair.webp") - ) + def userFlair(flair: Flair): Tag = img(cls := "uflair", src := flairSrc(flair)) private def renderRating(perf: Perf): Frag = frag(" (", perf.intRating, perf.provisional.yes option "?", ")") diff --git a/app/ui/scalatags.scala b/app/ui/scalatags.scala index 2dfebd6079773..f00989b6a68a2 100644 --- a/app/ui/scalatags.scala +++ b/app/ui/scalatags.scala @@ -24,6 +24,7 @@ trait ScalatagsAttrs: val novalidate = attr("novalidate").empty val datetimeAttr = attr("datetime") val dataBotAttr = attr("data-bot").empty + val dataUser = attr("data-user") val deferAttr = attr("defer").empty val downloadAttr = attr("download").empty val viewBoxAttr = attr("viewBox") diff --git a/app/views/account/kid.scala b/app/views/account/kid.scala index 0d2daa82986d2..ef1646c966446 100644 --- a/app/views/account/kid.scala +++ b/app/views/account/kid.scala @@ -14,7 +14,7 @@ object kid: active = "kid" ): div(cls := "box box-pad")( - h1(cls := "box__top")(trans.kidMode()), + h1(cls := "box__top")(if u.kid then trans.kidModeIsEnabled() else trans.kidMode()), standardFlash, p(trans.kidModeExplanation()), br, diff --git a/app/views/account/profile.scala b/app/views/account/profile.scala index ebf6c811b51ae..4752d26ba70ed 100644 --- a/app/views/account/profile.scala +++ b/app/views/account/profile.scala @@ -23,7 +23,7 @@ object profile: div(cls := "box box-pad")( h1(cls := "box__top")(trans.editProfile()), standardFlash, - postForm(cls := "form3", action := routes.Account.profileApply)( + postForm(cls := "form3 dirty-alert", action := routes.Account.profileApply)( div(cls := "form-group")(trans.allInformationIsPublicAndOptional()), form3.split( ctx.kid.no option @@ -31,30 +31,16 @@ object profile: .group(form("bio"), trans.biography(), half = true, help = trans.biographyDescription().some): f => form3.textarea(f)(rows := 5) , - form3.group(form("flair"), "Flair", half = true): f => - frag( - details(cls := "form-control emoji-details")( - summary(cls := "button button-metal button-no-upper")( - trans.setFlair(), - userSpan(u, withPowerTip = false) - ), - form3.hidden(f, u.flair.map(_.value)), - div(cls := "emoji-picker") - ), - u.flair.isDefined option p( - button( - cls := "button button-red button-thin button-empty text emoji-remove" - )(trans.delete()) - ), - p(cls := "form-help")( - a( - href := s"${routes.Pref.form("display")}#showFlairs", - cls := "text", - dataIcon := licon.InfoCircle - ): - trans.youCanHideFlair() - ) - ) + form3.flairPickerGroup(form("flair"), u.flair, label = trans.setFlair())( + userSpan(u, withPowerTip = false, cssClass = "flair-container".some) + ): + p(cls := "form-help"): + a( + href := s"${routes.Pref.form("display")}#showFlairs", + cls := "text", + dataIcon := licon.InfoCircle + ): + trans.youCanHideFlair() ), form3.split( form3.group(form("flag"), trans.countryRegion(), half = true): f => diff --git a/app/views/appeal/tree.scala b/app/views/appeal/tree.scala index eac10831a03ff..943c50fa33810 100644 --- a/app/views/appeal/tree.scala +++ b/app/views/appeal/tree.scala @@ -23,6 +23,7 @@ object tree: val accountMuted = "Your account is muted."; val excludedFromLeaderboards = "Your account has been excluded from leaderboards."; val closedByModerators = "Your account was closed by moderators."; + val hiddenBlog = "Your blogs have been hidden by moderators." private def cleanMenu(using PageContext): Branch = Branch( @@ -251,6 +252,39 @@ object tree: ) ) + private def hiddenBlogMenu(using PageContext): Branch = + val accept = + "I accept that I have broken the blog rules" + val deny = + "I deny having broken the blog rules." + Branch( + "root", + hiddenBlog, + List( + Leaf( + "hidden-blog-accept", + accept, + frag( + sendUsAnAppeal, + newAppeal(accept) + ) + ), + Leaf( + "hidden-blog-deny", + deny, + frag( + sendUsAnAppeal, + newAppeal(deny) + ) + ) + ), + content = frag( + "Make sure to read again our ", + a(href := routes.ContentPage.loneBookmark("blog-etiquette"))("blog rules"), + "." + ).some + ) + private def prizebanMenu(using PageContext): Branch = val prizebanExpired = "My ban duration has expired, as I was informed by moderators." val deny = "I reject any allegation of wrongdoing that may have prompted a prizeban." @@ -338,10 +372,11 @@ object tree: newAppeal() ) - def apply(me: User, playban: Boolean)(using ctx: PageContext) = + def apply(me: User, playban: Boolean, ublogIsVisible: Boolean)(using ctx: PageContext) = bits.layout("Appeal a moderation decision") { - val query = isGranted(_.Appeals) so ctx.req.queryString.toMap - val isMarked = playban || me.marks.engine || me.marks.boost || me.marks.troll || me.marks.rankban + val query = isGranted(_.Appeals) so ctx.req.queryString.toMap.pp + val isMarked = + playban || me.marks.engine || me.marks.boost || me.marks.troll || me.marks.rankban || me.marks.arenaBan || me.marks.prizeban || !ublogIsVisible main(cls := "page page-small box box-pad appeal force-ltr")( h1(cls := "box__top")("Appeal"), div(cls := s"nav-tree${if isMarked then " marked" else ""}")( @@ -356,6 +391,7 @@ object tree: else if me.marks.rankban || query.contains("rankban") then rankBanMenu else if me.marks.arenaBan || query.contains("arenaban") then arenaBanMenu else if me.marks.prizeban || query.contains("prizeban") then prizebanMenu + else if !ublogIsVisible || query.contains("blog") then hiddenBlogMenu else cleanMenu }, none, diff --git a/app/views/auth/bits.scala b/app/views/auth/bits.scala index 9227e22c3b692..428b64aef6742 100644 --- a/app/views/auth/bits.scala +++ b/app/views/auth/bits.scala @@ -19,12 +19,11 @@ object bits: username, if register then trans.username() else trans.usernameOrEmail(), help = register option trans.signupUsernameHint() - ) { f => + ): f => frag( form3.input(f)(autofocus, required, autocomplete := "username"), register option p(cls := "error username-exists none")(trans.usernameAlreadyUsed()) - ) - }, + ), form3.passwordModified(password, trans.password())( autocomplete := (if register then "new-password" else "current-password") ), @@ -42,7 +41,7 @@ object bits: moreCss = cssTag("auth"), moreJs = views.html.base.hcaptcha.script(form), csp = defaultCsp.withHcaptcha.some - ) { + ): main(cls := "auth auth-signup box box-pad")( boxTop( h1( @@ -58,18 +57,16 @@ object bits: form3.action(form3.submit(trans.emailMeALink())) ) ) - } def passwordResetSent(email: String)(using PageContext) = views.html.base.layout( title = trans.passwordReset.txt() - ) { + ): main(cls := "page-small box box-pad")( boxTop(h1(cls := "is-green text", dataIcon := licon.Checkmark)(trans.checkYourEmail())), p(trans.weHaveSentYouAnEmailTo(email)), p(trans.ifYouDoNotSeeTheEmailCheckOtherPlaces()) ) - } def passwordResetConfirm(token: String, form: Form[?], ok: Option[Boolean] = None)(using PageContext)(using me: Me @@ -78,7 +75,7 @@ object bits: title = s"${me.username} - ${trans.changePassword.txt()}", moreCss = cssTag("form3"), moreJs = jsModuleInit("passwordComplexity", "'form3-newPasswd1'") - ) { + ): main(cls := "page-small box box-pad")( boxTop( (ok match @@ -105,7 +102,6 @@ object bits: form3.action(form3.submit(trans.changePassword())) ) ) - } def magicLink(form: HcaptchaForm[?], fail: Boolean)(using PageContext) = views.html.base.layout( @@ -113,7 +109,7 @@ object bits: moreCss = cssTag("auth"), moreJs = views.html.base.hcaptcha.script(form), csp = defaultCsp.withHcaptcha.some - ) { + ): main(cls := "auth auth-signup box box-pad")( boxTop( h1( @@ -130,24 +126,22 @@ object bits: form3.action(form3.submit(trans.emailMeALink())) ) ) - } def magicLinkSent(using PageContext) = views.html.base.layout( title = "Log in by email" - ) { + ): main(cls := "page-small box box-pad")( boxTop(h1(cls := "is-green text", dataIcon := licon.Checkmark)(trans.checkYourEmail())), p("We've sent you an email with a link."), p(trans.ifYouDoNotSeeTheEmailCheckOtherPlaces()) ) - } def tokenLoginConfirmation(user: User, token: String, referrer: Option[String])(using PageContext) = views.html.base.layout( title = s"Log in as ${user.username}", moreCss = cssTag("form3") - ) { + ): main(cls := "page-small box box-pad")( boxTop(h1("Log in as ", userLink(user))), postForm(action := routes.Auth.loginWithTokenPost(token, referrer))( @@ -157,7 +151,6 @@ object bits: ) ) ) - } def checkYourEmailBanner(userEmail: lila.security.EmailConfirm.UserEmail) = frag( @@ -193,24 +186,18 @@ body { margin-top: 45px; } ) def tor()(using PageContext) = - views.html.base.layout( - title = "Tor exit node" - ) { + views.html.base.layout(title = "Tor exit node"): main(cls := "page-small box box-pad")( boxTop(h1(cls := "text", dataIcon := "2")("Ooops")), p("Sorry, you can't signup to Lichess through Tor!"), p("You can play, train and use almost all Lichess features as an anonymous user.") ) - } def logout()(using PageContext) = - views.html.base.layout( - title = trans.logOut.txt() - ) { + views.html.base.layout(title = trans.logOut.txt()): main(cls := "page-small box box-pad")( h1(cls := "box__top")(trans.logOut()), form(action := routes.Auth.logout, method := "post")( button(cls := "button button-red", tpe := "submit")(trans.logOut.txt()) ) ) - } diff --git a/app/views/base/layout.scala b/app/views/base/layout.scala index 522e2e8cd505f..6bb7594642f2a 100644 --- a/app/views/base/layout.scala +++ b/app/views/base/layout.scala @@ -1,6 +1,7 @@ package views.html.base import controllers.report.routes.{ Report as reportRoutes } +import controllers.team.routes.{ Team as teamRoutes } import controllers.routes import play.api.i18n.Lang @@ -105,7 +106,7 @@ object layout: s""" """ @@ -121,13 +122,13 @@ object layout: spaceless: s"""
""" @@ -137,7 +138,7 @@ object layout: spaceless: s"""
@@ -183,14 +184,14 @@ object layout: ctx.pref.bg == lila.pref.Pref.Bg.SYSTEM option embedJsUnsafe(systemThemePolyfillJs) ) - private def hrefLang(lang: String, path: String) = - s"""""" + private def hrefLang(langStr: String, path: String) = + s"""""" private def hrefLangs(path: LangPath) = raw { val pathEnd = if path.value == "/" then "" else path.value hrefLang("x-default", path.value) + hrefLang("en", path.value) + - lila.i18n.LangList.popularAlternateLanguageCodes.map { lang => - hrefLang(lang, s"/$lang$pathEnd") + lila.i18n.LangList.popularAlternateLanguages.map { l => + hrefLang(l.value, s"/$l$pathEnd") }.mkString } @@ -205,8 +206,14 @@ object layout: private val spaceRegex = """\s{2,}+""".r private def spaceless(html: String) = raw(spaceRegex.replaceAllIn(html.replace("\\n", ""), "")) + private val dailyNewsAtom = link( + href := routes.DailyFeed.atom, + st.title := "Lichess Updates Feed", + tpe := "application/atom+xml", + rel := "alternate" + ) + private val dataVapid = attr("data-vapid") - private val dataUser = attr("data-user") private val dataSocketDomains = attr("data-socket-domains") := netConfig.socketDomains.mkString(",") private val dataNonce = attr("data-nonce") private val dataAnnounce = attr("data-announce") @@ -265,13 +272,7 @@ object layout: !robots option raw(""""""), noTranslate, openGraph.map(_.frags), - (atomLinkTag | link( - href := routes.Blog.atom, - st.title := trans.blog.txt() - ))( - tpe := "application/atom+xml", - rel := "alternate" - ), + atomLinkTag | dailyNewsAtom, pref.bg == lila.pref.Pref.Bg.TRANSPARENT option pref.bgImgOrDefault map { img => raw: s""" + + + + + diff --git a/public/flair/list.sh b/public/flair/list.sh new file mode 100755 index 0000000000000..e017a853bae8e --- /dev/null +++ b/public/flair/list.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Create list.txt from img/*.webp + +pushd "$(dirname "$0")" +ls img/*.webp | sed 's/.webp//g' | sed 's/img\///g' >list.txt +popd + +echo "Done creating flair/list.txt." diff --git a/public/flair/list.txt b/public/flair/list.txt new file mode 100644 index 0000000000000..1d777b96195a0 --- /dev/null +++ b/public/flair/list.txt @@ -0,0 +1,3435 @@ +activity.1st-place-medal +activity.2024 +activity.2nd-place-medal +activity.3rd-place-medal +activity.admission-tickets +activity.american-football +activity.artist-palette +activity.badminton +activity.balloon +activity.baseball +activity.basketball +activity.bowling +activity.boxing-glove +activity.carp-streamer +activity.chess-pawn +activity.chess +activity.christmas-tree +activity.club-suit +activity.confetti-ball +activity.cricket-game +activity.crystal-ball +activity.curling-stone +activity.diamond-suit +activity.direct-hit +activity.diving-mask +activity.field-hockey +activity.firecracker +activity.fireworks +activity.fishing-pole +activity.flag-in-hole +activity.flower-playing-cards +activity.flying-disc +activity.framed-picture +activity.game-die +activity.goal-net +activity.heart-suit +activity.ice-hockey +activity.ice-skate +activity.jack-o-lantern +activity.japanese-dolls +activity.joker +activity.joystick +activity.kite +activity.lacrosse +activity.lichess-berserk +activity.lichess-blitz +activity.lichess-bullet +activity.lichess-classical +activity.lichess-correspondence +activity.lichess-hogger +activity.lichess-horsey +activity.lichess-rapid +activity.lichess-ultrabullet +activity.lichess-variant-960 +activity.lichess-variant-antichess +activity.lichess-variant-atomic +activity.lichess-variant-crazyhouse +activity.lichess-variant-horde +activity.lichess-variant-king-of-the-hill +activity.lichess-variant-racing-kings +activity.lichess-variant-three-check +activity.lichess +activity.magic-wand +activity.mahjong-red-dragon +activity.martial-arts-uniform +activity.military-medal +activity.mirror-ball +activity.moon-viewing-ceremony +activity.nesting-dolls +activity.party-popper +activity.performing-arts +activity.pinata +activity.pine-decoration +activity.ping-pong +activity.pistol +activity.pool-8-ball +activity.puzzle-piece +activity.red-envelope +activity.rugby-football +activity.running-shirt +activity.shogi-bigsby +activity.shogi-king +activity.skis +activity.sled +activity.slot-machine +activity.soccer-ball +activity.softball +activity.spade-suit +activity.sparkler +activity.sparkles +activity.sports-medal +activity.tanabata-tree +activity.tennis +activity.ticket +activity.trophy +activity.video-game +activity.volleyball +activity.wind-chime +activity.wrapped-gift +activity.xmas-lichess-horsey +activity.yo-yo +food-drink.amphora +food-drink.avocado +food-drink.baby-bottle +food-drink.bacon +food-drink.bagel +food-drink.baguette-bread +food-drink.banana +food-drink.beans +food-drink.beer-mug +food-drink.bell-pepper +food-drink.bento-box +food-drink.beverage-box +food-drink.birthday-cake +food-drink.blueberries +food-drink.bottle-with-popping-cork +food-drink.bowl-with-spoon +food-drink.bread +food-drink.broccoli +food-drink.brown-mushroom +food-drink.bubble-tea +food-drink.burrito +food-drink.butter +food-drink.candy +food-drink.canned-food +food-drink.carrot +food-drink.cheese-wedge +food-drink.cherries +food-drink.chestnut +food-drink.chocolate-bar +food-drink.chopsticks +food-drink.clinking-beer-mugs +food-drink.clinking-glasses +food-drink.cocktail-glass +food-drink.coconut +food-drink.cooked-rice +food-drink.cookie +food-drink.cooking +food-drink.croissant +food-drink.cucumber +food-drink.cupcake +food-drink.cup-with-straw +food-drink.curry-rice +food-drink.custard +food-drink.cut-of-meat +food-drink.dango +food-drink.doughnut +food-drink.dumpling +food-drink.ear-of-corn +food-drink.egg +food-drink.falafel +food-drink.fish-cake-with-swirl +food-drink.flatbread +food-drink.fondue +food-drink.fork-and-knife +food-drink.fork-and-knife-with-plate +food-drink.fortune-cookie +food-drink.french-fries +food-drink.fried-shrimp +food-drink.garlic +food-drink.ginger +food-drink.glass-of-milk +food-drink.grapes +food-drink.green-apple +food-drink.green-salad +food-drink.hamburger +food-drink.honey-pot +food-drink.hot-beverage +food-drink.hot-dog +food-drink.hot-pepper +food-drink.ice-cream +food-drink.ice +food-drink.jar +food-drink.kitchen-knife +food-drink.kiwi-fruit +food-drink.leafy-green +food-drink.lemon +food-drink.lime +food-drink.lollipop +food-drink.mango +food-drink.mate +food-drink.meat-on-bone +food-drink.melon +food-drink.moon-cake +food-drink.oden +food-drink.olive +food-drink.onion +food-drink.pancakes +food-drink.peanuts +food-drink.pea-pod +food-drink.pear +food-drink.pie +food-drink.pineapple +food-drink.pizza +food-drink.popcorn +food-drink.potato +food-drink.pot-of-food +food-drink.poultry-leg +food-drink.pouring-liquid +food-drink.pretzel +food-drink.red-apple +food-drink.rice-ball +food-drink.rice-cracker +food-drink.roasted-sweet-potato +food-drink.sake +food-drink.salt +food-drink.sandwich +food-drink.shallow-pan-of-food +food-drink.shaved-ice +food-drink.shortcake +food-drink.soft-ice-cream +food-drink.spaghetti +food-drink.spoon +food-drink.steaming-bowl +food-drink.strawberry +food-drink.stuffed-flatbread +food-drink.sushi +food-drink.taco +food-drink.takeout-box +food-drink.tamale +food-drink.tangerine +food-drink.teacup-without-handle +food-drink.teapot +food-drink.tomato +food-drink.tropical-drink +food-drink.tumbler-glass +food-drink.waffle +food-drink.watermelon +food-drink.wine-glass +nature.ant +nature.baby-chick +nature.badger +nature.bat +nature.bear +nature.beaver +nature.beetle +nature.bird +nature.bison +nature.black-bird +nature.black-cat +nature.blossom +nature.blowfish +nature.boar +nature.bouquet +nature.bug +nature.butterfly +nature.cactus +nature.camel +nature.cat-face +nature.cat +nature.cherry-blossom +nature.chicken +nature.chipmunk +nature.closed-umbrella +nature.cloud +nature.cloud-with-lightning-and-rain +nature.cloud-with-lightning +nature.cloud-with-rain +nature.cloud-with-snow +nature.cockroach +nature.comet +nature.coral +nature.cow-face +nature.cow +nature.crab +nature.crescent-moon +nature.cricket +nature.crocodile +nature.cyclone +nature.deciduous-tree +nature.deer +nature.dodo +nature.dog-face +nature.dog +nature.dolphin +nature.donkey +nature.dove +nature.dragon-face +nature.dragon +nature.droplet +nature.duck +nature.eagle +nature.elephant +nature.empty-nest +nature.evergreen-tree +nature.ewe +nature.fallen-leaf +nature.feather +nature.fire +nature.first-quarter-moon-face +nature.first-quarter-moon +nature.fish +nature.flamingo +nature.fly +nature.fog +nature.four-leaf-clover +nature.fox +nature.frog +nature.front-facing-baby-chick +nature.full-moon-face +nature.full-moon +nature.giraffe +nature.glowing-star +nature.goat +nature.goose +nature.gorilla +nature.guide-dog +nature.hamster +nature.hatching-chick +nature.hedgehog +nature.herb +nature.hibiscus +nature.high-voltage +nature.hippopotamus +nature.honeybee +nature.horse-face +nature.horse +nature.hyacinth +nature.jellyfish +nature.kangaroo +nature.koala +nature.lady-beetle +nature.last-quarter-moon-face +nature.last-quarter-moon +nature.leaf-fluttering-in-wind +nature.leopard +nature.lion +nature.lizard +nature.llama +nature.lobster +nature.lotus +nature.mammoth +nature.maple-leaf +nature.microbe +nature.milky-way +nature.monkey-face +nature.monkey +nature.moose +nature.mosquito +nature.mouse-face +nature.mouse +nature.mushroom +nature.nest-with-eggs +nature.new-moon-face +nature.new-moon +nature.octopus-howard +nature.octopus +nature.orangutan +nature.otter +nature.owl +nature.ox +nature.oyster +nature.palm-tree +nature.panda +nature.parrot +nature.paw-prints +nature.peacock +nature.penguin +nature.phoenix-bird +nature.pig-face +nature.pig-nose +nature.pig +nature.polar-bear +nature.poodle +nature.potted-plant +nature.rabbit-face +nature.rabbit +nature.raccoon +nature.rainbow +nature.ram +nature.rat +nature.rhinoceros +nature.ringed-planet +nature.rock +nature.rooster +nature.rosette +nature.rose +nature.rubber-duck +nature.sauropod +nature.scorpion +nature.seal +nature.seedling +nature.service-dog +nature.shamrock +nature.shark +nature.sheaf-of-rice +nature.shooting-star +nature.shrimp +nature.skunk +nature.sloth +nature.snail +nature.snake +nature.snowflake +nature.snowman +nature.snowman-without-snow +nature.spider +nature.spider-web +nature.spiral-shell +nature.spouting-whale +nature.squid +nature.star +nature.sun-behind-cloud +nature.sun-behind-large-cloud +nature.sun-behind-rain-cloud +nature.sun-behind-small-cloud +nature.sunflower +nature.sun +nature.sun-with-face +nature.swan +nature.tiger-face +nature.tiger +nature.tornado +nature.t-rex +nature.tropical-fish +nature.tulip +nature.turkey +nature.turtle +nature.two-hump-camel +nature.umbrella-on-ground +nature.umbrella +nature.umbrella-with-rain-drops +nature.unicorn +nature.waning-crescent-moon +nature.waning-gibbous-moon +nature.water-buffalo +nature.water-wave +nature.waxing-crescent-moon +nature.waxing-gibbous-moon +nature.whale +nature.white-flower +nature.wilted-flower +nature.wind-face +nature.wing +nature.wolf +nature.wood +nature.worm +nature.xmas-tree +nature.zebra +objects.abacus +objects.accordion +objects.adhesive-bandage +objects.alarm-clock +objects.alembic +objects.backpack +objects.balance-scale +objects.ballet-shoes +objects.ballot-box-with-ballot +objects.banjo +objects.bar-chart +objects.basket +objects.bathtub +objects.battery +objects.bed +objects.bellhop-bell +objects.bell +objects.bell-with-slash +objects.bikini +objects.billed-cap +objects.black-nib +objects.blue-book +objects.bookmark-tabs +objects.bookmark +objects.books +objects.boomerang +objects.bow-and-arrow +objects.briefcase +objects.briefs +objects.broken-chain +objects.broom +objects.bubbles +objects.bucket +objects.calendar +objects.camera +objects.camera-with-flash +objects.candle +objects.card-file-box +objects.card-index-dividers +objects.card-index +objects.carpentry-saw +objects.chains +objects.chair +objects.chart-decreasing +objects.chart-increasing +objects.chart-increasing-with-yen +objects.cigarette +objects.clamp +objects.clapper-board +objects.clipboard +objects.closed-book +objects.closed-mailbox-with-lowered-flag +objects.closed-mailbox-with-raised-flag +objects.clutch-bag +objects.coat +objects.coffin +objects.coin +objects.computer-disk +objects.computer-mouse +objects.control-knobs +objects.couch-and-lamp +objects.crayon +objects.credit-card +objects.crossed-swords +objects.crown +objects.crutch +objects.desktop-computer +objects.diya-lamp +objects.dollar-banknote +objects.door +objects.dress +objects.drum +objects.dvd +objects.eight-oclock +objects.eight-thirty +objects.electric-plug +objects.eleven-oclock +objects.eleven-thirty +objects.e-mail +objects.envelope +objects.envelope-with-arrow +objects.euro-banknote +objects.fax-machine +objects.file-cabinet +objects.file-folder +objects.film-frames +objects.film-projector +objects.fire-extinguisher +objects.five-oclock +objects.five-thirty +objects.flashlight +objects.flat-shoe +objects.floppy-disk +objects.flute +objects.folding-hand-fan +objects.fountain-pen +objects.four-oclock +objects.four-thirty +objects.funeral-urn +objects.gear +objects.gem-stone +objects.glasses +objects.gloves +objects.goggles +objects.graduation-cap +objects.green-book +objects.guitar +objects.hair-pick +objects.hammer-and-pick +objects.hammer-and-wrench +objects.hammer +objects.hamsa +objects.handbag +objects.headphone +objects.headstone +objects.high-heeled-shoe +objects.hiking-boot +objects.hook +objects.hourglass-done +objects.hourglass-not-done +objects.identification-card +objects.inbox-tray +objects.incoming-envelope +objects.jeans +objects.keyboard +objects.key +objects.kimono +objects.knot +objects.lab-coat +objects.label +objects.ladder +objects.laptop +objects.ledger +objects.level-slider +objects.light-bulb +objects.linked-paperclips +objects.link +objects.lipstick +objects.locked +objects.locked-with-key +objects.locked-with-pen +objects.long-drum +objects.lotion-bottle +objects.loudspeaker +objects.low-battery +objects.luggage +objects.magnet +objects.magnifying-glass-tilted-left +objects.magnifying-glass-tilted-right +objects.mans-shoe +objects.mantelpiece-clock +objects.maracas +objects.megaphone +objects.memo +objects.microphone +objects.microscope +objects.mirror +objects.mobile-phone +objects.mobile-phone-with-arrow +objects.money-bag +objects.money-with-wings +objects.mouse-trap +objects.movie-camera +objects.musical-keyboard +objects.musical-notes +objects.musical-note +objects.musical-score +objects.muted-speaker +objects.nazar-amulet +objects.necktie +objects.newspaper +objects.nine-oclock +objects.nine-thirty +objects.notebook +objects.notebook-with-decorative-cover +objects.nut-and-bolt +objects.old-key +objects.one-oclock +objects.one-piece-swimsuit +objects.one-thirty +objects.open-book +objects.open-file-folder +objects.open-mailbox-with-lowered-flag +objects.open-mailbox-with-raised-flag +objects.optical-disk +objects.orange-book +objects.outbox-tray +objects.package +objects.page-facing-up +objects.pager +objects.page-with-curl +objects.paintbrush +objects.paperclip +objects.pencil +objects.pen +objects.petri-dish +objects.pick +objects.pill +objects.placard +objects.plunger +objects.postal-horn +objects.postbox +objects.pound-banknote +objects.prayer-beads +objects.printer +objects.purse +objects.pushpin +objects.radio +objects.razor +objects.receipt +objects.red-paper-lantern +objects.reminder-ribbon +objects.rescue-workers-helmet +objects.ribbon +objects.ring +objects.rolled-up-newspaper +objects.roll-of-paper +objects.round-pushpin +objects.running-shoe +objects.safety-pin +objects.safety-vest +objects.sari +objects.satellite-antenna +objects.saxophone +objects.scarf +objects.scissors +objects.screwdriver +objects.scroll +objects.seven-oclock +objects.seven-thirty +objects.sewing-needle +objects.shield +objects.shopping-bags +objects.shopping-cart +objects.shorts +objects.shower +objects.six-oclock +objects.six-thirty +objects.soap +objects.socks +objects.speaker-high-volume +objects.speaker-low-volume +objects.speaker-medium-volume +objects.spiral-calendar +objects.spiral-notepad +objects.sponge +objects.stethoscope +objects.stopwatch +objects.straight-ruler +objects.studio-microphone +objects.sunglasses +objects.syringe +objects.tear-off-calendar +objects.teddy-bear +objects.telephone-receiver +objects.telephone +objects.telescope +objects.television +objects.ten-oclock +objects.ten-thirty +objects.test-tube +objects.thermometer +objects.thong-sandal +objects.thread +objects.three-oclock +objects.three-thirty +objects.timer-clock +objects.toilet +objects.toolbox +objects.toothbrush +objects.top-hat +objects.trackball +objects.triangular-ruler +objects.trumpet +objects.t-shirt +objects.twelve-oclock +objects.twelve-thirty +objects.two-oclocktime +objects.two-thirty +objects.unlocked +objects.video-camera +objects.videocassette +objects.violin +objects.wastebasket +objects.watch +objects.white-cane +objects.window +objects.womans-boot +objects.womans-clothes +objects.womans-hat +objects.womans-sandal +objects.wrench +objects.xmas-bell +objects.xmas-candle +objects.xmas-hat +objects.x-ray +objects.yarn +objects.yen-banknote +people.anatomical-heart +people.artist-dark-skin-tone +people.artist-light-skin-tone +people.artist-medium-dark-skin-tone +people.artist-medium-light-skin-tone +people.artist-medium-skin-tone +people.artist +people.astronaut-dark-skin-tone +people.astronaut-light-skin-tone +people.astronaut-medium-dark-skin-tone +people.astronaut-medium-light-skin-tone +people.astronaut-medium-skin-tone +people.astronaut +people.baby-angel-dark-skin-tone +people.baby-angel-light-skin-tone +people.baby-angel-medium-dark-skin-tone +people.baby-angel-medium-light-skin-tone +people.baby-angel-medium-skin-tone +people.baby-angel +people.baby-dark-skin-tone +people.baby-light-skin-tone +people.baby-medium-dark-skin-tone +people.baby-medium-light-skin-tone +people.baby-medium-skin-tone +people.baby +people.backhand-index-pointing-down-dark-skin-tone +people.backhand-index-pointing-down-light-skin-tone +people.backhand-index-pointing-down-medium-dark-skin-tone +people.backhand-index-pointing-down-medium-light-skin-tone +people.backhand-index-pointing-down-medium-skin-tone +people.backhand-index-pointing-down +people.backhand-index-pointing-left-dark-skin-tone +people.backhand-index-pointing-left-light-skin-tone +people.backhand-index-pointing-left-medium-dark-skin-tone +people.backhand-index-pointing-left-medium-light-skin-tone +people.backhand-index-pointing-left-medium-skin-tone +people.backhand-index-pointing-left +people.backhand-index-pointing-right-dark-skin-tone +people.backhand-index-pointing-right-light-skin-tone +people.backhand-index-pointing-right-medium-dark-skin-tone +people.backhand-index-pointing-right-medium-light-skin-tone +people.backhand-index-pointing-right-medium-skin-tone +people.backhand-index-pointing-right +people.backhand-index-pointing-up-dark-skin-tone +people.backhand-index-pointing-up-light-skin-tone +people.backhand-index-pointing-up-medium-dark-skin-tone +people.backhand-index-pointing-up-medium-light-skin-tone +people.backhand-index-pointing-up-medium-skin-tone +people.backhand-index-pointing-up +people.bald +people.biting-lip +people.bone +people.boy-dark-skin-tone +people.boy-light-skin-tone +people.boy-medium-dark-skin-tone +people.boy-medium-light-skin-tone +people.boy-medium-skin-tone +people.boy +people.brain +people.breast-feeding-dark-skin-tone +people.breast-feeding-light-skin-tone +people.breast-feeding-medium-dark-skin-tone +people.breast-feeding-medium-light-skin-tone +people.breast-feeding-medium-skin-tone +people.breast-feeding +people.bust-in-silhouette +people.busts-in-silhouette +people.call-me-hand-dark-skin-tone +people.call-me-hand-light-skin-tone +people.call-me-hand-medium-dark-skin-tone +people.call-me-hand-medium-light-skin-tone +people.call-me-hand-medium-skin-tone +people.call-me-hand +people.child-dark-skin-tone +people.child-light-skin-tone +people.child-medium-dark-skin-tone +people.child-medium-light-skin-tone +people.child-medium-skin-tone +people.child +people.clapping-hands-dark-skin-tone +people.clapping-hands-light-skin-tone +people.clapping-hands-medium-dark-skin-tone +people.clapping-hands-medium-light-skin-tone +people.clapping-hands-medium-skin-tone +people.clapping-hands +people.construction-worker-dark-skin-tone +people.construction-worker-light-skin-tone +people.construction-worker-medium-dark-skin-tone +people.construction-worker-medium-light-skin-tone +people.construction-worker-medium-skin-tone +people.construction-worker +people.cook-dark-skin-tone +people.cook-light-skin-tone +people.cook-medium-dark-skin-tone +people.cook-medium-light-skin-tone +people.cook-medium-skin-tone +people.cook +people.couple-with-heart-dark-skin-tone +people.couple-with-heart-light-skin-tone +people.couple-with-heart-man-man-dark-skin-tone-light-skin-tone +people.couple-with-heart-man-man-dark-skin-tone-medium-dark-skin-tone +people.couple-with-heart-man-man-dark-skin-tone-medium-light-skin-tone +people.couple-with-heart-man-man-dark-skin-tone-medium-skin-tone +people.couple-with-heart-man-man-dark-skin-tone +people.couple-with-heart-man-man-light-skin-tone-dark-skin-tone +people.couple-with-heart-man-man-light-skin-tone-medium-dark-skin-tone +people.couple-with-heart-man-man-light-skin-tone-medium-light-skin-tone +people.couple-with-heart-man-man-light-skin-tone-medium-skin-tone +people.couple-with-heart-man-man-light-skin-tone +people.couple-with-heart-man-man-medium-dark-skin-tone-dark-skin-tone +people.couple-with-heart-man-man-medium-dark-skin-tone-light-skin-tone +people.couple-with-heart-man-man-medium-dark-skin-tone-medium-light-skin-tone +people.couple-with-heart-man-man-medium-dark-skin-tone-medium-skin-tone +people.couple-with-heart-man-man-medium-dark-skin-tone +people.couple-with-heart-man-man-medium-light-skin-tone-dark-skin-tone +people.couple-with-heart-man-man-medium-light-skin-tone-light-skin-tone +people.couple-with-heart-man-man-medium-light-skin-tone-medium-dark-skin-tone +people.couple-with-heart-man-man-medium-light-skin-tone-medium-skin-tone +people.couple-with-heart-man-man-medium-light-skin-tone +people.couple-with-heart-man-man-medium-skin-tone-dark-skin-tone +people.couple-with-heart-man-man-medium-skin-tone-light-skin-tone +people.couple-with-heart-man-man-medium-skin-tone-medium-dark-skin-tone +people.couple-with-heart-man-man-medium-skin-tone-medium-light-skin-tone +people.couple-with-heart-man-man-medium-skin-tone +people.couple-with-heart-man-man +people.couple-with-heart-medium-dark-skin-tone +people.couple-with-heart-medium-light-skin-tone +people.couple-with-heart-medium-skin-tone +people.couple-with-heart-person-person-dark-skin-tone-light-skin-tone +people.couple-with-heart-person-person-dark-skin-tone-medium-dark-skin-tone +people.couple-with-heart-person-person-dark-skin-tone-medium-light-skin-tone +people.couple-with-heart-person-person-dark-skin-tone-medium-skin-tone +people.couple-with-heart-person-person-light-skin-tone-dark-skin-tone +people.couple-with-heart-person-person-light-skin-tone-medium-dark-skin-tone +people.couple-with-heart-person-person-light-skin-tone-medium-light-skin-tone +people.couple-with-heart-person-person-light-skin-tone-medium-skin-tone +people.couple-with-heart-person-person-medium-dark-skin-tone-dark-skin-tone +people.couple-with-heart-person-person-medium-dark-skin-tone-light-skin-tone +people.couple-with-heart-person-person-medium-dark-skin-tone-medium-light-skin-tone +people.couple-with-heart-person-person-medium-dark-skin-tone-medium-skin-tone +people.couple-with-heart-person-person-medium-light-skin-tone-dark-skin-tone +people.couple-with-heart-person-person-medium-light-skin-tone-light-skin-tone +people.couple-with-heart-person-person-medium-light-skin-tone-medium-dark-skin-tone +people.couple-with-heart-person-person-medium-light-skin-tone-medium-skin-tone +people.couple-with-heart-person-person-medium-skin-tone-dark-skin-tone +people.couple-with-heart-person-person-medium-skin-tone-light-skin-tone +people.couple-with-heart-person-person-medium-skin-tone-medium-dark-skin-tone +people.couple-with-heart-person-person-medium-skin-tone-medium-light-skin-tone +people.couple-with-heart +people.couple-with-heart-woman-man-dark-skin-tone-light-skin-tone +people.couple-with-heart-woman-man-dark-skin-tone-medium-dark-skin-tone +people.couple-with-heart-woman-man-dark-skin-tone-medium-light-skin-tone +people.couple-with-heart-woman-man-dark-skin-tone-medium-skin-tone +people.couple-with-heart-woman-man-dark-skin-tone +people.couple-with-heart-woman-man-light-skin-tone-dark-skin-tone +people.couple-with-heart-woman-man-light-skin-tone-medium-dark-skin-tone +people.couple-with-heart-woman-man-light-skin-tone-medium-light-skin-tone +people.couple-with-heart-woman-man-light-skin-tone-medium-skin-tone +people.couple-with-heart-woman-man-light-skin-tone +people.couple-with-heart-woman-man-medium-dark-skin-tone-dark-skin-tone +people.couple-with-heart-woman-man-medium-dark-skin-tone-light-skin-tone +people.couple-with-heart-woman-man-medium-dark-skin-tone-medium-light-skin-tone +people.couple-with-heart-woman-man-medium-dark-skin-tone-medium-skin-tone +people.couple-with-heart-woman-man-medium-dark-skin-tone +people.couple-with-heart-woman-man-medium-light-skin-tone-dark-skin-tone +people.couple-with-heart-woman-man-medium-light-skin-tone-light-skin-tone +people.couple-with-heart-woman-man-medium-light-skin-tone-medium-dark-skin-tone +people.couple-with-heart-woman-man-medium-light-skin-tone-medium-skin-tone +people.couple-with-heart-woman-man-medium-light-skin-tone +people.couple-with-heart-woman-man-medium-skin-tone-dark-skin-tone +people.couple-with-heart-woman-man-medium-skin-tone-light-skin-tone +people.couple-with-heart-woman-man-medium-skin-tone-medium-dark-skin-tone +people.couple-with-heart-woman-man-medium-skin-tone-medium-light-skin-tone +people.couple-with-heart-woman-man-medium-skin-tone +people.couple-with-heart-woman-man +people.couple-with-heart-woman-woman-dark-skin-tone-light-skin-tone +people.couple-with-heart-woman-woman-dark-skin-tone-medium-dark-skin-tone +people.couple-with-heart-woman-woman-dark-skin-tone-medium-light-skin-tone +people.couple-with-heart-woman-woman-dark-skin-tone-medium-skin-tone +people.couple-with-heart-woman-woman-dark-skin-tone +people.couple-with-heart-woman-woman-light-skin-tone-dark-skin-tone +people.couple-with-heart-woman-woman-light-skin-tone-medium-dark-skin-tone +people.couple-with-heart-woman-woman-light-skin-tone-medium-light-skin-tone +people.couple-with-heart-woman-woman-light-skin-tone-medium-skin-tone +people.couple-with-heart-woman-woman-light-skin-tone +people.couple-with-heart-woman-woman-medium-dark-skin-tone-dark-skin-tone +people.couple-with-heart-woman-woman-medium-dark-skin-tone-light-skin-tone +people.couple-with-heart-woman-woman-medium-dark-skin-tone-medium-light-skin-tone +people.couple-with-heart-woman-woman-medium-dark-skin-tone-medium-skin-tone +people.couple-with-heart-woman-woman-medium-dark-skin-tone +people.couple-with-heart-woman-woman-medium-light-skin-tone-dark-skin-tone +people.couple-with-heart-woman-woman-medium-light-skin-tone-light-skin-tone +people.couple-with-heart-woman-woman-medium-light-skin-tone-medium-dark-skin-tone +people.couple-with-heart-woman-woman-medium-light-skin-tone-medium-skin-tone +people.couple-with-heart-woman-woman-medium-light-skin-tone +people.couple-with-heart-woman-woman-medium-skin-tone-dark-skin-tone +people.couple-with-heart-woman-woman-medium-skin-tone-light-skin-tone +people.couple-with-heart-woman-woman-medium-skin-tone-medium-dark-skin-tone +people.couple-with-heart-woman-woman-medium-skin-tone-medium-light-skin-tone +people.couple-with-heart-woman-woman-medium-skin-tone +people.couple-with-heart-woman-woman +people.crossed-fingers-dark-skin-tone +people.crossed-fingers-light-skin-tone +people.crossed-fingers-medium-dark-skin-tone +people.crossed-fingers-medium-light-skin-tone +people.crossed-fingers-medium-skin-tone +people.crossed-fingers +people.curly-hair +people.dark-skin-tone +people.deaf-man-dark-skin-tone +people.deaf-man-light-skin-tone +people.deaf-man-medium-dark-skin-tone +people.deaf-man-medium-light-skin-tone +people.deaf-man-medium-skin-tone +people.deaf-man +people.deaf-person-dark-skin-tone +people.deaf-person-light-skin-tone +people.deaf-person-medium-dark-skin-tone +people.deaf-person-medium-light-skin-tone +people.deaf-person-medium-skin-tone +people.deaf-person +people.deaf-woman-dark-skin-tone +people.deaf-woman-light-skin-tone +people.deaf-woman-medium-dark-skin-tone +people.deaf-woman-medium-light-skin-tone +people.deaf-woman-medium-skin-tone +people.deaf-woman +people.detective-dark-skin-tone +people.detective-light-skin-tone +people.detective-medium-dark-skin-tone +people.detective-medium-light-skin-tone +people.detective-medium-skin-tone +people.detective +people.dna +people.drop-of-blood +people.ear-dark-skin-tone +people.ear-light-skin-tone +people.ear-medium-dark-skin-tone +people.ear-medium-light-skin-tone +people.ear-medium-skin-tone +people.ear +people.ear-with-hearing-aid-dark-skin-tone +people.ear-with-hearing-aid-light-skin-tone +people.ear-with-hearing-aid-medium-dark-skin-tone +people.ear-with-hearing-aid-medium-light-skin-tone +people.ear-with-hearing-aid-medium-skin-tone +people.ear-with-hearing-aid +people.elf-dark-skin-tone +people.elf-light-skin-tone +people.elf-medium-dark-skin-tone +people.elf-medium-light-skin-tone +people.elf-medium-skin-tone +people.elf +people.eyes +people.eye +people.factory-worker-dark-skin-tone +people.factory-worker-light-skin-tone +people.factory-worker-medium-dark-skin-tone +people.factory-worker-medium-light-skin-tone +people.factory-worker-medium-skin-tone +people.factory-worker +people.fairy-dark-skin-tone +people.fairy-light-skin-tone +people.fairy-medium-dark-skin-tone +people.fairy-medium-light-skin-tone +people.fairy-medium-skin-tone +people.fairy +people.family-adult-adult-child-child +people.family-adult-adult-child +people.family-adult-child-child +people.family-adult-child +people.family-man-boy-boy +people.family-man-boy +people.family-man-girl-boy +people.family-man-girl-girl +people.family-man-girl +people.family-man-man-boy-boy +people.family-man-man-boy +people.family-man-man-girl-boy +people.family-man-man-girl-girl +people.family-man-man-girl +people.family-man-woman-boy-boy +people.family-man-woman-boy +people.family-man-woman-girl-boy +people.family-man-woman-girl-girl +people.family-man-woman-girl +people.family +people.family-woman-boy-boy +people.family-woman-boy +people.family-woman-girl-boy +people.family-woman-girl-girl +people.family-woman-girl +people.family-woman-woman-boy-boy +people.family-woman-woman-boy +people.family-woman-woman-girl-boy +people.family-woman-woman-girl-girl +people.family-woman-woman-girl +people.farmer-dark-skin-tone +people.farmer-light-skin-tone +people.farmer-medium-dark-skin-tone +people.farmer-medium-light-skin-tone +people.farmer-medium-skin-tone +people.farmer +people.firefighter-dark-skin-tone +people.firefighter-light-skin-tone +people.firefighter-medium-dark-skin-tone +people.firefighter-medium-light-skin-tone +people.firefighter-medium-skin-tone +people.firefighter +people.flexed-biceps-dark-skin-tone +people.flexed-biceps-light-skin-tone +people.flexed-biceps-medium-dark-skin-tone +people.flexed-biceps-medium-light-skin-tone +people.flexed-biceps-medium-skin-tone +people.flexed-biceps +people.folded-hands-dark-skin-tone +people.folded-hands-light-skin-tone +people.folded-hands-medium-dark-skin-tone +people.folded-hands-medium-light-skin-tone +people.folded-hands-medium-skin-tone +people.folded-hands +people.foot-dark-skin-tone +people.foot-light-skin-tone +people.foot-medium-dark-skin-tone +people.foot-medium-light-skin-tone +people.foot-medium-skin-tone +people.footprints +people.foot +people.genie +people.girl-dark-skin-tone +people.girl-light-skin-tone +people.girl-medium-dark-skin-tone +people.girl-medium-light-skin-tone +people.girl-medium-skin-tone +people.girl +people.guard-dark-skin-tone +people.guard-light-skin-tone +people.guard-medium-dark-skin-tone +people.guard-medium-light-skin-tone +people.guard-medium-skin-tone +people.guard +people.handshake-dark-skin-tone-light-skin-tone +people.handshake-dark-skin-tone-medium-dark-skin-tone +people.handshake-dark-skin-tone-medium-light-skin-tone +people.handshake-dark-skin-tone-medium-skin-tone +people.handshake-dark-skin-tone +people.handshake-light-skin-tone-dark-skin-tone +people.handshake-light-skin-tone-medium-dark-skin-tone +people.handshake-light-skin-tone-medium-light-skin-tone +people.handshake-light-skin-tone-medium-skin-tone +people.handshake-light-skin-tone +people.handshake-medium-dark-skin-tone-dark-skin-tone +people.handshake-medium-dark-skin-tone-light-skin-tone +people.handshake-medium-dark-skin-tone-medium-light-skin-tone +people.handshake-medium-dark-skin-tone-medium-skin-tone +people.handshake-medium-dark-skin-tone +people.handshake-medium-light-skin-tone-dark-skin-tone +people.handshake-medium-light-skin-tone-light-skin-tone +people.handshake-medium-light-skin-tone-medium-dark-skin-tone +people.handshake-medium-light-skin-tone-medium-skin-tone +people.handshake-medium-light-skin-tone +people.handshake-medium-skin-tone-dark-skin-tone +people.handshake-medium-skin-tone-light-skin-tone +people.handshake-medium-skin-tone-medium-dark-skin-tone +people.handshake-medium-skin-tone-medium-light-skin-tone +people.handshake-medium-skin-tone +people.handshake +people.hand-with-fingers-splayed-dark-skin-tone +people.hand-with-fingers-splayed-light-skin-tone +people.hand-with-fingers-splayed-medium-dark-skin-tone +people.hand-with-fingers-splayed-medium-light-skin-tone +people.hand-with-fingers-splayed-medium-skin-tone +people.hand-with-fingers-splayed +people.hand-with-index-finger-and-thumb-crossed-dark-skin-tone +people.hand-with-index-finger-and-thumb-crossed-light-skin-tone +people.hand-with-index-finger-and-thumb-crossed-medium-dark-skin-tone +people.hand-with-index-finger-and-thumb-crossed-medium-light-skin-tone +people.hand-with-index-finger-and-thumb-crossed-medium-skin-tone +people.hand-with-index-finger-and-thumb-crossed +people.health-worker-dark-skin-tone +people.health-worker-light-skin-tone +people.health-worker-medium-dark-skin-tone +people.health-worker-medium-light-skin-tone +people.health-worker-medium-skin-tone +people.health-worker +people.heart-hands-dark-skin-tone +people.heart-hands-light-skin-tone +people.heart-hands-medium-dark-skin-tone +people.heart-hands-medium-light-skin-tone +people.heart-hands-medium-skin-tone +people.heart-hands +people.horse-racing-dark-skin-tone +people.horse-racing-light-skin-tone +people.horse-racing-medium-dark-skin-tone +people.horse-racing-medium-light-skin-tone +people.horse-racing-medium-skin-tone +people.horse-racing +people.index-pointing-at-the-viewer-dark-skin-tone +people.index-pointing-at-the-viewer-light-skin-tone +people.index-pointing-at-the-viewer-medium-dark-skin-tone +people.index-pointing-at-the-viewer-medium-light-skin-tone +people.index-pointing-at-the-viewer-medium-skin-tone +people.index-pointing-at-the-viewer +people.index-pointing-up-dark-skin-tone +people.index-pointing-up-light-skin-tone +people.index-pointing-up-medium-dark-skin-tone +people.index-pointing-up-medium-light-skin-tone +people.index-pointing-up-medium-skin-tone +people.index-pointing-up +people.judge-dark-skin-tone +people.judge-light-skin-tone +people.judge-medium-dark-skin-tone +people.judge-medium-light-skin-tone +people.judge-medium-skin-tone +people.judge +people.kiss-dark-skin-tone +people.kiss-light-skin-tone +people.kiss-medium-dark-skin-tone +people.kiss-medium-light-skin-tone +people.kiss-medium-skin-tone +people.kiss +people.left-facing-fist-dark-skin-tone +people.left-facing-fist-light-skin-tone +people.left-facing-fist-medium-dark-skin-tone +people.left-facing-fist-medium-light-skin-tone +people.left-facing-fist-medium-skin-tone +people.left-facing-fist +people.leftwards-hand-dark-skin-tone +people.leftwards-hand-light-skin-tone +people.leftwards-hand-medium-dark-skin-tone +people.leftwards-hand-medium-light-skin-tone +people.leftwards-hand-medium-skin-tone +people.leftwards-hand +people.leftwards-pushing-hand-dark-skin-tone +people.leftwards-pushing-hand-light-skin-tone +people.leftwards-pushing-hand-medium-dark-skin-tone +people.leftwards-pushing-hand-medium-light-skin-tone +people.leftwards-pushing-hand-medium-skin-tone +people.leftwards-pushing-hand +people.leg-dark-skin-tone +people.leg-light-skin-tone +people.leg-medium-dark-skin-tone +people.leg-medium-light-skin-tone +people.leg-medium-skin-tone +people.leg +people.light-skin-tone +people.love-you-gesture-dark-skin-tone +people.love-you-gesture-light-skin-tone +people.love-you-gesture-medium-dark-skin-tone +people.love-you-gesture-medium-light-skin-tone +people.love-you-gesture-medium-skin-tone +people.love-you-gesture +people.lungs +people.mage-dark-skin-tone +people.mage-light-skin-tone +people.mage-medium-dark-skin-tone +people.mage-medium-light-skin-tone +people.mage-medium-skin-tone +people.mage +people.man-artist-dark-skin-tone +people.man-artist-light-skin-tone +people.man-artist-medium-dark-skin-tone +people.man-artist-medium-light-skin-tone +people.man-artist-medium-skin-tone +people.man-artist +people.man-astronaut-dark-skin-tone +people.man-astronaut-light-skin-tone +people.man-astronaut-medium-dark-skin-tone +people.man-astronaut-medium-light-skin-tone +people.man-astronaut-medium-skin-tone +people.man-astronaut +people.man-bald +people.man-beard +people.man-biking-dark-skin-tone +people.man-biking-light-skin-tone +people.man-biking-medium-dark-skin-tone +people.man-biking-medium-light-skin-tone +people.man-biking-medium-skin-tone +people.man-biking +people.man-blond-hair +people.man-bouncing-ball-dark-skin-tone +people.man-bouncing-ball-light-skin-tone +people.man-bouncing-ball-medium-dark-skin-tone +people.man-bouncing-ball-medium-light-skin-tone +people.man-bouncing-ball-medium-skin-tone +people.man-bouncing-ball +people.man-bowing-dark-skin-tone +people.man-bowing-light-skin-tone +people.man-bowing-medium-dark-skin-tone +people.man-bowing-medium-light-skin-tone +people.man-bowing-medium-skin-tone +people.man-bowing +people.man-cartwheeling-dark-skin-tone +people.man-cartwheeling-light-skin-tone +people.man-cartwheeling-medium-dark-skin-tone +people.man-cartwheeling-medium-light-skin-tone +people.man-cartwheeling-medium-skin-tone +people.man-cartwheeling +people.man-climbing-dark-skin-tone +people.man-climbing-light-skin-tone +people.man-climbing-medium-dark-skin-tone +people.man-climbing-medium-light-skin-tone +people.man-climbing-medium-skin-tone +people.man-climbing +people.man-construction-worker-dark-skin-tone +people.man-construction-worker-light-skin-tone +people.man-construction-worker-medium-dark-skin-tone +people.man-construction-worker-medium-light-skin-tone +people.man-construction-worker-medium-skin-tone +people.man-construction-worker +people.man-cook-dark-skin-tone +people.man-cook-light-skin-tone +people.man-cook-medium-dark-skin-tone +people.man-cook-medium-light-skin-tone +people.man-cook-medium-skin-tone +people.man-cook +people.man-curly-hair +people.man-dancing-dark-skin-tone +people.man-dancing-light-skin-tone +people.man-dancing-medium-dark-skin-tone +people.man-dancing-medium-light-skin-tone +people.man-dancing-medium-skin-tone +people.man-dancing +people.man-dark-skin-tone-bald +people.man-dark-skin-tone-beard +people.man-dark-skin-tone-blond-hair +people.man-dark-skin-tone-curly-hair +people.man-dark-skin-tone-red-hair +people.man-dark-skin-tone +people.man-dark-skin-tone-white-hair +people.man-detective-dark-skin-tone +people.man-detective-light-skin-tone +people.man-detective-medium-dark-skin-tone +people.man-detective-medium-light-skin-tone +people.man-detective-medium-skin-tone +people.man-detective +people.man-elf-dark-skin-tone +people.man-elf-light-skin-tone +people.man-elf-medium-dark-skin-tone +people.man-elf-medium-light-skin-tone +people.man-elf-medium-skin-tone +people.man-elf +people.man-facepalming-dark-skin-tone +people.man-facepalming-light-skin-tone +people.man-facepalming-medium-dark-skin-tone +people.man-facepalming-medium-light-skin-tone +people.man-facepalming-medium-skin-tone +people.man-facepalming +people.man-factory-worker-dark-skin-tone +people.man-factory-worker-light-skin-tone +people.man-factory-worker-medium-dark-skin-tone +people.man-factory-worker-medium-light-skin-tone +people.man-factory-worker-medium-skin-tone +people.man-factory-worker +people.man-fairy-dark-skin-tone +people.man-fairy-light-skin-tone +people.man-fairy-medium-dark-skin-tone +people.man-fairy-medium-light-skin-tone +people.man-fairy-medium-skin-tone +people.man-fairy +people.man-farmer-dark-skin-tone +people.man-farmer-light-skin-tone +people.man-farmer-medium-dark-skin-tone +people.man-farmer-medium-light-skin-tone +people.man-farmer-medium-skin-tone +people.man-farmer +people.man-feeding-baby-dark-skin-tone +people.man-feeding-baby-light-skin-tone +people.man-feeding-baby-medium-dark-skin-tone +people.man-feeding-baby-medium-light-skin-tone +people.man-feeding-baby-medium-skin-tone +people.man-feeding-baby +people.man-firefighter-dark-skin-tone +people.man-firefighter-light-skin-tone +people.man-firefighter-medium-dark-skin-tone +people.man-firefighter-medium-light-skin-tone +people.man-firefighter-medium-skin-tone +people.man-firefighter +people.man-frowning-dark-skin-tone +people.man-frowning-light-skin-tone +people.man-frowning-medium-dark-skin-tone +people.man-frowning-medium-light-skin-tone +people.man-frowning-medium-skin-tone +people.man-frowning +people.man-genie +people.man-gesturing-no-dark-skin-tone +people.man-gesturing-no-light-skin-tone +people.man-gesturing-no-medium-dark-skin-tone +people.man-gesturing-no-medium-light-skin-tone +people.man-gesturing-no-medium-skin-tone +people.man-gesturing-no +people.man-gesturing-ok-dark-skin-tone +people.man-gesturing-ok-light-skin-tone +people.man-gesturing-ok-medium-dark-skin-tone +people.man-gesturing-ok-medium-light-skin-tone +people.man-gesturing-ok-medium-skin-tone +people.man-gesturing-ok +people.man-getting-haircut-dark-skin-tone +people.man-getting-haircut-light-skin-tone +people.man-getting-haircut-medium-dark-skin-tone +people.man-getting-haircut-medium-light-skin-tone +people.man-getting-haircut-medium-skin-tone +people.man-getting-haircut +people.man-getting-massage-dark-skin-tone +people.man-getting-massage-light-skin-tone +people.man-getting-massage-medium-dark-skin-tone +people.man-getting-massage-medium-light-skin-tone +people.man-getting-massage-medium-skin-tone +people.man-getting-massage +people.man-golfing-dark-skin-tone +people.man-golfing-light-skin-tone +people.man-golfing-medium-dark-skin-tone +people.man-golfing-medium-light-skin-tone +people.man-golfing-medium-skin-tone +people.man-golfing +people.man-guard-dark-skin-tone +people.man-guard-light-skin-tone +people.man-guard-medium-dark-skin-tone +people.man-guard-medium-light-skin-tone +people.man-guard-medium-skin-tone +people.man-guard +people.man-health-worker-dark-skin-tone +people.man-health-worker-light-skin-tone +people.man-health-worker-medium-dark-skin-tone +people.man-health-worker-medium-light-skin-tone +people.man-health-worker-medium-skin-tone +people.man-health-worker +people.man-in-lotus-position-dark-skin-tone +people.man-in-lotus-position-light-skin-tone +people.man-in-lotus-position-medium-dark-skin-tone +people.man-in-lotus-position-medium-light-skin-tone +people.man-in-lotus-position-medium-skin-tone +people.man-in-lotus-position +people.man-in-manual-wheelchair-dark-skin-tone +people.man-in-manual-wheelchair-facing-right-dark-skin-tone +people.man-in-manual-wheelchair-facing-right-light-skin-tone +people.man-in-manual-wheelchair-facing-right-medium-dark-skin-tone +people.man-in-manual-wheelchair-facing-right-medium-light-skin-tone +people.man-in-manual-wheelchair-facing-right-medium-skin-tone +people.man-in-manual-wheelchair-facing-right +people.man-in-manual-wheelchair-light-skin-tone +people.man-in-manual-wheelchair-medium-dark-skin-tone +people.man-in-manual-wheelchair-medium-light-skin-tone +people.man-in-manual-wheelchair-medium-skin-tone +people.man-in-manual-wheelchair +people.man-in-motorized-wheelchair-dark-skin-tone +people.man-in-motorized-wheelchair-facing-right-dark-skin-tone +people.man-in-motorized-wheelchair-facing-right-light-skin-tone +people.man-in-motorized-wheelchair-facing-right-medium-dark-skin-tone +people.man-in-motorized-wheelchair-facing-right-medium-light-skin-tone +people.man-in-motorized-wheelchair-facing-right-medium-skin-tone +people.man-in-motorized-wheelchair-facing-right +people.man-in-motorized-wheelchair-light-skin-tone +people.man-in-motorized-wheelchair-medium-dark-skin-tone +people.man-in-motorized-wheelchair-medium-light-skin-tone +people.man-in-motorized-wheelchair-medium-skin-tone +people.man-in-motorized-wheelchair +people.man-in-steamy-room-dark-skin-tone +people.man-in-steamy-room-light-skin-tone +people.man-in-steamy-room-medium-dark-skin-tone +people.man-in-steamy-room-medium-light-skin-tone +people.man-in-steamy-room-medium-skin-tone +people.man-in-steamy-room +people.man-in-tuxedo-dark-skin-tone +people.man-in-tuxedo-light-skin-tone +people.man-in-tuxedo-medium-dark-skin-tone +people.man-in-tuxedo-medium-light-skin-tone +people.man-in-tuxedo-medium-skin-tone +people.man-in-tuxedo +people.man-judge-dark-skin-tone +people.man-judge-light-skin-tone +people.man-judge-medium-dark-skin-tone +people.man-judge-medium-light-skin-tone +people.man-judge-medium-skin-tone +people.man-judge +people.man-juggling-dark-skin-tone +people.man-juggling-light-skin-tone +people.man-juggling-medium-dark-skin-tone +people.man-juggling-medium-light-skin-tone +people.man-juggling-medium-skin-tone +people.man-juggling +people.man-kneeling-dark-skin-tone +people.man-kneeling-facing-right-dark-skin-tone +people.man-kneeling-facing-right-light-skin-tone +people.man-kneeling-facing-right-medium-dark-skin-tone +people.man-kneeling-facing-right-medium-light-skin-tone +people.man-kneeling-facing-right-medium-skin-tone +people.man-kneeling-facing-right +people.man-kneeling-light-skin-tone +people.man-kneeling-medium-dark-skin-tone +people.man-kneeling-medium-light-skin-tone +people.man-kneeling-medium-skin-tone +people.man-kneeling +people.man-lifting-weights-dark-skin-tone +people.man-lifting-weights-light-skin-tone +people.man-lifting-weights-medium-dark-skin-tone +people.man-lifting-weights-medium-light-skin-tone +people.man-lifting-weights-medium-skin-tone +people.man-lifting-weights +people.man-light-skin-tone-bald +people.man-light-skin-tone-beard +people.man-light-skin-tone-blond-hair +people.man-light-skin-tone-curly-hair +people.man-light-skin-tone-red-hair +people.man-light-skin-tone +people.man-light-skin-tone-white-hair +people.man-mage-dark-skin-tone +people.man-mage-light-skin-tone +people.man-mage-medium-dark-skin-tone +people.man-mage-medium-light-skin-tone +people.man-mage-medium-skin-tone +people.man-mage +people.man-mechanic-dark-skin-tone +people.man-mechanic-light-skin-tone +people.man-mechanic-medium-dark-skin-tone +people.man-mechanic-medium-light-skin-tone +people.man-mechanic-medium-skin-tone +people.man-mechanic +people.man-medium-dark-skin-tone-bald +people.man-medium-dark-skin-tone-beard +people.man-medium-dark-skin-tone-blond-hair +people.man-medium-dark-skin-tone-curly-hair +people.man-medium-dark-skin-tone-red-hair +people.man-medium-dark-skin-tone +people.man-medium-dark-skin-tone-white-hair +people.man-medium-light-skin-tone-bald +people.man-medium-light-skin-tone-beard +people.man-medium-light-skin-tone-blond-hair +people.man-medium-light-skin-tone-curly-hair +people.man-medium-light-skin-tone-red-hair +people.man-medium-light-skin-tone +people.man-medium-light-skin-tone-white-hair +people.man-medium-skin-tone-bald +people.man-medium-skin-tone-beard +people.man-medium-skin-tone-blond-hair +people.man-medium-skin-tone-curly-hair +people.man-medium-skin-tone-red-hair +people.man-medium-skin-tone +people.man-medium-skin-tone-white-hair +people.man-mountain-biking-dark-skin-tone +people.man-mountain-biking-light-skin-tone +people.man-mountain-biking-medium-dark-skin-tone +people.man-mountain-biking-medium-light-skin-tone +people.man-mountain-biking-medium-skin-tone +people.man-mountain-biking +people.man-office-worker-dark-skin-tone +people.man-office-worker-light-skin-tone +people.man-office-worker-medium-dark-skin-tone +people.man-office-worker-medium-light-skin-tone +people.man-office-worker-medium-skin-tone +people.man-office-worker +people.man-pilot-dark-skin-tone +people.man-pilot-light-skin-tone +people.man-pilot-medium-dark-skin-tone +people.man-pilot-medium-light-skin-tone +people.man-pilot-medium-skin-tone +people.man-pilot +people.man-playing-handball-dark-skin-tone +people.man-playing-handball-light-skin-tone +people.man-playing-handball-medium-dark-skin-tone +people.man-playing-handball-medium-light-skin-tone +people.man-playing-handball-medium-skin-tone +people.man-playing-handball +people.man-playing-water-polo-dark-skin-tone +people.man-playing-water-polo-light-skin-tone +people.man-playing-water-polo-medium-dark-skin-tone +people.man-playing-water-polo-medium-light-skin-tone +people.man-playing-water-polo-medium-skin-tone +people.man-playing-water-polo +people.man-police-officer-dark-skin-tone +people.man-police-officer-light-skin-tone +people.man-police-officer-medium-dark-skin-tone +people.man-police-officer-medium-light-skin-tone +people.man-police-officer-medium-skin-tone +people.man-police-officer +people.man-pouting-dark-skin-tone +people.man-pouting-light-skin-tone +people.man-pouting-medium-dark-skin-tone +people.man-pouting-medium-light-skin-tone +people.man-pouting-medium-skin-tone +people.man-pouting +people.man-raising-hand-dark-skin-tone +people.man-raising-hand-light-skin-tone +people.man-raising-hand-medium-dark-skin-tone +people.man-raising-hand-medium-light-skin-tone +people.man-raising-hand-medium-skin-tone +people.man-raising-hand +people.man-red-hair +people.man-rowing-boat-dark-skin-tone +people.man-rowing-boat-light-skin-tone +people.man-rowing-boat-medium-dark-skin-tone +people.man-rowing-boat-medium-light-skin-tone +people.man-rowing-boat-medium-skin-tone +people.man-rowing-boat +people.man-running-dark-skin-tone +people.man-running-facing-right-dark-skin-tone +people.man-running-facing-right-light-skin-tone +people.man-running-facing-right-medium-dark-skin-tone +people.man-running-facing-right-medium-light-skin-tone +people.man-running-facing-right-medium-skin-tone +people.man-running-facing-right +people.man-running-light-skin-tone +people.man-running-medium-dark-skin-tone +people.man-running-medium-light-skin-tone +people.man-running-medium-skin-tone +people.man-running +people.man-scientist-dark-skin-tone +people.man-scientist-light-skin-tone +people.man-scientist-medium-dark-skin-tone +people.man-scientist-medium-light-skin-tone +people.man-scientist-medium-skin-tone +people.man-scientist +people.man-shrugging-dark-skin-tone +people.man-shrugging-light-skin-tone +people.man-shrugging-medium-dark-skin-tone +people.man-shrugging-medium-light-skin-tone +people.man-shrugging-medium-skin-tone +people.man-shrugging +people.man-singer-dark-skin-tone +people.man-singer-light-skin-tone +people.man-singer-medium-dark-skin-tone +people.man-singer-medium-light-skin-tone +people.man-singer-medium-skin-tone +people.man-singer +people.man-standing-dark-skin-tone +people.man-standing-light-skin-tone +people.man-standing-medium-dark-skin-tone +people.man-standing-medium-light-skin-tone +people.man-standing-medium-skin-tone +people.man-standing +people.man-student-dark-skin-tone +people.man-student-light-skin-tone +people.man-student-medium-dark-skin-tone +people.man-student-medium-light-skin-tone +people.man-student-medium-skin-tone +people.man-student +people.man-superhero-dark-skin-tone +people.man-superhero-light-skin-tone +people.man-superhero-medium-dark-skin-tone +people.man-superhero-medium-light-skin-tone +people.man-superhero-medium-skin-tone +people.man-superhero +people.man-supervillain-dark-skin-tone +people.man-supervillain-light-skin-tone +people.man-supervillain-medium-dark-skin-tone +people.man-supervillain-medium-light-skin-tone +people.man-supervillain-medium-skin-tone +people.man-supervillain +people.man-surfing-dark-skin-tone +people.man-surfing-light-skin-tone +people.man-surfing-medium-dark-skin-tone +people.man-surfing-medium-light-skin-tone +people.man-surfing-medium-skin-tone +people.man-surfing +people.man-swimming-dark-skin-tone +people.man-swimming-light-skin-tone +people.man-swimming-medium-dark-skin-tone +people.man-swimming-medium-light-skin-tone +people.man-swimming-medium-skin-tone +people.man-swimming +people.man-teacher-dark-skin-tone +people.man-teacher-light-skin-tone +people.man-teacher-medium-dark-skin-tone +people.man-teacher-medium-light-skin-tone +people.man-teacher-medium-skin-tone +people.man-teacher +people.man-technologist-dark-skin-tone +people.man-technologist-light-skin-tone +people.man-technologist-medium-dark-skin-tone +people.man-technologist-medium-light-skin-tone +people.man-technologist-medium-skin-tone +people.man-technologist +people.man-tipping-hand-dark-skin-tone +people.man-tipping-hand-light-skin-tone +people.man-tipping-hand-medium-dark-skin-tone +people.man-tipping-hand-medium-light-skin-tone +people.man-tipping-hand-medium-skin-tone +people.man-tipping-hand +people.man-vampire-dark-skin-tone +people.man-vampire-light-skin-tone +people.man-vampire-medium-dark-skin-tone +people.man-vampire-medium-light-skin-tone +people.man-vampire-medium-skin-tone +people.man-vampire +people.man-walking-dark-skin-tone +people.man-walking-facing-right-dark-skin-tone +people.man-walking-facing-right-light-skin-tone +people.man-walking-facing-right-medium-dark-skin-tone +people.man-walking-facing-right-medium-light-skin-tone +people.man-walking-facing-right-medium-skin-tone +people.man-walking-facing-right +people.man-walking-light-skin-tone +people.man-walking-medium-dark-skin-tone +people.man-walking-medium-light-skin-tone +people.man-walking-medium-skin-tone +people.man-walking +people.man-wearing-turban-dark-skin-tone +people.man-wearing-turban-light-skin-tone +people.man-wearing-turban-medium-dark-skin-tone +people.man-wearing-turban-medium-light-skin-tone +people.man-wearing-turban-medium-skin-tone +people.man-wearing-turban +people.man +people.man-white-hair +people.man-with-veil-dark-skin-tone +people.man-with-veil-light-skin-tone +people.man-with-veil-medium-dark-skin-tone +people.man-with-veil-medium-light-skin-tone +people.man-with-veil-medium-skin-tone +people.man-with-veil +people.man-with-white-cane-dark-skin-tone +people.man-with-white-cane-facing-right-dark-skin-tone +people.man-with-white-cane-facing-right-light-skin-tone +people.man-with-white-cane-facing-right-medium-dark-skin-tone +people.man-with-white-cane-facing-right-medium-light-skin-tone +people.man-with-white-cane-facing-right-medium-skin-tone +people.man-with-white-cane-facing-right +people.man-with-white-cane-light-skin-tone +people.man-with-white-cane-medium-dark-skin-tone +people.man-with-white-cane-medium-light-skin-tone +people.man-with-white-cane-medium-skin-tone +people.man-with-white-cane +people.man-zombie +people.mechanical-arm +people.mechanical-leg +people.mechanic-dark-skin-tone +people.mechanic-light-skin-tone +people.mechanic-medium-dark-skin-tone +people.mechanic-medium-light-skin-tone +people.mechanic-medium-skin-tone +people.mechanic +people.medium-dark-skin-tone +people.medium-light-skin-tone +people.medium-skin-tone +people.men-holding-hands-dark-skin-tone-light-skin-tone +people.men-holding-hands-dark-skin-tone-medium-dark-skin-tone +people.men-holding-hands-dark-skin-tone-medium-light-skin-tone +people.men-holding-hands-dark-skin-tone-medium-skin-tone +people.men-holding-hands-dark-skin-tone +people.men-holding-hands-light-skin-tone-dark-skin-tone +people.men-holding-hands-light-skin-tone-medium-dark-skin-tone +people.men-holding-hands-light-skin-tone-medium-light-skin-tone +people.men-holding-hands-light-skin-tone-medium-skin-tone +people.men-holding-hands-light-skin-tone +people.men-holding-hands-medium-dark-skin-tone-dark-skin-tone +people.men-holding-hands-medium-dark-skin-tone-light-skin-tone +people.men-holding-hands-medium-dark-skin-tone-medium-light-skin-tone +people.men-holding-hands-medium-dark-skin-tone-medium-skin-tone +people.men-holding-hands-medium-dark-skin-tone +people.men-holding-hands-medium-light-skin-tone-dark-skin-tone +people.men-holding-hands-medium-light-skin-tone-light-skin-tone +people.men-holding-hands-medium-light-skin-tone-medium-dark-skin-tone +people.men-holding-hands-medium-light-skin-tone-medium-skin-tone +people.men-holding-hands-medium-light-skin-tone +people.men-holding-hands-medium-skin-tone-dark-skin-tone +people.men-holding-hands-medium-skin-tone-light-skin-tone +people.men-holding-hands-medium-skin-tone-medium-dark-skin-tone +people.men-holding-hands-medium-skin-tone-medium-light-skin-tone +people.men-holding-hands-medium-skin-tone +people.men-holding-hands +people.men-with-bunny-ears +people.men-wrestling +people.mermaid-dark-skin-tone +people.mermaid-light-skin-tone +people.mermaid-medium-dark-skin-tone +people.mermaid-medium-light-skin-tone +people.mermaid-medium-skin-tone +people.mermaid +people.merman-dark-skin-tone +people.merman-light-skin-tone +people.merman-medium-dark-skin-tone +people.merman-medium-light-skin-tone +people.merman-medium-skin-tone +people.merman +people.merperson-dark-skin-tone +people.merperson-light-skin-tone +people.merperson-medium-dark-skin-tone +people.merperson-medium-light-skin-tone +people.merperson-medium-skin-tone +people.merperson +people.mouth +people.mrs-claus-dark-skin-tone +people.mrs-claus-light-skin-tone +people.mrs-claus-medium-dark-skin-tone +people.mrs-claus-medium-light-skin-tone +people.mrs-claus-medium-skin-tone +people.mrs-claus +people.mx-claus-dark-skin-tone +people.mx-claus-light-skin-tone +people.mx-claus-medium-dark-skin-tone +people.mx-claus-medium-light-skin-tone +people.mx-claus-medium-skin-tone +people.mx-claus +people.nail-polish-dark-skin-tone +people.nail-polish-light-skin-tone +people.nail-polish-medium-dark-skin-tone +people.nail-polish-medium-light-skin-tone +people.nail-polish-medium-skin-tone +people.nail-polish +people.ninja-dark-skin-tone +people.ninja-light-skin-tone +people.ninja-medium-dark-skin-tone +people.ninja-medium-light-skin-tone +people.ninja-medium-skin-tone +people.ninja +people.nose-dark-skin-tone +people.nose-light-skin-tone +people.nose-medium-dark-skin-tone +people.nose-medium-light-skin-tone +people.nose-medium-skin-tone +people.nose +people.office-worker-dark-skin-tone +people.office-worker-light-skin-tone +people.office-worker-medium-dark-skin-tone +people.office-worker-medium-light-skin-tone +people.office-worker-medium-skin-tone +people.office-worker +people.ok-hand-dark-skin-tone +people.ok-hand-light-skin-tone +people.ok-hand-medium-dark-skin-tone +people.ok-hand-medium-light-skin-tone +people.ok-hand-medium-skin-tone +people.ok-hand +people.older-person-dark-skin-tone +people.older-person-light-skin-tone +people.older-person-medium-dark-skin-tone +people.older-person-medium-light-skin-tone +people.older-person-medium-skin-tone +people.older-person +people.old-man-dark-skin-tone +people.old-man-light-skin-tone +people.old-man-medium-dark-skin-tone +people.old-man-medium-light-skin-tone +people.old-man-medium-skin-tone +people.old-man +people.old-woman-dark-skin-tone +people.old-woman-light-skin-tone +people.old-woman-medium-dark-skin-tone +people.old-woman-medium-light-skin-tone +people.old-woman-medium-skin-tone +people.old-woman +people.oncoming-fist-dark-skin-tone +people.oncoming-fist-light-skin-tone +people.oncoming-fist-medium-dark-skin-tone +people.oncoming-fist-medium-light-skin-tone +people.oncoming-fist-medium-skin-tone +people.oncoming-fist +people.open-hands-dark-skin-tone +people.open-hands-light-skin-tone +people.open-hands-medium-dark-skin-tone +people.open-hands-medium-light-skin-tone +people.open-hands-medium-skin-tone +people.open-hands +people.palm-down-hand-dark-skin-tone +people.palm-down-hand-light-skin-tone +people.palm-down-hand-medium-dark-skin-tone +people.palm-down-hand-medium-light-skin-tone +people.palm-down-hand-medium-skin-tone +people.palm-down-hand +people.palms-up-together-dark-skin-tone +people.palms-up-together-light-skin-tone +people.palms-up-together-medium-dark-skin-tone +people.palms-up-together-medium-light-skin-tone +people.palms-up-together-medium-skin-tone +people.palms-up-together +people.palm-up-hand-dark-skin-tone +people.palm-up-hand-light-skin-tone +people.palm-up-hand-medium-dark-skin-tone +people.palm-up-hand-medium-light-skin-tone +people.palm-up-hand-medium-skin-tone +people.palm-up-hand +people.people-holding-hands-dark-skin-tone-light-skin-tone +people.people-holding-hands-dark-skin-tone-medium-dark-skin-tone +people.people-holding-hands-dark-skin-tone-medium-light-skin-tone +people.people-holding-hands-dark-skin-tone-medium-skin-tone +people.people-holding-hands-dark-skin-tone +people.people-holding-hands-light-skin-tone-dark-skin-tone +people.people-holding-hands-light-skin-tone-medium-dark-skin-tone +people.people-holding-hands-light-skin-tone-medium-light-skin-tone +people.people-holding-hands-light-skin-tone-medium-skin-tone +people.people-holding-hands-light-skin-tone +people.people-holding-hands-medium-dark-skin-tone-dark-skin-tone +people.people-holding-hands-medium-dark-skin-tone-light-skin-tone +people.people-holding-hands-medium-dark-skin-tone-medium-light-skin-tone +people.people-holding-hands-medium-dark-skin-tone-medium-skin-tone +people.people-holding-hands-medium-dark-skin-tone +people.people-holding-hands-medium-light-skin-tone-dark-skin-tone +people.people-holding-hands-medium-light-skin-tone-light-skin-tone +people.people-holding-hands-medium-light-skin-tone-medium-dark-skin-tone +people.people-holding-hands-medium-light-skin-tone-medium-skin-tone +people.people-holding-hands-medium-light-skin-tone +people.people-holding-hands-medium-skin-tone-dark-skin-tone +people.people-holding-hands-medium-skin-tone-light-skin-tone +people.people-holding-hands-medium-skin-tone-medium-dark-skin-tone +people.people-holding-hands-medium-skin-tone-medium-light-skin-tone +people.people-holding-hands-medium-skin-tone +people.people-holding-hands +people.people-hugging +people.people-with-bunny-ears +people.people-wrestling +people.person-bald +people.person-beard +people.person-biking-dark-skin-tone +people.person-biking-light-skin-tone +people.person-biking-medium-dark-skin-tone +people.person-biking-medium-light-skin-tone +people.person-biking-medium-skin-tone +people.person-biking +people.person-blond-hair +people.person-bouncing-ball-dark-skin-tone +people.person-bouncing-ball-light-skin-tone +people.person-bouncing-ball-medium-dark-skin-tone +people.person-bouncing-ball-medium-light-skin-tone +people.person-bouncing-ball-medium-skin-tone +people.person-bouncing-ball +people.person-bowing-dark-skin-tone +people.person-bowing-light-skin-tone +people.person-bowing-medium-dark-skin-tone +people.person-bowing-medium-light-skin-tone +people.person-bowing-medium-skin-tone +people.person-bowing +people.person-cartwheeling-dark-skin-tone +people.person-cartwheeling-light-skin-tone +people.person-cartwheeling-medium-dark-skin-tone +people.person-cartwheeling-medium-light-skin-tone +people.person-cartwheeling-medium-skin-tone +people.person-cartwheeling +people.person-climbing-dark-skin-tone +people.person-climbing-light-skin-tone +people.person-climbing-medium-dark-skin-tone +people.person-climbing-medium-light-skin-tone +people.person-climbing-medium-skin-tone +people.person-climbing +people.person-curly-hair +people.person-dark-skin-tone-bald +people.person-dark-skin-tone-beard +people.person-dark-skin-tone-blond-hair +people.person-dark-skin-tone-curly-hair +people.person-dark-skin-tone-red-hair +people.person-dark-skin-tone +people.person-dark-skin-tone-white-hair +people.person-facepalming-dark-skin-tone +people.person-facepalming-light-skin-tone +people.person-facepalming-medium-dark-skin-tone +people.person-facepalming-medium-light-skin-tone +people.person-facepalming-medium-skin-tone +people.person-facepalming +people.person-feeding-baby-dark-skin-tone +people.person-feeding-baby-light-skin-tone +people.person-feeding-baby-medium-dark-skin-tone +people.person-feeding-baby-medium-light-skin-tone +people.person-feeding-baby-medium-skin-tone +people.person-feeding-baby +people.person-fencing +people.person-frowning-dark-skin-tone +people.person-frowning-light-skin-tone +people.person-frowning-medium-dark-skin-tone +people.person-frowning-medium-light-skin-tone +people.person-frowning-medium-skin-tone +people.person-frowning +people.person-gesturing-no-dark-skin-tone +people.person-gesturing-no-light-skin-tone +people.person-gesturing-no-medium-dark-skin-tone +people.person-gesturing-no-medium-light-skin-tone +people.person-gesturing-no-medium-skin-tone +people.person-gesturing-no +people.person-gesturing-ok-dark-skin-tone +people.person-gesturing-ok-light-skin-tone +people.person-gesturing-ok-medium-dark-skin-tone +people.person-gesturing-ok-medium-light-skin-tone +people.person-gesturing-ok-medium-skin-tone +people.person-gesturing-ok +people.person-getting-haircut-dark-skin-tone +people.person-getting-haircut-light-skin-tone +people.person-getting-haircut-medium-dark-skin-tone +people.person-getting-haircut-medium-light-skin-tone +people.person-getting-haircut-medium-skin-tone +people.person-getting-haircut +people.person-getting-massage-dark-skin-tone +people.person-getting-massage-light-skin-tone +people.person-getting-massage-medium-dark-skin-tone +people.person-getting-massage-medium-light-skin-tone +people.person-getting-massage-medium-skin-tone +people.person-getting-massage +people.person-golfing-dark-skin-tone +people.person-golfing-light-skin-tone +people.person-golfing-medium-dark-skin-tone +people.person-golfing-medium-light-skin-tone +people.person-golfing-medium-skin-tone +people.person-golfing +people.person-in-bed-dark-skin-tone +people.person-in-bed-light-skin-tone +people.person-in-bed-medium-dark-skin-tone +people.person-in-bed-medium-light-skin-tone +people.person-in-bed-medium-skin-tone +people.person-in-bed +people.person-in-lotus-position-dark-skin-tone +people.person-in-lotus-position-light-skin-tone +people.person-in-lotus-position-medium-dark-skin-tone +people.person-in-lotus-position-medium-light-skin-tone +people.person-in-lotus-position-medium-skin-tone +people.person-in-lotus-position +people.person-in-manual-wheelchair-dark-skin-tone +people.person-in-manual-wheelchair-facing-right-dark-skin-tone +people.person-in-manual-wheelchair-facing-right-light-skin-tone +people.person-in-manual-wheelchair-facing-right-medium-dark-skin-tone +people.person-in-manual-wheelchair-facing-right-medium-light-skin-tone +people.person-in-manual-wheelchair-facing-right-medium-skin-tone +people.person-in-manual-wheelchair-facing-right +people.person-in-manual-wheelchair-light-skin-tone +people.person-in-manual-wheelchair-medium-dark-skin-tone +people.person-in-manual-wheelchair-medium-light-skin-tone +people.person-in-manual-wheelchair-medium-skin-tone +people.person-in-manual-wheelchair +people.person-in-motorized-wheelchair-dark-skin-tone +people.person-in-motorized-wheelchair-facing-right-dark-skin-tone +people.person-in-motorized-wheelchair-facing-right-light-skin-tone +people.person-in-motorized-wheelchair-facing-right-medium-dark-skin-tone +people.person-in-motorized-wheelchair-facing-right-medium-light-skin-tone +people.person-in-motorized-wheelchair-facing-right-medium-skin-tone +people.person-in-motorized-wheelchair-facing-right +people.person-in-motorized-wheelchair-light-skin-tone +people.person-in-motorized-wheelchair-medium-dark-skin-tone +people.person-in-motorized-wheelchair-medium-light-skin-tone +people.person-in-motorized-wheelchair-medium-skin-tone +people.person-in-motorized-wheelchair +people.person-in-steamy-room-dark-skin-tone +people.person-in-steamy-room-light-skin-tone +people.person-in-steamy-room-medium-dark-skin-tone +people.person-in-steamy-room-medium-light-skin-tone +people.person-in-steamy-room-medium-skin-tone +people.person-in-steamy-room +people.person-in-suit-levitating-dark-skin-tone +people.person-in-suit-levitating-light-skin-tone +people.person-in-suit-levitating-medium-dark-skin-tone +people.person-in-suit-levitating-medium-light-skin-tone +people.person-in-suit-levitating-medium-skin-tone +people.person-in-suit-levitating +people.person-in-tuxedo-dark-skin-tone +people.person-in-tuxedo-light-skin-tone +people.person-in-tuxedo-medium-dark-skin-tone +people.person-in-tuxedo-medium-light-skin-tone +people.person-in-tuxedo-medium-skin-tone +people.person-in-tuxedo +people.person-juggling-dark-skin-tone +people.person-juggling-light-skin-tone +people.person-juggling-medium-dark-skin-tone +people.person-juggling-medium-light-skin-tone +people.person-juggling-medium-skin-tone +people.person-juggling +people.person-kneeling-dark-skin-tone +people.person-kneeling-facing-right-dark-skin-tone +people.person-kneeling-facing-right-light-skin-tone +people.person-kneeling-facing-right-medium-dark-skin-tone +people.person-kneeling-facing-right-medium-light-skin-tone +people.person-kneeling-facing-right-medium-skin-tone +people.person-kneeling-facing-right +people.person-kneeling-light-skin-tone +people.person-kneeling-medium-dark-skin-tone +people.person-kneeling-medium-light-skin-tone +people.person-kneeling-medium-skin-tone +people.person-kneeling +people.person-lifting-weights-dark-skin-tone +people.person-lifting-weights-light-skin-tone +people.person-lifting-weights-medium-dark-skin-tone +people.person-lifting-weights-medium-light-skin-tone +people.person-lifting-weights-medium-skin-tone +people.person-lifting-weights +people.person-light-skin-tone-bald +people.person-light-skin-tone-beard +people.person-light-skin-tone-blond-hair +people.person-light-skin-tone-curly-hair +people.person-light-skin-tone-red-hair +people.person-light-skin-tone +people.person-light-skin-tone-white-hair +people.person-medium-dark-skin-tone-bald +people.person-medium-dark-skin-tone-beard +people.person-medium-dark-skin-tone-blond-hair +people.person-medium-dark-skin-tone-curly-hair +people.person-medium-dark-skin-tone-red-hair +people.person-medium-dark-skin-tone +people.person-medium-dark-skin-tone-white-hair +people.person-medium-light-skin-tone-bald +people.person-medium-light-skin-tone-beard +people.person-medium-light-skin-tone-blond-hair +people.person-medium-light-skin-tone-curly-hair +people.person-medium-light-skin-tone-red-hair +people.person-medium-light-skin-tone +people.person-medium-light-skin-tone-white-hair +people.person-medium-skin-tone-bald +people.person-medium-skin-tone-beard +people.person-medium-skin-tone-blond-hair +people.person-medium-skin-tone-curly-hair +people.person-medium-skin-tone-red-hair +people.person-medium-skin-tone +people.person-medium-skin-tone-white-hair +people.person-mountain-biking-dark-skin-tone +people.person-mountain-biking-light-skin-tone +people.person-mountain-biking-medium-dark-skin-tone +people.person-mountain-biking-medium-light-skin-tone +people.person-mountain-biking-medium-skin-tone +people.person-mountain-biking +people.person-playing-handball-dark-skin-tone +people.person-playing-handball-light-skin-tone +people.person-playing-handball-medium-dark-skin-tone +people.person-playing-handball-medium-light-skin-tone +people.person-playing-handball-medium-skin-tone +people.person-playing-handball +people.person-playing-water-polo-dark-skin-tone +people.person-playing-water-polo-light-skin-tone +people.person-playing-water-polo-medium-dark-skin-tone +people.person-playing-water-polo-medium-light-skin-tone +people.person-playing-water-polo-medium-skin-tone +people.person-playing-water-polo +people.person-pouting-dark-skin-tone +people.person-pouting-light-skin-tone +people.person-pouting-medium-dark-skin-tone +people.person-pouting-medium-light-skin-tone +people.person-pouting-medium-skin-tone +people.person-pouting +people.person-raising-hand-dark-skin-tone +people.person-raising-hand-light-skin-tone +people.person-raising-hand-medium-dark-skin-tone +people.person-raising-hand-medium-light-skin-tone +people.person-raising-hand-medium-skin-tone +people.person-raising-hand +people.person-red-hair +people.person-rowing-boat-dark-skin-tone +people.person-rowing-boat-light-skin-tone +people.person-rowing-boat-medium-dark-skin-tone +people.person-rowing-boat-medium-light-skin-tone +people.person-rowing-boat-medium-skin-tone +people.person-rowing-boat +people.person-running-dark-skin-tone +people.person-running-facing-right-dark-skin-tone +people.person-running-facing-right-light-skin-tonet +people.person-running-facing-right-medium-dark-skin-tone +people.person-running-facing-right-medium-light-skin-tone +people.person-running-facing-right-medium-skin-tone +people.person-running-facing-right +people.person-running-light-skin-tone +people.person-running-medium-dark-skin-tone +people.person-running-medium-light-skin-tone +people.person-running-medium-skin-tone +people.person-running +people.person-shrugging-dark-skin-tone +people.person-shrugging-light-skin-tone +people.person-shrugging-medium-dark-skin-tone +people.person-shrugging-medium-light-skin-tone +people.person-shrugging-medium-skin-tone +people.person-shrugging +people.person-standing-dark-skin-tone +people.person-standing-light-skin-tone +people.person-standing-medium-dark-skin-tone +people.person-standing-medium-light-skin-tone +people.person-standing-medium-skin-tone +people.person-standing +people.person-surfing-dark-skin-tone +people.person-surfing-light-skin-tone +people.person-surfing-medium-dark-skin-tone +people.person-surfing-medium-light-skin-tone +people.person-surfing-medium-skin-tone +people.person-surfing +people.person-swimming-dark-skin-tone +people.person-swimming-light-skin-tone +people.person-swimming-medium-dark-skin-tone +people.person-swimming-medium-light-skin-tone +people.person-swimming-medium-skin-tone +people.person-swimming +people.person-taking-bath-dark-skin-tone +people.person-taking-bath-light-skin-tone +people.person-taking-bath-medium-dark-skin-tone +people.person-taking-bath-medium-light-skin-tone +people.person-taking-bath-medium-skin-tone +people.person-taking-bath +people.person-tipping-hand-dark-skin-tone +people.person-tipping-hand-light-skin-tone +people.person-tipping-hand-medium-dark-skin-tone +people.person-tipping-hand-medium-light-skin-tone +people.person-tipping-hand-medium-skin-tone +people.person-tipping-hand +people.person-walking-dark-skin-tone +people.person-walking-facing-right-dark-skin-tone +people.person-walking-facing-right-light-skin-tone +people.person-walking-facing-right-medium-dark-skin-tone +people.person-walking-facing-right-medium-light-skin-tone +people.person-walking-facing-right-medium-skin-tone +people.person-walking-facing-right +people.person-walking-light-skin-tone +people.person-walking-medium-dark-skin-tone +people.person-walking-medium-light-skin-tone +people.person-walking-medium-skin-tone +people.person-walking +people.person-wearing-turban-dark-skin-tone +people.person-wearing-turban-light-skin-tone +people.person-wearing-turban-medium-dark-skin-tone +people.person-wearing-turban-medium-light-skin-tone +people.person-wearing-turban-medium-skin-tone +people.person-wearing-turban +people.person +people.person-white-hair +people.person-with-crown-dark-skin-tone +people.person-with-crown-light-skin-tone +people.person-with-crown-medium-dark-skin-tone +people.person-with-crown-medium-light-skin-tone +people.person-with-crown-medium-skin-tone +people.person-with-crown +people.person-with-headscarf-dark-skin-tone +people.person-with-headscarf-light-skin-tone +people.person-with-headscarf-medium-dark-skin-tone +people.person-with-headscarf-medium-light-skin-tone +people.person-with-headscarf-medium-skin-tone +people.person-with-headscarf +people.person-with-skullcap-dark-skin-tone +people.person-with-skullcap-light-skin-tone +people.person-with-skullcap-medium-dark-skin-tone +people.person-with-skullcap-medium-light-skin-tone +people.person-with-skullcap-medium-skin-tone +people.person-with-skullcap +people.person-with-veil-dark-skin-tone +people.person-with-veil-light-skin-tone +people.person-with-veil-medium-dark-skin-tone +people.person-with-veil-medium-light-skin-tone +people.person-with-veil-medium-skin-tone +people.person-with-veil +people.person-with-white-cane-dark-skin-tone +people.person-with-white-cane-facing-right-dark-skin-tone +people.person-with-white-cane-facing-right-light-skin-tone +people.person-with-white-cane-facing-right-medium-dark-skin-tone +people.person-with-white-cane-facing-right-medium-light-skin-tone +people.person-with-white-cane-facing-right-medium-skin-tone +people.person-with-white-cane-facing-right +people.person-with-white-cane-light-skin-tone +people.person-with-white-cane-medium-dark-skin-tone +people.person-with-white-cane-medium-light-skin-tone +people.person-with-white-cane-medium-skin-tone +people.person-with-white-cane +people.pilot-dark-skin-tone +people.pilot-light-skin-tone +people.pilot-medium-dark-skin-tone +people.pilot-medium-light-skin-tone +people.pilot-medium-skin-tone +people.pilot +people.pinched-fingers-dark-skin-tone +people.pinched-fingers-light-skin-tone +people.pinched-fingers-medium-dark-skin-tone +people.pinched-fingers-medium-light-skin-tone +people.pinched-fingers-medium-skin-tone +people.pinched-fingers +people.police-officer-dark-skin-tone +people.police-officer-light-skin-tone +people.police-officer-medium-dark-skin-tone +people.police-officer-medium-light-skin-tone +people.police-officer-medium-skin-tone +people.police-officer +people.pregnant-man-dark-skin-tone +people.pregnant-man-light-skin-tone +people.pregnant-man-medium-dark-skin-tone +people.pregnant-man-medium-light-skin-tone +people.pregnant-man-medium-skin-tone +people.pregnant-man +people.pregnant-person-dark-skin-tone +people.pregnant-person-light-skin-tone +people.pregnant-person-medium-dark-skin-tone +people.pregnant-person-medium-light-skin-tone +people.pregnant-person-medium-skin-tone +people.pregnant-person +people.pregnant-woman-dark-skin-tone +people.pregnant-woman-light-skin-tone +people.pregnant-woman-medium-dark-skin-tone +people.pregnant-woman-medium-light-skin-tone +people.pregnant-woman-medium-skin-tone +people.pregnant-woman +people.prince-dark-skin-tone +people.prince-light-skin-tone +people.prince-medium-dark-skin-tone +people.prince-medium-light-skin-tone +people.prince-medium-skin-tone +people.princess-dark-skin-tone +people.princess-light-skin-tone +people.princess-medium-dark-skin-tone +people.princess-medium-light-skin-tone +people.princess-medium-skin-tone +people.princess +people.prince +people.raised-back-of-hand-dark-skin-tone +people.raised-back-of-hand-light-skin-tone +people.raised-back-of-hand-medium-dark-skin-tone +people.raised-back-of-hand-medium-light-skin-tone +people.raised-back-of-hand-medium-skin-tone +people.raised-back-of-hand +people.raised-fist-dark-skin-tone +people.raised-fist-light-skin-tone +people.raised-fist-medium-dark-skin-tone +people.raised-fist-medium-light-skin-tone +people.raised-fist-medium-skin-tone +people.raised-fist +people.raised-hand-dark-skin-tone +people.raised-hand-light-skin-tone +people.raised-hand-medium-dark-skin-tone +people.raised-hand-medium-light-skin-tone +people.raised-hand-medium-skin-tone +people.raised-hand +people.raising-hands-dark-skin-tone +people.raising-hands-light-skin-tone +people.raising-hands-medium-dark-skin-tone +people.raising-hands-medium-light-skin-tone +people.raising-hands-medium-skin-tone +people.raising-hands +people.red-hair +people.right-facing-fist-dark-skin-tone +people.right-facing-fist-light-skin-tone +people.right-facing-fist-medium-dark-skin-tone +people.right-facing-fist-medium-light-skin-tone +people.right-facing-fist-medium-skin-tone +people.right-facing-fist +people.rightwards-hand-dark-skin-tone +people.rightwards-hand-light-skin-tone +people.rightwards-hand-medium-dark-skin-tone +people.rightwards-hand-medium-light-skin-tone +people.rightwards-hand-medium-skin-tone +people.rightwards-hand +people.rightwards-pushing-hand-dark-skin-tone +people.rightwards-pushing-hand-light-skin-tone +people.rightwards-pushing-hand-medium-dark-skin-tone +people.rightwards-pushing-hand-medium-light-skin-tone +people.rightwards-pushing-hand-medium-skin-tone +people.rightwards-pushing-hand +people.santa-claus-dark-skin-tone +people.santa-claus-light-skin-tone +people.santa-claus-medium-dark-skin-tone +people.santa-claus-medium-light-skin-tone +people.santa-claus-medium-skin-tone +people.santa-claus +people.scientist-dark-skin-tone +people.scientist-light-skin-tone +people.scientist-medium-dark-skin-tone +people.scientist-medium-light-skin-tone +people.scientist-medium-skin-tone +people.scientist +people.selfie-dark-skin-tone +people.selfie-light-skin-tone +people.selfie-medium-dark-skin-tone +people.selfie-medium-light-skin-tone +people.selfie-medium-skin-tone +people.selfie +people.sign-of-the-horns-dark-skin-tone +people.sign-of-the-horns-light-skin-tone +people.sign-of-the-horns-medium-dark-skin-tone +people.sign-of-the-horns-medium-light-skin-tone +people.sign-of-the-horns-medium-skin-tone +people.sign-of-the-horns +people.singer-dark-skin-tone +people.singer-light-skin-tone +people.singer-medium-dark-skin-tone +people.singer-medium-light-skin-tone +people.singer-medium-skin-tone +people.singer +people.skier +people.snowboarder-dark-skin-tone +people.snowboarder-light-skin-tone +people.snowboarder-medium-dark-skin-tone +people.snowboarder-medium-light-skin-tone +people.snowboarder-medium-skin-tone +people.snowboarder +people.speaking-head +people.student-dark-skin-tone +people.student-light-skin-tone +people.student-medium-dark-skin-tone +people.student-medium-light-skin-tone +people.student-medium-skin-tone +people.student +people.superhero-dark-skin-tone +people.superhero-light-skin-tone +people.superhero-medium-dark-skin-tone +people.superhero-medium-light-skin-tone +people.superhero-medium-skin-tone +people.superhero +people.supervillain-dark-skin-tone +people.supervillain-light-skin-tone +people.supervillain-medium-dark-skin-tone +people.supervillain-medium-light-skin-tone +people.supervillain-medium-skin-tone +people.supervillain +people.teacher-dark-skin-tone +people.teacher-light-skin-tone +people.teacher-medium-dark-skin-tone +people.teacher-medium-light-skin-tone +people.teacher-medium-skin-tone +people.teacher +people.technologist-dark-skin-tone +people.technologist-light-skin-tone +people.technologist-medium-dark-skin-tone +people.technologist-medium-light-skin-tone +people.technologist-medium-skin-tone +people.technologist +people.thumbs-up-dark-skin-tone +people.thumbs-up-light-skin-tone +people.thumbs-up-medium-dark-skin-tone +people.thumbs-up-medium-light-skin-tone +people.thumbs-up-medium-skin-tone +people.thumbs-up +people.tongue +people.tooth +people.troll +people.vampire-dark-skin-tone +people.vampire-light-skin-tone +people.vampire-medium-dark-skin-tone +people.vampire-medium-light-skin-tone +people.vampire-medium-skin-tone +people.vampire +people.victory-hand-dark-skin-tone +people.victory-hand-light-skin-tone +people.victory-hand-medium-dark-skin-tone +people.victory-hand-medium-light-skin-tone +people.victory-hand-medium-skin-tone +people.victory-hand +people.vulcan-salute-dark-skin-tone +people.vulcan-salute-light-skin-tone +people.vulcan-salute-medium-dark-skin-tone +people.vulcan-salute-medium-light-skin-tone +people.vulcan-salute-medium-skin-tone +people.vulcan-salute +people.waving-hand-dark-skin-tone +people.waving-hand-light-skin-tone +people.waving-hand-medium-dark-skin-tone +people.waving-hand-medium-light-skin-tone +people.waving-hand-medium-skin-tone +people.waving-hand +people.white-hair +people.woman-and-man-holding-hands-dark-skin-tone-light-skin-tone +people.woman-and-man-holding-hands-dark-skin-tone-medium-dark-skin-tone +people.woman-and-man-holding-hands-dark-skin-tone-medium-light-skin-tone +people.woman-and-man-holding-hands-dark-skin-tone-medium-skin-tone +people.woman-and-man-holding-hands-dark-skin-tone +people.woman-and-man-holding-hands-light-skin-tone-dark-skin-tone +people.woman-and-man-holding-hands-light-skin-tone-medium-dark-skin-tone +people.woman-and-man-holding-hands-light-skin-tone-medium-light-skin-tone +people.woman-and-man-holding-hands-light-skin-tone-medium-skin-tone +people.woman-and-man-holding-hands-light-skin-tone +people.woman-and-man-holding-hands-medium-dark-skin-tone-dark-skin-tone +people.woman-and-man-holding-hands-medium-dark-skin-tone-light-skin-tone +people.woman-and-man-holding-hands-medium-dark-skin-tone-medium-light-skin-tone +people.woman-and-man-holding-hands-medium-dark-skin-tone-medium-skin-tone +people.woman-and-man-holding-hands-medium-dark-skin-tone +people.woman-and-man-holding-hands-medium-light-skin-tone-dark-skin-tone +people.woman-and-man-holding-hands-medium-light-skin-tone-light-skin-tone +people.woman-and-man-holding-hands-medium-light-skin-tone-medium-dark-skin-tone +people.woman-and-man-holding-hands-medium-light-skin-tone-medium-skin-tone +people.woman-and-man-holding-hands-medium-light-skin-tone +people.woman-and-man-holding-hands-medium-skin-tone-dark-skin-tone +people.woman-and-man-holding-hands-medium-skin-tone-light-skin-tone +people.woman-and-man-holding-hands-medium-skin-tone-medium-dark-skin-tone +people.woman-and-man-holding-hands-medium-skin-tone-medium-light-skin-tone +people.woman-and-man-holding-hands-medium-skin-tone +people.woman-and-man-holding-hands +people.woman-artist-dark-skin-tone +people.woman-artist-light-skin-tone +people.woman-artist-medium-dark-skin-tone +people.woman-artist-medium-light-skin-tone +people.woman-artist-medium-skin-tone +people.woman-artist +people.woman-astronaut-dark-skin-tone +people.woman-astronaut-light-skin-tone +people.woman-astronaut-medium-dark-skin-tone +people.woman-astronaut-medium-light-skin-tone +people.woman-astronaut-medium-skin-tone +people.woman-astronaut +people.woman-bald +people.woman-beard +people.woman-biking-dark-skin-tone +people.woman-biking-light-skin-tone +people.woman-biking-medium-dark-skin-tone +people.woman-biking-medium-light-skin-tone +people.woman-biking-medium-skin-tone +people.woman-biking +people.woman-blond-hair +people.woman-bouncing-ball-dark-skin-tone +people.woman-bouncing-ball-light-skin-tone +people.woman-bouncing-ball-medium-dark-skin-tone +people.woman-bouncing-ball-medium-light-skin-tone +people.woman-bouncing-ball-medium-skin-tone +people.woman-bouncing-ball +people.woman-bowing-dark-skin-tone +people.woman-bowing-light-skin-tone +people.woman-bowing-medium-dark-skin-tone +people.woman-bowing-medium-light-skin-tone +people.woman-bowing-medium-skin-tone +people.woman-bowing +people.woman-cartwheeling-dark-skin-tone +people.woman-cartwheeling-light-skin-tone +people.woman-cartwheeling-medium-dark-skin-tone +people.woman-cartwheeling-medium-light-skin-tone +people.woman-cartwheeling-medium-skin-tone +people.woman-cartwheeling +people.woman-climbing-dark-skin-tone +people.woman-climbing-light-skin-tone +people.woman-climbing-medium-dark-skin-tone +people.woman-climbing-medium-light-skin-tone +people.woman-climbing-medium-skin-tone +people.woman-climbing +people.woman-construction-worker-dark-skin-tone +people.woman-construction-worker-light-skin-tone +people.woman-construction-worker-medium-dark-skin-tone +people.woman-construction-worker-medium-light-skin-tone +people.woman-construction-worker-medium-skin-tone +people.woman-construction-worker +people.woman-cook-dark-skin-tone +people.woman-cook-light-skin-tone +people.woman-cook-medium-dark-skin-tone +people.woman-cook-medium-light-skin-tone +people.woman-cook-medium-skin-tone +people.woman-cook +people.woman-curly-hair +people.woman-dancing-dark-skin-tone +people.woman-dancing-light-skin-tone +people.woman-dancing-medium-dark-skin-tone +people.woman-dancing-medium-light-skin-tone +people.woman-dancing-medium-skin-tone +people.woman-dancing +people.woman-dark-skin-tone-bald +people.woman-dark-skin-tone-beard +people.woman-dark-skin-tone-blond-hair +people.woman-dark-skin-tone-curly-hair +people.woman-dark-skin-tone-red-hair +people.woman-dark-skin-tone +people.woman-dark-skin-tone-white-hair +people.woman-detective-dark-skin-tone +people.woman-detective-light-skin-tone +people.woman-detective-medium-dark-skin-tone +people.woman-detective-medium-light-skin-tone +people.woman-detective-medium-skin-tone +people.woman-detective +people.woman-elf-dark-skin-tone +people.woman-elf-light-skin-tone +people.woman-elf-medium-dark-skin-tone +people.woman-elf-medium-light-skin-tone +people.woman-elf-medium-skin-tone +people.woman-elf +people.woman-facepalming-dark-skin-tone +people.woman-facepalming-light-skin-tone +people.woman-facepalming-medium-dark-skin-tone +people.woman-facepalming-medium-light-skin-tone +people.woman-facepalming-medium-skin-tone +people.woman-facepalming +people.woman-factory-worker-dark-skin-tone +people.woman-factory-worker-light-skin-tone +people.woman-factory-worker-medium-dark-skin-tone +people.woman-factory-worker-medium-light-skin-tone +people.woman-factory-worker-medium-skin-tone +people.woman-factory-worker +people.woman-fairy-dark-skin-tone +people.woman-fairy-light-skin-tone +people.woman-fairy-medium-dark-skin-tone +people.woman-fairy-medium-light-skin-tone +people.woman-fairy-medium-skin-tone +people.woman-fairy +people.woman-farmer-dark-skin-tone +people.woman-farmer-light-skin-tone +people.woman-farmer-medium-dark-skin-tone +people.woman-farmer-medium-light-skin-tone +people.woman-farmer-medium-skin-tone +people.woman-farmer +people.woman-feeding-baby-dark-skin-tone +people.woman-feeding-baby-light-skin-tone +people.woman-feeding-baby-medium-dark-skin-tone +people.woman-feeding-baby-medium-light-skin-tone +people.woman-feeding-baby-medium-skin-tone +people.woman-feeding-baby +people.woman-firefighter-dark-skin-tone +people.woman-firefighter-light-skin-tone +people.woman-firefighter-medium-dark-skin-tone +people.woman-firefighter-medium-light-skin-tone +people.woman-firefighter-medium-skin-tone +people.woman-firefighter +people.woman-frowning-dark-skin-tone +people.woman-frowning-light-skin-tone +people.woman-frowning-medium-dark-skin-tone +people.woman-frowning-medium-light-skin-tone +people.woman-frowning-medium-skin-tone +people.woman-frowning +people.woman-genie +people.woman-gesturing-no-dark-skin-tone +people.woman-gesturing-no-light-skin-tone +people.woman-gesturing-no-medium-dark-skin-tone +people.woman-gesturing-no-medium-light-skin-tone +people.woman-gesturing-no-medium-skin-tone +people.woman-gesturing-no +people.woman-gesturing-ok-dark-skin-tone +people.woman-gesturing-ok-light-skin-tone +people.woman-gesturing-ok-medium-dark-skin-tone +people.woman-gesturing-ok-medium-light-skin-tone +people.woman-gesturing-ok-medium-skin-tone +people.woman-gesturing-ok +people.woman-getting-haircut-dark-skin-tone +people.woman-getting-haircut-light-skin-tone +people.woman-getting-haircut-medium-dark-skin-tone +people.woman-getting-haircut-medium-light-skin-tone +people.woman-getting-haircut-medium-skin-tone +people.woman-getting-haircut +people.woman-getting-massage-dark-skin-tone +people.woman-getting-massage-light-skin-tone +people.woman-getting-massage-medium-dark-skin-tone +people.woman-getting-massage-medium-light-skin-tone +people.woman-getting-massage-medium-skin-tone +people.woman-getting-massage +people.woman-golfing-dark-skin-tone +people.woman-golfing-light-skin-tone +people.woman-golfing-medium-dark-skin-tone +people.woman-golfing-medium-light-skin-tone +people.woman-golfing-medium-skin-tone +people.woman-golfing +people.woman-guard-dark-skin-tone +people.woman-guard-light-skin-tone +people.woman-guard-medium-dark-skin-tone +people.woman-guard-medium-light-skin-tone +people.woman-guard-medium-skin-tone +people.woman-guard +people.woman-health-worker-dark-skin-tone +people.woman-health-worker-light-skin-tone +people.woman-health-worker-medium-dark-skin-tone +people.woman-health-worker-medium-light-skin-tone +people.woman-health-worker-medium-skin-tone +people.woman-health-worker +people.woman-in-lotus-position-dark-skin-tone +people.woman-in-lotus-position-light-skin-tone +people.woman-in-lotus-position-medium-dark-skin-tone +people.woman-in-lotus-position-medium-light-skin-tone +people.woman-in-lotus-position-medium-skin-tone +people.woman-in-lotus-position +people.woman-in-manual-wheelchair-dark-skin-tone +people.woman-in-manual-wheelchair-facing-right-dark-skin-tone +people.woman-in-manual-wheelchair-facing-right-light-skin-tone +people.woman-in-manual-wheelchair-facing-right-medium-dark-skin-tone +people.woman-in-manual-wheelchair-facing-right-medium-light-skin-tone +people.woman-in-manual-wheelchair-facing-right-medium-skin-tone +people.woman-in-manual-wheelchair-facing-right +people.woman-in-manual-wheelchair-light-skin-tone +people.woman-in-manual-wheelchair-medium-dark-skin-tone +people.woman-in-manual-wheelchair-medium-light-skin-tone +people.woman-in-manual-wheelchair-medium-skin-tone +people.woman-in-manual-wheelchair +people.woman-in-motorized-wheelchair-dark-skin-tone +people.woman-in-motorized-wheelchair-facing-right-dark-skin-tone +people.woman-in-motorized-wheelchair-facing-right-light-skin-tone +people.woman-in-motorized-wheelchair-facing-right-medium-dark-skin-tone +people.woman-in-motorized-wheelchair-facing-right-medium-light-skin-tone +people.woman-in-motorized-wheelchair-facing-right-medium-skin-tone +people.woman-in-motorized-wheelchair-facing-right +people.woman-in-motorized-wheelchair-light-skin-tone +people.woman-in-motorized-wheelchair-medium-dark-skin-tone +people.woman-in-motorized-wheelchair-medium-light-skin-tone +people.woman-in-motorized-wheelchair-medium-skin-tone +people.woman-in-motorized-wheelchair +people.woman-in-steamy-room-dark-skin-tone +people.woman-in-steamy-room-light-skin-tone +people.woman-in-steamy-room-medium-dark-skin-tone +people.woman-in-steamy-room-medium-light-skin-tone +people.woman-in-steamy-room-medium-skin-tone +people.woman-in-steamy-room +people.woman-in-tuxedo-dark-skin-tone +people.woman-in-tuxedo-light-skin-tone +people.woman-in-tuxedo-medium-dark-skin-tone +people.woman-in-tuxedo-medium-light-skin-tone +people.woman-in-tuxedo-medium-skin-tone +people.woman-in-tuxedo +people.woman-judge-dark-skin-tone +people.woman-judge-light-skin-tone +people.woman-judge-medium-dark-skin-tone +people.woman-judge-medium-light-skin-tone +people.woman-judge-medium-skin-tone +people.woman-judge +people.woman-juggling-dark-skin-tone +people.woman-juggling-light-skin-tone +people.woman-juggling-medium-dark-skin-tone +people.woman-juggling-medium-light-skin-tone +people.woman-juggling-medium-skin-tone +people.woman-juggling +people.woman-kneeling-dark-skin-tone +people.woman-kneeling-facing-right-dark-skin-tone +people.woman-kneeling-facing-right-light-skin-tone +people.woman-kneeling-facing-right-medium-dark-skin-tone +people.woman-kneeling-facing-right-medium-light-skin-tone +people.woman-kneeling-facing-right-medium-skin-tone +people.woman-kneeling-facing-right +people.woman-kneeling-light-skin-tone +people.woman-kneeling-medium-dark-skin-tone +people.woman-kneeling-medium-light-skin-tone +people.woman-kneeling-medium-skin-tone +people.woman-kneeling +people.woman-lifting-weights-dark-skin-tone +people.woman-lifting-weights-light-skin-tone +people.woman-lifting-weights-medium-dark-skin-tone +people.woman-lifting-weights-medium-light-skin-tone +people.woman-lifting-weights-medium-skin-tone +people.woman-lifting-weights +people.woman-light-skin-tone-bald +people.woman-light-skin-tone-beard +people.woman-light-skin-tone-blond-hair +people.woman-light-skin-tone-curly-hair +people.woman-light-skin-tone-red-hair +people.woman-light-skin-tone +people.woman-light-skin-tone-white-hair +people.woman-mage-dark-skin-tone +people.woman-mage-light-skin-tone +people.woman-mage-medium-dark-skin-tone +people.woman-mage-medium-light-skin-tone +people.woman-mage-medium-skin-tone +people.woman-mage +people.woman-mechanic-dark-skin-tone +people.woman-mechanic-light-skin-tone +people.woman-mechanic-medium-dark-skin-tone +people.woman-mechanic-medium-light-skin-tone +people.woman-mechanic-medium-skin-tone +people.woman-mechanic +people.woman-medium-dark-skin-tone-bald +people.woman-medium-dark-skin-tone-beard +people.woman-medium-dark-skin-tone-blond-hair +people.woman-medium-dark-skin-tone-curly-hair +people.woman-medium-dark-skin-tone-red-hair +people.woman-medium-dark-skin-tone +people.woman-medium-dark-skin-tone-white-hair +people.woman-medium-light-skin-tone-bald +people.woman-medium-light-skin-tone-beard +people.woman-medium-light-skin-tone-blond-hair +people.woman-medium-light-skin-tone-curly-hair +people.woman-medium-light-skin-tone-red-hair +people.woman-medium-light-skin-tone +people.woman-medium-light-skin-tone-white-hair +people.woman-medium-skin-tone-bald +people.woman-medium-skin-tone-beard +people.woman-medium-skin-tone-blond-hair +people.woman-medium-skin-tone-curly-hair +people.woman-medium-skin-tone-red-hair +people.woman-medium-skin-tone +people.woman-medium-skin-tone-white-hair +people.woman-mountain-biking-dark-skin-tone +people.woman-mountain-biking-light-skin-tone +people.woman-mountain-biking-medium-dark-skin-tone +people.woman-mountain-biking-medium-light-skin-tone +people.woman-mountain-biking-medium-skin-tone +people.woman-mountain-biking +people.woman-office-worker-dark-skin-tone +people.woman-office-worker-light-skin-tone +people.woman-office-worker-medium-dark-skin-tone +people.woman-office-worker-medium-light-skin-tone +people.woman-office-worker-medium-skin-tone +people.woman-office-worker +people.woman-pilot-dark-skin-tone +people.woman-pilot-light-skin-tone +people.woman-pilot-medium-dark-skin-tone +people.woman-pilot-medium-light-skin-tone +people.woman-pilot-medium-skin-tone +people.woman-pilot +people.woman-playing-handball-dark-skin-tone +people.woman-playing-handball-light-skin-tone +people.woman-playing-handball-medium-dark-skin-tone +people.woman-playing-handball-medium-light-skin-tone +people.woman-playing-handball-medium-skin-tone +people.woman-playing-handball +people.woman-playing-water-polo-dark-skin-tone +people.woman-playing-water-polo-light-skin-tone +people.woman-playing-water-polo-medium-dark-skin-tone +people.woman-playing-water-polo-medium-light-skin-tone +people.woman-playing-water-polo-medium-skin-tone +people.woman-playing-water-polo +people.woman-police-officer-dark-skin-tone +people.woman-police-officer-light-skin-tone +people.woman-police-officer-medium-dark-skin-tone +people.woman-police-officer-medium-light-skin-tone +people.woman-police-officer-medium-skin-tone +people.woman-police-officer +people.woman-pouting-dark-skin-tone +people.woman-pouting-light-skin-tone +people.woman-pouting-medium-dark-skin-tone +people.woman-pouting-medium-light-skin-tone +people.woman-pouting-medium-skin-tone +people.woman-pouting +people.woman-raising-hand-dark-skin-tone +people.woman-raising-hand-light-skin-tone +people.woman-raising-hand-medium-dark-skin-tone +people.woman-raising-hand-medium-light-skin-tone +people.woman-raising-hand-medium-skin-tone +people.woman-raising-hand +people.woman-red-hair +people.woman-rowing-boat-dark-skin-tone +people.woman-rowing-boat-light-skin-tone +people.woman-rowing-boat-medium-dark-skin-tone +people.woman-rowing-boat-medium-light-skin-tone +people.woman-rowing-boat-medium-skin-tone +people.woman-rowing-boat +people.woman-running-dark-skin-tone +people.woman-running-facing-right-dark-skin-tone +people.woman-running-facing-right-light-skin-tone +people.woman-running-facing-right-medium-dark-skin-tone +people.woman-running-facing-right-medium-light-skin-tone +people.woman-running-facing-right-medium-skin-tone +people.woman-running-facing-right +people.woman-running-light-skin-tone +people.woman-running-medium-dark-skin-tone +people.woman-running-medium-light-skin-tone +people.woman-running-medium-skin-tone +people.woman-running +people.woman-scientist-dark-skin-tone +people.woman-scientist-light-skin-tone +people.woman-scientist-medium-dark-skin-tone +people.woman-scientist-medium-light-skin-tone +people.woman-scientist-medium-skin-tone +people.woman-scientist +people.woman-shrugging-dark-skin-tone +people.woman-shrugging-light-skin-tone +people.woman-shrugging-medium-dark-skin-tone +people.woman-shrugging-medium-light-skin-tone +people.woman-shrugging-medium-skin-tone +people.woman-shrugging +people.woman-singer-dark-skin-tone +people.woman-singer-light-skin-tone +people.woman-singer-medium-dark-skin-tone +people.woman-singer-medium-light-skin-tone +people.woman-singer-medium-skin-tone +people.woman-singer +people.woman-standing-dark-skin-tone +people.woman-standing-light-skin-tone +people.woman-standing-medium-dark-skin-tone +people.woman-standing-medium-light-skin-tone +people.woman-standing-medium-skin-tone +people.woman-standing +people.woman-student-dark-skin-tone +people.woman-student-light-skin-tone +people.woman-student-medium-dark-skin-tone +people.woman-student-medium-light-skin-tone +people.woman-student-medium-skin-tone +people.woman-student +people.woman-superhero-dark-skin-tone +people.woman-superhero-light-skin-tone +people.woman-superhero-medium-dark-skin-tone +people.woman-superhero-medium-light-skin-tone +people.woman-superhero-medium-skin-tone +people.woman-superhero +people.woman-supervillain-dark-skin-tone +people.woman-supervillain-light-skin-tone +people.woman-supervillain-medium-dark-skin-tone +people.woman-supervillain-medium-light-skin-tone +people.woman-supervillain-medium-skin-tone +people.woman-supervillain +people.woman-surfing-dark-skin-tone +people.woman-surfing-light-skin-tone +people.woman-surfing-medium-dark-skin-tone +people.woman-surfing-medium-light-skin-tone +people.woman-surfing-medium-skin-tone +people.woman-surfing +people.woman-swimming-dark-skin-tone +people.woman-swimming-light-skin-tone +people.woman-swimming-medium-dark-skin-tone +people.woman-swimming-medium-light-skin-tone +people.woman-swimming-medium-skin-tone +people.woman-swimming +people.woman-teacher-dark-skin-tone +people.woman-teacher-light-skin-tone +people.woman-teacher-medium-dark-skin-tone +people.woman-teacher-medium-light-skin-tone +people.woman-teacher-medium-skin-tone +people.woman-teacher +people.woman-technologist-dark-skin-tone +people.woman-technologist-light-skin-tone +people.woman-technologist-medium-dark-skin-tone +people.woman-technologist-medium-light-skin-tone +people.woman-technologist-medium-skin-tone +people.woman-technologist +people.woman-tipping-hand-dark-skin-tone +people.woman-tipping-hand-light-skin-tone +people.woman-tipping-hand-medium-dark-skin-tone +people.woman-tipping-hand-medium-light-skin-tone +people.woman-tipping-hand-medium-skin-tone +people.woman-tipping-hand +people.woman-vampire-dark-skin-tone +people.woman-vampire-light-skin-tone +people.woman-vampire-medium-dark-skin-tone +people.woman-vampire-medium-light-skin-tone +people.woman-vampire-medium-skin-tone +people.woman-vampire +people.woman-walking-dark-skin-tone +people.woman-walking-facing-right-dark-skin-tone +people.woman-walking-facing-right-light-skin-tone +people.woman-walking-facing-right-medium-dark-skin-tone +people.woman-walking-facing-right-medium-light-skin-tone +people.woman-walking-facing-right-medium-skin-tone +people.woman-walking-facing-right +people.woman-walking-light-skin-tone +people.woman-walking-medium-dark-skin-tone +people.woman-walking-medium-light-skin-tone +people.woman-walking-medium-skin-tone +people.woman-walking +people.woman-wearing-turban-dark-skin-tone +people.woman-wearing-turban-light-skin-tone +people.woman-wearing-turban-medium-dark-skin-tone +people.woman-wearing-turban-medium-light-skin-tone +people.woman-wearing-turban-medium-skin-tone +people.woman-wearing-turban +people.woman +people.woman-white-hair +people.woman-with-veil-dark-skin-tone +people.woman-with-veil-light-skin-tone +people.woman-with-veil-medium-dark-skin-tone +people.woman-with-veil-medium-light-skin-tone +people.woman-with-veil-medium-skin-tone +people.woman-with-veil +people.woman-with-white-cane-dark-skin-tone +people.woman-with-white-cane-facing-right-dark-skin-tone +people.woman-with-white-cane-facing-right-light-skin-tone +people.woman-with-white-cane-facing-right-medium-dark-skin-tone +people.woman-with-white-cane-facing-right-medium-light-skin-tone +people.woman-with-white-cane-facing-right-medium-skin-tone +people.woman-with-white-cane-facing-right +people.woman-with-white-cane-light-skin-tone +people.woman-with-white-cane-medium-dark-skin-tone +people.woman-with-white-cane-medium-light-skin-tone +people.woman-with-white-cane-medium-skin-tone +people.woman-with-white-cane +people.woman-zombie +people.women-holding-hands-dark-skin-tone-light-skin-tone +people.women-holding-hands-dark-skin-tone-medium-dark-skin-tone +people.women-holding-hands-dark-skin-tone-medium-light-skin-tone +people.women-holding-hands-dark-skin-tone-medium-skin-tone +people.women-holding-hands-dark-skin-tone +people.women-holding-hands-light-skin-tone-dark-skin-tone +people.women-holding-hands-light-skin-tone-medium-dark-skin-tone +people.women-holding-hands-light-skin-tone-medium-light-skin-tone +people.women-holding-hands-light-skin-tone-medium-skin-tone +people.women-holding-hands-light-skin-tone +people.women-holding-hands-medium-dark-skin-tone-dark-skin-tone +people.women-holding-hands-medium-dark-skin-tone-light-skin-tone +people.women-holding-hands-medium-dark-skin-tone-medium-light-skin-tone +people.women-holding-hands-medium-dark-skin-tone-medium-skin-tone +people.women-holding-hands-medium-dark-skin-tone +people.women-holding-hands-medium-light-skin-tone-dark-skin-tone +people.women-holding-hands-medium-light-skin-tone-light-skin-tone +people.women-holding-hands-medium-light-skin-tone-medium-dark-skin-tone +people.women-holding-hands-medium-light-skin-tone-medium-skin-tone +people.women-holding-hands-medium-light-skin-tone +people.women-holding-hands-medium-skin-tone-dark-skin-tone +people.women-holding-hands-medium-skin-tone-light-skin-tone +people.women-holding-hands-medium-skin-tone-medium-dark-skin-tone +people.women-holding-hands-medium-skin-tone-medium-light-skin-tone +people.women-holding-hands-medium-skin-tone +people.women-holding-hands +people.women-with-bunny-ears +people.women-wrestling +people.writing-hand-dark-skin-tone +people.writing-hand-light-skin-tone +people.writing-hand-medium-dark-skin-tone +people.writing-hand-medium-light-skin-tone +people.writing-hand-medium-skin-tone +people.writing-hand +people.zombie +smileys.alien-monster +smileys.alien +smileys.angry-face +smileys.angry-face-with-horns +smileys.anguished-face +smileys.anxious-face-with-sweat +smileys.astonished-face +smileys.beaming-face-with-smiling-eyes +smileys.cat-with-tears-of-joy +smileys.cat-with-wry-smile +smileys.clown-face +smileys.cold-face +smileys.confounded-face +smileys.confused-face +smileys.cowboy-hat-face +smileys.crying-cat +smileys.crying-face +smileys.disappointed-face +smileys.disguised-face +smileys.dizzy-face +smileys.dotted-line-face +smileys.downcast-face-with-sweat +smileys.drooling-face +smileys.exploding-head +smileys.expressionless-face +smileys.face-blowing-a-kiss +smileys.face-exhaling +smileys.face-holding-back-tears +smileys.face-in-clouds +smileys.face-savoring-food +smileys.face-screaming-in-fear +smileys.face-with-diagonal-mouth +smileys.face-with-hand-over-mouth +smileys.face-with-head-bandage +smileys.face-with-medical-mask +smileys.face-with-monocle +smileys.face-with-open-eyes-and-hand-over-mouth +smileys.face-with-open-mouth +smileys.face-without-mouth +smileys.face-with-peeking-eye +smileys.face-with-raised-eyebrow +smileys.face-with-rolling-eyes +smileys.face-with-spiral-eyes +smileys.face-with-steam-from-nose +smileys.face-with-symbols-on-mouth +smileys.face-with-tears-of-joy +smileys.face-with-thermometer +smileys.face-with-tongue +smileys.fearful-face +smileys.flushed-face +smileys.frowning-face +smileys.frowning-face-with-open-mouth +smileys.ghost +smileys.goblin +smileys.grimacing-face +smileys.grinning-cat +smileys.grinning-cat-with-smiling-eyes +smileys.grinning-face +smileys.grinning-face-with-big-eyes +smileys.grinning-face-with-smiling-eyes +smileys.grinning-face-with-sweat +smileys.grinning-squinting-face +smileys.head-shaking-horizontally +smileys.head-shaking-vertically +smileys.hear-no-evil-monkey +smileys.hot-face +smileys.hugging-face +smileys.hushed-face +smileys.kissing-cat +smileys.kissing-face +smileys.kissing-face-with-closed-eyes +smileys.kissing-face-with-smiling-eyes +smileys.loudly-crying-face +smileys.lying-face +smileys.melting-face +smileys.money-mouth-face +smileys.nauseated-face +smileys.nerd-face +smileys.neutral-face +smileys.ogre +smileys.partying-face +smileys.pensive-face +smileys.persevering-face +smileys.pleading-face +smileys.pouting-cat +smileys.pouting-face +smileys.relieved-face +smileys.robot +smileys.rolling-on-the-floor-laughing +smileys.sad-but-relieved-face +smileys.saluting-face +smileys.see-no-evil-monkey +smileys.shaking-face +smileys.shushing-face +smileys.skull-and-crossbones +smileys.skull +smileys.sleeping-face +smileys.sleepy-face +smileys.slightly-frowning-face +smileys.slightly-smiling-face +smileys.smiling-cat-with-heart-eyes +smileys.smiling-face +smileys.smiling-face-with-halo +smileys.smiling-face-with-heart-eyes +smileys.smiling-face-with-hearts +smileys.smiling-face-with-horns +smileys.smiling-face-with-smiling-eyes +smileys.smiling-face-with-sunglasses +smileys.smiling-face-with-tear +smileys.smirking-face +smileys.sneezing-face +smileys.speak-no-evil-monkey +smileys.squinting-face-with-tongue +smileys.star-struck +smileys.thinking-face +smileys.tired-face +smileys.unamused-face +smileys.upside-down-face +smileys.weary-cat +smileys.weary-face +smileys.winking-face +smileys.winking-face-with-tongue +smileys.woozy-face +smileys.worried-face +smileys.yawning-face +smileys.zany-face +smileys.zipper-mouth-face +symbols.ab-button-blood-type +symbols.a-button-blood-type +symbols.anger-symbol +symbols.antenna-bars +symbols.aquarius +symbols.aries +symbols.asterisk +symbols.atm-sign +symbols.atom-symbol +symbols.baby-symbol +symbols.back-arrow +symbols.baggage-claim +symbols.b-button-blood-type +symbols.beating-heart +symbols.black-circle +symbols.black-flag +symbols.black-heart +symbols.black-large-square +symbols.black-medium-small-square +symbols.black-square-button +symbols.blue-heart +symbols.bright-button +symbols.broken-heart +symbols.brown-heart +symbols.cancer +symbols.capricorn +symbols.chequered-flag +symbols.children-crossing +symbols.cinema +symbols.circled-m +symbols.cl-button +symbols.clockwise-vertical-arrows +symbols.collision +symbols.combining-enclosing-keycap +symbols.cool-button +symbols.copyright +symbols.counterclockwise-arrows-button +symbols.crossed-flags +symbols.cross-mark-button +symbols.cross-mark +symbols.curly-loop +symbols.currency-exchange +symbols.customs +symbols.dashing-away +symbols.diamond-with-a-dot +symbols.digit-eight +symbols.digit-five +symbols.digit-four +symbols.digit-nine +symbols.digit-one +symbols.digit-seven +symbols.digit-six +symbols.digit-three +symbols.digit-two +symbols.digit-zero +symbols.dim-button +symbols.divide +symbols.dizzy +symbols.double-curly-loop +symbols.double-exclamation-mark +symbols.down-arrow +symbols.down-left-arrow +symbols.down-right-arrow +symbols.downwards-button +symbols.eight-pointed-star +symbols.eight-spoked-asterisk +symbols.eject-button +symbols.elevator +symbols.end-arrow +symbols.esperanto-flag +symbols.esperanto +symbols.exclamation-mark +symbols.exclamation-question-mark +symbols.extinction +symbols.eye-in-speech-bubble +symbols.fast-down-button +symbols.fast-forward-button +symbols.fast-reverse-button +symbols.fast-up-button +symbols.female-sign +symbols.fleur-de-lis +symbols.free-button +symbols.gemini +symbols.gnu-logo +symbols.green-heart +symbols.grey-heart +symbols.growing-heart +symbols.heart-decoration +symbols.heart-exclamation +symbols.heart-on-fire +symbols.heart-with-arrow +symbols.heart-with-ribbon +symbols.heavy-dollar-sign +symbols.heavy-equals-sign +symbols.hole +symbols.hollow-red-circle +symbols.hundred-points +symbols.id-button +symbols.infinity +symbols.information +symbols.input-latin-letters +symbols.input-latin-lowercase +symbols.input-latin-uppercase +symbols.input-numbers +symbols.input-symbols +symbols.japanese-acceptable-button +symbols.japanese-application-button +symbols.japanese-bargain-button +symbols.japanese-congratulations-button +symbols.japanese-discount-button +symbols.japanese-free-of-charge-button +symbols.japanese-here-button +symbols.japanese-monthly-amount-button +symbols.japanese-not-free-of-charge-button +symbols.japanese-no-vacancy-button +symbols.japanese-open-for-business-button +symbols.japanese-passing-grade-button +symbols.japanese-prohibited-button +symbols.japanese-reserved-button +symbols.japanese-secret-button +symbols.japanese-service-charge-button +symbols.japanese-symbol-for-beginner +symbols.japanese-vacancy-button +symbols.keycap-10 +symbols.keycap-asterisk +symbols.keycap-digit-eight +symbols.keycap-digit-five +symbols.keycap-digit-four +symbols.keycap-digit-nine +symbols.keycap-digit-one +symbols.keycap-digit-seven +symbols.keycap-digit-six +symbols.keycap-digit-three +symbols.keycap-digit-two +symbols.keycap-digit-zero +symbols.keycap-number-sign +symbols.kiss-mark +symbols.large-blue-circle +symbols.large-blue-diamond +symbols.large-blue-square +symbols.large-brown-circle +symbols.large-brown-square +symbols.large-green-circle +symbols.large-green-square +symbols.large-orange-circle +symbols.large-orange-diamond +symbols.large-orange-square +symbols.large-purple-circle +symbols.large-purple-square +symbols.large-red-circle +symbols.large-red-square +symbols.large-yellow-circle +symbols.large-yellow-square +symbols.last-track-button +symbols.left-arrow-curving-right +symbols.left-arrow +symbols.left-luggage +symbols.left-right-arrow +symbols.left-speech-bubble +symbols.leo +symbols.libra +symbols.lichess-4545-flag +symbols.light-blue-heart +symbols.linux-tux-penguin +symbols.litter-in-bin-sign +symbols.love-letter +symbols.male-sign +symbols.medical-symbol +symbols.mending-heart +symbols.mens-room +symbols.minus +symbols.mobile-phone-off +symbols.move-blunder +symbols.move-brilliant +symbols.move-dubious +symbols.move-good +symbols.move-interesting +symbols.move-mistake +symbols.multiply +symbols.name-badge +symbols.new-button +symbols.next-track-button +symbols.ng-button +symbols.no-bicycles +symbols.no-entry +symbols.no-littering +symbols.no-mobile-phones +symbols.non-potable-water +symbols.no-one-under-eighteen +symbols.no-pedestrians +symbols.no-smoking +symbols.number-sign +symbols.o-button-blood-type +symbols.ok-button +symbols.on-arrow +symbols.ophiuchus +symbols.orange-heart +symbols.part-alternation-mark +symbols.passport-control +symbols.pause-button +symbols.p-button +symbols.peace-symbol +symbols.pink-heart +symbols.pirate-flag +symbols.pisces +symbols.play-button +symbols.play-or-pause-button +symbols.plus +symbols.potable-water +symbols.purple-heart +symbols.puzzle-racer +symbols.puzzle-storm +symbols.puzzle-streak +symbols.question-mark +symbols.radio-button +symbols.rainbow-flag +symbols.record-button +symbols.recycling-symbol +symbols.red-heart +symbols.red-triangle-pointed-down +symbols.red-triangle-pointed-up +symbols.registered +symbols.repeat-button +symbols.repeat-single-button +symbols.restroom +symbols.reverse-button +symbols.revolving-hearts +symbols.right-anger-bubble +symbols.right-arrow-curving-down +symbols.right-arrow-curving-left +symbols.right-arrow-curving-up +symbols.right-arrow +symbols.sagittarius +symbols.scorpio +symbols.shuffle-tracks-button +symbols.small-blue-diamond +symbols.small-orange-diamond +symbols.soon-arrow +symbols.sos-button +symbols.sparkle +symbols.sparkling-heart +symbols.speech-balloon +symbols.stop-button +symbols.taurus +symbols.thought-balloon +symbols.top-arrow +symbols.trade-mark +symbols.transgender-flag +symbols.transgender-symbol +symbols.triangular-flag +symbols.trident-emblem +symbols.two-hearts +symbols.up-arrow +symbols.up-button +symbols.up-down-arrow +symbols.up-left-arrow +symbols.up-right-arrow +symbols.upwards-button +symbols.vibration-mode +symbols.vim-logo +symbols.virgo +symbols.vs-button +symbols.water-closet +symbols.wavy-dash +symbols.wheelchair-symbol +symbols.white-circle +symbols.white-exclamation-mark +symbols.white-flag +symbols.white-heart +symbols.white-large-square +symbols.white-medium-small-square +symbols.white-question-mark +symbols.white-square-button +symbols.white-star +symbols.wireless +symbols.womens-room +symbols.yellow-heart +symbols.zzz +travel-places.aerial-tramway +travel-places.airplane-arrival +travel-places.airplane-departure +travel-places.airplane +travel-places.ambulance +travel-places.anchor +travel-places.articulated-lorry +travel-places.automobile +travel-places.auto-rickshaw +travel-places.bank +travel-places.barber-pole +travel-places.beach-with-umbrella +travel-places.bicycle +travel-places.brick +travel-places.bridge-at-night +travel-places.building-construction +travel-places.bullet-train +travel-places.bus-stop +travel-places.bus +travel-places.camping +travel-places.canoe +travel-places.carousel-horse +travel-places.castle +travel-places.church +travel-places.circus-tent +travel-places.cityscape-at-dusk +travel-places.cityscape +travel-places.classical-building +travel-places.compass +travel-places.construction +travel-places.convenience-store +travel-places.delivery-truck +travel-places.department-store +travel-places.derelict-house +travel-places.desert-island +travel-places.desert +travel-places.earth-blue +travel-places.factory +travel-places.ferris-wheel +travel-places.ferry +travel-places.fire-engine +travel-places.flying-saucer +travel-places.foggy +travel-places.fountain +travel-places.fuel-pump +travel-places.globe-showing-americas +travel-places.globe-showing-asia-australia +travel-places.globe-showing-europe-africa +travel-places.globe-with-meridians +travel-places.helicopter +travel-places.high-speed-train +travel-places.hindu-temple +travel-places.horizontal-traffic-light +travel-places.hospital +travel-places.hotel +travel-places.hot-springs +travel-places.houses +travel-places.house +travel-places.house-with-garden +travel-places.hut +travel-places.japanese-castle +travel-places.japanese-post-office +travel-places.kaaba +travel-places.kick-scooter +travel-places.light-rail +travel-places.locomotive +travel-places.love-hotel +travel-places.manual-wheelchair +travel-places.map-of-japan +travel-places.metro +travel-places.minibus +travel-places.moai +travel-places.monorail +travel-places.mosque +travel-places.motor-boat +travel-places.motorcycle +travel-places.motorized-wheelchair +travel-places.motor-scooter +travel-places.motorway +travel-places.mountain-cableway +travel-places.mountain-railway +travel-places.mountain +travel-places.mount-fuji +travel-places.national-park +travel-places.night-with-stars +travel-places.office-building +travel-places.oil-drum +travel-places.oncoming-automobile +travel-places.oncoming-bus +travel-places.oncoming-police-car +travel-places.oncoming-taxi +travel-places.parachute +travel-places.passenger-ship +travel-places.pickup-truck +travel-places.playground-slide +travel-places.police-car-light +travel-places.police-car +travel-places.post-office +travel-places.racing-car +travel-places.railway-car +travel-places.railway-track +travel-places.ring-buoy +travel-places.rocket +travel-places.roller-coaster +travel-places.roller-skate +travel-places.sailboat +travel-places.satellite +travel-places.school +travel-places.seat +travel-places.shinto-shrine +travel-places.ship +travel-places.skateboard +travel-places.small-airplane +travel-places.snow-capped-mountain +travel-places.speedboat +travel-places.sport-utility-vehicle +travel-places.stadium +travel-places.station +travel-places.statue-of-liberty +travel-places.stop-sign +travel-places.sunrise-over-mountains +travel-places.sunrise +travel-places.sunset +travel-places.suspension-railway +travel-places.synagogue +travel-places.taxi +travel-places.tent +travel-places.tokyo-tower +travel-places.tractor +travel-places.train +travel-places.tram-car +travel-places.tram +travel-places.trolleybus +travel-places.vertical-traffic-light +travel-places.volcano +travel-places.wedding +travel-places.wheel +travel-places.wooden-ship +travel-places.world-map diff --git a/public/images/board/newspaper.png b/public/images/board/newspaper.png deleted file mode 100644 index 8385a740f45bc5343d314657fc98caa58ddab9f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18359 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 IIsV)K0mv^&6951J diff --git a/public/images/board/newspaper.thumbnail.png b/public/images/board/newspaper.thumbnail.png deleted file mode 100644 index c18709956e16242ce2be96aca985eb74faeca43d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2744 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 yp^qC(Pk0`OUSupqr0PVbZ}3- + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/ES-AR.png b/public/images/flags/ES-AR.png new file mode 100644 index 0000000000000000000000000000000000000000..df058a93ade76f6cfb67288c04900eb697d83a4b GIT binary patch literal 799 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 F{{VPo`%2$s पर %1$s स्थितियों का अभ्यास किया - %s सामरिक पहेली को हल किया - %s सामरिक पहेलियाँ को हल किया + %s प्रशिक्षण पहेली को हल किया + %s प्रशिक्षण पहेलियों को हल किया %1$s %2$s का खेल खेला @@ -55,8 +55,8 @@ %s नए अध्ययनों का निर्माण किया - %s टूर्नामेंट में भाग लिया - %s टूर्नामेंट में भाग लिया + %s arena टूर्नामेंट में भाग लिया + %s arena टूर्नामेटों में भाग लिया %4$s में %3$s खेल के साथ %1$s (टॉप %2$s%%) रैंक किया गया @@ -67,7 +67,7 @@ %s स्विस टुर्नामेंटों में भाग लिया #%1$s स्थान %2$s मे - lichess.org पर साइन किया गया + lichess.org पर साइन अप किया गया %s टीम में शामिल हुए %s टीमों में शामिल हुए diff --git a/translation/dest/activity/ta-IN.xml b/translation/dest/activity/ta-IN.xml index aaf016c829e83..e6b810361e7e1 100644 --- a/translation/dest/activity/ta-IN.xml +++ b/translation/dest/activity/ta-IN.xml @@ -2,4 +2,38 @@ செயல்பாடு இணைய நேரலை + + %1$s மாதத்திற்கு %2$s ஆக lichess.org ஆதரிக்கப்படுகிறது + %1$s மாதங்களுக்கு %2$s ஆக lichess.org ஆதரிக்கப்படுகிறது + + + %2$s இல் %1$s நிலையைப் பயிற்சி செய்தேன் + %2$s இல் %1$s பதவிகளைப் பயிற்சி செய்தேன் + + + %s பயிற்சி புதிர் தீர்க்கப்பட்டது + %s பயிற்சி புதிர்கள் தீர்க்கப்பட்டன + + + %1$s %2$s ஆட்டம் விளையாடினார் + %1$s %2$s ஆட்டங்களை விளையாடினார் + + + %2$s இல் %1$s செய்தி வெளியிடப்பட்டது + %2$s இல் %1$s செய்திகள் வெளியிடப்பட்டன + + + %1$s நகர்த்தப்பட்டது + %1$s நகர்வுகளை விளையாடியது + + + %s சுவிஸ் போட்டியில் போட்டியிட்டார் + %s சுவிஸ் போட்டிகளில் போட்டியிட்டார் + + %2$s இல் #%1$s தரவரிசை + Lichess.org இல் பதிவு செய்துள்ளார் + + %s குழுவில் சேர்ந்தார் + %s குழுக்களில் சேர்ந்தார் + diff --git a/translation/dest/arena/an-ES.xml b/translation/dest/arena/an-ES.xml index b3663136f92ae..aeae3b2106f4c 100644 --- a/translation/dest/arena/an-ES.xml +++ b/translation/dest/arena/an-ES.xml @@ -59,4 +59,5 @@ Chuga rapido y torna a lo recibidor pa chugar mas partidas y ganar mas puntos.Sin rachas de torneyo Rendimiento meyo Puntuación meya + Los míos torneyos diff --git a/translation/dest/arena/ar-SA.xml b/translation/dest/arena/ar-SA.xml index 12e0a36e7d1fa..8cc2b9d4fb42e 100644 --- a/translation/dest/arena/ar-SA.xml +++ b/translation/dest/arena/ar-SA.xml @@ -2,11 +2,11 @@ مسابقات الساحة هل هي مقيمة؟ - سيتم إعلامك عند بدء البطولة، لذا فإنه من الآمن اللعب في علامة تبويب أخرى أثناء الانتظار. + سيتم إعلامك عند بدء البطولة، لذا يمكنك اللعب في علامة تبويب أخرى أثناء الانتظار. هذه البطولة مقيمة وسوف تؤثر على تقييمك. - هذه البطولة *ليست* مقيمة *ولن* تؤثر على تقييمك. - بعض البطولات تكون مقيمة وسوف تؤثر على تقييمك. - كيف يتم احتساب النقاط؟ + هذه البطولة ليست مقيمة ولن تؤثر على تقييمك. + بعض البطولات تكون مقيمة وتؤثر على تقييمك. + كيف تحتسب النقاط؟ الفوز نتيجته الأساسية  2 نقطة، التعادل: 1 نقطة، والخسارة 0 نقطة. إذا ربحت مباراتين على التوالي سوف تبدأ مرحلة مضاعفة النقاط، ويمثلها رمز الشعلة. وسوف تستمر المباريات التالية مضاعفة النقاط حتى تفشل في الفوز في مباراة. @@ -14,12 +14,13 @@ كمثال، انتصاران يليهما تعادل سيساوي 6 نقاط: 2 + 2 + (2 × 1) مخاطرة الساحة عندما يضغط اللاعب زر المخاطرة في بداية المباراة سيفقد اللاعب نصف وقته لكنه في حال الفوز يحصل على نقطة إضافية. -المخاطرة في حالة الوقت المتزايد يلغي الزيادة مع كل نقلة (يستثنى من ذلك نمط دقيقة +2 ث زيادة/نقلة ، ستلغى الزيادة لكن الوقت سيكون دقيقة فقط) +المخاطرة في حالة الوقت المتزايد يلغي الزيادة مع كل نقلة (يستثنى من ذلك نمط دقيقة +2 ث زيادة/نقلة ، ستلغى الزيادة لكن الوقت سيكون دقيقة كاملة) المخاطرة غير متاحة للمباريات ذات التوقيت صفر +زيادة بالثواني (0+1ث/نقلة, 0+2ث). المخاطرة تضمن النقطة الإضافية إذا لعبت على الأقل 7 نقلات. كيف يحدد الفائز؟ - اللاعب (اللاعبون) ذو النقاط الأعلى في نهاية الوقت المحدد للبطولة يتم إعلانه/م فائز/ين. - كيف يتم التزويج؟ + اللاعب (اللاعبون) ذو النقاط الأعلى في نهاية الوقت المحدد للبطولة يتم إعلانه/م فائز/ين. +عندما يحصل لاعبان أو أكثر على نفس عدد النقاط، يكون معدل الأداء في البطولة هو كسر التعادل. + كيف يتم تحديد الخصوم؟ في بداية البطولة، يتم إزواج اللاعبين على أساس تقييمهم. بمجرد الانتهاء من مباراتك، والعودة إلى بهو البطولة: سيتم ازواجك مع لاعب قريب من ترتيبك. وهذا ما يضمن وقت إنتظار أقل،بأي حال قد لا تواجه سائر اللاعبين في هذه البطولة. العب سريعًا وعد إلى المسابقة للعب مباريات أكثر واكسب المزيد من النقاط. كيف تنتهي البطولة؟ للبطولة ساعة عد تنازلي. عندما تصل إلى الصفر، يتم تجميد ترتيب مراكز البطولة، ويتم الإعلان عن الفائز. يجب أن يتم الانتهاء من المباريات قيد اللعب، ومع ذلك فإنها لا تحتسب نتائجها في البطولة. diff --git a/translation/dest/arena/be-BY.xml b/translation/dest/arena/be-BY.xml index 498eef0ac8677..95b959d2078a3 100644 --- a/translation/dest/arena/be-BY.xml +++ b/translation/dest/arena/be-BY.xml @@ -55,4 +55,6 @@ Дазволіць гульцам абмеркаванне ў чаце Серыі Арэны Пасля 2 перамог запар, наступныя перамогі прынясуць 4 ачкі, замест 2. + Берсерк не дазволены + Мае турніры diff --git a/translation/dest/arena/bg-BG.xml b/translation/dest/arena/bg-BG.xml index fa337beaef8ee..3e18474518f98 100644 --- a/translation/dest/arena/bg-BG.xml +++ b/translation/dest/arena/bg-BG.xml @@ -54,7 +54,7 @@ Позволи на играчите да обсъждат в чата Поредици в арената След 2 победи, всяка последователна победа носи 4 точки вместо 2. - No Berserk allowed + No Berserk allowed No Arena streaks Средна производителност Average score diff --git a/translation/dest/arena/el-GR.xml b/translation/dest/arena/el-GR.xml index 0b0949eebfb46..757d78f2b3e54 100644 --- a/translation/dest/arena/el-GR.xml +++ b/translation/dest/arena/el-GR.xml @@ -38,7 +38,7 @@ To Berserk δεν ισχύει για παρτίδες με μηδενικό α Αυτός ο διαγωνισμός είναι ιδιωτικός Μοιραστείτε αυτήν την διεύθυνση URL για να συμμετάσχουν άλλα άτομα: %s - Συνεχόμενες ισοπαλίες: Όταν ένας παίκτης έχει συνεχόμενες ισοπαλίες στην αρένα, είτε μόνο η πρώτη θα δώσει ένα πόντο, είτε ισοπαλίες που διήρκησαν πάνω από %s κινήσεις. Οι συνεχόμενες ισοπαλίες σταματούν να θεωρούνται συνεχόμενες μόνο εφόσον τις ακολουθήσει μία νίκη και όχι μία ήττα ή ισοπαλία. + Συνεχόμενες ισοπαλίες: Όταν ένας παίκτης έχει συνεχόμενες ισοπαλίες στην αρένα, είτε μόνο η πρώτη θα δώσει ένα πόντο, είτε ισοπαλίες που διήρκησαν πάνω από %s κινήσεις. Οι συνεχόμενες ισοπαλίες σταματούν να θεωρούνται συνεχόμενες μόνο εφόσον τις ακολουθήσει μία νίκη και όχι μία ήττα ή ισοπαλία. Ο ελάχιστος αριθμός των κινήσεων που απαιτούνται στα ισόπαλα παιχνίδια για να δώσουν πόντους εξαρτάται από την κάθε παραλλαγή, όπως φαίνεται και στον παρακάτω πίνακα. Εκδοχή Ελάχιστος αριθμός κινήσεων @@ -57,5 +57,5 @@ To Berserk δεν ισχύει για παρτίδες με μηδενικό α Μετά από 2 νίκες, επιπλέον διαδοχικές νίκες σας 4 πόντους αντί για 2. Μέση επίδοση Μέση βαθμολογία - Τα τουρνουά μου + Τα τουρνουά μου diff --git a/translation/dest/arena/fa-IR.xml b/translation/dest/arena/fa-IR.xml index 02bf9b951c76e..8239b66efd534 100644 --- a/translation/dest/arena/fa-IR.xml +++ b/translation/dest/arena/fa-IR.xml @@ -36,7 +36,7 @@ این یک تورنومنت خصوصی است این لینک را برای پیوستن دیگران به اشتراک بگذارید.%s - سلسله تساوی: وقتی یک بازیکن در Arena چند تساوی پشت سر هم بدست بیاورد، تنها اولین تساوی یا تساوی‌هایی با حداقل %s حرکت، دارای امتیاز خواهند بود. سلسله تساوی تنها با برد شکسته خواهد شد، نه با باخت یا تساوی. + سلسله تساوی: وقتی یک بازیکن در Arena چند تساوی پشت سر هم بدست بیاورد، تنها اولین تساوی یا تساوی‌هایی با حداقل %s حرکت، دارای امتیاز خواهند بود. سلسله تساوی تنها با برد شکسته خواهد شد، نه با باخت یا تساوی. حداقل طول بازی برای بازی های قرعه کشی شده برای کسب امتیاز بر اساس نوع بازی متفاوت است. جدول زیر آستانه انواع مختلف را فهرست می کند. نوع حداقل طول بازی @@ -53,7 +53,7 @@ اجازه دادن بحث به بازیکنان در چت روم آرنا استریکز بعد از دو برد، بردهای پی در پی به‌جای 2 امتیاز 4 امتیاز می دهند. - برزرک مجاز نیست + برزرک مجاز نیست سلسله برد ناموجود میانگین عملکرد میانگین امتیاز diff --git a/translation/dest/arena/gl-ES.xml b/translation/dest/arena/gl-ES.xml index a3990929e717d..7df7c1e302314 100644 --- a/translation/dest/arena/gl-ES.xml +++ b/translation/dest/arena/gl-ES.xml @@ -54,7 +54,7 @@ Xoga rápido e volta á sala de espera para xogar máis partidas e gañar máis Permite que os xogadores se comuniquen nunha sala de conversas Secuencia de vitorias na Arena Tras 2 vitorias, as seguintes vitorias consecutivas dan 4 puntos en lugar de 2. - Non está permitido facer o Berserk + Non está permitido facer o Berserk Arena sen secuencias de vitorias Rendemento medio Puntuación media diff --git a/translation/dest/arena/ka-GE.xml b/translation/dest/arena/ka-GE.xml index a4faab02bf133..b2757a3403a81 100644 --- a/translation/dest/arena/ka-GE.xml +++ b/translation/dest/arena/ka-GE.xml @@ -30,4 +30,8 @@ დროა გამოყოფილი თქვენს პირველ სვლაზე. თუ ვერ ჩაეტევით დროში, მოწინააღმდეგე გაიმარჯვებს. ეს არის პირადი ტურნირი გაუზიარე ლინკი სხვებს რათა შემოგიერთდნენ: %s + ვარიანტი + არენას ჯაჭვი + საშუალო ქულა + ჩემი ტურნირები diff --git a/translation/dest/arena/nl-NL.xml b/translation/dest/arena/nl-NL.xml index 8b6da0359f7d4..998f7a3f87dbe 100644 --- a/translation/dest/arena/nl-NL.xml +++ b/translation/dest/arena/nl-NL.xml @@ -37,7 +37,7 @@ Speel snel en ga terug naar de toernooilobby om meer partijen te spelen en meer Dit is een privétoernooi Deel deze URL om mensen deel te laten nemen: %s - Reeksen remises: wanneer een speler opeenvolgende keren gelijk speelt, zal enkel de eerste remise een punt opleveren. Bij standaard partijen leveren ook remises van meer dan %s zetten punten op. Deze reeks van remises kan enkel door een winst verbroken worden, niet door verlies of remise. + Reeksen remises: wanneer een speler opeenvolgende keren gelijk speelt, zal enkel de eerste remise een punt opleveren. Bij standaard partijen leveren ook remises van meer dan %s zetten punten op. Deze reeks van remises kan enkel door een winst verbroken worden, niet door verlies of remise. De minimale lengte die nodig is opdat een remisepartij punten zou opleveren hangt af van de variant. De tabel hieronder geeft een lijst van de ondergrens voor elke variant. Variant Minimale spellengte @@ -54,7 +54,7 @@ Speel snel en ga terug naar de toernooilobby om meer partijen te spelen en meer Spelers kunnen chatten in de chatruimte Arena streaks Na 2 overwinningen geven opeenvolgende overwinningen 4 punten in plaats van 2. - Berserk niet toegestaan + Berserk niet toegestaan Geen Arena-streaks Gemiddelde prestatie Gemiddelde score diff --git a/translation/dest/arena/ro-RO.xml b/translation/dest/arena/ro-RO.xml index 6a9eca3a8d25d..ca9a4b5dfec53 100644 --- a/translation/dest/arena/ro-RO.xml +++ b/translation/dest/arena/ro-RO.xml @@ -58,7 +58,7 @@ Joacă rapid și întoarce-te la lobby pentru a juca mai multe partide și pentr Permiteți jucătorilor să discute într-o cameră de chat Serie de victorii in arenă După 2 partide câştigate, câştigurile consecutive aduc 4 puncte în loc de 2. - Niciun Berserk permis + Niciun Berserk permis Nicio serie de victorii în arenă Performanță medie Scor mediu diff --git a/translation/dest/arena/sv-SE.xml b/translation/dest/arena/sv-SE.xml index b3611a18fcdf8..3fa393d9984a1 100644 --- a/translation/dest/arena/sv-SE.xml +++ b/translation/dest/arena/sv-SE.xml @@ -31,7 +31,7 @@ Berserk ger en extrapoäng endast om du spelar minst 7 drag i partiet. Detta är en privat turnering Dela denna länk för att låta spelare delta: %s - Remiserier: När en spelare har konsekutiva remier i en arena så kommer bara den första att ge ett poäng, eller remier som varar mer än %s drag i standardpartier. Remiserien kan bara brytas av en vinst, inte en förlust eller remi. + Remiserier: När en spelare har konsekutiva remier i en arena så kommer bara den första att ge ett poäng, eller remier som varar mer än %s drag i standardpartier. Remiserien kan bara brytas av en vinst, inte en förlust eller remi. Minsta partilängden för att remier ska ge poäng är olika mellan varianter. Tabellen nedan listar gränsvärden för varje variant. Variant Minsta partilängd @@ -48,7 +48,7 @@ Berserk ger en extrapoäng endast om du spelar minst 7 drag i partiet. Låt spelare diskutera i ett chattrum Arenavinster i rad Efter 2 vinster, ger efterföljande vinster 4 poäng i stället för 2. - Bärsärk tillåts ej + Bärsärk tillåts ej Inga arena streaks Genomsnittlig prestanda Medelpoäng diff --git a/translation/dest/arena/ta-IN.xml b/translation/dest/arena/ta-IN.xml index a3a4f295b6964..a8b4ab9526026 100644 --- a/translation/dest/arena/ta-IN.xml +++ b/translation/dest/arena/ta-IN.xml @@ -1,5 +1,64 @@ - அரங்கப் போட்டிகள் + கோதா போட்டிகள் மதிப்பிடப்பட்டதா? + போட்டி தொடங்கும் போது உங்களுக்குத் தெரிவிக்கப்படும், எனவே காத்திருக்கும் போது மற்றொரு தாவலில் விளையாடுவது பாதுகாப்பானது. + இந்தப் போட்டி மதிப்பிடப்பட்டது மற்றும் உங்கள் மதிப்பீட்டைப் பாதிக்கும். + இந்தப் போட்டி * மதிப்பிடப்படவில்லை* மேலும் உங்கள் மதிப்பீட்டைப் பாதிக்காது. + சில போட்டிகள் மதிப்பிடப்பட்டு உங்கள் மதிப்பீட்டைப் பாதிக்கும். + மதிப்பெண்கள் எவ்வாறு கணக்கிடப்படுகின்றன? + ஒரு வெற்றியின் அடிப்படை மதிப்பெண் 2 புள்ளிகள், ஒரு சமநிலை 1 புள்ளி, மற்றும் தோல்விக்கு எந்தப் புள்ளியும் இல்லை. +நீங்கள் தொடர்ந்து இரண்டு ஆடங்களை வென்றால், நெருப்பு சின்னத்தால் குறிக்கப்படும் இரட்டை புள்ளி தொடரைத் தொடங்குவீர்கள். +நீங்கள் ஒரு ஆட்டத்தை வெல்லத் தவறிய வரை பின்வரும் ஆட்டங்கள் இரட்டைப் புள்ளிகள் மதிப்புடையதாகத் தொடரும். +அதாவது, ஒரு வெற்றி 4 புள்ளிகள் மதிப்புடையதாக இருக்கும், ஒரு சமநிலை 2 புள்ளிகள், மற்றும் தோல்விக்கு இன்னும் புள்ளிகள் வழங்கப்படாது. + +எடுத்துக்காட்டாக, இரண்டு வெற்றிகளும் அதைத் தொடர்ந்து சமநிலையும் 6 புள்ளிகள் மதிப்புடையதாக இருக்கும்: 2 + 2 + (2 x 1) + மூர்க்க கோதா + ஆட்டத்தின் தொடக்கத்தில் ஒரு வீரர் மூர்க்கம் பொத்தானைக் சொடுக்கும் போது, அவர்கள் கடிகார நேரத்தின் பாதியை இழக்கிறார்கள், ஆனால் வெற்றியானது ஒரு கூடுதல் போட்டிப் புள்ளிக்கு மதிப்புள்ளது. + +நேரக் கட்டுப்பாடுகளில் மூர்கத்திற்குள் செல்வது நேர அதிகரிப்பை ரத்து செய்கிறது (1+2 என்பது விதிவிலக்கு, அது 1+0 தருகிறது). + +பூஜ்ஜிய ஆரம்ப நேரம் (0+1, 0+2) கொண்ட ஆடங்களுக்கு மூர்க்கம் கிடைக்காது. + +நீங்கள் விளையாட்டில் குறைந்தது 7 நகர்வுகளை விளையாடினால் மட்டுமே மூர்க்கம் கூடுதல் புள்ளியை வழங்குகிறது. + வெற்றியாளர் எவ்வாறு தீர்மானிக்கப்படுகிறார்? + போட்டியின் நிர்ணயிக்கப்பட்ட காலக்கெடு முடிவில் அதிக புள்ளிகளைப் பெற்ற வீரர்(கள்) வெற்றியாளர்(கள்) ஆக அறிவிக்கப்படுவார்கள். + +இரண்டு அல்லது அதற்கு மேற்பட்ட வீரர்கள் ஒரே எண்ணிக்கையிலான புள்ளிகளைப் பெற்றிருந்தால், போட்டியின் செயல்திறன் சமன் முறிவு ஆகும். + இணைத்தல் எவ்வாறு செயல்படுகிறது? + போட்டியின் தொடக்கத்தில், வீரர்கள் தங்கள் மதிப்பீட்டின் அடிப்படையில் ஜோடிகளாக இணைக்கப்படுகிறார்கள். +நீங்கள் ஒரு விளையாட்டை முடித்தவுடன், போட்டியின் கூடத்திற்கு திரும்பவும்: உங்கள் தரவரிசைக்கு நெருக்கமான ஒரு வீரருடன் நீங்கள் ஜோடியாக இருப்பீர்கள். இது குறைந்தபட்ச காத்திருப்பு நேரத்தை உறுதி செய்கிறது, இருப்பினும் நீங்கள் போட்டியில் மற்ற அனைத்து வீரர்களையும் எதிர்கொள்ள முடியாது. +அதிக ஆட்டங்களை விளையாடி அதிக புள்ளிகளை வெல்ல வேகமாக விளையாடி கூடத்திற்கு திரும்பவும். + அது எப்படி முடிகிறது? + போட்டியில் பின்நோக்கி எண் கடிகாரம் உள்ளது. அது பூச்சியத்தை அடையும்போது, போட்டித் தரவரிசைகள் முடக்கப்பட்டு, வெற்றியாளர் அறிவிக்கப்படுவார். நடந்து கொண்டிருக்கும் ஆட்டங்கள் முடிக்கப்பட வேண்டும், இருப்பினும் அவை போட்டிக்குக் கணக்கிடப்படாது. + மற்ற முக்கியமான விதிகள் + உங்கள் முதல் நடவடிக்கைக்கான பின்நோக்கி எண் உள்ளது. இந்த நேரத்திற்குள் ஒரு நகர்வைச் செய்யத் தவறினால், உங்கள் எதிரியிடம் விளையாட்டை இழக்க நேரிடும். + + முதல் %s நகர்த்தலுக்குள் விளையாட்டைச் சமன் செய்வதால் எந்த வீரருக்கும் எந்தப் புள்ளிகளும் கிடைக்காது. + முதல் %s நகர்த்தல்களுக்குள் விளையாட்டைச் சமன் செய்வதால் எந்த வீரருக்கும் எந்தப் புள்ளிகளும் கிடைக்காது. + + இது ஒரு தனிப்பட்ட போட்டி + மக்கள் சேர இந்த URL ஐப் பகிரவும்: %s + சமநிலை தொடர்கள்: ஒரு ஆட்டக்காரர் ஒரு கோதாவில் தொடர்ச்சியாகச் சமநிலைகளைப் பெற்றால், முதல் சமநிலை மட்டுமே ஒரு புள்ளியை ஏற்படுத்தும் அல்லது நிலையான விளையாட்டுகளில் %s நகர்வுகளுக்கு மேல் நீடிக்கும். சமநிலை தொடரை ஒரு வெற்றியால் மட்டுமே உடைக்க முடியும், தோல்வி அல்லது சமநிலை அல்ல. + புள்ளிகளைப் பெற வரையப்பட்ட ஆடங்களுக்கான குறைந்தபட்ச விளையாட்டு நீளம் மாறுபாட்டின் அடிப்படையில் வேறுபடும். கீழே உள்ள அட்டவணை ஒவ்வொரு மாறுபாட்டிற்கான வரம்பைப் பட்டியலிடுகிறது. + மாறுபாடு + குறைந்தபட்ச விளையாட்டு நீளம் + கோதா வரலாறு + + குழுவைப் பார்க்கவும் + அனைத்து %s குழுக்களையும் காண்க + + புதிய குழுப் போர் + தனிப்பயன் தொடக்க தேதி + உங்கள் சொந்த உள்ளூர் நேர மண்டலத்தில். இது \"போட்டி தொடங்குவதற்கு முந்தைய நேரம்\" அமைப்பை மீறுகிறது + மூர்க்கத்தை அனுமதிக்கவும் + கூடுதல் புள்ளியைப் பெற வீரர்கள் தங்கள் கடிகார நேரத்தைப் பாதியாகக் குறைக்கட்டும் + வீரர்கள் அரட்டை அறையில் விவாதிக்கலாம் + கோதா தொடர் + 2 வெற்றிகளுக்குப் பிறகு, தொடர்ச்சியான வெற்றிகள் 2க்குப் பதிலாக 4 புள்ளிகளை வழங்குகின்றன. + மூர்க்கம் அனுமதிக்கப்படவில்லை + கோதா தொடர் இல்லை + சராசரி செயல்திறன் + சராசரி மதிப்பெண் + எனது போட்டிகள் diff --git a/translation/dest/arena/th-TH.xml b/translation/dest/arena/th-TH.xml index 7abce3ababd38..1e7c0dd9c8f9b 100644 --- a/translation/dest/arena/th-TH.xml +++ b/translation/dest/arena/th-TH.xml @@ -29,7 +29,7 @@ นี่คือทัวร์นาเมนต์ส่วนตัว แชร์ URL นี้เพื่อให้คนอื่นๆได้เข้าร่วม: %s - การเสมอต่อเนื่อง: เมึ่อผู้เล่นเสมอต่อเนื่องใน Arena + การเสมอต่อเนื่อง: เมึ่อผู้เล่นเสมอต่อเนื่องใน Arena จะนับคะแนน การเสมอครั้งแรก หรือการเสมอที่มีตาเดินมากกว่า %s ตาเดินในเกมมาตรฐานเท่านั้น การเสมอต่อเนื่อง จะสามารถจบลงได้ด้วยการชนะเท่านั้น การแพ้หรือเสมอจะไม่ทำให้จบการเสมอต่อเนื่อง ความยาวเกมอย่างต่ำเพี่อที่จะให้เกมที่เสมอนั้นนับคะแนนจะต่างกันไปในแต่ละตัวแปรกติกา ตารางข้างล่างนี้แสดงจุดเริ่มต้นคิดคะแนนสำหรับแต่ละตัวแปรกติกา @@ -41,12 +41,13 @@ การสู้แบบทีมใหม่ กำหนดเวลาแข่งเอง + ในเขตเวลาท้องถิ่นของคุณ สิ่งนี้จะแทนที่การตั้งค่า \"เวลาก่อนเริ่มทัวร์นาเมนต์\" อนุญาตให้เบอร์เซิร์ก ผู้เล่นจะสามารถลดเวลาเล่นของตัวเองครึ่งหนึ่งเพี่อที่จะได้คะแนนเพิ่ม 1 คะแนนถ้าชนะ ผู้เล่นจะสามารถพูดคุยในห้องแชตได้ เพิ่มคะแนนเมื่อชนะต่อเนื่อง หลังจากชนะ 2 ครั้ง การชนะต่อเนื่องจะให้คะแนน 4 คะแนน แทน 2 คะแนน - ไม่มีการเบอร์เซิร์ก + ไม่มีการเบอร์เซิร์ก ไม่มีสตรีคอารีน่า ประสิทธิภาพโดยเฉลี่ย คะแนนเฉลี่ย diff --git a/translation/dest/arena/tp-TP.xml b/translation/dest/arena/tp-TP.xml index 5e5b7799cc5f5..06922b394e9bd 100644 --- a/translation/dest/arena/tp-TP.xml +++ b/translation/dest/arena/tp-TP.xml @@ -54,7 +54,7 @@ o musi lon tenpo lili la o tawa tawa tomo awen. tan ni la sina ken musi mute li jan musi li ken toki lon tomo toki nanpa pona sin pi pini pona poka jan li pini pona lon tenpo 2, pini pona kama li pana e nanpa pona 4, li pana ala e nanpa pona 2. - Berserk ala ken + Berserk ala ken meso wawa meso nanpa diff --git a/translation/dest/arena/vi-VN.xml b/translation/dest/arena/vi-VN.xml index 8d6474f5055ee..59fa8c85e29ae 100644 --- a/translation/dest/arena/vi-VN.xml +++ b/translation/dest/arena/vi-VN.xml @@ -22,7 +22,7 @@ Berserk không áp dụng cho ván đấu không có thời gian bắt đầu (0 Berserk chỉ thêm điểm nếu bạn chơi ít nhất 7 nước trong một ván. Cách xác định người chiến thắng? - (Những) kỳ thủ có điểm cao nhất khi kết thúc giải đấu sẽ là (những) người thắng cuộc. + (Những) kỳ thủ có điểm cao nhất sau khi giải đấu kết thúc sẽ là (những) người thắng cuộc. Nếu hai kỳ thủ bằng điểm nhau, kết quả sẽ quyết định qua tie break. Cặp đấu được chọn ra sao? @@ -37,7 +37,7 @@ Chơi nhanh và trở lại phòng chờ để chơi được nhiều ván và g Đây là giải đấu riêng tư Chia sẻ URL này để mọi người tham gia: %s - Chuỗi hòa: Khi một người chơi hòa liên tục ở một đấu trường, chỉ có ván hòa đầu tiên mới được tính điểm hoặc các ván tiêu chuẩn hòa mà có nhiều hơn %s nước đi. Chuỗi hòa chỉ có thể bị phá vỡ bởi một ván thắng chứ không phải một ván thua hay hòa khác. + Chuỗi hòa: Khi một người chơi hòa liên tục ở một đấu trường, chỉ có ván hòa đầu tiên mới được tính điểm hoặc các ván tiêu chuẩn hòa mà có nhiều hơn %s nước đi. Chuỗi hòa chỉ có thể bị phá vỡ bởi một ván thắng chứ không phải một ván thua hay hòa khác. Độ dài ván đấu tối thiểu cho các ván hòa để vẫn có điểm là khác nhau theo từng biến thể. Bảng dưới đây liệt kê ngưỡng cho từng biến thể. Biến thể Độ dài ván đấu tối thiểu @@ -53,9 +53,9 @@ Chơi nhanh và trở lại phòng chờ để chơi được nhiều ván và g Cho phép các kỳ thủ trò chuyện trong phòng trò chuyện Chuỗi đấu trường Sau 2 ván thắng, mỗi ván thắng liên tiếp sẽ được 4 điểm thay vì 2 điểm. - Không cho phép Berserk + Không cho phép Berserk Không có chuỗi Đấu trường Hiệu suất trung bình Điểm trung bình - Giải đấu của tôi + Giải đấu của tôi diff --git a/translation/dest/broadcast/af-ZA.xml b/translation/dest/broadcast/af-ZA.xml index cc2b2f7a694ce..0c01d0891ebe5 100644 --- a/translation/dest/broadcast/af-ZA.xml +++ b/translation/dest/broadcast/af-ZA.xml @@ -4,6 +4,7 @@ My uitsendings Regstreekse toernooi uitsendings Nuwe regstreekse uitsendings + Nog geen ronde nie. Voeg \'n ronde by Deurlopend Opkomend @@ -30,4 +31,5 @@ Skrap alle spelle van hierdie rondte. Die bron sal aktief moet wees om hulle te kan herskep. Vee hierdie toernooi uit Vee beslis die hele toernooi uit, met al sy rondtes en spelle. + Periode in sekondes diff --git a/translation/dest/broadcast/an-ES.xml b/translation/dest/broadcast/an-ES.xml index 5a9ad05877163..a4e5e55ad8eb1 100644 --- a/translation/dest/broadcast/an-ES.xml +++ b/translation/dest/broadcast/an-ES.xml @@ -3,6 +3,10 @@ Emisions Emisions de torneyos en directo Nueva emisión en directo + Sobre las retransmisions + Encara no i hai rondas. + Cómo fer servir las retransmisions de Lichess. + La nueva ronda habrá de tener los mesmos miembros y contribuidors que l\'anterior. Anyadir una ronda En curso Proximament diff --git a/translation/dest/broadcast/ar-SA.xml b/translation/dest/broadcast/ar-SA.xml index dac6e5e0b967f..3753cf47350ca 100644 --- a/translation/dest/broadcast/ar-SA.xml +++ b/translation/dest/broadcast/ar-SA.xml @@ -1,7 +1,8 @@ البثوث - + بثي + %s بث %s بث بثين @@ -11,10 +12,15 @@ بث البطولة المباشرة بث مباشر جديد + حول البث + لا جولات بعد. + كيفية استخدام بث ليتشيس. + ستضم الجولة الجديدة الأعضاء والمساهمين عينهم الذين اشتركوا في الجولة السابق. إضافة جولة الجارية القادمة المكتملة + يعرف ليتشيس بانتهاء الجولة استناداً إلى المصدر، استخدم هذا التبديل إذا لم يكن هناك مصدر. اسم الجولة رقم الجولة (الشوط) اسم البطولة @@ -38,4 +44,14 @@ تعديل دراسة الجولة حذف هذه المسابقة قم بحذف البطولة جميعها و جميع جولاتها و جميع ألعابها. + لوحة متصدرين تلقائية + حساب النتائج وعرض لوحة متصدرين بسيطة بناء على تلك النتائج + اختياري: استبدال أسماء اللاعبين وتقييماتهم وألقابهم + سطر واحد لكل لاعب، على النحو التالي: +الاسم الأصلي؛ الاسم البديل؛ التقييم البديل (اختياري)؛ لقب بديل (اختياري) +مثال: +DrNykterstein;Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + المدّة بالثواني + أختياري، كم مدة الانتظار بين الطلبات، تتراوح المدة بين الثانيتين والدقيقة، يحدد الإعداد الافتراضي بناء على عدد المشاهدين. diff --git a/translation/dest/broadcast/be-BY.xml b/translation/dest/broadcast/be-BY.xml index 185f060fd079a..7e41521fc0e6e 100644 --- a/translation/dest/broadcast/be-BY.xml +++ b/translation/dest/broadcast/be-BY.xml @@ -1,8 +1,12 @@ Трансляцыі + Мае трансляцыі Прамыя трансляцыі турніраў Новая прамая трансляцыя + Пра трансляцыіі + Пакуль няма тураў. + Як карыстацца трансляцыямі Lichess. Дадаць тур Бягучыя Надыходзячыя @@ -25,4 +29,9 @@ Спампаваць усе туры Скасаваць гэты тур Выдаліць гэты тур + Канчаткова выдаліць ​​тур і ўсе яго гульні. + Выдаліць усе гульні гэтага тура. Для іх паўторнага стварэння крыніца павінна быць актыўнай. + Рэдагаваць навучанне туру + Выдаліць гэты турнір + Канчаткова выдаліць увесь турнір, усе яго туры і ўсе гульні. diff --git a/translation/dest/broadcast/bg-BG.xml b/translation/dest/broadcast/bg-BG.xml index 5e8bf81740a4e..520b3bded3f5d 100644 --- a/translation/dest/broadcast/bg-BG.xml +++ b/translation/dest/broadcast/bg-BG.xml @@ -2,7 +2,7 @@ Излъчване Моите излъчвания - + %s излъчване %s излъчвания diff --git a/translation/dest/broadcast/bs-BA.xml b/translation/dest/broadcast/bs-BA.xml index 567a374d9909d..130447e5c09a0 100644 --- a/translation/dest/broadcast/bs-BA.xml +++ b/translation/dest/broadcast/bs-BA.xml @@ -2,7 +2,7 @@ Emitovanja Moja emitiranja - + %s emitovanje %s emitovanja %s emitovanja diff --git a/translation/dest/broadcast/ca-ES.xml b/translation/dest/broadcast/ca-ES.xml index cbbb56daeeb53..eb5330d8ee068 100644 --- a/translation/dest/broadcast/ca-ES.xml +++ b/translation/dest/broadcast/ca-ES.xml @@ -2,16 +2,21 @@ Retransmissions Les meves retransmissions - + %s retransmissió %s retransmissions Retransmissions de tornejos en directe Nova retransmissió en directe + Sobre les retransmissions + Cap ronda encara. + Com utilitzar les retransmissions de Lichess. + La nova ronda tindrà els mateixos membres i contribuïdors que l\'anterior. Afegir una ronda En curs Properes Acabada + Lichess detecta el final de la ronda en funció de les partides de l\'origen. Utilitzeu aquesta opció si no hi ha origen. Nom de ronda Ronda número Nom del torneig @@ -35,4 +40,14 @@ Edita l\'estudi de la ronda Elimina aquest torneig Elimina el torneig de forma definitiva, amb totes les seves rondes i les seves partides. + Taula de classificació automàtica + Calcula i mostra una taula de classificació simple basada en els resultats de les partides + Opcional: Reemplaça noms dels jugadors, puntuacions i títols + Una línia per jugador, seguint el següent format: +Nom Original; Nom a mostrar; (Opcional) Puntuació opcional a mostrar; (Opcional) Títol a mostrar +Per exemple: +DrNykterstein;Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + Període en segons + Opcional, quant de temps esperar entre sol·licituds. Mínim 2 segons, màxim 60 segons. Per defecte es gestiona automàticament en funció del nombre de visualitzadors. diff --git a/translation/dest/broadcast/ckb-IR.xml b/translation/dest/broadcast/ckb-IR.xml index f0f8e7f122061..e4261f4a244dd 100644 --- a/translation/dest/broadcast/ckb-IR.xml +++ b/translation/dest/broadcast/ckb-IR.xml @@ -1,12 +1,16 @@ پەخشەکان. - + %s پەخش دەکرێت %s پەخش دەکرێت پەخشی ڕاستەوخۆی پاڵەوانییەتیەکان پەخشێکی تازە + دەربارەی پەخش + هێشتا هیچ خولێک نییە. + چۆنیەتی بەکارهێنانی پەخشی لیچێس. + خولی نوێ هەمان ئەندام و بەشداربووانی پێشووی دەبێت. زیادکردنی خولێکی تر بەردەوامە لە داھاتوو دا diff --git a/translation/dest/broadcast/cs-CZ.xml b/translation/dest/broadcast/cs-CZ.xml index 4c628941d9792..5298985e6b137 100644 --- a/translation/dest/broadcast/cs-CZ.xml +++ b/translation/dest/broadcast/cs-CZ.xml @@ -2,7 +2,7 @@ Přenosy Moje vysílání - + %s vysílání %s vysílání %s vysílání diff --git a/translation/dest/broadcast/da-DK.xml b/translation/dest/broadcast/da-DK.xml index 01853b3bf9f98..b8164d4360560 100644 --- a/translation/dest/broadcast/da-DK.xml +++ b/translation/dest/broadcast/da-DK.xml @@ -2,16 +2,21 @@ Udsendelser Mine udsendelser - + %s udsendelse %s udsendelser Live turnerings-udsendelser Ny live-udsendelse + Om udsendelse + Ingen runder endnu. + Sådan bruges Lichess-udsendelser. + Den nye runde vil have de samme medlemmer og bidragydere som den foregående. Tilføj en runde I gang Kommende Afsluttet + Lichess registrerer rund-færdiggørelse baseret på kildepartierne. Brug denne skifter, hvis der ikke er nogen kilde. Rundenavn Rundenummer Turneringsnavn @@ -35,4 +40,14 @@ Rediger rundestudie Slet denne turnering Slet hele turneringen, alle dens runder og alle dens partier. + Automatisk rangliste + Beregn og vis en simpel rangliste baseret på resultater af partier + Valgfrit: udskift spillernavne, ratings og titler + En linje pr. spiller, formateret således: +Oprindeligt navn; Erstatningsnavn; Valgfri erstatnings-rating; Valgfri oprindelige erstatningstitel +Eksempel: +DrNykterstein;Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + Periode i sekunder + Valgfri, hvor lang tid der skal ventes mellem anmodninger. Min 2s, maks. 60s. Er som standard automatisk baseret på antallet af seere. diff --git a/translation/dest/broadcast/de-DE.xml b/translation/dest/broadcast/de-DE.xml index d09b10a0ee086..a4ec07e7352de 100644 --- a/translation/dest/broadcast/de-DE.xml +++ b/translation/dest/broadcast/de-DE.xml @@ -2,16 +2,21 @@ Übertragungen Meine Übertragungen - + %s Übertragung %s Übertragungen Live-Turnierübertragungen Neue Liveübertragung + Über Übertragungen + Noch keine Runden. + Wie man Lichess-Übertragungen benutzt. + Die nächste Runde wird die gleichen Mitspieler und Mitwirkende haben wie die vorhergehende. Eine Runde hinzufügen Laufend Demnächst Beendet + Lichess erkennt den Abschluss einer Runde anhand der Quellspiele. Verwenden Sie diesen Schalter, wenn keine Quelle vorhanden ist. Rundenname Rundennummer Turniername @@ -35,4 +40,14 @@ Rundenstudie bearbeiten Dieses Turnier löschen Lösche definitiv das gesamte Turnier, alle seine Runden und Partien. + Automatische Rangliste + Berechne und zeige eine einfache Rangliste basierend auf den Spielergebnissen + Optional: Spielernamen, Wertungen und Titel ersetzen + Eine Zeile pro Spieler, wie folgt formatiert: +Originalname; Ersatzname; optional Ersatzwertung; optional Ersatztitel +Beispiel: +DrNykterstein;Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + Dauer in Sekunden + Optional, wie lange zwischen den Anfragen gewartet werden soll. Mindestens 2s, maximal 60s. Standardmäßig auf der Zuschaueranzahl basierend. diff --git a/translation/dest/broadcast/el-GR.xml b/translation/dest/broadcast/el-GR.xml index 297b7f71983cc..486936d2a4ed6 100644 --- a/translation/dest/broadcast/el-GR.xml +++ b/translation/dest/broadcast/el-GR.xml @@ -1,8 +1,8 @@ Αναμεταδόσεις - Η αναμεταδόσεις μου - + Οι αναμεταδόσεις μου + %s αναμετάδοση %s αναμεταδόσεις diff --git a/translation/dest/broadcast/en-US.xml b/translation/dest/broadcast/en-US.xml index 4383f53f708ca..36d32dbe1bf74 100644 --- a/translation/dest/broadcast/en-US.xml +++ b/translation/dest/broadcast/en-US.xml @@ -2,16 +2,21 @@ Broadcasts My broadcasts - + %s broadcast %s broadcasts Live tournament broadcasts New live broadcast + About broadcasts + No rounds yet. + How to use Lichess Broadcasts. + The new round will have the same members and contributors as the previous one. Add a round Ongoing Upcoming Completed + Lichess detects round completion based on the source games. Use this toggle if there is no source. Round name Round number Tournament name @@ -35,4 +40,14 @@ Edit round study Delete this tournament Definitively delete the entire tournament, all its rounds and all its games. + Automatic leaderboard + Compute and display a simple leaderboard based on game results + Optional: replace player names, ratings and titles + One line per player, formatted as such: +Original name; Replacement name; Optional replacement rating; Optional replacement title +Example: +DrNykterstein;Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + Period in seconds + Optional, how long to wait between requests. Min 2s, max 60s. Defaults to automatic based on the number of viewers. diff --git a/translation/dest/broadcast/eo-UY.xml b/translation/dest/broadcast/eo-UY.xml index 324c2a16c9ac6..87c4cb8588fa1 100644 --- a/translation/dest/broadcast/eo-UY.xml +++ b/translation/dest/broadcast/eo-UY.xml @@ -2,7 +2,7 @@ Elsendoj Miaj elsendoj - + %s elsendo %s elsendoj diff --git a/translation/dest/broadcast/es-ES.xml b/translation/dest/broadcast/es-ES.xml index 9a8fd515faeb4..104432acc3277 100644 --- a/translation/dest/broadcast/es-ES.xml +++ b/translation/dest/broadcast/es-ES.xml @@ -2,16 +2,21 @@ Emisiones Mis transmisiones - + %s retransmisión %s retransmisiones Emisiones de torneos en directo Nueva emisión en directo + Acerca de las transmisiones + Aún no hay rondas. + Como utilizar las transmisiones de Lichess. + La nueva ronda tendrá los mismos miembros y contribuyentes que la anterior. Añadir una ronda En curso Próximamente Completadas + Lichess detecta la terminación de la ronda según las partidas de origen. Usa este interruptor si no hay ninguna. Nombre de la ronda Número de ronda Nombre del torneo @@ -35,4 +40,14 @@ Editar estudio de ronda Elimina este torneo Elimina definitivamente todo el torneo, rondas y partidas incluidas. + Tabla de clasificación automática + Calcula y muestra una tabla de clasificación simple según los resultados de la partida + Opcional: reemplazar nombres de jugadores, puntuaciones y títulos + Una línea por jugador, con este formato: +Nombre original; Nombre de reemplazo; Puntuación opcional de reemplazo; Título opcional de reemplazo +Ejemplo: +DrNykterstein;Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + Período en segundos + Opcional, cuánto tiempo esperar entre peticiones. Mín. 2 s., máx. 60 s. Por defecto es automático según el número de espectadores. diff --git a/translation/dest/broadcast/eu-ES.xml b/translation/dest/broadcast/eu-ES.xml index abf873e44e5d0..52760464c90d1 100644 --- a/translation/dest/broadcast/eu-ES.xml +++ b/translation/dest/broadcast/eu-ES.xml @@ -2,16 +2,21 @@ Emanaldiak Nire zuzenekoak - + Zuzeneko %s %s zuzeneko Txapelketen zuzeneko emanaldiak Zuzeneko emanaldi berria + Zuzeneko emanaldiei buruz + Ez dago txandarik. + Nola erabili Lichessen Zuzenekoak. + Txanda berriak aurrekoak beste kide eta laguntzaile izango ditu. Gehitu txanda bat Orain martxan Hurrengo emanaldiak Amaitutako emanaldiak + Txanda amaitu dela jatorrizko partidekin detektatzen du Lichessek. Erabili aukera hau jatorririk ez badago. Txandaren izena Txanda zenbaki Txapelketaren izena @@ -35,4 +40,14 @@ Editatu txandako azterlana Ezabatu txapelketa hau Txapelketa behin betiko ezabatu, bere txanda eta partida guztiak barne. + Sailkapen automatikoa + Partiden emaitzetan oinarritutako sailkapen sinplea kalkulatu eta erakutsi + Hautazkoa: aldatu jokalarien izen, puntuazio eta tituluak + Lerro bat jokalari bakotizeko, horrela formateatuta: +Jatorrizko izena; Ordezko izena; Ordezko puntuazioa (hautazkoa); Ordezko titulua (hautazkoa) +Adibidea: +DrNykterstein;Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + Aldia segundotan + Hautazkoa, zenbat itxaron eskaeren artean. Gutxienez 2 segundo, gehienez 60 segundo. Automatikora itzuliko da ikusle kopuruaren arabera. diff --git a/translation/dest/broadcast/fa-IR.xml b/translation/dest/broadcast/fa-IR.xml index 2f84c609cf6d1..9f7f67482eba2 100644 --- a/translation/dest/broadcast/fa-IR.xml +++ b/translation/dest/broadcast/fa-IR.xml @@ -2,7 +2,7 @@ پخش می شود پخش زنده من - + %s پخش زنده‌ %s پخش زنده‌ @@ -32,6 +32,8 @@ این دور بازی را حذف نمایید حذف کردن دور و بازی ها. حذف کردن تمام بازی های دور. منبع نیاز به فعال شدن برای ساخت مجدد آن ها دارد. + ویرایش مطالعه راند این مسابقه را حذف کن به طور قطعی کل مسابقه٬ به شمول تمام دورها و تمام بازی‌ها٬ را حذف کن. + لیدربورد خودکار diff --git a/translation/dest/broadcast/fi-FI.xml b/translation/dest/broadcast/fi-FI.xml index bcb58a6798a9f..feefe1496dff3 100644 --- a/translation/dest/broadcast/fi-FI.xml +++ b/translation/dest/broadcast/fi-FI.xml @@ -2,16 +2,21 @@ Lähetykset Omat lähetykset - + %s lähetys %s lähetystä Suorat lähetykset turnauksista Uusi livelähetys + Lähetyksistä + Ei vielä kierroksia. + Kuinka Lichess-lähetyksiä käytetään. + Uudella kierroksella on samat jäsenet ja osallistujat kuin edellisellä. Lisää kierros Käynnissä Tulossa Päättyneet + Lichess tunnistaa lähteenä olevista peleistä, milloin kierros on viety päätökseen. Lähteen puuttuessa voit käyttää tätä asetusta. Kierroksen nimi Kierroksen numero Turnauksen nimi @@ -35,4 +40,14 @@ Kierrostutkielman muokkaus Poista tämä turnaus Poista lopullisesti koko turnaus, sen kaikki kierrokset ja kaikki pelit. + Automaattinen tulostaulukko + Laske ja näytä yksinkertainen tulostaulukko pelien tulosten pohjalta + Valinnainen: korvaa pelaajien nimet, vahvuusluvut ja arvonimet + Yksi rivi pelaajaa kohden tässä muodossa: +Alkuperäinen nimi; Korvaava nimi; Haluttaessa korvaava vahvuusluku; Haluttaessa korvaava arvonimi +Esimerkki: +DrNykterstein;Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + Jakso sekunteina + Tarvittaessa pyyntöjen välinen odotusaika: vähintään 2 s, enintään 60 s. Oletuksena on katsojien määrään perustuva automaattinen arvo. diff --git a/translation/dest/broadcast/fr-FR.xml b/translation/dest/broadcast/fr-FR.xml index 2401bdef8438c..e762ae23b6ec7 100644 --- a/translation/dest/broadcast/fr-FR.xml +++ b/translation/dest/broadcast/fr-FR.xml @@ -2,16 +2,21 @@ Diffusions Ma diffusion - + %s diffusion %s diffusions Diffusions de tournois en direct Nouvelle diffusion en direct + À propos des diffusions + Aucune ronde pour le moment + Comment utiliser les diffusions dans Lichess + La nouvelle ronde aura les mêmes participants et contributeurs que la précédente. Ajouter une ronde En cours À venir Terminé + Lichess détecte la fin des rondes en fonction des parties sources. Utilisez cette option s\'il n\'y a pas de source. Nom de la ronde Numéro de la ronde Nom du tournoi @@ -35,4 +40,14 @@ Modifier l\'étude de la ronde Supprimer ce tournoi Supprimer définitivement le tournoi, toutes ses rondes et toutes ses parties. + Classement automatique + Calculer et afficher un échiquier principal simplifié basé sur les résultats de parties + Facultatif : remplacer les noms des joueurs, les cotes et les titres + Une ligne par joueur, formatée comme suit : +Nom d\'origine; Nom de remplacement; Cote de remplacement facultative; Titre de remplacement facultatif +Exemple : +DrNykterstein;Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + Période en secondes + Facultatif : temps d\'attente entre les requêtes. Min. 2 sec, max. 60 sec. Par défaut automatique selon le nombre de spectateurs. diff --git a/translation/dest/broadcast/gl-ES.xml b/translation/dest/broadcast/gl-ES.xml index 8e2c80e879267..3bc02572b428f 100644 --- a/translation/dest/broadcast/gl-ES.xml +++ b/translation/dest/broadcast/gl-ES.xml @@ -2,18 +2,23 @@ Emisións en directo As miñas emisións - + %s emisión %s emisións Emisións de torneos en directo Nova emisión en directo - Engadir unha ronda + Sobre as retransmisións + Aínda non hai roldas. + Como usar as Retransmisións de Lichess. + A nova rolda terá os mesmos membros e colaboradores cá rolda anterior. + Engadir unha rolda En curso Proximamente Completadas - Nome de ronda - Número de ronda + Lichess detecta o final das roldas en función das partidas de orixe. Usa esta opción se non hai orixe. + Nome da rolda + Número de rolda Nome do torneo Breve descrición do torneo Descrición completa do evento @@ -25,14 +30,25 @@ Opcional, se sabes cando comeza o evento Cita a fonte Ligazón da transmisión - Ligazón da ronda actual + Ligazón da rolda actual Ligazón da partida actual - Descargar todas as rondas - Restablecer esta ronda - Borrar esta ronda - Eliminar definitivamente a ronda e as súas partidas. - Eliminar todas as partidas desta ronda. A transmisión en orixe terá que estar activa para volver crealas. - Editar o estudo da ronda + Descargar todas as roldas + Restablecer esta rolda + Borrar esta rolda + Eliminar definitivamente a rolda e as súas partidas. + Eliminar todas as partidas desta rolda. A transmisión en orixe terá que estar activa para volver crealas. + Editar o estudo da rolda Eliminar este torneo - Eliminar o torneo de forma definitiva, con todas as súas rondas e partidas. + Eliminar o torneo de forma definitiva, con todas as súas roldas e partidas. + Listaxe de líderes automática + Calcula e amosa unha listaxe de líderes simple baseada nos resultados das partidas + Opcional: substitúe os nomes dos xogadores, as puntuacións e os títulos + Unha liña por xogador, con este formato: +Nome orixinal; Nome alternativo; Puntuación alternativa (opcional); Título alternativo (opcional) + +Exemplo: +DrNykterstein;Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + Período en segundos + Opcional: canto tempo se agarda entre peticións. Min 2s, max 60s. Por defecto é automático baseado no número de espectadores. diff --git a/translation/dest/broadcast/gsw-CH.xml b/translation/dest/broadcast/gsw-CH.xml index 4fcf4a83a6e04..cd01d363550e4 100644 --- a/translation/dest/broadcast/gsw-CH.xml +++ b/translation/dest/broadcast/gsw-CH.xml @@ -1,17 +1,22 @@ Überträgige - Mini Überträgige - + Eigeni Überträgige + %s Überträgige %s Überträgige - Live Turnier Überträgige - Neui Live Überträgige + Live Turnier-Überträgige + Neui Live-Überträgige + Über Überträgige + No kei Rundene. + Wie mer Lichess-Überträgige benutzt. + Die neu Runde wird us de gliche Mitglieder und Mitwürkende beschtah, wie die Vorherig. E Rundi zuefüege Laufend Demnächscht Beändet + Lichess erchännt de Rundeabschluss ahand vu de Quällpartie. Verwänd de Schalter, wänns kei Quälle git. Runde Name Runde Nummere Turnier Name @@ -24,15 +29,25 @@ Startdatum in dinere eigene Zitzone Optional, falls du weisch, wänn das Ereignis afangt Erwähn die Quälle - Veröffentlichungs-URL + Überträgigs-URL URL vode laufende Rundi URL vode laufende Partie Alli Runde abelade Die Rundi zruggsetze Die Rundi lösche - Die Rundi und ihri Spiel definitiv lösche. - Lösch alli Spiel vu dere Rundi. D\'Quälle muess aktiv si, dass sie neu erschtellt werde chönd. - Studie Rundi bearbeite + Die Rundi und ihri Schpiel definitiv lösche. + Lösch alli Schpiel vu dere Rundi. D\'Quälle muess aktiv si, dass sie neu erschtellt werde chönd. + Schtudie Rundi bearbeite Lösch das Turnier - Lösch das ganze Turnier, alli Runde und alli Partie. + Das ganze Turnier, alli Runde und alli Partie definitiv lösche. + Automatischi Ranglischte + Eifachi Ranglischte, uf Grund vu Schpielergäbnis berächnet, azeige + Optional: Spielernäme, Bewertinge und Titel usblände + 1 Zile pro Schpiller, formatiert wie folgt: +Ursprüngliche Name; Ersatzname; Optionali Ersatzbewertig; Optionale Ersatzbezeichnig +zum Bischpiel so: +Grobian;Jan Grob;2134 +Musterpierre;Peter Muster;2345;FM + Periode i Sekunde + Optional, wie lang zwüsche Afrage gwartet werden söll. Minimal 2, maximal 60 Sekunde. Die Standardischtellig isch automatisch, basierend uf der Azahl vu de Zueschauer. diff --git a/translation/dest/broadcast/he-IL.xml b/translation/dest/broadcast/he-IL.xml index 3c2b785fca485..04052b0313188 100644 --- a/translation/dest/broadcast/he-IL.xml +++ b/translation/dest/broadcast/he-IL.xml @@ -2,7 +2,7 @@ הקרנות ההקרנות שלי - + הקרנה %s %s הקרנות %s הקרנות @@ -10,10 +10,15 @@ צפייה ישירה בטורנירים הקרנה ישירה חדשה + הסבר על הקרנות + טרם נוצרו סבבים. + איך להשתמש בהקרנות ב-Lichess. + הסבב החדש יכלול את אותם התורמים והחברים כמו בסבב הקודם. הוספת סבב כרגע בקרוב שהושלמו + ליצ׳ס מאתר מתי הושלם הסבב על פי המשחקים שבקישור למהלכים בשידור חי (המקור). הפעילו את האפשרות הזאת אם אין מקור שממנו נשאבים המשחקים. שם סבב מספר סבב שם הטורניר @@ -37,4 +42,14 @@ עריכת לוח הלמידה של הסבב מחיקת הטורניר הזה מחיקה לצמיתות של הטורניר הזה, על כל סבביו והמשחקים שבו. + טבלה אוטומטית + הצגת טבלה פשוטה שמתבססת על תוצאות המשחקים + אופציונאלי: החלפת השמות, הדירוגים והתארים של השחקנים + שחקן אחד בכל שורה, על פי התבנית הבאה: +שם מקורי; שם רצוי; דירוג להצגה; תואר להצגה +לדוגמה: +DrNykterstein;Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + תדירות בשניות + אופציונאלי. משפיע על משך הזמן שעובר בין משיכות הנתונים מהמקור. בין 2 ל-60 שניות. כתלות במספר הצופים, הקצב עשוי לחזור לברירת המחדל. diff --git a/translation/dest/broadcast/hi-IN.xml b/translation/dest/broadcast/hi-IN.xml index bb59824fcd357..b4d5a883c75df 100644 --- a/translation/dest/broadcast/hi-IN.xml +++ b/translation/dest/broadcast/hi-IN.xml @@ -1,7 +1,7 @@ प्रसारण - + %s प्रसारण %s प्रसारण diff --git a/translation/dest/broadcast/hr-HR.xml b/translation/dest/broadcast/hr-HR.xml index 9cb8bced4f51f..9abe54e1519b7 100644 --- a/translation/dest/broadcast/hr-HR.xml +++ b/translation/dest/broadcast/hr-HR.xml @@ -1,7 +1,7 @@ Prijenosi - + %s prijenos %s prijenosa %s prijenosa diff --git a/translation/dest/broadcast/hu-HU.xml b/translation/dest/broadcast/hu-HU.xml index 40bdbf664bb9a..832da2213ff91 100644 --- a/translation/dest/broadcast/hu-HU.xml +++ b/translation/dest/broadcast/hu-HU.xml @@ -1,7 +1,7 @@ Versenyközvetítések - + %s versenyközvetítés %s versenyközvetítés diff --git a/translation/dest/broadcast/it-IT.xml b/translation/dest/broadcast/it-IT.xml index 974367c8b2d4c..13b559c9b1f64 100644 --- a/translation/dest/broadcast/it-IT.xml +++ b/translation/dest/broadcast/it-IT.xml @@ -2,16 +2,21 @@ Dirette Le mie trasmissioni - + %s diretta %s dirette Tornei in diretta Nuova diretta + Informazioni sulle trasmissioni + Ancora nessun turno. + Istruzioni delle trasmissioni Lichess. + Il nuovo turno avrà gli stessi membri e contributori del precedente. Aggiungi un turno In corso Prossimamente Conclusa + Lichess rileva il completamento del turno a seconda delle partite di origine. Utilizza questo interruttore se non è presente alcuna origine. Nome turno Turno numero Nome del torneo @@ -35,4 +40,14 @@ Modifica lo studio del turno Elimina questo torneo Elimina definitivamente l\'intero torneo, tutti i turni e tutte le partite. + Classifica automatica + Calcola e mostra una semplice classifica basata sui risultati delle partite + Facoltativo: sostituisci i nomi dei giocatori, i punteggi e i titoli + Una riga per giocatore, formattata come segue: +Nome; Nome sostitutivo; Punteggio sostitutivo (facoltativo); Titolo sostitutivo (facoltativo) +Esempi: +DrNykterstein;Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + Intervallo in secondi + Facoltativo: quanto a lungo aspettare tra due richieste. Minimo 2s, massimo 60s. Il default è scelto in base al numeri di spettatori. diff --git a/translation/dest/broadcast/ja-JP.xml b/translation/dest/broadcast/ja-JP.xml index 170b25f4a880b..23e9c8277f892 100644 --- a/translation/dest/broadcast/ja-JP.xml +++ b/translation/dest/broadcast/ja-JP.xml @@ -2,15 +2,20 @@ イベント中継 自分の配信 - + %s ブロードキャスト 実戦トーナメントのライブ中継 新しいライブ中継 + 中継について + まだラウンドはありません。 + Lichess 中継の使い方。 + 新ラウンドには前回と同じメンバーと投稿者が参加します。 ラウンドを追加 配信中 予定 終了 + Lichess は元になる対局に基づいてラウンド終了を検出します。元になる対局がない時はこのトグルを使ってください。 ラウンド名 ラウンド 大会名 @@ -34,4 +39,14 @@ ラウンドの研究を編集 このトーナメントを削除 トーナメント全体(全ラウンド、全ゲーム)を削除する。 + 自動ランキング + ゲーム結果から簡単なランキングを計算して表示する + オプション:プレイヤーの名前、レーティング、タイトルの変更 + 1 人 1 行、次の形式で: +元の名前;変更後の名前;変更後のレーティング(オプション);変更後のタイトル(オプション) +例: +DrNykterstein;Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + 待機時間(秒) + オプション、次のリクエストまでの待機時間を指定。最小 2 秒、最大 60 秒。デフォルト値は視聴者数から自動的に決まります。 diff --git a/translation/dest/broadcast/kk-KZ.xml b/translation/dest/broadcast/kk-KZ.xml index 3a44abda681ca..854b352a05ea4 100644 --- a/translation/dest/broadcast/kk-KZ.xml +++ b/translation/dest/broadcast/kk-KZ.xml @@ -2,7 +2,7 @@ Көрсетілімдер Менің көрсетілімдерім - + %s көрсетілім %s көрсетілім diff --git a/translation/dest/broadcast/kn-IN.xml b/translation/dest/broadcast/kn-IN.xml index f115bc5413dfe..1132b463203e7 100644 --- a/translation/dest/broadcast/kn-IN.xml +++ b/translation/dest/broadcast/kn-IN.xml @@ -1,7 +1,7 @@ ಪ್ರಸಾರಗಳು - + %s ಪ್ರಸಾರ %s ಪ್ರಸಾರಗಳು diff --git a/translation/dest/broadcast/ko-KR.xml b/translation/dest/broadcast/ko-KR.xml index a6ecee4411068..9b17770d9f908 100644 --- a/translation/dest/broadcast/ko-KR.xml +++ b/translation/dest/broadcast/ko-KR.xml @@ -1,7 +1,7 @@ 방송 - + %s 방송 실시간 대회 방송 diff --git a/translation/dest/broadcast/lb-LU.xml b/translation/dest/broadcast/lb-LU.xml index 49839d4ac3ef6..f109544405737 100644 --- a/translation/dest/broadcast/lb-LU.xml +++ b/translation/dest/broadcast/lb-LU.xml @@ -1,12 +1,13 @@ Iwwerdroungen - + %s Iwwerdroung %s Iwwerdroungen Live Turnéier Iwwerdroungen Nei Live Iwwerdroung + Nach keng Ronnen. Ronn hinzufügen Am Gaang Demnächst @@ -34,4 +35,5 @@ Ronnen-Etüd modifiéieren Dësen Turnéier läschen De ganzen Turnéier definitiv läschen, all seng Ronnen an all seng Partien. + Period a Sekonnen diff --git a/translation/dest/broadcast/lt-LT.xml b/translation/dest/broadcast/lt-LT.xml index 8914120a33f5c..8198580d28b4d 100644 --- a/translation/dest/broadcast/lt-LT.xml +++ b/translation/dest/broadcast/lt-LT.xml @@ -2,7 +2,7 @@ Transliacijos Mano transliacijos - + %s transliacija %s transliacijos %s transliacijos diff --git a/translation/dest/broadcast/nb-NO.xml b/translation/dest/broadcast/nb-NO.xml index 7cdc1f0544e11..a8b2c2a3751a5 100644 --- a/translation/dest/broadcast/nb-NO.xml +++ b/translation/dest/broadcast/nb-NO.xml @@ -2,16 +2,21 @@ Overføringer Mine overføringer - + %s overføring %s overføringer Direkteoverføringer av turneringer Ny direkteoverføring + Om overføringer + Ingen runder ennå. + Hvordan bruke overføringer hos Lichess. + Den nye runden vil ha de samme medlemmene og bidragsyterne som den forrige. Legg til runde Pågående Kommende Fullført + Lichess oppdager fullførte runder basert på kildepartiene. Bruk denne knappen hvis det ikke finnes noen kilde. Rundenavn Rundenummer Turneringsnavn @@ -35,4 +40,14 @@ Rediger rundestudie Slett denne turneringen Slett hele turneringen for godt, sammen med alle rundene og alle partiene. + Automatisk ledertabell + Beregn og vis en enkel ledertabell basert på resultatene av partiene + Valgfritt: erstatt spillernavn, ratinger og titler + Én linje per spiller, formatert slik: +Originalt navn; Erstattet navn; Valgfri erstattet rating; Valgfri erstattet tittel +Eksempel: +DrNykterstein;Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + Tidsrom i sekunder + Valgfritt, hvor lenge man skal vente mellom forespørslene. Minst 2 sekunder, maks 60 sekunder. Standardinnstillingen er automatisk basert på antall seere. diff --git a/translation/dest/broadcast/nl-NL.xml b/translation/dest/broadcast/nl-NL.xml index bd44b238ca4b6..b8260c257ddb7 100644 --- a/translation/dest/broadcast/nl-NL.xml +++ b/translation/dest/broadcast/nl-NL.xml @@ -2,16 +2,21 @@ Uitzendingen Mijn uitzendingen - + %s uitzending %s uitzendingen Live toernooi uitzendingen Nieuwe live uitzending + Over uitzending + Nog geen rondes. + Hoe Lichess Uitzendingen te gebruiken. + De nieuwe ronde zal dezelfde leden en bijdragers hebben als de vorige. Ronde toevoegen Lopend Aankomend Voltooid + Lichess detecteert voltooiing van de ronde op basis van de bronpartijen. Gebruik deze schakelaar als er geen bron is. Naam ronde Ronde Naam toernooi @@ -35,4 +40,9 @@ Studieronde bewerken Verwijder dit toernooi Verwijder definitief het hele toernooi, inclusief alle rondes en partijen. + Automatisch scorebord + Bereken en toon een eenvoudig scorebord gebaseerd op de uitslagen + Optioneel: vervang spelersnamen, ratings en titels + Periode in seconden + Optioneel, hoe lang te wachten tussen aanvragen. Minimaal 2 seconden, maximaal 60 seconden. Standaard automatisch gebaseerd op het aantal kijkers. diff --git a/translation/dest/broadcast/nn-NO.xml b/translation/dest/broadcast/nn-NO.xml index 411b00df1e0e8..59f459461b130 100644 --- a/translation/dest/broadcast/nn-NO.xml +++ b/translation/dest/broadcast/nn-NO.xml @@ -2,16 +2,21 @@ Overføringar Mine sendingar - + %s sending %s sendingar Direktesende turneringar Ny direktesending + Om sending + Så langt ingen rundar. + Korleis bruke Lichess-sendingar. + Den nye runden vil ha same medlemar og bidragsytarar som den førre. Legg til ein runde Pågåande Kommande Fullførde + Lichess detekterer ferdigspela rundar basert på kjeldeparita. Bruk denne innstillinga om det ikkje finst ei kjelde. Rundenamn Rundenummer Turneringsnamn @@ -35,4 +40,14 @@ Rediger rundestudie Slett denne turneringa Slett heile turneringa med alle rundene og alle partia. + Automatisk rangliste + Berekne og vise ei enkel rangliste baseret på resultatar frå partia + Valfritt: erstatt spelaramn, rangeringar og titlar + Ei linje per spelar, formattert slik: +Opphavleg namn; valfritt alternativt namn; valfri alternativ rating; valfri alternativ tittel +Døme: +DrNykterstein;Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + Periode i sekund + Ventetida mellom førespurnadene er valfri frå 2 til 60 sekund. Default tid er basert på sjåartalet. diff --git a/translation/dest/broadcast/pl-PL.xml b/translation/dest/broadcast/pl-PL.xml index 8af73cfe08ae8..b49aa24368e54 100644 --- a/translation/dest/broadcast/pl-PL.xml +++ b/translation/dest/broadcast/pl-PL.xml @@ -2,7 +2,7 @@ Transmisje Moje transmisje - + %s transmisja %s transmisje %s transmisji @@ -10,10 +10,15 @@ Transmisje turniejów na żywo Nowa transmisja na żywo + O transmisji + Nie ma jeszcze rund. + Jak korzystać z transmisji na Lichess. + Nowa runda będzie miała tych samych uczestników co poprzednia. Dodaj rundę Trwające Nadchodzące Zakończone + Lichess wykrywa ukończenie rundy w oparciu o śledzone partie. Użyj tego przełącznika, jeśli nie ma takich partii. Nazwa rundy Numer rundy Nazwa turnieju @@ -37,4 +42,14 @@ Edytuj opracowanie rundy Usuń ten turniej Ostatecznie usuń cały turniej, jego wszystkie rundy i partie. + Automatyczna tablica wyników + Oblicz i wyświetl prostą tablicę wyników w oparciu o wyniki partii + Opcjonalnie: zastąp nazwy, rankingi i tytuły graczy + Jeden wiersz na gracza, w następującym formacie: +oryginalna nazwa; nowa nazwa; nowy ranking (opcjonalnie); nowy tytuł (opcjonalnie) +Przykład: +DrNykterstein; Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + Przedział czasu w sekundach + Opcjonalnie, jak długo czekać pomiędzy odpytaniami. Min 2s, max 60s. Domyślnie jest to ustawiane automatycznie na podstawie liczby widzów. diff --git a/translation/dest/broadcast/pt-BR.xml b/translation/dest/broadcast/pt-BR.xml index 517c64cdf591f..5a71bd57c7e76 100644 --- a/translation/dest/broadcast/pt-BR.xml +++ b/translation/dest/broadcast/pt-BR.xml @@ -2,16 +2,21 @@ Transmissões Minhas transmissões - + %s transmissão %s transmissões Transmissões ao vivo do torneio Nova transmissão ao vivo + Sobre as transmissões + Sem rodadas ainda. + Como usar as transmissões do Lichess. + A nova rodada terá os mesmos membros e colaboradores que a anterior. Adicionar uma rodada Em andamento Próximos Concluído + O Lichess detecta o fim da rodada baseado nos jogos fonte. Use essa opção se não houver fonte. Nome da rodada Número da rodada Nome do torneio @@ -35,4 +40,14 @@ Editar estudo da rodada Excluir este torneio Excluir permanentemente todo o torneio, incluindo todas as rodadas e jogos. + Placar automático + Calcula e exibe um placar simples baseado nos resultados dos jogos + Opcional: nomes de jogador, classificações e títulos alternativos + Uma linha por jogador, no seguinte formato: +Nome original; Nome alternativo; Classificação alternativa opcional; Título alternativo opcional +Exemplo: +DrNykterstein;Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + Período em segundos + Opcional: tempo entre as solicitações. Mín. 2s, máx. 60s. Por padrão, é calculado com base no número de espectadores. diff --git a/translation/dest/broadcast/pt-PT.xml b/translation/dest/broadcast/pt-PT.xml index 093952fdfae73..829a0bb4493d2 100644 --- a/translation/dest/broadcast/pt-PT.xml +++ b/translation/dest/broadcast/pt-PT.xml @@ -2,16 +2,21 @@ Transmissões As minhas transmissões - + %s transmissão %s transmissões Transmissões do torneio em direto Nova transmissão em direto + Sobre Transmissões + Sem rondas ainda. + Como usar as Transmissões do Lichess. + A nova ronda terá os mesmos membros e contribuidores que a anterior. Adicionar uma ronda A decorrer Brevemente Concluído + Lichess deteta a conclusão da ronda baseada nos jogos da fonte. Use essa opção se não houver fonte. Nome da ronda Número da ronda Nome do torneio @@ -35,4 +40,14 @@ Editar estudo da ronda Eliminar este torneio Excluir definitivamente todo o torneio, todas as rondas e todos os jogos. + Tabela de classificação automática + Calcula e exibe uma tabela de classificação simples baseada nos resultados dos jogos + Opcional: substituir nomes de jogadores, ratings e títulos + Uma linha por jogador, formatada da seguinte forma: +Nome original; Nome substituto; Opcional: Ranting de substituição; Opcional: Título de substituição +Exemplo: +DrNykterstein;Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + Período em segundos + Opcional, quanto tempo de espera entre as requisições. Mínimo 2s, máximo de 60s. O padrão é automático com base no número de espetadores. diff --git a/translation/dest/broadcast/ro-RO.xml b/translation/dest/broadcast/ro-RO.xml index 2098f5ce417a4..d1312b575136c 100644 --- a/translation/dest/broadcast/ro-RO.xml +++ b/translation/dest/broadcast/ro-RO.xml @@ -2,7 +2,7 @@ Transmisiuni Transmisiile mele - + %s transmisiune %s transmisiuni %s de transmisiuni diff --git a/translation/dest/broadcast/ru-RU.xml b/translation/dest/broadcast/ru-RU.xml index 521974fb2f47f..82fd8d29ff295 100644 --- a/translation/dest/broadcast/ru-RU.xml +++ b/translation/dest/broadcast/ru-RU.xml @@ -2,7 +2,7 @@ Трансляции Мои трансляции - + %s трансляция %s трансляции %s трансляций @@ -10,10 +10,15 @@ Прямые трансляции турнира Новая прямая трансляция + О трансляции + Пока нет туров. + Как пользоваться трансляциями Lichess. + В новом туре примут участие те же участники и редакторы, что и в предыдущем туре. Добавить тур Текущие Предстоящие Завершённые + Lichess определяет завершение тура на основе источника партий. Используйте этот переключатель, если нет источника. Название тура Номер тура Название турнира @@ -37,4 +42,14 @@ Редактировать студию тура Удалить этот турнир Окончательно удалить весь турнир, его туры и партии. + Автоматическая таблица лидеров + Вычислять и отображать простую таблицу лидеров на основе результатов партий + Необязательно: заменять имена игроков, рейтинги и звания + По одной строке на каждого игрока, форматирование по образцу: +Имя в системе; Имя для замены; Рейтинг для замены (необязательно); Звание для замены (необязательно) +Например: +DrNykterstein;Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + Период в секундах + Необязательно: сколько времени ждать между запросами. Минимум 2 сек, максимум 60 сек. По умолчанию устанавливается автоматически, в зависимости от количества зрителей. diff --git a/translation/dest/broadcast/sk-SK.xml b/translation/dest/broadcast/sk-SK.xml index 81585ef15b3e4..0d3c335fbeed7 100644 --- a/translation/dest/broadcast/sk-SK.xml +++ b/translation/dest/broadcast/sk-SK.xml @@ -2,7 +2,7 @@ Vysielanie Moje vysielania - + %s vysielanie %s vysielania %s vysielaní diff --git a/translation/dest/broadcast/sl-SI.xml b/translation/dest/broadcast/sl-SI.xml index e2abf9d541bc2..aea3e99079bed 100644 --- a/translation/dest/broadcast/sl-SI.xml +++ b/translation/dest/broadcast/sl-SI.xml @@ -2,7 +2,7 @@ Prenosi Moje oddajanja - + %s oddaja %s oddaji %s oddaje @@ -10,10 +10,15 @@ Prenos turnirjev v živo Nov prenos v živo + O oddaji + Ni še krogov. + Kako uporabljati Lichess Broadcasts. + Novi krog bo imel iste člane in sodelavce kot prejšnji. Dodajte krog V teku Prihajajoči Zaključeno + Lichess zazna zaključek kroga na podlagi izvornih iger. Uporabite ta preklop, če ni vira. Ime kroga Številka kroga Turnirsko ime @@ -37,4 +42,14 @@ Uredi krog študije Zbrišite ta turnir Dokončno izbrišite celoten turnir, vse njegove kroge in vse njegove igre. + Samodejna lestvica najboljših + Izračunajte in prikažite preprosto lestvico najboljših na podlagi rezultatov igre + Izbirno: zamenjajte imena igralcev, rejtinge in nazive + Ena vrstica na igralca, oblikovana tako: +izvirno ime; Nadomestno ime; Izbirno rejting zamenjave; Izbirno nadomestni naslov +primer: +dr.Nykterstein;Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + Obdobje v sekundah + Izbirno, koliko časa je treba čakati med zahtevami. Najmanj 2 s, največ 60 s. Privzeto je samodejno glede na število gledalcev. diff --git a/translation/dest/broadcast/sq-AL.xml b/translation/dest/broadcast/sq-AL.xml index fdbd61422d715..0946a1d811acd 100644 --- a/translation/dest/broadcast/sq-AL.xml +++ b/translation/dest/broadcast/sq-AL.xml @@ -2,16 +2,21 @@ Transmetime Transmetimet e mia - + %s transmetim %s transmetime Transmetime të drejtpërdrejta turnesh Transmetim i ri i drejtpërdrejtë + Rreth transmetimeve + Ende pa raunde. + Si të përdoren Transmetimet Lichess. + Raundi i ri do të ketë të njëjtën anëtarë dhe kontribues si i mëparshmi. Shtoni një raund Në zhvillim I ardhshëm I mbaruar + Lichess-i e pikas plotësimin e raundit bazuar në lojërat burim. Përdoreni këtë buton, nëse s’ka burim. Emër raundi Numër raundi Emër turneu @@ -34,4 +39,14 @@ Përpunoni analizë raundi Fshije këtë turne Fshihe përfundimisht krejt turneun, krejt raundet e tij dhe krejt lojërat në të. + Tabelë automatike + Harto dhe shfaq një tabelë të thjeshtë bazuar në përfundime lojërash + Opsionale: zëvendësoni emra lojëtarësh, vlerësime dhe tituj + Një rresht për lojtar, formatuar kështu: +Emri origjinal name; Emri zëvendësim; Vlerësim opsional zëvendësim; Titull opsional zëvendësim +Shembull: +DrNykterstein;Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + Periudhë, në sekonda + Opsionale, sa gjatë të pritet mes kërkesash. Min. 2s, maks. 60s. Si parazgjedhje, përdoret vlera automatike bazuar në numrin e parësve. diff --git a/translation/dest/broadcast/sv-SE.xml b/translation/dest/broadcast/sv-SE.xml index 39069b58df58e..2187e62f1605e 100644 --- a/translation/dest/broadcast/sv-SE.xml +++ b/translation/dest/broadcast/sv-SE.xml @@ -2,16 +2,21 @@ Sändningar Mina sändningar - + %s sändning %s sändningar Direktsända turneringar Ny direktsändning + Om sändningar + Inga rundor än. + Hur man använder Lichess-Sändningar. + Den nya rundan kommer att ha samma medlemmar och bidragsgivare som den föregående. Lägg till en omgång Pågående Kommande Slutförda + Lichess upptäcker slutförandet av rundor baserat på källspelen. Använd detta alternativ om det inte finns någon källa. Omgångens namn Omgångens nummer Turneringens namn @@ -35,4 +40,14 @@ Redigera studie för ronden Radera turnering Definitivt radera turnering. + Automatisk topplista + Beräkna och visa en enkel topplista baserat på spelresultat + Valfritt: byt ut spelarnamn, ranking och titlar + En rad per spelare, formaterad som sådan: +Originalnamn; Ersättningsnamn; Valfritt ersättningsranking; Valfri ersättningstitel +Exempel: +DrNykterstein;Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + Period i sekunder + Valfritt, hur länge man väntar mellan förfrågningar. Min 2s, max 60s. Standardvärdet är automatiskt baserat på antalet tittare. diff --git a/translation/dest/broadcast/th-TH.xml b/translation/dest/broadcast/th-TH.xml index d5605bfde0d6a..46fe416b3247f 100644 --- a/translation/dest/broadcast/th-TH.xml +++ b/translation/dest/broadcast/th-TH.xml @@ -4,14 +4,27 @@ บรอดคาสของฉัน การออกอากาศทัวร์นาเมนต์สด ออกอากาศสดใหม่ + เกี่ยวกับการออกอากาศ + ยังไม่มีรอบการแข่ง + วิธีใช้การออกอากาศของ Lichess + รอบใหม่จะมีสมาชิกและผู้มีส่วนร่วมเท่าเดิม ดำเนินอยู่ ใกล้จะถึง เสร็จแล้ว + Lichess ตรวจจับการจบรอบตามเกมต้นฉบับ ใช้สลับนี้หากไม่มีแหล่งที่มา เลขรอบ + ชื่อทัวร์นาเมนต์ + คำบรรยายทัวร์นาเมนต์สั้นๆ คำอธิบายรายการแบบเต็ม คำอธิบายยาวซึ่งไม่บังคับของการออกอากาศ %1$s มีพร้อม ความยาวต้องน้อยกว่า %2$s ตัวอักษร URL ที่ Lichess จะทำโพลเพื่อรับอัพเดท PGN มันต้องเข้าถึงได้อย่างสาธารณะจากอินเทอร์เน็ต วันที่เริ่มในเขตเวลาของคุณ ไม่บังคับ, ถ้าคุณรู้ว่ารายการจะเริ่มเมื่อใด เครดิตแหล่ง + URL การถ่ายทอดสด + URL รอบปัจจุบัน + URL เกมปัจจุบัน + ตารางผู้นำอัตโนมัติ + คำนวณและแสดงกระดานผู้นำอย่างง่ายตามผลลัพธ์ของเกม + ตัวเลือก: แทนที่ชื่อผู้เล่น, เรดติ้ง และ ตำแหน่ง diff --git a/translation/dest/broadcast/tr-TR.xml b/translation/dest/broadcast/tr-TR.xml index 4057859db721c..75fe893cb6669 100644 --- a/translation/dest/broadcast/tr-TR.xml +++ b/translation/dest/broadcast/tr-TR.xml @@ -2,12 +2,14 @@ Canlı Turnuvalar Canlı Turnuvalarım - + %s canlı turnuva %s canlı turnuva Canlı Turnuva Yayınları Canlı Turnuva Ekle + Canlı Turnuvalar hakkında + Lichess Canlı Turnuvaları nasıl kullanılır. Bir tur ekle Devam eden turnuvalar Yaklaşan turnuvalar diff --git a/translation/dest/broadcast/uk-UA.xml b/translation/dest/broadcast/uk-UA.xml index c283c06ba84f7..eb47c0ddc2889 100644 --- a/translation/dest/broadcast/uk-UA.xml +++ b/translation/dest/broadcast/uk-UA.xml @@ -2,7 +2,7 @@ Трансляції Мої трансляції - + %s трансляція %s трансляції %s трансляцій @@ -10,10 +10,15 @@ Онлайн трансляції турнірів Нова трансляція + Про трансляцію + Поки що немає раундів. + Як користуватися Lichess трансляціями. + У новому раунді будуть ті самі учасники та редактори, що й у попередньому. Додати тур Поточні Майбутні Завершені + Lichess виявляє завершення раунду на основі ігор. Використовуйте цей перемикач якщо немає джерела. Назва туру Номер туру Назва турніру @@ -37,4 +42,14 @@ Редагувати дослідження туру Видалити турнір Остаточно видалити весь турнір, всі його раунди та всі його ігри. + Автоматична таблиця лідерів + Розрахунки та показ простої таблиці лідерів на основі результатів гри + Опціонально: замінити імена гравців, рейтинги та заголовки + Один рядок на одного гравця, приклад форматування нижче: +Справжнє ім\'я; ім\'я на заміну; замінити рейтинг (опціонально); замінити звання (опціонально) +Приклад: +DrNykterstein;Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + Період у секундах + Опціонально: час очікування між запитами. Мінімально 2 с, максимально 60 с. За замовчуванням - автоматично, виходячи з кількості запитів глядачів. diff --git a/translation/dest/broadcast/vi-VN.xml b/translation/dest/broadcast/vi-VN.xml index 6e88a905b1fbd..ca99a331a5658 100644 --- a/translation/dest/broadcast/vi-VN.xml +++ b/translation/dest/broadcast/vi-VN.xml @@ -1,16 +1,21 @@ - Phát sóng + Các phát sóng Các buổi phát sóng của tôi - + %s phát sóng Giải đấu phát trực tuyến Phát sóng trực tiếp mới + Giới thiệu về phát sóng + Chưa có vòng nào. + Cách sử dụng Phát sóng Lichess. + Vòng mới sẽ có các thành viên và cộng tác viên giống như vòng trước. Thêm một vòng Đang diễn ra Sắp diễn ra Đã hoàn thành + Lichess phát hiện việc hoàn thành vòng chơi dựa trên ván đấu nguồn. Sử dụng nút chuyển đổi này nếu không có nguồn. Tên vòng Vòng đấu số Tên giải đấu @@ -34,4 +39,14 @@ Chỉnh sửa vòng nghiên cứu Xóa giải đấu này Xóa dứt khoát toàn bộ giải đấu, tất cả các vòng và tất cả ván cờ trong đó. + Bảng xếp hạng tự động + Tính toán và hiển thị bảng xếp hạng đơn giản dựa trên kết quả ván đấu + Tùy chọn: biệt danh, hệ số Elo và danh hiệu + Một dòng cho mỗi người chơi, được định dạng như sau: +Tên khai sinh; Tên thay thế; Hệ số Elo thay thế tùy chọn; Danh hiệu thay thế tùy chọn +Ví dụ: +DrNykterstein;Magnus Carlsen;2863 +AnishGiri;Anish Giri;2764;GM + Khoảng thời gian tính bằng giây + Tùy chọn, thời gian chờ đợi giữa các yêu cầu. Tối thiểu 2 giây, tối đa 60 giây. Mặc định là tự động dựa trên số lượng người xem. diff --git a/translation/dest/broadcast/zh-CN.xml b/translation/dest/broadcast/zh-CN.xml index 98e2017c95f29..755c0e8db619d 100644 --- a/translation/dest/broadcast/zh-CN.xml +++ b/translation/dest/broadcast/zh-CN.xml @@ -2,7 +2,7 @@ 转播 我的直播 - + %s 直播 赛事转播 diff --git a/translation/dest/challenge/an-ES.xml b/translation/dest/challenge/an-ES.xml index 4f595ae0d15af..4ba55264fab4d 100644 --- a/translation/dest/challenge/an-ES.xml +++ b/translation/dest/challenge/an-ES.xml @@ -1,5 +1,6 @@ + Desafíos: %1$s Desafiar a una partida Desafío refusau Desafío acceptau! diff --git a/translation/dest/challenge/br-FR.xml b/translation/dest/challenge/br-FR.xml index a69f4dc84911d..a74bdf1f4ccb9 100644 --- a/translation/dest/challenge/br-FR.xml +++ b/translation/dest/challenge/br-FR.xml @@ -12,11 +12,11 @@ Ne c\'hallit ket daeañ abalamour d\'ar renkadur %s da c\'hortoz. Ne vez ket degemeret daeoù nemet digant mignoned gant %s. Ne zegemeran ket daeoù er mare-mañ. - N\'on ket dijabl evit poent, kasit ur goulenn din en-dro diwezhatoc\'h mar plij ganeoc\'h. - Kasit din un dae renket kentoc\'h, mar plij ganeoc\'h. - Kasit un dae ordin din kentoc\'h mar plij ganeoc\'h. - Ne fell ket din c\'hoari an doare echedoù-mañ evit poent. + N\'on ket dijabl evit poent, kasit ur goulenn din en-dro diwezhatoc\'h mar plij ganeoc\'h. + Kasit din un dae renket kentoc\'h, mar plij ganeoc\'h. + Kasit un dae ordin din kentoc\'h mar plij ganeoc\'h. + Ne fell ket din c\'hoari an doare echedoù-mañ evit poent. Ne zegemeran ket daeoù kaset gant robotoù. Daeoù kaset gant robotoù a zegemeran nemetken. - Pediñ ur c\'hoarier war Lichess mod-all: + Pediñ ur c\'hoarier war Lichess mod-all: diff --git a/translation/dest/challenge/gl-ES.xml b/translation/dest/challenge/gl-ES.xml index 6bf79acc9bdde..cef75706fd3f1 100644 --- a/translation/dest/challenge/gl-ES.xml +++ b/translation/dest/challenge/gl-ES.xml @@ -12,15 +12,15 @@ Non podes desafiar dado que a túa puntuación en %s é provisional. %s só acepta desafíos de amigos. Neste momento non acepto desafíos. - Agora mesmo non podo. Por favor, téntao máis tarde. - Paréceme pouco tempo. Proba cun ritmo máis lento. - Paréceme moito tempo. Proba cun ritmo máis rápido. - Non acepto desafíos a este ritmo. - Por favor, rétame a unha partida puntuada. - Por favor, rétame a unha partida amigable. - Agora mesmo non acepto desafíos en variantes. - Neste momento non me apetece xogar esa variante. + Agora mesmo non podo. Por favor, téntao máis tarde. + Paréceme pouco tempo. Proba cun ritmo máis lento. + Paréceme moito tempo. Proba cun ritmo máis rápido. + Non acepto desafíos a este ritmo. + Por favor, rétame a unha partida puntuada. + Por favor, rétame a unha partida amigable. + Agora mesmo non acepto desafíos en variantes. + Neste momento non me apetece xogar esa variante. Non acepto desafíos de bots. Só acepto desafíos de bots. - Ou convida a un usuario de Lichess: + Ou convida a un usuario de Lichess: diff --git a/translation/dest/challenge/ms-MY.xml b/translation/dest/challenge/ms-MY.xml index 5c7d09716ba64..dc87f8c768240 100644 --- a/translation/dest/challenge/ms-MY.xml +++ b/translation/dest/challenge/ms-MY.xml @@ -11,14 +11,14 @@ Tidak boleh mencabar orang lain kerana penilaian %s hanya sementara. %s hanya menerima cabaran daripada rakan. Saya tidak menerima cabaran pada masa ini. - Ini bukan masa yang sesuai untuk saya, sila tanya lagi nanti. - Kawalan masa ini terlalu cepat untuk saya, sila cabar lagi dengan permainan yang lebih perlahan. - Kawalan masa ini terlalu perlahan untuk saya, sila cabar lagi dengan permainan yang lebih cepat. - Saya tidak menerima cabaran dengan kawalan masa ini. - Sila hantarkan saya dengan cabaran yang dirating. - Sila hantar cabaran santai kepada saya. - Saya tidak menerima cabaran variasi sekarang. - Saya tidak ingin bermain variasi ini sekarang. + Ini bukan masa yang sesuai untuk saya, sila tanya lagi nanti. + Kawalan masa ini terlalu cepat untuk saya, sila cabar lagi dengan permainan yang lebih perlahan. + Kawalan masa ini terlalu perlahan untuk saya, sila cabar lagi dengan permainan yang lebih cepat. + Saya tidak menerima cabaran dengan kawalan masa ini. + Sila hantarkan saya dengan cabaran yang dirating. + Sila hantar cabaran santai kepada saya. + Saya tidak menerima cabaran variasi sekarang. + Saya tidak ingin bermain variasi ini sekarang. Saya tidak menerima cabaran daripada bot. Saya hanya menerima cabaran daripada bot. diff --git a/translation/dest/challenge/tp-TP.xml b/translation/dest/challenge/tp-TP.xml index b5a9f733fd598..30bc07cd0946d 100644 --- a/translation/dest/challenge/tp-TP.xml +++ b/translation/dest/challenge/tp-TP.xml @@ -1,6 +1,6 @@ - utala: %1$s + utala: %1$s wile musi ona li wile ala musi ona li wile musi a! @@ -12,15 +12,15 @@ sina ken ala utala musi e jan. nasin %s la nanpa wawa sina li ken pini. jan pona li ken utala musi e jan %s. sina li jan pona ala. mi wile ala musi lon tenpo ni. - mi ken ala musi lon tenpo ni. toki tawa mi lon tenpo ante. - tenpo musi ni li lili mute tawa mi. musi pi tenpo mute la o utala musi e mi. - tenpo musi ni li suli mute tawa mi. musi pi tenpo lili la o utala musi e mi. - tenpo li ni la mi wile ala musi. - mi wile kama jo e nanpa wawa. sina wile e ni la o utala musi e mi. - mi wile ala kama jo e nanpa wawa. sina wile ala e ni la o utala musi e mi. - mi wile ala musi kepeken nasin ante lon tenpo ni. - mi wile ala musi kepeken nasin ni lon tenpo ni. + mi ken ala musi lon tenpo ni. toki tawa mi lon tenpo ante. + tenpo musi ni li lili mute tawa mi. musi pi tenpo mute la o utala musi e mi. + tenpo musi ni li suli mute tawa mi. musi pi tenpo lili la o utala musi e mi. + tenpo li ni la mi wile ala musi. + mi wile kama jo e nanpa wawa. sina wile e ni la o utala musi e mi. + mi wile ala kama jo e nanpa wawa. sina wile ala e ni la o utala musi e mi. + mi wile ala musi kepeken nasin ante lon tenpo ni. + mi wile ala musi kepeken nasin ni lon tenpo ni. mi wile ala e ni: jan ilo li utala musi e mi. mi wile e ni: jan ilo li utala musi e mi. - anu tawa e jan ilo Lichess: + anu tawa e jan ilo Lichess: diff --git a/translation/dest/challenge/vi-VN.xml b/translation/dest/challenge/vi-VN.xml index 3239f84b497bd..8e39dc857e376 100644 --- a/translation/dest/challenge/vi-VN.xml +++ b/translation/dest/challenge/vi-VN.xml @@ -1,8 +1,8 @@ - Thách đấu (%1$s) + Thách đấu (%1$s) Yêu cầu chơi - Lời thách đấu bị từ chối + Lời thách đấu bị từ chối. Lời thách đấu được chấp nhận! Lời thách đấu bị hủy bỏ. Xin vui lòng đăng ký để gửi những lời thách đấu. @@ -12,15 +12,15 @@ Không thể thách đấu do xếp hạng %s tạm thời. %s chỉ chấp nhận những thách đấu từ bạn bè. Hiện tại tôi đang không chấp nhận thách đấu. - Tôi chưa sẵn sàng, hãy hỏi lại sau. - Tùy chọn thời gian quá nhanh đối với tôi, hãy thách đấu lại với một tùy chọn chậm hơn. - Tùy chọn thời gian quá chậm đối với tôi, hãy thách đấu lại với một tùy chọn nhanh hơn. - Tôi không chấp nhận thách đấu với tùy chọn thời gian này. - Hãy gửi yêu cầu thách đấu có tính xếp hạng cho tôi. - Hãy gửi tôi yêu cầu thách đấu không tính elo. - Tôi không muốn chơi biến thể bây giờ. - Tôi chưa sẵn sàng chơi biến thể này bây giờ. + Tôi chưa sẵn sàng, hãy hỏi lại sau. + Tùy chọn thời gian quá nhanh đối với tôi, hãy thách đấu lại với một tùy chọn chậm hơn. + Tùy chọn thời gian quá chậm đối với tôi, hãy thách đấu lại với một tùy chọn nhanh hơn. + Tôi không chấp nhận thách đấu với tùy chọn thời gian này. + Hãy gửi yêu cầu thách đấu có tính xếp hạng cho tôi. + Hãy gửi tôi yêu cầu thách đấu không xếp hạng. + Tôi không muốn chơi biến thể bây giờ. + Tôi chưa sẵn sàng chơi biến thể này bây giờ. Tôi không chấp nhận thách đấu từ bot. Tôi chỉ chấp nhận thách đấu từ bot. - Hoặc mời một Người dùng Lichess: + Hoặc mời một người dùng Lichess: diff --git a/translation/dest/challenge/zh-TW.xml b/translation/dest/challenge/zh-TW.xml index d36b2ef0ce42f..c613fd83462ec 100644 --- a/translation/dest/challenge/zh-TW.xml +++ b/translation/dest/challenge/zh-TW.xml @@ -1,5 +1,6 @@ + 挑戰: %1$s 邀請對弈 對弈邀請已拒絕 對弈邀請已接受 @@ -21,4 +22,5 @@ 我現在不想玩這個變體。 我不接受機器人的對弈。 我目前只接受機器人的對弈。 + 或邀請一位 Lichess 用户: diff --git a/translation/dest/class/gl-ES.xml b/translation/dest/class/gl-ES.xml index 0fde6bbbeac10..88406933a65f5 100644 --- a/translation/dest/class/gl-ES.xml +++ b/translation/dest/class/gl-ES.xml @@ -100,7 +100,7 @@ Aquí está a ligazón para acceder. Separa as novas con --- Amosará unha liña separadora horizontal. Convidar - Fuches convidado por %s. + Fuches convidado por %s. Aceptaches esta invitación. Rexeitaches esta invitación. ou diff --git a/translation/dest/class/gsw-CH.xml b/translation/dest/class/gsw-CH.xml index bbd4e1ed9f6c5..6b9b6e11b8ef6 100644 --- a/translation/dest/class/gsw-CH.xml +++ b/translation/dest/class/gsw-CH.xml @@ -100,7 +100,7 @@ Da isch de Link für de Zuegriff uf die Klass. Tränn Neuigkeite mit --- Es wird als horizontali Linie azeigt. ilade - Du bisch iglade vu %s. + Du bisch iglade vu %s. Du häsch die Iladig agnah. Du häsch die Iladig abglehnt. oder diff --git a/translation/dest/class/kn-IN.xml b/translation/dest/class/kn-IN.xml index 9437cb1c06cd6..489113ece74e4 100644 --- a/translation/dest/class/kn-IN.xml +++ b/translation/dest/class/kn-IN.xml @@ -100,7 +100,7 @@ ಇದರೊಂದಿಗೆ ಪ್ರತ್ಯೇಕ ಸುದ್ದಿ --- ಇದು ಸಮತಲ ರೇಖೆಯನ್ನು ಪ್ರದರ್ಶಿಸುತ್ತದೆ. ಆಹ್ವಾನಿಸಿ - ನಿಮ್ಮನ್ನು %s ಅವರು ಆಹ್ವಾನಿಸಿದ್ದಾರೆ. + ನಿಮ್ಮನ್ನು %s ಅವರು ಆಹ್ವಾನಿಸಿದ್ದಾರೆ. ನೀವು ಈ ಆಹ್ವಾನವನ್ನು ಒಪ್ಪಿಕೊಂಡಿದ್ದೀರಿ. ನೀವು ಆಹ್ವಾನವನ್ನು ನಿರಾಕರಿಸಿದ್ದೀರಿ. ಅಥವಾ diff --git a/translation/dest/class/lb-LU.xml b/translation/dest/class/lb-LU.xml index b13eb9da9330f..b8415497bb1d2 100644 --- a/translation/dest/class/lb-LU.xml +++ b/translation/dest/class/lb-LU.xml @@ -92,7 +92,7 @@ Hei ass de Link fir op de Cours zouzegräifen. Trenn déi verschidden Nouvellë mat --- Doduerch gëtt eng hotizontal Linn ugewisen. Invitéieren - Du goufs vum %s invitéiert. + Du goufs vum %s invitéiert. Du hues dës Invitatioun ugeholl. Du hues dës Invitatioun ofgeleent. oder diff --git a/translation/dest/class/pt-PT.xml b/translation/dest/class/pt-PT.xml index bba7c02da7750..2f44d94833fa7 100644 --- a/translation/dest/class/pt-PT.xml +++ b/translation/dest/class/pt-PT.xml @@ -99,7 +99,7 @@ Aqui está o link para acederes à aula. Adiciona as novidades recentes no topo. Não excluas novidades anteriores. Separa as novidades com --- tal irá mostrar uma linha horizontal. Convidar - Foi convidado por %s. + Foi convidado por %s. Aceitou este convite. Recusou este convite. ou diff --git a/translation/dest/class/sk-SK.xml b/translation/dest/class/sk-SK.xml index d805256687caa..78df55878152f 100644 --- a/translation/dest/class/sk-SK.xml +++ b/translation/dest/class/sk-SK.xml @@ -24,7 +24,7 @@ Viditeľné aj pre učiteľov aj pre študentov triedy Učitelia triedy Pridať Lichess užívateľov za účelom ich pozvania ako učiteľov. Jeden na riadok. - Obnoviť heslo + Vynulovať heslo Teraz si zkopírujte alebo zapíšte heslo! Nebude možné aby ste ho videli znovu. Heslo: %s Vygenerovať nové heslo pre študenta diff --git a/translation/dest/class/th-TH.xml b/translation/dest/class/th-TH.xml index f1dc29f5cad40..b79c5329602c2 100644 --- a/translation/dest/class/th-TH.xml +++ b/translation/dest/class/th-TH.xml @@ -25,12 +25,32 @@ ให้แน่ใจว่าคุณคัดลอกหรือเขียนพาสเวิร์ดไปแล้ว คุณจะไม่สามารถเห็นพาสเวิร์ดได้อีก พาสเวิร์ด %s ชื่อจริง + เพิ่มนักเรียน + โปรไฟล์ Lichess %1$s ถูกสร้างสำหรับ %2$s + นักเรียน: %1$s +ชื่อผู้ใช้: %2$s +รหัสผ่าน: %3$s + เชิญบัญชี Lichess + ถ้านักเรียนของคุณมีบัญชี Lichess แล้ว คุณสามารถเชิญพวกเขาเข้าชั้นเรียนได้ + เขาจะได้รับข้อความจาก Lichess พร้อมลิ้งค์เข้าร่วมชั้นเรียน + โปรดเชิญเฉพาะคนที่คุณรู้จักและคนที่ต้องการเข้าร่วมการศึกษานี้จริงๆ สร้างบัญชี Lichess ใหม่ + ชี่อผู้ใช้ Lichess + สร้างชื่อผู้ใช้ใหม่ + ยังไม่มีนักเรียนในชั้นเรียน อัตราชนะ ภาพรวม ข่าวสาร + แก้ไขข่าวสาร + แจ้งเตือนนักเรียนทุกคน ยังไม่มีอะไรที่นี่ + เชิญ + คุณได้รับคำเชิญจาก %s + คุณได้รับคำเชิญ + คุณได้ปฎิเสธคำเชิญ หรือ สร้างบัญชี Lichess หลายบัญชีในครั้งเดียว ใช้แบบฟอร์มนี้ + สร้างชั้นเรียนเพิ่ม + ส่งคำเชิญให้ %s แล้ว diff --git a/translation/dest/class/vi-VN.xml b/translation/dest/class/vi-VN.xml index 6e9eb7c9799cd..0fe29a2e8e5e6 100644 --- a/translation/dest/class/vi-VN.xml +++ b/translation/dest/class/vi-VN.xml @@ -12,7 +12,7 @@ Giáo viên: %s Lớp học mới Đóng lớp học - Đã xóa bởi %s + Đã bị xóa bởi %s Mở lại Loại bỏ học viên Đã loại bỏ @@ -25,12 +25,12 @@ Giáo viên của lớp Thêm tên người dùng Lichess để mời họ làm giáo viên. Mỗi người một dòng. Đặt lại mật khẩu - Hãy chắc chắn sao chép hoặc ghi lại mật khẩu ngay bây giờ. Bạn sẽ không thể nhìn thấy nó sau này nữa! + Hãy chắc chắn sao chép hoặc ghi lại mật khẩu ngay bây giờ. Bạn sẽ không bao giờ có thể nhìn thấy nó nữa! Mật khẩu: %s Tạo mật khẩu mới cho học viên %1$s được mời bởi %2$s Tên thật - Riêng tư. Sẽ không bao giờ được hiển thị bên ngoài lớp học. Giúp nhớ học viên là ai. + Riêng tư. Sẽ không bao giờ được hiển thị bên ngoài lớp học. Giúp bạn nhớ học viên là ai. Thêm học viên Đã tạo %1$s làm hồ sơ Lichess cho %2$s. Học viên:%1$s @@ -39,7 +39,7 @@ Mật khẩu: %3$s Mời một tài khoản Lichess Nếu học viên đã có tài khoản Lichess, bạn có thể mời họ vào lớp. Họ sẽ nhận được một tin nhắn trên Lichess với một liên kết để tham gia lớp học. - Quan trọng: chỉ mời những sinh viên mà bạn biết và là những người thực sự muốn tham gia lớp học. + Quan trọng: chỉ mời những học viên mà bạn biết và là những người thực sự muốn tham gia lớp học. Không bao giờ gửi lời mời không mong muốn đến người chơi tùy ý. Tạo một tài khoản Lichess mới Nếu học viên chưa có tài khoản Lichess, bạn có thể tạo một tài khoản cho họ tại đây. @@ -48,7 +48,7 @@ Mật khẩu: %3$s Nếu họ đã có, hãy sử dụng biểu mẫu mời thay thế. Chỉ được tạo tài khoản cho học sinh có thật. Không được lợi dụng để tạo nhiều tài khoản cho bản thân. Bạn sẽ bị ban. Tên người dùng Lichess - Tạo một tài khoản mới + Gợi ý tạo một tên người dùng mới Chào mừng đến với lớp học của bạn: %s. Đây là đường link để tham gia lớp học. Bạn được mời đến vào lớp học \"%s\" với tư cách là một học viên. @@ -58,12 +58,12 @@ Mật khẩu: %3$s Chỉ hiển thị cho các giáo viên Hoạt động - Quản lý + Được quản lý Tài khoản học viên này đã được quản lý - Nâng cấp từ được quản lý lên tự chủ + Nâng cấp từ bị quản lý sang tự chủ Bản phát hành Giải phóng tài khoản để học viên có thể quản lý nó một cách tự chủ. - Một tài khoản đã được giải phóng không thể được quản lý lại. Học viên sẽ có thể chuyển đổi chế độ trẻ em và tự đặt lại mật khẩu. + Một tài khoản đã được tốt nghiệp không thể được quản lý lại. Học viên sẽ có thể chuyển đổi chế độ trẻ em và tự đặt lại mật khẩu. Học viên sẽ vẫn ở trong lớp sau khi tài khoản của họ được giải phóng. Địa chỉ email thực, duy nhất của học viên. Chúng tôi sẽ gửi email xác nhận tới đó, kèm theo liên kết để giải phóng tài khoản. Đóng tài khoản @@ -79,11 +79,11 @@ Mật khẩu: %3$s Học viên Tiến trình - Không có học viên nào trong lớp. - Không học viên nào được loại bỏ. - Qua nhiều ngày + Chưa có học viên nào trong lớp. + Không có học viên nào bị loại bỏ. + Qua số ngày Thời gian chơi - %1$s qua %2$s cuối + %1$s qua %2$s qua Tỉ lệ thắng Không xác định Tổng quan @@ -91,12 +91,12 @@ Mật khẩu: %3$s Tin tức mới về lớp học Sửa tin tức Thông báo tất cả học viên - Không có gì ở đây. + Chưa có gì ở đây. Tất cả các tin tức lớp học trong một lĩnh vực đơn lẻ. Thêm những tin tức mới nhất lên đầu trang. Đừng xóa tin tức trước đó. Phân cách tin tức bởi --- nó sẽ hiển thị một đường ngang. Mời - Bạn được mời bởi %s. + Bạn được mời bởi %s. Bạn đã chấp nhận lời mời. Bạn đã từ chối lời mời. hoặc diff --git a/translation/dest/coach/ta-IN.xml b/translation/dest/coach/ta-IN.xml index 3fdbc5eee1363..830aa97665217 100644 --- a/translation/dest/coach/ta-IN.xml +++ b/translation/dest/coach/ta-IN.xml @@ -1,4 +1,27 @@ + லிசெஸ் பயிற்சியாளர்கள் + லிசெஸ் பயிற்சியாளர் + நீங்கள் %s உடன் சிறந்த சதுரங்க பயிற்சியாளரா? + NM அல்லது FIDE தலைப்பு + உங்கள் தலைப்பை இங்கே உறுதிப்படுத்தவும், உங்கள் விண்ணப்பத்தை மதிப்பாய்வு செய்வோம். + %s இல் எங்களுக்கு மின்னஞ்சல் அனுப்பவும், நாங்கள் உங்கள் விண்ணப்பத்தை மதிப்பாய்வு செய்வோம். + இடம் + மொழி மதிப்பீடு + நேர விலை + அணுகத் தக்க தன்மை + மாணவர்களை ஏற்றுக்கொள்வது + தற்போது மாணவர்களை ஏற்றுக்கொள்ளவில்லை + %s சதுரங்க மாணவர்களுக்குப் பயிற்சி அளிக்கிறார் + %s லிசெஸ் சுயவிவரத்தைப் பார்க்கவும் + தனிப்பட்ட செய்தியை அனுப்பவும் + என்னை பற்றி + விளையாடிய அனுபவம் + கற்பித்தல் அனுபவம் + மற்ற அனுபவங்கள் + சிறந்த திறமைகள் + கற்பித்தல் முறை + பொது படிப்புகள் + YouTube காணொளிகள் diff --git a/translation/dest/coach/vi-VN.xml b/translation/dest/coach/vi-VN.xml index 107eb1a6081f7..f9071ed1d2e66 100644 --- a/translation/dest/coach/vi-VN.xml +++ b/translation/dest/coach/vi-VN.xml @@ -14,14 +14,14 @@ Đang nhận học viên Hiện tại không nhận học viên %s huấn luyện học viên - Xem hồ sơ của %s + Xem hồ sơ Lichess của %s Gửi tin nhắn riêng - Thông tin về tôi + Giới thiệu và các thông tin về tôi Kinh nghiệm chơi cờ Kinh nghiệm dạy cờ Kinh nghiệm khác Những kỹ năng tốt nhất Phương pháp giảng dạy - Nghiên cứu công khai + Nghiên cứu công khai Các video trên YouTube diff --git a/translation/dest/contact/be-BY.xml b/translation/dest/contact/be-BY.xml index 845b94d48db0e..6ae22945e9543 100644 --- a/translation/dest/contact/be-BY.xml +++ b/translation/dest/contact/be-BY.xml @@ -55,6 +55,9 @@ У пэўных абставінах, калі гуляючы супраць уліковых запісаў ботаў, рэйтангавая партыя можа не прыводзіць да змены рэйтынгу. Калі ўстаноўлена, што гулец злоўжывае бота дзеля павялячэння рэйтынгу. Старонка памылкі Калі вы траміце на старонку з памылкай, вы можаце паведаміць пра яе: + Я хачу трансляваць турнір + Дазнайцеся, як карыстацца трансляцыямі Lichess + Вы можаце звязацца з камандай трансляцый пра афіцыйныя эфіры. Абскарджанне бана ўліковага запісу ці абмежавання IP-адраса Выкарыстанне рухавіка або несумленная гульня Вы можаце накіраваць апеляцыю праз %s. diff --git a/translation/dest/contact/gl-ES.xml b/translation/dest/contact/gl-ES.xml index 7168c021aa59b..9088e9cf6d4f4 100644 --- a/translation/dest/contact/gl-ES.xml +++ b/translation/dest/contact/gl-ES.xml @@ -51,7 +51,7 @@ Segundo as Leis do Xadrez da FIDE (§6.9), se un xaque mate é posible con calquera secuencia legal de movementos, entón o a partida non son táboas É posible dar xaque mate con só un cabalo ou un alfil se o opoñente posúe algunha peza máis no taboleiro. Non se concederon puntos - Asegúrate que xogaches unha partida puntuada. Partidas amistosas non afectan á puntuación dos xogadores/as. + Asegúrate de que xogaches unha partida puntuada. As partidas amistosas non afectan á puntuación dos xogadores. Ó xogar contra unha conta de bot, pode que unha partida puntuada non se contabilice se se determina que o xogador abusa do bot para conseguir puntos. Páxina de erro Se unha páxina dá erro, podes reportala: diff --git a/translation/dest/contact/gsw-CH.xml b/translation/dest/contact/gsw-CH.xml index 72f753fc111f3..0e6becb19946b 100644 --- a/translation/dest/contact/gsw-CH.xml +++ b/translation/dest/contact/gsw-CH.xml @@ -56,8 +56,8 @@ Fählersite Wänn du e Fählersite entdeckt häsch, chasch sie mälde: Ich wott es Turnier überträge - Lern wie du - uf Lichess - dini eigeni Sändig machsch - Du chasch - für offizielli Sändige - au s\'broadcast-team kontaktiere. + Lern wie du - uf Lichess - dini eigeni Überträgig machsch + Du chasch - für offizielli Überträgige - au s\'Broadcast-Team kontaktiere. Ischpruch gäge en Usschluss oder IP-Beschränkig Markierig vu Computer Underschtützig oder Betrug Du chasch en Ischpruch a %s sände. diff --git a/translation/dest/contact/lb-LU.xml b/translation/dest/contact/lb-LU.xml index 0d481162f181b..f4c9369168088 100644 --- a/translation/dest/contact/lb-LU.xml +++ b/translation/dest/contact/lb-LU.xml @@ -23,8 +23,18 @@ Géi op dës Säit, fir d\'Grouss- a Klengschreiwung vun dengem Benotzernumm ze änneren Ech wëll mäin Verlaf oder meng Wäertung läschen Ech wëll een Spiller mellen + Fir ee Spiller ze mellen, benotz de Mellformulaire + Du kënns och op déi Säit, andeems de op den %s-Mell-Knäppchen op enger Profilsäit klicks. + Mell keng Spiller am Forum. + Schéck eis keng Mell-E-Mailen. + Schéck wgl. keng direkt Messagen un d\'Moderatoren. + Just d\'Melle vu Spiller iwwert de Mellformulaire hëlleft eppes. Ech wëll een Bug mellen + An der Lichess-Feedback-Sektioun vum Forum + Als e Lichess-Websäiteproblem op GitHub + Als e Lichess-Mobil-Applikatiouns-Problem op GitHub Am Lichess-Discordserver + Beschreif wgl., wéi de Feeler ausgesäit, wat s de amplaz erwaart hues a wat ee maache muss, fir de Feeler ze reproduzéieren. Illegalen Bauerenschlagzuch Dat gëtt „en passant“ genannt an ass eng vun de Reegele vum Schach. Probéiert dëst klengt, interaktiivt Spill fir méi iwwer „en passant“ ze léieren. @@ -43,6 +53,7 @@ Asproch géint een Bann oder eng IP Restriktioun Engine- oder Bedruchsmarkéierung Du kanns däin Asproch un %s schécken. + Falsch-Positiv-Resultater kënnen heiansdo virkommen a mir entschëllegen eis dofir. Aner Aschränkung Zesummenaarbecht, Legales, Kommerzielles Lichess monetiséieren diff --git a/translation/dest/contact/tp-TP.xml b/translation/dest/contact/tp-TP.xml index 9f2257fe697db..c8c9adc8c53e1 100644 --- a/translation/dest/contact/tp-TP.xml +++ b/translation/dest/contact/tp-TP.xml @@ -3,6 +3,15 @@ toki o toki tawa jan pi ilo Lichess mi wile e ni: o pana lukin e nimi wawa mi lon lipu \"Lichess\" + mi wile pini e lipu mi + sina ken pini e lipu sina lon lipu ni + mi wile toki e jan ike tawa sina + mi wile toki e pakala tawa sina + lon ma Siko pi lipu Lichess + o toki lon ni tu wan: +pakala ni li seme? +pakala ni li weka la ilo li pali ante seme? +nasin seme la mi ken kama sin e pakala ni? ijo musi li ken anpa ike e ijo \"jan utala\" anpa ni li \"anpa tawa\" (\"en passant\"). ona li pona tawa nasin musi. sina wile kama sona e \"anpa tawa\", o lukin e musi lili ni. diff --git a/translation/dest/contact/vi-VN.xml b/translation/dest/contact/vi-VN.xml index ba5a32b1fae3c..2de7f8d01eb19 100644 --- a/translation/dest/contact/vi-VN.xml +++ b/translation/dest/contact/vi-VN.xml @@ -23,7 +23,7 @@ Tôi muốn thay đổi tên đăng nhập của mình Xem trang này để thay đổi chữ cái tên người dùng Chúng tôi không thể đổi nhiều chữ cái hơn. Vì lý do kỹ thuật, điều đó dứt khoát không thể. - Tuy nhiên, bạn có thể đóng tài khoản hiện tại của mình, và tạo một tài khoản mới. + Tuy nhiên, bạn có thể đóng tài khoản hiện tại của mình và tạo một tài khoản mới. Tôi muốn xóa lịch sử hoặc hệ số Bạn không thể xóa lịch sử các ván đấu, câu đố hay hệ số. Tôi muốn báo cáo một người chơi @@ -32,7 +32,7 @@ Vui lòng không báo cáo người chơi trong diễn đàn. Vui lòng không gửi cho chúng tôi email báo cáo. Vui lòng không gửi tin nhắn trực tiếp đến các quản trị viên. - Chỉ những báo cáo người chơi thông qua biểu mẫu báo cáo mới có hiệu quả. + Chỉ báo cáo những kỳ thủ thông qua biểu mẫu báo cáo mới có hiệu lực. Tôi muốn gửi báo cáo về một lỗi Trong diễn đàn phản hồi Lichess Bằng một vấn đề (issue) về trang web Lichess trên Github @@ -46,13 +46,13 @@ Nhập thành chỉ bị cấm khi vua đi qua ô bị đối phương kiểm soát. Hãy chắc việc bạn hiểu luật nhập thành Thử trò chơi tương tác nhỏ này để thực hành về nhập thành trong cờ vua - Nếu bạn nhập ván đấu, hoặc bắt đầu từ một vị trí ván đấu, đảm bảo rằng bạn đặt đúng quyền nhập thành. + Nếu bạn nhập ván đấu hoặc bắt đầu từ một thế cờ, đảm bảo rằng bạn đặt đúng quyền nhập thành. Không đủ quân để chiếu hết Theo Luật cờ vua của FIDE §6.9, nếu tình huống chiếu hết có thể xảy ra với bất kì thứ tự nước đi hợp lệ nào sau đó, ván cờ không được tính là hòa Có thể xẩy ra trường hợp chiếu hết chỉ với một quân mã hoặc một quân tượng, nếu đối thủ có nhiều hơn chỉ có vua trên bàn cờ. Không có hệ số Elo nào được tăng Xin hãy chắc việc bạn chơi ván cờ xếp hạng. Các ván đấu thường sẽ không ảnh hưởng đến hệ số của người chơi. - Trong một số trường hợp nhất định khi chơi với tài khoản bot, trò chơi được xếp hạng có thể không trao điểm nếu xác định được rằng người chơi đang lạm dụng bot để lấy điểm xếp hạng. + Trong một số trường hợp nhất định khi chơi với tài khoản Bot, ván cờ có xếp hạng có thể không trao điểm nếu xác định được rằng người chơi đang lạm dụng Bot để lấy điểm xếp hạng. Trang lỗi Nếu bạn gặp một trang lỗi, bạn có thẻ báo cáo nó: Tôi muốn phát sóng một giải đấu @@ -64,7 +64,7 @@ Những giá trị dương do đánh giá sai đôi khi vẫn xẩy ra và chúng tôi xin thứ lỗi vì điều đó. Nếu khiếu nại của bạn hợp lệ, chúng tôi sẽ dỡ bỏ lệnh cấm sớm nhất có thể. Tuy nhiên nếu bạn quả thực dùng máy tính trợ giúp, dù chỉ một lần, tài khoản của bạn sẽ bị mất. - Đừng phủ nhận đã lừa dối. Nếu bạn muốn được phép tạo một tài khoản mới, chỉ cần thừa nhận những gì bạn đã làm và cho thấy rằng bạn hiểu rằng đó là một sai lầm. + Đừng phủ nhận đã gian lận. Nếu bạn muốn được phép tạo một tài khoản mới, chỉ cần thừa nhận những gì bạn đã làm và chứng tỏ bạn hiểu rằng đó là một sai lầm. Hạn chế khác Cộng tác, hợp pháp, thương mại Kiếm tiền với Lichess diff --git a/translation/dest/coordinates/vi-VN.xml b/translation/dest/coordinates/vi-VN.xml index 1fddd0f9449bd..3366fa4d5e225 100644 --- a/translation/dest/coordinates/vi-VN.xml +++ b/translation/dest/coordinates/vi-VN.xml @@ -4,7 +4,7 @@ Luyện tập tầm nhìn Điểm trung bình khi cầm quân trắng: %s Điểm trung bình khi cầm quân đen: %s - Biết tọa độ bàn cờ là kỹ năng rất quan trọng khi chơi cờ: + Biết tọa độ bàn cờ là một kỹ năng rất quan trọng vì nhiều lý do: Hầu hết các khóa học và bài tập cờ vua dùng ký hiệu đại số rộng rãi. Nó giúp bạn cờ dễ nói chuyện với nhau hơn, vì cả hai đều hiểu \"ngôn ngữ cờ vua\". Bạn sẽ phân tích ván cờ hiệu quả hơn nếu bạn có thể tìm ra ngay ô cờ từ tọa độ. diff --git a/translation/dest/dgt/ar-SA.xml b/translation/dest/dgt/ar-SA.xml index 3ea04e700dfa8..0c6f274d12115 100644 --- a/translation/dest/dgt/ar-SA.xml +++ b/translation/dest/dgt/ar-SA.xml @@ -1,2 +1,48 @@ - + + رقعة DGT + ليتشيس & DGT + متطلبات رقعة DGT + قيود استخدام رقعة DGT + هذه الصفحة تسمح لك بربط رقعة DGT الخاصة بك في ليتشيس، واستخدامها لعرض المباريات عليه. + لربط رقعة DGT الإلكترونية، يجب عليك أن تثبت%s. + يمكنك تنزيل البرنامَج من هنا:%s. + إذا كان %1$s يعمل على هذا الحاسوب، يمكنك التحقق من اتصالك به بواسطة%2$s. + فتح هذا الرابط + إذا كان %1$s يعمل على جهاز أو شبكة أخرى، يجب أن تعين عنوان IP والمنفذ في%2$s. + قسم الإعدادات + يجب أن تظل صفحة التشغيل مفتوحة على متصفحك.، ليس من الضروري أن تكون ظاهرة، يمكنك تصغيرها أو وضعها جنبًا إلى جنب مع صفحة المباراة على Lichess، لكن لا تغلقها وإلا ستتوقف رقعتك عن العمل. + الرقعة ستتصل تِلْقائيًا بأي مباراة تلعب، أو تبدأ، كما يمكنها تحديد المباريات التي ستبدأ قريبا. + يمكن ضبط الوقت في المباريات غير المقيمة على: قياسي، والسريع والمراسلة فقط. + يمكن ضبط الوقت في المباريات المقيمة على: القياسي، والمراسلة وبعض فئات الشطرنج السريع مثل: 10+15 و0+20 + عندما تكون رقعتك جاهزة، انقر على%s. + إذا لم تكتشف الرقعة النقلة + تأكد أن خصمك لعب نقلته على رقعة DGT أولاً، ثم أعد لعب نقلتك، إذا لم تحل المشكلة، اعد المباراة من البداية. + كملاذ أخير، اعد اللوحة بشكل متطابق مع Lichess، ثم %s + أعد تحويل هذه الصفحة + تهيئة رقعة DGT + الربط بليتشيس + يجب أن يكون لديك رمز OAuth مناسب لتشغيل رقعة DGT. + يضيف إدخال %s المباراة إلى أعلى قائمة المباريات الخاصة بك. + لم ينشأ رمز OAuth. + اتصال لوحة DGT + اضغط لإنشاء رمز + %s رابط الويب + استخدم \"%1$s\" إذا لم يكن %2$s يعمل على جهاز آخر أو شبكة أخرى. + تحويل النص إلى كلام + إصدار صوت ينطق بالحركات التي تلعب، مما يساعدك على التركيز في حركاتك. + تمكين ربط الكلام + صوت الكلام + نطق كل الحركات + اختر نعم لنطق نقلاتك ونقلات خصمك، حدد لا لنطق نقلات خصمك فقط. + طريقة نطق الحركة + SAN هو نظام التدوين على Lichess مثل Nf6. +UCI شائع في المحركات مثل \"g8f6\". + الكلمات المفتاحية + الكلمات المفتاحية بتنسيق JSON. تستخدم لترجمة التحركات والنتائج إلى لغتك، اللغة الافتراضية هي الإنجليزية، ولكن لا تتردد في تغييرها. + تصحيح الأخطاء + التسجيل المطول + لرؤية رسالة وحدة التحكم اضغط Command + Option + C (ماك) أو Control + Shift + C (ويندوز، لينكس ،كروم OS) + اللعب على رقعة DGT + تهيئة + diff --git a/translation/dest/dgt/cs-CZ.xml b/translation/dest/dgt/cs-CZ.xml index c54cd11c08fc6..ef0edcf497426 100644 --- a/translation/dest/dgt/cs-CZ.xml +++ b/translation/dest/dgt/cs-CZ.xml @@ -1,12 +1,31 @@ + DGT šachovnice + Lichess a DGT + Požadavky na připojení DGT šachovnice + Omezení DGT šachovnice Tato stránka umožňuje připojení DGT desky k Lichess a její použití pro hraní her. + Pro připojení k DGT elektronické šachovnice si musíte nainstalovat %s. + Software si můžete stáhnout zde: %s. + Pokud %1$s běží na tomto počítači, můžete zkontrolovat připojení %2$s. otevírám tento odkaz + Pokud %1$s běží na jiném stroji nebo jiném portu, musíte nastavit IP adresu a port zde v %2$s. + sekci konfigurace + Stránka přehrávání musí zůstat otevřená ve vašem prohlížeči. Nemusí být viditelná (můžete jej minimalizovat nebo posunout vedle stránky se samotnou hrou), ale stránku nezavírejte nebo deska přestane fungovat. + Šachovnice se automaticky připojí k jakékoli hře, která již probíhá, nebo k jakékoli nové hře, která začne. Možnost vybrat si, kterou hru chcete hrát, bude brzy k dispozici. + Pokud není tah rozpoznán + Nejprve zkontrolujte, zda jste na šachovnici DGT provedli tah svého soupeře. Vraťte svůj tah. Hrajte znovu. + Jako poslední možnost, nastavte šachovnici do stejné pozice jako na Lichess, pak %s Obnovit tuto stránku DGT - Konfigurace Připojení k Lichess + Máte vhodný OAuth token pro hraní na DGT šachovnici. + Nebyl vytvořen žádný vhodný OAuth token. + Připojení DGT šachovnice + Kliknutím vygenerujete Převod textu na řeč Povolit syntézu řeči Hlas pro syntézu řeči Oznámit všechny tahy + Konfigurace diff --git a/translation/dest/dgt/de-DE.xml b/translation/dest/dgt/de-DE.xml index 565192e18b000..2324c4ac1f99d 100644 --- a/translation/dest/dgt/de-DE.xml +++ b/translation/dest/dgt/de-DE.xml @@ -1,6 +1,6 @@ - DGT-Schachbrett + DGT-Brett Lichess & DGT DGT-Brett-Anforderungen DGT-Brett-Beschränkungen @@ -8,18 +8,18 @@ Um das elektronische DGT-Brett zu verbinden, musst du %s installieren. Du kannst die Software hier herunterladen: %s. Falls %1$s auf diesem Computer läuft, kannst du die Verbindung mittels %2$s überprüfen. - Öffne diesen Link + Öffnen dieses Links Falls %1$s auf einem anderen System oder Port läuft, musst du die IP-Adresse und Port hier in %2$s eingeben. Konfigurations-Abschnitt Die Partieseite muss in deinem Browser geöffnet bleiben. Sie muss nicht sichtbar sein, du kannst sie minimieren oder neben die Lichess Partieseite ziehen, jedoch nicht schließen, da ansonsten das Brett nicht mehr funktionieren wird. Das Brett wird sich automatisch zu der Partie verbinden, die gerade im Gange ist, oder zu einer neuen Partie, sobald diese gestartet wurde. Die Möglichkeit auszuwählen, welcher Partie beigetreten werden soll, folgt in Kürze. Die möglichen Zeitkontrollen für ungewertete Partien sind: Klassisch, Fernschach und Schnellschach. - Die Zeitkontrollen für gewertete Partien sind: Klassisch, Fernschach und einige Schnellschachformate wie 15+10 und 20+0 + Zeitkontrollen für gewertete Partien: Klassisch, Fernschach und einige Schnellschachvarianten einschließlich 15+10 und 20+0 Wenn du bereit bist, stelle dein Brett auf und klicke auf %s. Fallls ein Zug nicht erkannt wurde Überprüfe, ob du den Zug deines Gegners auf dem DGT-Brett bereits ausgeführt hast. Nimm deinen Zug zurück. Ziehe erneut. Als letztes Hilfsmittel, stelle dein Brett identisch zu dem von Lichess auf, dann %s - Seitenansicht neu laden + Diese Seite neu laden DGT - Konfigurieren Lichess Konnektivität Du hast einen OAuth Schlüssel, der für DGT-Partien geeignet ist. @@ -28,9 +28,9 @@ DGT-Brett-Verbindung Klicke, um einen zu generieren %s WebSocket-URL - Benutze \"%1$s\" wenn %2$s nicht auf einem anderen Rechner oder einem anderen Gerät läuft. + Benutze \"%1$s\" wenn %2$s nicht auf einem anderen Rechner oder einem anderen Port läuft. Text-zu-Sprache - Konfiguriere die Sprachausgabe des gespieten Zugs, damit du deine Augen auf dem Brett behalten kannst. + Konfiguriere die Sprachausgabe der gespielten Züge, damit du deine Augen auf dem Brett behalten kannst. Sprachsynthese aktivieren Stimme der Sprachsynthese Alle Züge vorlesen diff --git a/translation/dest/dgt/th-TH.xml b/translation/dest/dgt/th-TH.xml index 3ea04e700dfa8..12488f33b205e 100644 --- a/translation/dest/dgt/th-TH.xml +++ b/translation/dest/dgt/th-TH.xml @@ -1,2 +1,5 @@ - + + การแปลงข้อความเป็นเสียง + คีย์เวิร์ด + diff --git a/translation/dest/dgt/tr-TR.xml b/translation/dest/dgt/tr-TR.xml index 3a8d8e735c194..82e0d9a67129a 100644 --- a/translation/dest/dgt/tr-TR.xml +++ b/translation/dest/dgt/tr-TR.xml @@ -1,5 +1,28 @@ + DGT tahtası + Lichess ve DGT + Programı indirmek için tıklayın: %s. + Eğer %1$s bilgisayarınızda çalışıyorsa, kontrol etmek için %2$s. + bu bağlantıyı açın + Eğer %1$s farklı bir bilgisayarda veya portta çalışıyorsa, IP adresini ve portu %2$s ayarlamanız gerekiyor. + Yapılandırma Sekmesinde + \"Oyna\" sayfası tarayıcınızda açık olmak zorunda. Görünür olmak zorunda değil, küçültebilir veya Lichess oyun sayfasıyla yanyana koyabilirsiniz, ancak kapatırsanız tahta çalışmayı durduracaktır. + Hazır olduğunuzda tahtanızı kurun ve %s butonuna tıklayın. + Eğer bir hamle algılanmadıysa Sayfayı yeniden yükle + DGT - Yapılandır + Lichess bağlantısı + %2$s farklı bir bilgisayarda veya portta çalışmıyorsa \"%1$s\" sayfasını kullanın. + Hamlelerin seslendirilişini yapılandırın, böylece tahtaya odaklanmaya devam edebilirsiniz. + Bütün Hamleleri Seslendir + Hem sizin hem de rakibinizin hamlelerini seslendirmek için EVET\'i, yalnızca rakibinizin hamlelerini seslendirmek için HAYIR\'ı seçin. + Hamle Seslendirme Biçimi Anahtar sözcükler + Anahtar kelimeler JSON formatındadır. Hamlelerinizi ve maçların sonucunu dilinize çevirmek için kullanılırlar. Varsayılan İngilizce\'dir ancak isteğinize göre değiştirebilirsiniz. + Hata ayıklama + Ayrıntılı hata kaydı + Konsol mesajını görmek için Control+ Shift + C (Windows, Linux, Chrome OS) veya Command + Option + C (Mac) kısayolunu kullanın + Bir DGT tahtasıyla oynayın + Yapılandır diff --git a/translation/dest/dgt/uk-UA.xml b/translation/dest/dgt/uk-UA.xml index 623a741bae8cf..e0184a0d2fb43 100644 --- a/translation/dest/dgt/uk-UA.xml +++ b/translation/dest/dgt/uk-UA.xml @@ -1,5 +1,12 @@ DGT дошка + Lichess & DGT + Вимоги до дошки DGT + Обмеження дошки DGT + Ця сторінка дозволяє приєднати вашу DGT дошку до Lichess і використовувати її для гри. + Для підключення до DGT Electronic Board вам необхідно встановити %s. + Ви можете завантажити програмне забезпечення тут: %s. + Якщо %1$s запущено на цьому комп\'ютері, ви можете перевірити своє підключення до нього за допомогою %2$s. Налагодження diff --git a/translation/dest/dgt/vi-VN.xml b/translation/dest/dgt/vi-VN.xml index d0abf811578ba..5d0eab4132337 100644 --- a/translation/dest/dgt/vi-VN.xml +++ b/translation/dest/dgt/vi-VN.xml @@ -12,8 +12,8 @@ Nếu %1$s đang chạy trên một máy khác hoặc cổng khác, bạn sẽ cần đặt địa chỉ IP và cổng tại đây trong %2$s. Phần cấu hình Trang ván đấu cần được mở trên trình duyệt của bạn. Nó không cần phải hiển thị, bạn có thể thu nhỏ nó hoặc đặt nó cạnh trang ván đấu Lichess, nhưng đừng đóng nó nếu không bàn cờ sẽ ngừng hoạt động. - Bàn cờ sẽ tự động kết nối với bất kỳ ván đấu nào đang diễn ra hoặc bất kỳ trò chơi mới nào bắt đầu. Khả năng chọn ván đấu để chơi sắp ra mắt. - Kiểm soát thời gian cho các ván cờ không tính Elo: Chỉ Cờ Nhanh, Cờ Chậm và Cờ qua thư. + Bàn cờ sẽ tự động kết nối với bất kỳ ván đấu nào đang diễn ra hoặc bất kỳ ván cờ mới nào bắt đầu. Khả năng chọn ván đấu để chơi sắp ra mắt. + Kiểm soát thời gian cho các ván cờ không xếp hạng: chỉ Cờ nhanh, Cờ chậm và Cờ qua thư. Kiểm soát thời gian cho các ván cờ có tính Elo: Cờ Chậm, Cờ qua thư và một số thể loại Cờ Nhanh bao gồm 15+10 và 20+0 Khi đã sẵn sàng, hãy thiết lập bàn cờ của bạn rồi nhấp vào %s. Nếu không phát hiện được nước cờ diff --git a/translation/dest/emails/da-DK.xml b/translation/dest/emails/da-DK.xml index bd8f55692111e..260236677cbf6 100644 --- a/translation/dest/emails/da-DK.xml +++ b/translation/dest/emails/da-DK.xml @@ -5,7 +5,7 @@ Hvis du ikke har registreret dig hos Lichess, kan du roligt ignorere denne meddelelse. Nulstil din adgangskode til lichess.org, %s Vi har modtaget en anmodning om at nulstille adgangskoden til din konto. - Hvis du har foretaget denne anmodning, skal du klikke på linket nedenfor. Hvis ikke, kan du ignorere denne email. + Hvis du har foretaget denne anmodning, skal du klikke på linket nedenfor. Hvis ikke, kan du ignorere denne e-mail. Bekræft ny e-mailadresse, %s Du har anmodet om at ændre din e-mailadresse. For at bekræfte at du har adgang til denne e-mail, skal du klikke på linket nedenfor: @@ -16,7 +16,7 @@ Her er din profilside: %1$s. Du kan tilpasse den på %2$s. God fornøjelse, og må din brikker altid finde vej til din modstanders konge! Log ind på lichess.org, %s - (Fungerer det ikke at klikke? Prøv at kopiere linket ind i din browser!) + (Fungerer det ikke at klikke? Prøv at indsætte linket i din browser!) Dette er en servicemail vedrørende din brug af %s. Vil du kontakte os, bedes du bruge %s. diff --git a/translation/dest/emails/so-SO.xml b/translation/dest/emails/so-SO.xml index 3ea04e700dfa8..81eb984006617 100644 --- a/translation/dest/emails/so-SO.xml +++ b/translation/dest/emails/so-SO.xml @@ -1,2 +1,22 @@ - + + Hubi akoonkaaga lichess.org, %s + Riix laynkan hoose si aad u furtid akoonkaaga Lichess: + Haddii aanad iska diwaan-gelin Lichess uma baahnid farriintan. + Cusboonaysii furahaaga lichess.org, %s + Wuu na soo gaadhay codsigii cusboonaysiinta furaha akoonkaagu. + Haddii uu codsigani adiga kaa yimid, riix laynkan hoose. Haddii kale, iska dhaaf farriintan. + Hubi cinwaankaaga cusub, %s + Waxaad codsatay beddelidda cinwaankaaga. + Si aad u caddeysid in aad cinwaankan leedahay, fadlan riix laynkan hoose: + Ku soo dhawow lichess.org, %s + Waxaad si guul leh akoon uga samaysatay https://lichess.org. + +Waakan boggaagii gaarka ahaa: %1$s. Halkan ka qurxiso %2$s. + +Baashaal, oo boqorka colka baallaha ka yaac! + Gal lichess.org, %s + (Lama riixi karo? Ku day in ad laynka toos ugu qortid interneedka!) + Kani waa cinwaan adeeg oo la xidiidha isticmaalkaaga %s. + Si aad noolasoo xidhiidhid, fadlan isticmaal %s. + diff --git a/translation/dest/emails/uk-UA.xml b/translation/dest/emails/uk-UA.xml index 36f8e01f842a4..63683d9620763 100644 --- a/translation/dest/emails/uk-UA.xml +++ b/translation/dest/emails/uk-UA.xml @@ -1,6 +1,6 @@ - Підтвердьте ваш обліковий запис на lichess.org, %s + Підтвердьте свій обліковий запис на lichess.org, %s Натисніть на посилання для активації облікового запису Lichess: Якщо ви не реєструвалися на Lichess, просто проігноруйте це повідомлення. %s, скидання вашого паролю lichess.org diff --git a/translation/dest/faq/an-ES.xml b/translation/dest/faq/an-ES.xml index 4aa23f6dcd715..30015da980576 100644 --- a/translation/dest/faq/an-ES.xml +++ b/translation/dest/faq/an-ES.xml @@ -54,6 +54,7 @@ L\'analisi informatica en Lichess ye proporcionada per Stockfish. En raras ocasions esto puede estar de mal decidir automaticament (linias forzadas, fuerzas). Per defecto, siempre somos de parte d\'o chugador que no se quedó sin tiempo. Considera que ye posible dar mate con un nomás caballo u alfil, siempre que l\'oponent tienga una pieza que pueda blocar a lo rei. + Manual d\'a FIDE Manual de la FIDE %s Per qué un peón puede capturar unatro peón cuan ya ye pasau? (en passant) Este movimiento ye legal, y se diz \"captura a lo paso\". L\'articlo de Wikipedia fa una %1$s a este movimiento. @@ -142,4 +143,28 @@ Ye millor pensar en as puntuacions como cifras \"relativas\" (a diferencia de la Fe clic en l\'icono de bloqueyo chunto a l\'adreza de lichess.org, en a barra d\'URL d\'o tuyo navegador. Dimpués podrás triar si permitir u blocar notificacions de Lichess. + Activar la reproducción automatica de sons? + La mayoría d\'os navegadors pueden privar la reproducción de son en una pachina cargada recientment pa protechir a los usuarios. Imachina-te que totas las pachinas web te podesen bombardiar decamín con anuncios con soniu. + +L\'icono royo de silencio amaneix cuan lo tuyo navegador priva que lichess.org reproduzca un soniu. Per un regular, esta restricción se devanta cuan fas clic en bella cosa. En bels navegadors móbils, mover una pieza arrocegando-la no conta como un clic. En ixe caso, has de tocar lo taulero pa permitir lo soniu a l\'inicio de cada partida. + +Amostramos l\'icono royo pa avisar-te cuan ixo pasa. A sobén puez permitir que lichess.org reproduzca sonius explicitament. Aquí tiens las instruccions pa fer-lo en versions recients de bels navegadors populars. + escritorio + 1. Dentra en lichess.org +2. Preta Ctrl-i en Linux/Windows u cmd-i en MacOS +3. Fe clic en a pestanya de permisos +4. Permite audio y video en lichess.org + 1. Dentra en lichess.org +2. Fe clic en l\'icono d\'o candau en a barra d\'adrezas +3. Ves ta la configuración de puesto +4. Permite l\'audio + 1. Dentra en lichess.org +2. Fe clic en Safari en a barra de fayenas +3. Ves ta la configuración pa lichess.org ... +4. Permite la reproducción automatica + 1. Fe clic en os tres puntos d\'a cantonada superior dreita +2. Fe clic en Configuración +3. Fe click en Cookies y Permisos d\'o puesto +4. Desplaza-te enta alto y fe clic en Reproducción automatica de conteniu multimedia +5. Anyade lichess.org en Permitir diff --git a/translation/dest/faq/ar-SA.xml b/translation/dest/faq/ar-SA.xml index e25af8ba303be..a4ad22af87521 100644 --- a/translation/dest/faq/ar-SA.xml +++ b/translation/dest/faq/ar-SA.xml @@ -54,6 +54,7 @@ وفي حالات نادرة قد يكون من الصعب تحديد ذلك تلقائيًا (سلسلة من التحركات القسرية) وبشكل افتراضي، فإننا نقف دائمًا إلى جانب اللاعب الذي لم ينته وقته. ضع في اعتبارك أن كش ملك ممكن مع كل من الحصان أو الفيل إذا كان لدى الخصم قطعة أو بيدق يمكنها صد ملكه. + دليل الاتحاد الدَّوْليّ للشطرنج دليل \"الاتحاد الدولي للشطرنج\" %s لماذا يمكنك أن يأكل بيدق الاخر عندما يكون قد سلك بالفعل؟ (الأخذ بالتجاوز) إنها نقلة قانونية تُعرف باسم \"الأخذ بالتجاوز\". وتقدم ويكيبيديا مقالة %1$s: @@ -138,4 +139,28 @@ يرسل Lichess بشكل إختياري إشعارات منبثقة، على سبيل المثال عندما تحصل على رسالة خاصة أو عندما يكون دورك في اللعب. اضغط على زر القفل بجانب رابط lichess.org في متصفحك. ثم اختر إما تفعيل أو تعطيل الإشعارات من موقعنا. + تمكين التشغيل التلقائي للأصوات؟ + يمكن لمعظم المتصفحات منع تشغيل الصوت على الصفحات المحملة حديثًا وذل لحماية المستخدمين. تخيل لو كان بإمكان كل موقع ويب أن يفاجئك بإعلان صوتي فور دخولك. + +تظهر أيقونة كتم الصوت الحمراء عندما يمنع متصفحك موقعنا من تشغيل الصوت، يزال هذا القيد عادة بمجرد النقر في أي مكان، لكن في بعض متصفحات الأجهزة المحمولة، لا يعد سحب القطعة بمثابة نقرة، لذا يجب عليك النقر على الرقعة لتشغيل الصوت في بداية كل مباراة تلعبها. + +نعرض الرمز الأحمر لتنبيهك عند حدوث ذلك، في كثير من الأحيان يمكنك السماح لنا بتشغيل الأصوات عن طريق متصفحك، وفيما يلي تعليمات للقيام بذلك على الإصدارات الحديثة من بعض المتصفحات الشائعة. + سطح المكتب + 1. اذهب إلى lichess.org +2. اضغط Ctrl-i في نظامي Linux/Windows أو cmd-i على MacOS +3. انقر فوق علامة تبويب الأذونات +4. اختر السماح للصوت والفيديو على lichess.org + 1. انتقل إلى lichess.org +2. انقر على أيقونة القُفْل في شريط العنوان +3. انقر فوق إعدادات الموقع +4.اختر السماح بالصوت + 1. انتقل إلى lichess.org +2. انقر على Safari في شريط القوائم +3. انقر فوق إعدادات lichess.org ... +4. اختر السماح للاجراءات التلقائي + 1. انقر فوق النُّقَط الثلاث في الزاوية اليمنى العلوية +2. اختر الإعدادات +3. انقر على ملفات تعريف الارتباط و أذونات الموقع +4. مرر للأسفل وانقر على Media Autoplay +5. أضف lichess.org للقائمة المسموح بها diff --git a/translation/dest/faq/da-DK.xml b/translation/dest/faq/da-DK.xml index 498fbba459226..c83fcb6a9f7e6 100644 --- a/translation/dest/faq/da-DK.xml +++ b/translation/dest/faq/da-DK.xml @@ -118,14 +118,14 @@ Noget andet, som er værd at bemærke, er, at som tiden går vil konfidensinterv modstandere af tilsvarende styrke Spilleren har ikke spillet nok partier fornylig. Afhængigt af antallet af partier du har spillet, kan det tage omkring et års inaktivitet for din rating at blive provisorisk igen. Konkret betyder det, at Glicko-2 afvigelsen er større end 110. Afvigelsen er et udtryk for den tiltro, som systemet har til ratingen. Desto lavere afvigelse, desto mere stabil er en rating. - Hvordan fungerer rangering og klassementerne? + Hvordan fungerer rangering og ranglister? For at blive opført i %1$s skal du: - rating-klassementer: + rating-rangliste have spillet mindst 30 ratede partier med en given rating, have spillet et ratet parti inden for den seneste uge ved denne rating, have en rating-afvigelse mindre end %1$s, i standardskak, og mindre end %2$s i varianter, være i top 10 for denne rating. - Det andet krav sikrer at spillere, som ikke længere benytter deres konto, ikke fylder i klassementer. + Det andet krav sikrer at spillere, som ikke længere benytter deres konto, ikke fylder op i ranglister. Hvorfor er ratings højere sammenlignet med andre websteder og organisationer som FIDE, USCF og ICC? Det er bedst ikke at tænke på ratings som absolutte tal eller at sammenligne dem med andre organisationers. Forskellige organisationer har forskellige niveauer af spillere, forskellige ratingsystemer (Elo, Glicko, Glicko-2 eller en modificeret udgave af de førnævnte). Disse faktorer kan drastisk påvirke de absolutte tal (ratings). @@ -150,19 +150,19 @@ Det røde mute-ikon vises, når din browser forhindrer lichess.org i at afspille Vi viser det røde ikon for at advare dig, når dette sker. Ofte kan du udtrykkeligt tillade lichess.org at afspille lyde. Her er instruktioner til, hvordan du gør det i nyere versioner af nogle populære browsere. desktop - 1. Gå til lichess.org + 1. Gå til lichess.org 2. Tryk Ctrl-i på Linux/Windows eller cmd-i på MacOS 3. Klik på fanen Tilladelser 4. Tillad lyd og video på lichess.org - 1. Gå til lichess.org + 1. Gå til lichess.org 2. Klik på låseikonet i adresselinjen 3. Klik på Webstedsindstillinger 4. Tillad lyd - 1. Gå til lichess.org + 1. Gå til lichess.org 2. Klik på Safari i menulinjen 3. Klik på Indstillinger for lichess.org ... 4. Tillad alle Auto-Play - 1. Klik på de tre prikker i øverste højre hjørne + 1. Klik på de tre prikker i øverste højre hjørne 2. Klik på Indstillinger 3. Klik på Cookies og Website-tilladelser 4. Rul ned og klik på Media autoplay diff --git a/translation/dest/faq/de-DE.xml b/translation/dest/faq/de-DE.xml index f77b1aa044597..a1724bbd4a6be 100644 --- a/translation/dest/faq/de-DE.xml +++ b/translation/dest/faq/de-DE.xml @@ -143,7 +143,7 @@ Klicke auf das Schlosssymbol neben der lichess.org Adresse in der URL-Leiste dei Wähle dann aus, ob Benachrichtigungen von Lichess erlaubt oder blockiert werden sollen. Das automatische Abspielen von Tönen erlauben? - Die meisten Browser können die Tonwiedergabe auf einer neu geladenen Seite verhindern, um die Nutzer zu schützen. Stell dir vor, jede Website könnte dich sofort mit Audio-Werbung bombardieren. + Die meisten Browser können die Tonwiedergabe auf einer neu geladenen Seite unterbinden, um die Nutzer nicht zu belästigen. Stell dir vor, jede Website könnte dich sofort mit Audio-Werbung bombardieren. Das rote Stummschaltungssymbol erscheint, wenn dein Browser verhindert, dass lichess.org einen Ton abspielt. Normalerweise wird diese Einschränkung aufgehoben, sobald du etwas anklickst. Bei einigen mobilen Browsern gilt das Ziehen einer Figur durch Berührung nicht als Klick. In diesem Fall musst du zu Beginn einer Partie auf das Brett tippen, um den Ton zu aktivieren. diff --git a/translation/dest/faq/fi-FI.xml b/translation/dest/faq/fi-FI.xml index 3f13ca96b51b4..9c082ff801b49 100644 --- a/translation/dest/faq/fi-FI.xml +++ b/translation/dest/faq/fi-FI.xml @@ -149,6 +149,7 @@ Katso %3$ssta tämä siirto ja harjoittele sen käyttöä. Kun selaimesi estää lichess.org-sivun äänten toiston, näytetään punainen mykistyskuvake. Tavallisesti rajoitus poistuu, kun klikkaat jotain. Joissakin mobiiliselaimissa nappulan siirtoa vetämällä ei lasketa klikkaukseksi. Tässä tapauksessa voit pelin alkaessa napauttaa lautaa salliaksesi äänet. Tällöin sinulle ilmoitetaan asiasta näyttämällä punainen kuvake. Usein voit nimenomaisesti sallia sen, että lichess.org toistaa äänet. Ohessa ovat ohjeet äänten sallimiseksi joidenkin suosituimpien selainten viimeaikaisia versioita varten. + työpöytä 1. Mene osoitteeseen lichess.org 2. Paina Ctrl-i Linuxissa/Windowsissa tai cmd-i MacOS:ssa 3. Klikkaa käyttöoikeusvälilehteä diff --git a/translation/dest/faq/gsw-CH.xml b/translation/dest/faq/gsw-CH.xml index e9c25c3b6f294..a8da7bd712e98 100644 --- a/translation/dest/faq/gsw-CH.xml +++ b/translation/dest/faq/gsw-CH.xml @@ -102,7 +102,7 @@ Also verlang de Titel nöd. Einzigartigi Trophä\'ä Die Trophäe isch einzigartig i de Gschicht vo Lichess, niemert usser em %1$s wird si jemals ha. Er hät si übercho, will er 100%% vu de Partie vumene %s mit Berserk gschpillt- und gunne hät. - e schtündlichi Bullet Arena + es schtündlichs Bullet-Turnier De Schpiller \"ZugAddict\" hät mal - ime Stream - über 2 Schtund lang versuecht de Computer, uf Schtufe 8, im Bullet 1+0 z\'besiege - aber ohni Erfolg. De \"Thibault\" hät ihm dänn e einzigartigi Trophäe versproche, falls er das doch mal schaffe würd. Ei Schtund spöter hät de \"ZugAddicr\" de \"Stockfish\" besiegt und das Verschpräche isch ighalte worde. Lichess-Wertige Was für es Wertigs-System benutzt Lichess? @@ -135,15 +135,15 @@ Es isch am beschte, Wertige als \"relativi\" Wert z\'betrachte (im Gägesatz zu Aktivier de Zen-Modus i de %1$s oder truck %2$s während de Partie. Azeige-Ischtellige Ich han es Schpiel wäge Lag / Verbindigsunderbruch verlore. Chann ich mini Wertigspünkt zrugg verlange? - Leider chönnd mir kei Wertigspünkt zrugg geh, wo wäge Lag oder Underbrüch verlore worde sind. Unabhängig devoo, ob das Problem vu dinere oder vu eusere Site - was sehr sälte passiert - verursacht worde isch. Pass also au uf, wänn Mäldige für en Lichess Neustart chömmed, will bim Abefahre vum Syschtem werded alli Partie eifach abbroche und mer chann so unfair verlüre. + Leider chönd mir kei Wertigspünkt zrugg geh, wo wäge Lag oder eme Underbruch verlore worde sind. Unabhängig devo, ob das Problem vu dinere oder vu eusere Site - was sehr sälte passiert - verursacht worde isch. Pass also au uf, wänn Mäldige für en Neuschtart chömmed, will bim Abefahre vum Syschtem werded alli Partie abbroche und mer chann es Schpiel unfair verlüre. Wiä chan ich... - Benochrichtigungs-Popups aktiviärä oder deaktiviärä? + Benachrichtigungs-Popups aktiviere oder deaktiviere? Site Informations-Popup azeige - Lichess chan optional Popup-Benachrichtigunga versände, zum Bischpil wänn du am Zug bisch oder wänn du e privati Nachricht übercho häsch. + Lichess chan optional Popup-Benachrichtigunge versände, zum Bischpil wänn du am Zug bisch oder wänn du e privati Nachricht übercho häsch. Klick uf s\'Schlosssymbol, näbe de lichess.org Adrässe, i de URL-Lischte vu dim Browser. -Wähl dänn us, ob Benachrichtigunge vo Lichess erlaubt oder blockiert wärde sölled. +Wähl dänn us, ob Benachrichtigunge vo Lichess erlaubt oder blockiert werde sölled. Autoplay für de Ton aktiviere? Die meischte Browser chönd - zum de Nutzer schütze - d\'Tonwidergab uf ere frisch gladene Site verhindere. Stell dir vor, jedi Website chönnt dich direkt mit Audio-Werbig stresse! @@ -151,19 +151,19 @@ Das roti Stummschaltigssymbol erschint, wänn de Browser verhinderet, dass liche Das roti Symbol macht dich druf ufmerksam, dass das passiert. Oft chann mer lichess.org usdrücklich erlaube, Tön ab z\'schpille. Da findsch du e Aleitig, wie mer das - ide neuschte Versione, vu einige gängige Browser - ischtelle chann. Desktop - 1. Gang zu Lichess.org + 1. Gang zu Lichess.org 2. Taschte Ctrl-i bi Linux / Windos oder cmd-i bi MacOS 3. Klick de Tab \"Permissions\" 4. Erlaub \"Audio and Video\" uf Lichess.org - 1. Gang zu Lichess.org + 1. Gang zu Lichess.org 2. Klick i de Adrässzile ufs Schloss-Icon 3. Klick d\'Site \"Settings\" 4. Erlaub \"Sound\" - 1. Gang zu Lichess.org + 1. Gang zu Lichess.org 2. Klick ide Menuezile uf \"Safari\" 3. Klick \"Settings für lichess.org\"... 4. Erlaub \"All Autoplay\" - 1. Klick uf die 3 Pünkt ganz obe, im rächte Egge + 1. Klick uf die 3 Pünkt ganz obe, im rächte Egge 2. Klick \"Settings\" 3. Klick \"Cookies and Site Permissions\" 4. Scroll abwärts und klick \"Media autoplay\" diff --git a/translation/dest/faq/ko-KR.xml b/translation/dest/faq/ko-KR.xml index 354bcf55a968c..e9727759ad88d 100644 --- a/translation/dest/faq/ko-KR.xml +++ b/translation/dest/faq/ko-KR.xml @@ -143,4 +143,10 @@ LM 타이틀에 대해 문의하지 마세요. 브라우저의 URL 표시 줄에서 lichess.org 주소 옆에 있는 자물쇠 아이콘을 클릭합니다. 그런 다음 Lichess의 알림을 허용할지 차단할지 선택합니다. + 소리 자동 재생을 활성화하고 싶어요. + 대부분의 브라우저가 사용자를 보호하기 위해 새롭게 로드된 페이지가 소리를 재생하는 것을 방지할 수 있습니다. 모든 웹사이트가 들어가자마자 음성 광고를 쏟아붓는다고 상상해 보세요. + +브라우저에서 lichess.org가 소리를 재생하는 것을 방지했을 때 붉은 음소거 아이콘이 나타납니다. 보통 무언가를 클릭하면 이 제한은 없어집니다. 일부 모바일 브라우저에서는, 기물을 드래그하는 것은 클릭으로 간주되지 않습니다. 이 경우 소리 재생을 허용하려면 매 게임이 시작할 때마다 체스판을 탭해야 합니다. + +리체스는 이러한 경우가 발생했을 때 알려주기 위하여 붉은색 아이콘을 보여줍니다. 당신은 lichess.org가 소리를 재생하도록 명시적으로 허용할 수 있습니다. 다음은 최근 버전의 몇몇 인기 브라우저에서 소리 재생을 허용하는 방법입니다. diff --git a/translation/dest/faq/pt-PT.xml b/translation/dest/faq/pt-PT.xml index 53ee2646c957b..7ab62e23179d6 100644 --- a/translation/dest/faq/pt-PT.xml +++ b/translation/dest/faq/pt-PT.xml @@ -161,4 +161,9 @@ Mostramos o ícone vermelho para o alertar quando isto acontece. Muitas vezes, p 2. Clique no ícone do cadeado na barra de endereços 3. Clique em Configurações do Site 4. Permitir Som + 1. Clique nos três pontos no canto superior direito +2. Clique em Configurações +3. Clique em Cookies e Permissões de Site +4. Vá para baixo e clique Reprodução automática de mídia +5. Adicione lichess.org para permitir diff --git a/translation/dest/faq/so-SO.xml b/translation/dest/faq/so-SO.xml index 84ca86c29629a..7406c8d724402 100644 --- a/translation/dest/faq/so-SO.xml +++ b/translation/dest/faq/so-SO.xml @@ -1,6 +1,7 @@ - Su\'aalaha Inta Badan La Isweydiiyo + SBSN + Su\'aalaha badanka soo noqda lee-chess Ciyaar cadaalad ah Xisaabaadka diff --git a/translation/dest/faq/th-TH.xml b/translation/dest/faq/th-TH.xml index b4fcaeb6adc65..fe19004d404df 100644 --- a/translation/dest/faq/th-TH.xml +++ b/translation/dest/faq/th-TH.xml @@ -56,6 +56,24 @@ การฝึกฝนใน Lichess ดู อินเตอร์เนชั่นแนลมาสเตอร์ เอริค โรเซน รุกจน %s เดินซ้ำสามครั้ง + การเดินซ้ำสามรอบ + พวกเราไม่ได้เดินซ้ำ ทำไมเกมถึงยังเสมอโดยการเดินซ้ำ + ตำแหน่ง + พวกเราเดินซ้ำกันสามครั้งทำไมเกมถึงไม่เสมอ + การเดินซ้ำจะต้องได้รับการอ้างสิทธิ์โดยผู้เล่นคนใดคนหนึ่ง คุณสามารถทำได้โดยการกดปุ่มที่แสดงอยู่ หรือโดยเสนอเสมอก่อนการตาเดินซ้ำครั้งสุดท้ายของคุณ โดยไม่สำคัญว่าคู่ต่อสู้ของคุณจะปฏิเสธข้อเสนอเสมอหรือไม่ก็ตาม ยังไงก็ตามการเสมอโดยการเดินซ้ำสามรอบจะถูกรับ คุณยังสามารถ %1$s Lichess เพื่อขอรับสิทธิ์การเดินซ้ำให้คุณได้โดยอัตโนมัติ นอกจากนี้ การเดินซ้ำห้าครั้งจะทำให้จบเกมในทันที + กำหนดค่า + บัญชี + มีตำแหน่งหมากรุกใดบ้างใน Lichess + Lichess จดจำชื่อ FIDE ทั้งหมดที่ได้รับจากการเล่น OTB (บนกระดาน) รวมถึง %1$s นี่คือรายชื่อชื่อตำแหน่งหมากรุกของ FIDE: + หากคุณมีตำแหน่งใน OTB คุณสามารถสมัครเพื่อให้แสดงสิ่งนี้ในบัญชีของคุณโดยกรอก %1$s รวมถึงถ่ายภาพที่ชัดเจนของเอกสารหรือบัตรระบุตัวตน และภาพเซลฟี่ของคุณกำลังถือเอกสารหรือบัตร + +การยืนยันในฐานะผู้เล่นที่มีชื่อบน Lichess ทำให้สามารถเข้าถึงการเล่นในกิจกรรม Titled Arena + +ในที่สุดก็มีตำแหน่งกิตติมศักดิ์ %2$s แล้ว + ตำแหน่งระดับปรมาจารย์ระดับชาติมากมาย + ยืนยันจาก + ฉันสามารถได้ตำแหน่ง Lichess Master (LM) ได้หรือไม่ + ไม่ ฉันสามารถเปลี่ยนชื่อผู้ใช้งานของฉันได้ไหม? เปิดใช้งานการเล่นอัตโนมัติสำหรับเสียงไหม diff --git a/translation/dest/faq/tr-TR.xml b/translation/dest/faq/tr-TR.xml index b8de7f15dc017..c30fe620600c3 100644 --- a/translation/dest/faq/tr-TR.xml +++ b/translation/dest/faq/tr-TR.xml @@ -157,4 +157,13 @@ Bu yaşandığında sizi uyarmak için kırmızı simgeyi gösteririz. Çoğunlu 2. Linux/Windows\'da Ctrl-i ya da MacOS\'de cmd-i bas 3. İzinler sekmesine tıkla 4. lichess.org\'da Ses ve Video\'ya izin ver + 1. Lichess.org\'a gidin +2. Adres çubuğundaki kilit simgesine tıklayın +3. Site ayarlarına tıklayın +4. Sese izin verin + 1. Sağ üst köşedeki üç noktaya tıklayın +2. Ayarlara tıklayın +3. Sol taraftan Tanılama bilgileri ve site izinlerine tıklayın +4. Aşağıya inin ve Medya otomatik yürütmeye tıklayın +5. İzin Ver listesine lichess.org\'u ekleyin diff --git a/translation/dest/faq/vi-VN.xml b/translation/dest/faq/vi-VN.xml index 1d0510f47f119..52a03053553cd 100644 --- a/translation/dest/faq/vi-VN.xml +++ b/translation/dest/faq/vi-VN.xml @@ -29,7 +29,7 @@ Để biết thêm thông tin, vui lòng đọc %s trang chơi công bằng Trên Lichess, sự khác biệt chính trong các quy tắc cho cờ qua thư là sách khai cuộc được cho phép. Việc sử dụng máy tính phân tích vẫn bị cấm và sẽ bị gắn cờ vì sử dụng máy tính phân tích hỗ trợ. Mặc dù ICCF cho phép sử dụng máy tính phân tích trong đánh cờ qua thư nhưng Lichess thì không. - Lối Chơi + Cách Chơi Cờ Siêu Chớp, Cờ Chớp và các định giờ khác được quyết định thế nào? Việc xác định loại cờ của Lichess dựa trên thời lượng ván đấu ước tính = %1$s Ví dụ: thời lượng ước tính của một ván cờ 5+3 là 5 × 60 + 40 × 3 = 420 giây. @@ -78,11 +78,11 @@ Xem %3$s về nước này để biết một cách thực hiện nó. Tài Khoản Có những danh hiệu nào trên Lichess? Lichess công nhận tất cả các danh hiệu do FIDE ghi nhận từ OTB (thi đấu bên ngoài), cũng như %1$s. Đây là danh sách các danh hiệu được FIDE công nhận: - Nếu bạn có danh hiệu OTB, bạn có thể đăng ký để hiển thị tiêu đề này trên tài khoản của mình bằng cách hoàn thành %1$s, bao gồm hình ảnh rõ ràng của tài liệu/thẻ nhận dạng và ảnh tự chụp của bạn đang cầm tài liệu/thẻ. + Nếu bạn có danh hiệu OTB, bạn có thể đăng ký để hiển thị danh hiệu này trên tài khoản của mình bằng cách hoàn thành %1$s, bao gồm hình ảnh rõ ràng của tài liệu/thẻ nhận dạng và ảnh tự chụp của bạn đang cầm tài liệu/thẻ. -Việc xác minh là người chơi có danh hiệu trên Lichess sẽ cho phép bạn chơi trong các sự kiện Đấu trường kỳ thủ có danh hiệu. +Việc xác minh là người chơi có danh hiệu trên Lichess sẽ cho phép bạn chơi trong các sự kiện Titled Arena. -Cuối cùng là danh hiệu danh dự %2$s. +Cuối cùng là danh hiệu %2$s danh dự. nhiều danh hiệu quốc gia biểu mẫu xác minh Tôi có thể nhận được danh hiệu Lichess Master (LM) không? @@ -124,7 +124,7 @@ Một điểm cần lưu ý nữa là, khi thời gian trôi qua, khoảng tin c đã chơi một ván xếp hạng trong tuần trước cho xếp hạng này, có độ lệch xếp hạng thấp hơn %1$s trong cờ vua tiêu chuẩn và thấp hơn %2$s trong các biến thể, nằm trong top 10 trong bảng xếp hạng này. - Yêu cầu thứ hai là những người chơi không còn sử dụng tài khoản của họ sẽ được ngừng đưa vào bảng xếp hạng. + Yêu cầu thứ hai là những người chơi không còn sử dụng tài khoản của họ sẽ bị ngừng đưa vào bảng xếp hạng. Tại sao hệ số Elo lại cao hơn so với các trang web và tổ chức khác như FIDE, USCF và ICC? Tốt nhất là không nên coi xếp hạng là con số tuyệt đối hoặc so sánh chúng với các tổ chức khác. Các tổ chức khác nhau có các cấp độ người chơi khác nhau, các hệ thống xếp hạng khác nhau (Elo, Glicko, Glicko-2 hoặc một phiên bản sửa đổi đã nói ở trên). Những yếu tố này có thể ảnh hưởng mạnh đến con số tuyệt đối (xếp hạng). diff --git a/translation/dest/lag/ko-KR.xml b/translation/dest/lag/ko-KR.xml index 2164156517b95..6f3738f5e86b7 100644 --- a/translation/dest/lag/ko-KR.xml +++ b/translation/dest/lag/ko-KR.xml @@ -9,7 +9,7 @@ 리체스 서버 지연 행마를 서버에서 처리하는 시간. 모두에게 동일하고, 서버의 작업량에만 관련이 있습니다. 사람이 많아질수록 커지지만, 리체스 개발자들은 이 수치가 낮도록 최선을 다합니다. 거의 10ms를 넘지 않습니다. 리체스와 귀하의 컴퓨터 사이의 네트워크 - 당신의 컴퓨터에서 리체서 서버로 행마를 전송하고, 다시 수신하는 시간. 당신과 리체스(프랑스) 사이의 거리, 당신의 인터넷 품질에 딷라 정해집니다. 리체스 개발자는 당신의 와이파이를 고칠 수 없고 빛이 더 빨리 가도록 할 수도 없습니다. + 당신의 컴퓨터에서 리체스 서버로 행마를 전송하고, 다시 수신하는 시간. 당신과 리체스(프랑스) 사이의 거리, 당신의 인터넷 품질에 따라 정해집니다. 리체스 개발자는 당신의 와이파이를 고칠 수 없고 빛이 더 빨리 가도록 할 수도 없습니다. 상단 바의 사용자명을 클릭해서 언제든 이 두 수치를 확인할 수 있습니다. 렉 보상 리체스는 렉에 대한 보상을 합니다. 지속되는 렉과 갑자기 일어나는 렉에 대해 최대한 공평하게 보상을 하고 있습니다. 그렇기 때문에 상대보다 네트워크에 렉이 심해도 크게 불리해지지 않습니다. diff --git a/translation/dest/lag/sl-SI.xml b/translation/dest/lag/sl-SI.xml index cf0e5d66ec9c8..db715ec91ffb0 100644 --- a/translation/dest/lag/sl-SI.xml +++ b/translation/dest/lag/sl-SI.xml @@ -5,12 +5,12 @@ Ne. In vaše omrežje je v redu. Ne. In vaše omrežje je slabo. Da. Kmalu bo rešeno! - Pa še doljše pojasnilo. Zakasnitev igre je sestavljena iz dveh nepovezanih vrednosti (nižje je boljše): + Pa še daljše pojasnilo. Zakasnitev igre je sestavljena iz dveh nepovezanih vrednosti (nižje je boljše): Zakasnitev strežnika Lichess Čas, potreben za obdelavo poteze na strežniku. Enak je za vse in je odvisen samo od obremenitve strežnika. Večje število igralcev ga podaljšuje, a Lichessovi razvijalci se trudijo, da je kratek. Redko presega 10 ms. Omrežje med Lichessom in vami Čas, ki je potreben, da vaša poteza prispe iz vašega računalnika na Lichessov strežnik in da prejmete njegov odgovor. Odvisen je od vaše oddaljenosti do Lichesovega strežnika (v Franciji) in hitrosti vaše internetne povezave. Lichessovi razvijalci ne morejo popraviti vašega wifija ali pospešiti vaše internetne povezave. Obe vrednosti lahko kadar koli najdete s klikom na uporabniško ime v zgornji vrstici. Nadomestilo zaostajanja - Lichess kompenzira zaostajanje omrežja. To vključuje trajne zamike in občasne zaostanke. Zaenkrat obstajajo omejitve in hevristike, ki temeljijo na nadzoru časa in kompenziranem zaostajanju, tako da bi moral biti rezultat razumljiv za oba igralca. Posledično zaradi večjega zaostajanja omrežja od nasprotnika ni ovira! + Lichess kompenzira časovni zamik omrežja. To vključuje trajne zamike in občasne večje zaostanke. Zaenkrat obstajajo omejitve in metode, ki temeljijo na nadzoru časa in kompenziranem zaostajanju, tako da bi morala biti končna izkušnja sprejemljiva za oba igralca. Zaradi daljšega časovnega zamika omrežja od nasprotnikovega zato niste prikrajšani! diff --git a/translation/dest/learn/ar-SA.xml b/translation/dest/learn/ar-SA.xml index 1c1de9da2dd35..030b97b5893c3 100644 --- a/translation/dest/learn/ar-SA.xml +++ b/translation/dest/learn/ar-SA.xml @@ -6,161 +6,162 @@ التقدم: %s إعادة ضبط تقدمي سيتم محو كل سجلات تقييمك! - اِلعب! + اِلعب! قطع الشطرنج الرخ تتحرك في خطوط مستقيمة - الرخ قطعة قوية. أ أنت مستعد لقيادتها؟ + الرخ قطعة قوية، هل أنت مستعد لقيادتها؟ انقر على الرخ لتحضرها إلى النجمة! - إجمع كل النجوم! + اجمع كل النجوم! كلما قلت النقلات التي تقوم بها ، كلما كسبت المزيد من النقاط! استخدم الرخين لتسريع الأمور! - تهانينا! لقد أتقنت الرخ بنجاح. + تهانينا! لقد أتقنت استعمال الرخ. الفيل - إنه يتحرك وتريًا + يتحرك بشكل قطري التالي سنتعلم كيفية المناورة بالفيل! - فيل واحد للمربعات الفاتحة، وفيل للمربعات الداكنة. ستحتاج كلاهما! - تهانينا! يمكنك قيادة الفيل. - الملكة - الملكة = رخ + فيل - أقوى قطعة شطرنج تتقدم. صاحبة الجلالة الملكة! - تهانينا! الملكات لدى لا تخفي أسرار عنك. + فيل للمربعات الفاتحة، والآخر للمربعات الداكنة. ستحتاج كليهما! + تهانينا! لقد تعلمت قيادة الفيل. + الوزير + الوزير = رخ + فيل + اسمحوا لي أن أقدم قطعة الشطرنج الأقوى. إنه الوزير! + تهانينا! الوزراء لا يخفون أسرارا عنك. الملك أهم قطعة أنت الملك. إذا سقطت في المعركة، خسرت المباراة. الملك بطيء. - أخر واحد! + آخر واحدة! يمكنك الآن قيادة القائد! الحصان - أنه يتحرك بشكل L - هنا تحدي لك. الحصان.. قطعة صعبة. + يتحرك بشكل حرف L + هذا تحد لك. الحصان قطعة ماكرة. للأحصنة طريقتها المميزة للقفز بالأرجاء! - الأحصنة يمكنها القفز فوق العوائق! -الهروب وإخضاع النجوم! - تهانينا! لقد أتقنت الحصان. + يمكن للأحصنة القفز فوق العوائق! +اقفز ثم اجمع كل النجوم! + تهانينا! لقد روضت الحصان. البيدق يتحرك إلى الأمام فقط - البيادق ضعيفة، إلا أنها مليئة بالكثير من الإمكانيات. - البيادق تتحرك مربع واحد فقط. ولكن عندما تصل إلى الجانب الآخر من الرقعة، فإنها تصبح قطعة أقوى! - معظم الوقت الترقية إلى الملكة تكون الأفضل. -ولكن في بعض الأحيان الحصان يمكن أن يأتي بالمساعدة! + البيادق ضعيفة، إلا أنها تملك الكثير من القدرات. + البيادق تتحرك لمربع واحد فقط، ولكن عندما تصل إلى الجانب الآخر من الرقعة، فإنها تصبح قطعة أقوى! + في معظم الحالات الترقية إلى وزير هي الأفضل. +ولكن في بعض الأحيان يمكن أن يكون الحصان أفضل! البيادق تتحرك إلى الأمام، ولكن تأسر وتريًا! التقط، ثم رقي! استخدم جميع البيادق! لا حاجة للترقية. - البيدق على الصف الثاني يمكن أن يتحرك مربعان مرة واحدة! + البيدق على الصف الثاني يمكن أن يتحرك لمربعين في نقلة واحدة! إجمع كل النجوم! لا حاجة للترقية. - تهانينا! البيادق لا تخفي أسرار عنك. + تهانينا! لقد أصبحت تعرف كل شيء عن البيادق. ترقية البيدق - بلغ بيدقك نهاية الرقعة! - يرقى الآن إلى قطعة أقوى. - حدد القطعة التي تريدها! + وصل بيدقك إلى الصف الأخير! + يمكنك ترقيته الآن إلى قطعة أقوى. + حدد القطعة التي تريد الترقية إليها! أساسيات الأسر - إلتقط قطع المنافس - حدد قطع المنافس الغير محمية، والتقطها! - إلتقط القطع السوداء! - التقط القطع السوداء! ولا تفقد قطعك. - تهانينا! تعرف كيفية القتال بقطع الشطرنج! + ألتقط قطع خصمك + حدد قطع خصمك غير المحمية، والتقطها! + ألتقط القطع السوداء! + التقط القطع السوداء! دون أن تفقد قطعك. + تهانينا! لقد تعلمتَ كيف تأسر قطع خصمك! الحماية - حافظ على قطعك آمنة - تحديد قطعك التي يهاجمها المنافس، ودافع عنها! - تهانينا! القطعة التي لا تفقدها هي قطعة تربحها! - أنت تحت الهجوم! إهرب من التهديد! - لا مفر، ولكن يمكنك الدفاع! - لا تسمح لهم بأخذ أي قطعة غير محمية! + ابقِ قطعك آمنة + حدد قطعك التي هاجمها خصمك، ودافع عنها! + تهانينا! القطعة التي لم تفقدها هي قطعة ربحتها! + الخَصْم يهاجمك! أهرب من التهديد! + لا مهرب لقطعك، ولكن يمكنك الدفاع عنها! + أمنع خصمك من أخذ أي قطعة غير محمية! الاشتباك - الإلتقاط والدفاع عن القطع - المحارب الجيد يعرف الهجوم والدفاع على حد سواء! - تهانينا! تعرف كيفية القتال بقطع الشطرنج! - كش في واحد - الهجوم على ملك الخصم + ألتقط قطع خصمك ودافع عن قطعك + المحارب الجيد يجيد الهجوم والدفاع! + تهانينا! تعلمت كيف تقاتل بقطع الشطرنج! + كش ملك خصمك بنقلة واحدة + هاجم ملك خصمك لكش خصمك، هاجم ملكه. فتجبره على الدفاع عنه! استهدف الملك المنافس في نقلة واحدة! تهانينا! لقد قمت بكش خصمك، واجبرته على الدفاع عن ملكه! البعد عن الكش دافع عن ملكك أنت في كش! يجب عليك الهروب أو إعاقة الهجوم. - الهروب بالملك! + اهرب بالملك! الملك لا يمكنه الهروب، ولكن يمكنك إعاقة الهجوم! يمكنك الخروج من الكش بأخذ القطعة المهاجمة. هذا الحصان يعطي كش متجاوزًا دفاعاتك! - الهروب بالملك أو إعاقة الهجوم! - تهانينا! لا يمكن ابدأ أن يؤخذ ملكك، تأكد من أنك يمكنك الدفاع ضد الكش! - مات في واحد - إهزم ملك المنافس - يمكنك الفوز عندما لا يمكن لمنافسك الدفاع ضد الكش. - هاجم ملك منافسك بطريقة لا يمكنه الدفاع عنه! - تهانينا! هذا هو كيف يمكنك الفوز بمبارة شطرنج! + اهرب بالملك أو أعق الهجوم! + تهانينا! لا يمكن ابدأ أن يؤخذ ملكك، تأكد دائما أنك تستطيع الدفاع ضد الكش! + كش مات بنقلة واحدة + اقتل ملك خصمك + ستفوز إذا لم يتمكن خصمك من الدفاع عن ملكه من الكش. + هاجم ملك خصمك بحيث لا يستطيع الدفاع عنه! + تهانينا! هكذا تُكسب مباراة الشطرنج! متوسط رص الرقعة كيف تبدأ المباراة الجيشان يواجه بعضهما البعض، مستعدان للمعركة. - وهذا هو الموقف الأولي لكل دور شطرنج! قم بأي نقلة للاستمرار. - أولاً ضع الرخان! في الزوايا. - ثم ضع الحصانان! إلى جانب الرخان. - ضع الفيلان! إلى جانب الحصانان. - مكان الملكة! في نفس لونها. - مكان الملك! بجوار الملكة. - البيادق تشكل الخط الأمامي. قم بأي نقلة للإستمرار. - تهانينا! يمكنك معرفة كيفية إعداد رقعة الشطرنج. - تبييت - نقلة الملك الخاصة - إحضار ملكك إلى بر الأمان، ونشر رخك للهجوم! - حرك ملكك مربعان لتبيت في جناح الملك! - حرك ملكك مربعان لتبيت في جناح الملكة! - الحصان في الطريق! إنقله ، ثم بيت في جناح-الملك. - بيت في جناح-الملك! تحتاج إلى نقل القطع بعيدًا أولاً. - بيت في جناح-الملكة! تحتاج إلى نقل القطع بعيدًا أولاً. - لا يمكنك التبييت إذا كان الملك قد تحرك بالفعل أو الرخ قد تحركت بالفعل. + هذا هو الموقف الابتدائي لكل مباراة شطرنج! العب أي نقلة للاستمرار. + أولاً ضع الرخين في الزوايا. + ثم ضع الحصانين! إلى جانب الرخين. + ضع الفيلين! إلى جانب الحصانين. + مكان الوزير في مربع من لونه نفسه. + مكان الملك بجوار وزيره. + تصطف البيادق أمام القطع. +العب أي نقلة للمتابعة. + تهانينا! تعلمت كيف ترتب القطع على رقعة الشطرنج. + التبييت + نقلة الملك المميزة + أمن ملكك، وأحضر رخك إلى وَسَط الرقعة! + حرك ملكك مربعين للتبييت في جناح الملك! + حرك ملكك مربعين للتبييت في جناح الوزير! + الحصان في الطريق! أبعده، ثم بيت في جناح-الملك. + بيت في جناح-الملك! تحتاج إلى تحريك القطع بعيدًا أولاً. + بيت في جناح-الوزير! تحتاج إلى نقل القطع بعيدًا أولاً. + تفقد الحق في التبييت إذا تحرك ملكك أو تحركت الرخ. لا يمكنك التبييت إذا هوجم الملك في الطريق. إغلق الكش ثم بيت! جد طريقة للتبييت في جناح-الملك! جد طريقة للتبييت في جناح-الملكة! - تهانينا! يجب أن تبيت غالبًا دائمًا في أي مباراة. - أسر بالمرور - نقلة البيدق الخاصة + تهانينا! يفضل أن تبيت في كل مباراة. + الأخذ بالتجاوز + نقلة البيدق المميزة عند تحرك بيدق الخصم مربعين، يمكنك أن تأخذه كما لو أنه تحرك مربع واحد فقط. الأسود للتو حرك البيدق مربعين! التقطه بالمرور. الأخذ بالمرور يكون بعد تحريك بيدق المنافس مباشرة. - يعمل الأخذ بالمرور فقط إذا كان البيدق على الصف الخامسة. - إلتقط جميع البيادق بالمرور! - تهانينا! يمكنك الآن أن تأسر بالمرور. - الملك مخنوق - المباراة تعادل - عندما يكون اللاعب ليس في كش وليس لديه نقلة قانونية، يكون مات مخنوق. المباراة تعادل: لا فائز، ولا خاسر. - لتميت الأسود خنقًا: --الأسود لا يمكنه التتحرك لأي مكان + يمكن تنفيذ طريقة بالتجاوز فقط +إذا كان البيدق في الصف الخامس. + التقط جميع البيادق بالمرور! + تهانينا! تعلمت كيف تأسر بالمرور. + الملك المخنوق + تنتهي المباراة بالتعادل + عندما لا يكون ملك اللاعب في كش، ولكنه لا يملك أي نقلة قانونية، يكون الملك مخنوقا، وتنتهي المباراة بالتعادل: لا فائز، ولا خاسر. + أخنق ملك الأسود حيث: +-لا يملك أي حركة قانونية -لا يوجد كش. - تهانينا! الموت خنقًا أفضل من الكش مات! + تهانينا! أن يخنق ملكك أفضل من أن يموت! متقدم قيمة القطعة تقييم قوة قطعة القطع ذات القدرة العالية على الحركة لها قيمة أعلى! - الملكة = 9 + الوزير = 9 الرخ= 5 الفيل = 3 الحصان= 3 البيدق = 1 الملك لا يقدر بثمن! فقدانه يعني خسارة المباراة. - خذ القطعة ذات أعلى قيمة! الملكة > الفيل - كل القطعة التي لها أعلى قيمة! -لا تستبدل -قطعة ذات قيمة عليا بأخرى أدنى قيمة. + خذ القطعة التي لها قيمة أعلى! الوزير > الفيل + خذ القطعة ذات القيمة الأعلى! +لا تستبدل قطعة ذات قيمة عالية بأخرى أدنى قيمة. كل القطعة التي لها أعلى قيمة! تأكد من أن نقلتك قانونية! خذ القطعة ذات أعلى قيمة! - تهانينا! أنت تعرف القيم المادية! -الملكة =9 + تهانينا! أنت تعرف قيم القطع! +الوزير =9 الرخ = 5 الفيل= 3 الحصان = 3 البيدق = 1 - كش في اثنين - نقلتين لإعطاء كش - جد التكوينة الصحيحة من نقلتين لإعطاء كش لملك المنافس! - استهدف الملك المنافس في نقلتين! + كش بنقلتين + نقلتان لإعطاء كش + جِدّ النقلتين الصحيحتين لإعطاء كش لملك المنافس! + هاجم الملك المنافس بنقلتين! تهانينا! لقد قمت بكش خصمك، واجبرته على الدفاع عن ملكه! ما التالي؟ أنت تعرف كيف تلعب الشطرنج، تهانينا! هل تريد أن تصبح لاعبًا أقوى؟ @@ -190,7 +191,7 @@ على الطريق الصحيح! المرحلة %s تمت التالي - موالي: %s + التالي: %s العودة إلى القائمة لقد خسرت اللغز! إعادة المحاولة diff --git a/translation/dest/learn/da-DK.xml b/translation/dest/learn/da-DK.xml index 58abeffb16ef1..df612f042cda9 100644 --- a/translation/dest/learn/da-DK.xml +++ b/translation/dest/learn/da-DK.xml @@ -6,7 +6,7 @@ Status: %s Nulstil min status Du vil miste alle dine fremskridt! - spil! + spil! Skakbrikker Tårnet Det bevæger sig i lige linjer @@ -175,7 +175,7 @@ Bonde = 1 Få en gratis Lichess konto Øvelser Lær almindelige skakstillinger - Opgaver + Taktikopgaver Træn dine taktiske evner Videoer Se lærerige skakvideoer @@ -199,6 +199,6 @@ Bonde = 1 Næste Næste: %s Tilbage til menu - Øvelse mislykkedes! + Opgave mislykkedes! Prøv igen diff --git a/translation/dest/learn/nl-NL.xml b/translation/dest/learn/nl-NL.xml index 3431252293804..b82cb9bd01b19 100644 --- a/translation/dest/learn/nl-NL.xml +++ b/translation/dest/learn/nl-NL.xml @@ -6,7 +6,7 @@ Voortgang: %s Voortgang verwijderen Je zult al je voortgang verliezen! - speel! + speel! Schaakstukken De toren Hij beweegt in rechte lijnen diff --git a/translation/dest/learn/sl-SI.xml b/translation/dest/learn/sl-SI.xml index 9b03b9a383b9e..778f4073a0536 100644 --- a/translation/dest/learn/sl-SI.xml +++ b/translation/dest/learn/sl-SI.xml @@ -6,21 +6,21 @@ Napredek: %s Ponastavi moj napredek Izgubili boste ves napredek! - igraj! + igraj! Šahovske figure Trdnjava Se premika v ravni črti Trdnjava je močna figura. Ali ste pripravljeni, da ji ukazujete? Kliknite na trdnjavo, da jo pripeljete na zvezdo! - Poberite vse zvezde! + Osvojite vse zvezde! Manj potez ko naredite, več točk dobite! Uporabite dve trdnjavi, da bo šlo hitreje! Čestitamo! Uspešno obvladate trdnjavo. Lovec Premika se diagonalno Naučili se bomo premikanja lovca! - En belo-poljni lovec, -en črno-poljni lovec. + En belopoljni lovec, +en črnopoljni lovec. Potrebujete oba! Čestitamo! Obvladate lovca. Kraljica @@ -36,8 +36,8 @@ Potrebujete oba! Skakač Premika se v obliki črke L Tukaj je izziv za vas. Skakač je... zvita figura. - Skakači imajo prebrisan način skakanja naokrog! - Skakač lahko skoči čez ovire. Pobegnite in ujemite zvezde! + Skakači imajo prebrisan način poskakovanja! + Skakač lahko preskoči ovire. Pobegnite in osvojite zvezde! Čestitamo! Obvladate skakača. Kmet Premika se samo naprej @@ -47,8 +47,8 @@ Potrebujete oba! Kmetje se premikajo naprej, vendar jemljejo diagonalno! Jemljite, nato promovirajte! Uporabite vse kmete! Ni potrebno promovirati. - Kmet v drugi vrsti se lahko premakne za 2 polji v potezi! - Pojejte vse zvezde! Promocija ni potrebna. + Kmet v drugi vrsti se lahko premakne za dve polji naenkrat! + Osvojite vse zvezde! Promocija ni potrebna. Čestitamo! Kmetje nimajo skrivnosti za vas. Promocija kmeta Vaš kmet je dosegel konec šahovnice! @@ -63,12 +63,12 @@ Potrebujete oba! Ne izgubite svojih. Čestitamo! Zdaj se znate boriti s šahovskimi figurami! Obramba - Držite figure na varnem + Zaščitite figure Poiščite figure, ki jih napada nasprotnik in jih zaščitite! Čestitamo! Figura, ki je ne izgubite je figura, ki jo dobite! Napadeni ste! Pobegnite grožnji! - Ni pobega, + Ni izhoda, vendar se lahko branite! Ne dovolite, da vam vzamejo nezaščitene figure! @@ -76,12 +76,12 @@ nezaščitene figure! Vzemite in ubranite figure Dober bojevnik obvlada napad in obrambo! Čestitamo! Obvladate bojevanje s figurami! - Šah v ena + Šah v eni potezi Napadite nasprotnikovega kralja Da šahirate nasprotnika mu napadite kralja. Morajo ga branit! Ciljajte na nasprotnikovega kralja v eni potezi! Čestitamo! Šahirali ste nasprotnika in ga prisilili, da brani kralja! - Obramba na šah + Obramba pred šahom Zaščitite kralja Ste v šahu! Morate se umakniti ali zaščititi napad. Pobegnite s kraljem! @@ -92,27 +92,27 @@ skozi vašo obrambo! Pobegnite s kraljem ali zaščitite napad! Čestitamo! Vašega kralja ni mogoče vzeti poskrbite, da se ubranite pred šahom! - Mat v ena + Mat v eni potezi Premagajte nasprotnikovega kralja - Zmagate kadar vaš nasprotnik ne more obraniti šah. + Zmagate, kadar vaš nasprotnik ne more obraniti šaha. Napadite nasprotnikovega kralja tako, da obramba ne bo mogoča! Čestitamo! Tako se zmaga šahovsko partijo! Srednji nivo Postavitev figur Kako začeti igro - Dve vojski ki stojita nasproti, sta pripravljeni na bitko. + Dve vojski si stojita nasproti, pripravljeni na bitko. To je začetna pozicija vsake šahovske partije! -Na nadaljevanje naredite potezo. +Za nadaljevanje naredite potezo. Najprej postavite trdnjavi! -Gresta v vogala. +Stojita v kotih. Nato postavite skakača. Stojita ob trdnjavah. Postavite lovca! Stojita ob skakačih. Postavite kraljico. -Gre na polje svoje barve. +Stoji na polju svoje barve. Postavite kralja! H kraljici. Kmetje sestavljajo prednjo vrsto. @@ -121,9 +121,9 @@ Za nadaljevanje naredite potezo. Rokada Posebna poteza s kraljem Spravite svojega kralja na varno in razvijte trdnjavo v napad! - Premaknite kralja dve polji, + Premaknite kralja za dve polji, da rokirate na kraljevo stran! - Premaknite kralja dve polji + Premaknite kralja za dve polji, da rokirate na kraljičino stran! Skakač je v napoto! Premaknite ga, nato rokirajte na kraljevo stran. @@ -131,9 +131,8 @@ Premaknite ga, nato rokirajte na kraljevo stran. Prej odmaknite figure. Rokirajte na kraljičino stran! Prej odmaknite figure. - Rokirati ni mogoče, če -se je kralj že premaknil -ali se je premaknila trdnjava. + Rokada ni dovoljena, če +sta se kralj ali trdnjava že premaknila. Rokirati ni mogoče, če je kralj na poti napaden. Ubranite šah in nato rokirajte! @@ -144,20 +143,20 @@ za rokado na kraljičino stran! Čestitamo! V skoraj vsaki partiji je dobro rokirati. En passant Posebna poteza s kmetom - Kadar se nasprotnikov kmet premakne za dve polji, ga lahko pojeste kot če bi se premaknil za eno polje. + Kadar se nasprotnikov kmet premakne za dve polji, ga lahko pojeste, kot če bi se premaknil za eno polje. Črni je ravno premaknil kmeta za dve polji! Vzemite \"en passant\". En passant je veljaven samo takoj po tem, ko je nasprotnik premaknil kmeta. - En passant deluje samo, + En passant je mogoč le, če je vaš kmet na 5 vrsti. Vzemite vse kmete en passant! Čestitamo! Zdaj znate jemati en passant. Pat - Rezultat partije je remi - Kadar igralec ni v šahu in nima na voljo veljavne poteze je to pat. Igra je remi: nihče ne zmaga, nihče ne izgubi. + Izid partije je remi + Kadar igralec ni v šahu in nima na voljo veljavne poteze, nastopi pat. Igra je remizirana: nihče ne zmaga, nihče ne izgubi. Da patirate črnega: - Črni se ne more premakniti - Ni v šahu. @@ -190,23 +189,23 @@ Lovec = 3 Skakač = 3 Kmet = 1 Šah v dve - Dajte šah v dveh pozerah + Dajte šah v dveh potezah Najdite pravo kombinacijo dveh potez, ki šahira nasprotnikovega kralja! Ogrozite nasprotnikovega kralja v dveh potezah! Čestitamo! Šahirali ste nasprotnika in ga prisilili v obrambo kralja! - Kaj naprej? + Kam naprej? Znate igrati šah, čestitamo! Ali želite postati močnejši šahist? Registrirajte se - Naredite si brezplačen Lichess račun + Ustvarite si brezplačen Lichess račun Treniraje Naučite se pogostih pozicij Problemi Vadite taktične veščine Videi - Poglejte poučne šahovske videote + Poglejte poučne šahovske videe Proti ljudem - Nasprotniki iz celega sveta + Nasprotniki s celega sveta Proti računalniku Preskusite svoje sposobnosti proti računalniku Začnimo! diff --git a/translation/dest/learn/so-SO.xml b/translation/dest/learn/so-SO.xml index 386de3897ac5b..4558b05e65703 100644 --- a/translation/dest/learn/so-SO.xml +++ b/translation/dest/learn/so-SO.xml @@ -1,10 +1,102 @@ + Baro jesta + adigoo ciyaaraya! + Kala dooro + Horumar: %s + Soo bilow horumarkayga + Wuu lumayaa horumarkaagi! + ciyaar! + Dhagxanta jesta + Qalcadda + Waxay u dhaqaaqdaa toos afarta jiho + Qalcaddu waa mid awood badan. Diyaar ma u tahay in ad masmusho? + Taabo qalcadda si ad u dishid xidigta! + Dil xidigaha oo dhan! + Dhaqaaqa yaree si aad dhibco badan u heshid! + Isticmaal laba qalcadood si aad howsha u dedejisid! + Hambalyo! Waad ku takhasustay qalcadda. + Maroodiga + Wuxu u socdaa janjeedh + Hadana waxan baranaynaa sida maroodiga loo wado! + Hal maroodi oo jago cad, +iyo hal maroodi oo jago madow. +Waad u baahantay labadaba! + Hambalyo! Waad maamuli taqaan maroodiga. + Abaanduulaha + Abaanduule = qalcad + maroodi + Dhaqaxa ugu awoodda badan jesta. Mundane abaanduule! + Hambalyo! Abaanfuulaha waxba kaagama qarsoona. + Boqorka + Dhagaxa ugu muhiimsan + Boqorkaad tahay. Haddii ad dagaalka ku dhimatid, ciyaartu waa guuldarro. + Boqorku wuu gaabiyaa. + Kii ugu dambeeyay! + Imika waad maamuli kartaa maamulaha! + Faraska + Sida L oo kaluu u dhaqaaqaa + Waa wax kaa fikirinaya. Farasku dhagax khiyaamo badan. + Farduhu waxay yaqaanaan si yar oo ay u boodboodaan! + Farduhu waxay ka boodi karaan wax hor yaalla! +U gas oo dhulka la goo xidigaha! + Hambalyo! Waad ku takhasustay faraska. + Askariga + Hore uun buu u socon karaa + Askartu wey jilicsanyiin, laakiin wey xoogaysan karaan. + Askartu waxay dhaqaaqaan hal jago. Balse markay gaadhaan looxa dhiniciisa kale, waxay noqdaan dhagax awood badan! + Inta badan waxa fiican in laga dhigao abaanduule. Balse marmarka qaar faras baa ku haboon! + Askartu toos bay u socdaan, +balse janjeedh bay wax u dilaan! + Dil, ka dibna dallac! + Isticmaal askarta oo dhan! +Uma baahnid dallacaad. + Askari jiidda labaad joogi wuxu guuri karaa 2 jago hal mar! + Dil xidigaha oo dhan! +Looma baahna dallacaad. + Hambalyo! Waxa kuuguma qarsoona askarta. + Dallicidda askariga + Askarigaagi wuxu gaadhay looxa dhamaadkiisii! + Imika wuxu u dallacmayaa dhagax awood badan. + Dooro dhagaxa ad rabtid! + Aasaaska + Dilidda + Dil ciidanka colkaaga + Hel horjeedahaaga dhagxantiisa aan difaaca lahayn oo dil! + Dil dhagxanta madmadow! + Dil dhagxanta madmadow! +Oo kuwaagana ilaali. + Hambalyo! Imika waad taqaan sida loogu dagaalamo dhagxanta jesta! + Ilaalin + Dhagxantaada ilaali + U fiirso dhagxanta uu horjeedahaagu weerarayo, oo iska difaac! + Hambalyo! Dhagax ad badbasdisay waa dhagax ad colka ka dishay oo kale! + Waa lagusoo weeraray! +Ka orod khatarta! + Meel loo ordaa ma jirto, +Balse waad is difaaci kartaa! + Ha yeelin in lagaa dilo dhagxan bilaa difaac ah! + Dagaal + Dil oo difaac dhagxanta + Dagaalyahanka fiicani wuu yaqaan weerarka iyo difaacaba! + Hambalyo! Imika waad taqaan sida loogu dagaalamo dhagxanta jesta! + Jeg hal mar + Weerar boqorka horjeedahaaga + Si ad u jegtid horjeedahaaga, weerar boqorkiisa. Wuu ku qasbanyay in uu difaaco! + Hal mar ku shiish boqorka horjeedahaag! + Hambalyo! Waad jegtay horjeedahaaga, kuna qasabtay in uu difaaco boqorkiisa! + Ka bax jeg + Difaac boqorkaaga + Jeg baad ku jirtaa! Waa in ad ka orodo ama dhufays ka gasho weerarka. + La orod boqorka! + Boqorku ma ordi karo, +balse dhufays baad gelin kartaa! + Waxad jeg kaga bixi kartaa in ad disho dhagaxa weerarka ah. + Faraskani jeg difaaca soo dhaafay buu kugu hayaa! Halxiraalaha Cajiib! - Wanaag! + Khatar! Shaqo wanaagsan! - Kaamil ah! - Wacan! - Jidka loo maro! + Kaamil! + Heer sare! + Ku soco! diff --git a/translation/dest/learn/vi-VN.xml b/translation/dest/learn/vi-VN.xml index a6e5878ca8b5d..bac7bb42c51a4 100644 --- a/translation/dest/learn/vi-VN.xml +++ b/translation/dest/learn/vi-VN.xml @@ -71,7 +71,7 @@ Nhưng không cần phải phong cấp đâu. Hãy ăn lấy quân đen! Ăn lấy quân đen! Nhưng đừng để mất quân của bạn. - Xin chúc mừng! Bạn đã nắm được cách đánh với các quân cờ! + Chúc mừng! Bạn đã nắm được cách đánh với các quân cờ! Phòng thủ Giữ an toàn cho quân của bạn Xác định các quân đang bị đối thủ tấn công và phòng thủ cho chúng! @@ -117,9 +117,9 @@ bằng một cách mà đối phương không thể phòng thủ! Đây là thế trận ban đầu của mọi ván cờ! Hãy đi nước bất kỳ để tiếp tục. - Đầu tiên hãy đặt những quân xe! + Đầu tiên hãy đặt các quân xe! Chúng nằm ở các góc bàn cờ. - Rồi đặt những quân Mã! + Rồi đặt các quân Mã! Ngay cạnh quân Xe. Đặt quân Tượng! Ngay cạnh quân Mã. @@ -214,7 +214,7 @@ trong 2 nước! Câu đố Luyện tập các kĩ năng chiến thuật của bạn Các video - Xem những video hướng dẫn chơi cờ + Xem các video hướng dẫn chơi cờ Đấu với người Các đối thủ từ khắp nơi trên thế giới Đấu với máy diff --git a/translation/dest/oauthScope/ar-SA.xml b/translation/dest/oauthScope/ar-SA.xml index 260c518fc70d3..5a1fa5985675b 100644 --- a/translation/dest/oauthScope/ar-SA.xml +++ b/translation/dest/oauthScope/ar-SA.xml @@ -8,27 +8,47 @@ ما الذي يمكن أن تفعله الtoken بالنيابة عنك: الرمز المميز سيمنح الوصول إلى حسابك. لا تقم بمشاركته مع أي شخص! احفظ أو اكتب كلمة المرور الخاصة بتطبيق. لن تراها مجددا! - قراءة التفضيلات - اكتب مرجع - اقرا عنوان البريد - قراءة التحديات الواردة - إرسال وقبول ورفض التحديات + قراءة التفضيلات + اكتب مرجع + اقرا عنوان البريد + قراءة التحديات الواردة + إرسال وقبول ورفض التحديات إنشاء العديد من الألعاب مرة واحدة للاعبين الآخرين - قراءة الدراسات والبث الخاص - إنشاء، تحديث، حذف الدراسات والبث - إنشاء وتحديث والالتحاق بالبطولات - إنشاء سباقات الألغاز والانضمام إليها - اقرا نشاط الالغاز - قراءة معلومات الفريق الخاص - انضم او غادر الفرق + قراءة الدراسات والبث الخاص + إنشاء، تحديث، حذف الدراسات والبث + إنشاء وتحديث والالتحاق بالبطولات + إنشاء سباقات الألغاز والانضمام إليها + اقرا نشاط الالغاز + قراءة معلومات الفريق الخاص + انضم او غادر الفرق إدارة الفرق التي تقودها: إرسال الرسائل الشخصية، طرد الأعضاء - قراءة اللاعبين المتابعين - متابعة/ الغاء متابعة اللاعبين الاخرين - ارسال رسالة خاصة للاعبين الاخرين - العب مبارايات ضد بوت API - العب مبارايات ضد بوت api + قراءة اللاعبين المتابعين + متابعة/ الغاء متابعة اللاعبين الاخرين + ارسال رسالة خاصة للاعبين الاخرين + العب مبارايات ضد بوت API + العب مبارايات ضد بوت api عرض و استخدام المحركات الخارجية إنشاء وتحديث محركات خارجية - إنشاء جلسات موقع مصادقة (منح الوصول الكامل!) - استخدام أدوات المشرف (ضمن حدود الأذونات الخاصة بك) + إنشاء جلسات موقع مصادقة (منح الوصول الكامل!) + استخدام أدوات المشرف (ضمن حدود الأذونات الخاصة بك) + رمز وصول API الشخصي + يمكنك إنشاء طلبات OAuth دون المرور خلال%s. + التحقق من رمز التفويض + بدلاً من ذلك، يمكنك استخدام %s في طلبات API. + إنشاء رمز وصول شخصي + استخدم هذه الرموز بعناية! مثل كلمات المرور، مِيزة استخدام هذه الرموز بدلا من كلمة المرور العادية هي سهولة إلغاءها، وإنشاء الكثير منها. + انظر %1$s و%2$s. + مثال على تطبيق للرمز الشخصي + دليلُ واجهة برمجة التطبيقات + رمز الوصول الجديد + رموز الوصول إلى API + أنشئت %s + آخر استخدام %s + لقد لعبتَ المباريات فعلًا! + ملاحظة لانتباه المطورين فقط: + من الممكن ملء هذا الاستمارة مسبقاً بتغيير معلمات الاستفسار الخاصة بالرابط. + على سبيل المثال:%s + حدد النطاقين %1$s و%2$s، ثم عين الرمز المميز. + يمكن العثور على رموز النطاق في كود HTML الخاص بالنموذج. + إن إعطاء عناوين URL المحددة مسبقًا لمستخدميك سيساعدهم في الحصول على نطاق الرمز المميز الصحيح. diff --git a/translation/dest/oauthScope/be-BY.xml b/translation/dest/oauthScope/be-BY.xml index 610a3e1cc1a95..a2c843d12001f 100644 --- a/translation/dest/oauthScope/be-BY.xml +++ b/translation/dest/oauthScope/be-BY.xml @@ -8,10 +8,11 @@ Што токен можа рабіць ад вашага імя: Токен дасць доступ да вашага ўліковага запісу. НЕ дзяліцеся ім ні з кім! Не забудзьцеся скапіраваць свой новы асабісты токен доступу. Вы не зможаце ўбачыць яго зноў! - Паглядзець налады - Змяніць налады - Паглядзець адрас электроннай пошты - Паглядзець уваходныя выклікі + Паглядзець налады + Змяніць налады + Паглядзець адрас электроннай пошты + Паглядзець уваходныя выклікі + Стварыць, абнавіць і далучыцца да турніраў Прагледзець і выкарыстаць вашы знешнія рухавікі Стварыць і абнавіць знешнія рухавікі diff --git a/translation/dest/oauthScope/de-DE.xml b/translation/dest/oauthScope/de-DE.xml index 0c8f6240df4d3..b37e2778cc15c 100644 --- a/translation/dest/oauthScope/de-DE.xml +++ b/translation/dest/oauthScope/de-DE.xml @@ -8,33 +8,33 @@ Was der Zugangsschlüssel in deinem Namen tun kann: Der Zugangsschlüssel wird Zugriff auf dein Konto ermöglichen. Teile ihn NIEMALS! Stelle sicher, dass du deinen persönlichen Zugangsschlüssel jetzt kopierst. Du wirst ihn nicht erneut sehen können! - Einstellungen lesen - Einstellungen ändern - E-Mail-Adresse lesen - Eingehende Herausforderungen lesen - Herausforderungen senden, akzeptieren und ablehnen + Einstellungen lesen + Einstellungen ändern + E-Mail-Adresse lesen + Eingehende Herausforderungen lesen + Herausforderungen senden, akzeptieren und ablehnen Erstelle viele Partien gleichzeitig für andere Spieler - Private Studien und Übertragungen lesen - Erstelle, aktualisiere und lösche Studien und Übertragungen - Erstelle, aktualisiere und trete Turnieren bei - Erstelle und trete Aufgaben-Rennen bei - Aufgaben-Aktivität lesen - Private Team-Informationen lesen - Teams beitreten und verlassen + Private Studien und Übertragungen lesen + Erstelle, aktualisiere und lösche Studien und Übertragungen + Erstelle, aktualisiere und trete Turnieren bei + Erstelle und trete Aufgaben-Rennen bei + Aufgaben-Aktivität lesen + Private Team-Informationen lesen + Teams beitreten und verlassen Verwalte von dir geleitete Teams: Sende PMs, entferne Mitglieder - Gefolgte Spieler lesen - Anderen Spielern folgen und entfolgen - Anderen Spielern private Nachrichten senden - Partien mit der Board-API spielen - Partien mit der Bot-API spielen + Gefolgte Spieler lesen + Anderen Spielern folgen und entfolgen + Anderen Spielern private Nachrichten senden + Partien mit der Board-API spielen + Partien mit der Bot-API spielen Deine externen Engines anzeigen und benutzen Externe Engines erstellen und aktualisieren - Authentifizierte Website-Sitzungen erstellen (gewährt vollen Zugriff!) - Moderator-Werkzeuge verwenden (innerhalb der Grenzen deiner Berechtigung) + Authentifizierte Website-Sitzungen erstellen (gewährt vollen Zugriff!) + Moderator-Werkzeuge verwenden (innerhalb der Grenzen deiner Berechtigung) Persönlicher API-Zugangsschlüssel Du kannst OAuth-Anfragen erstellen, ohne den %s zu durchlaufen. - Autorisierungs-Code Prozess - Stattdessen, %s, den du direkt in deinen API-Anfragen benutzen kannst. + Autorisierungs-Code-Prozess + Stattdessen %s, den du direkt in deinen API-Anfragen benutzen kannst. generiere einen persönlichen Zugangs-Schlüssel Hüte diese Schlüssel gewissenhaft! Sie sind wie Passwörter. Der Vorteil bei der Verwendung von Schlüsseln anstelle von Passwörtern in Skripten ist, dass Schlüssel widerrufen werden können und du viele von ihnen generieren kannst. Hier ist ein %1$s und die %2$s. @@ -43,12 +43,12 @@ Neuer Zugangs-Schlüssel API-Zugangs-Schlüssel Erstellt: %s - Zuletzt benutzt: %s + Zuletzt benutzt: %s Du hast bereits Partien gespielt! Hinweis nur für Entwickler: - Es ist möglich, dieses Formular vorauszufüllen, indem du die Abfrageparameter der URL bearbeitest. + Es ist möglich, dieses Formular im vorab auszufüllen, indem du die Abfrageparameter der URL bearbeitest. Zum Beispiel: %s %1$s und %2$s wählt die Bereiche aus und setzt die Schlüsselbeschreibung fest. - Die Bereichs-Codes können im HTML-Cide des Formulars gefunden werden. - Deinen Nutzern vorausgefüllte URLs bereit zu stellen, wird ihnen helfen, die passenden Zugangs-Schlüssel zu erhalten. + Die Bereichs-Codes können im HTML-Code des Formulars gefunden werden. + Die Bereitstellung von im vorab ausgefüllter URLs wird deinen Nutzern helfen, die passenden Zugangs-Schlüssel zu erhalten. diff --git a/translation/dest/oauthScope/gsw-CH.xml b/translation/dest/oauthScope/gsw-CH.xml index 9d52351a403f2..70228495606a3 100644 --- a/translation/dest/oauthScope/gsw-CH.xml +++ b/translation/dest/oauthScope/gsw-CH.xml @@ -8,47 +8,47 @@ Was das Passwort, under dim Name, mache chann: Das Passwort erlaubt en Zuegriff uf dis Konto. Gibs also NIE Anderne! Kopier unbedingt jetzt dis neui Passwort oder schribs dir uf! Du gsehsch es nachher nümme! - Preferänze läse - Preferänze schribe - d\'E-Mail-Adrässe läse - iträffendi Useforderige läse - iträffendi Useforderige schribe + Preferänze läse + Preferänze schribe + d\'E-Mail-Adrässe läse + iträffendi Useforderige läse + iträffendi Useforderige schribe Für anderi Schpiller viel Schpiel uf eimal erschtelle - Privati Schtudie und Sändige läse - Erschtell, aktualisier, lösch Schtudie und Sändige - Turnier erschtelle, aktualisiere und biträte - Puzzle Racer Ränne erschtelle und biträte - Ufgabe Aktivitäte läse - Privati Team Infos läse - Tritt bi, verlah, und organisier es Team + Privati Schtudie und Überträgige läse + Erschtell, aktualisier, lösch Schtudie und Überträgige + Turnier erschtelle, aktualisiere und biträte + Puzzle Racer Ränne erschtelle und biträte + Ufgabe Aktivitäte läse + Privati Team Infos läse + Tritt bi, verlah, und organisier es Team Teams, wo du fühersch betreue: persönlichi Nachrichte schicke, Mitglieder usschlüsse - Schpiller wo mer folgt läse - Folge und nümme folge vu andere Schpiller - Andere Schpiller privati Nachrichte schicke - Schpiel mit API-Brätt schpille - Schpiel mit em API-Bot schpille + Schpiller wo mer folgt läse + Folge und nümme folge vu andere Schpiller + Andere Schpiller privati Nachrichte schicke + Schpiel mit API-Brätt schpille + Schpiel mit em API-Bot schpille Zeig und benutz dini externi Engine Erschtell und aktualisier e externi Engine - Erschtelle vu authentifizierte Website-Sitzige (gewährt volle Zuegriff!) - Nutzig vu Moderatoretools (im Rahme vu ihrer Erlaubnis) + Erschtelle vu authentifizierte Website-Sitzige (gewährt volle Zuegriff!) + Nutzig vu Moderatoretools (im Rahme vu ihrer Erlaubnis) Persönlichs API Zuegangs Token Du chasch au OAuth Afrage mache, ohni %s. - Berächtigungs-Code-Fluss + Berächtigungs-Code-Fluss Anstatt %s, wo du diräkt in API Afrage verwände chasch. - generier es persönlichs Zuegangs Token + generier es persönlichs Zuegangs Token Bewahr die Tokens guet uf - sie sind wie Passwörter! De Vorteil vu Tokens, gägenüber Passwörter, für es Skript, isch, dass mer Tokens chann widerrüefe und dass mer viel vu dene erzüge chann. Da isch es %1$s und de %2$s. - persönlichs Token App Bischpil - API Dokumentation + persönlichs Token App Bischpil + API Dokumentation Neus Zugangs Token API Zuegangs Tokens Gmacht %s - Zletscht benutzt %s + Zletscht benutzt %s Du häsch scho Partie gschpillt! De Hiwis isch nur für Entwickler: Es isch möglich, das Formular vorab uszfülle, idem d\'Abfrageparameter vu de URL g\'änderet werded. - Zum Bischpil: %s - markiert d\'Bereich %1$s und %2$s und setzt d\'Tokenbeschribig. - Die Gältigsbereichcodes sind im HTML-Code vum Formular z\'finde. + Zum Bischpil: %s + markiert d\'Bereich %1$s und %2$s und setzt d\'Tokenbeschribig. + Die Gältigsbereichcodes sind im HTML-Code vum Formular z\'finde. Wänn du dine Benutzer die vorusgfüllte URLs zur Verfüegig schtellsch, chömeds die richtige Token-Bereich über. diff --git a/translation/dest/oauthScope/th-TH.xml b/translation/dest/oauthScope/th-TH.xml index 3ea04e700dfa8..94a3177312289 100644 --- a/translation/dest/oauthScope/th-TH.xml +++ b/translation/dest/oauthScope/th-TH.xml @@ -1,2 +1,11 @@ - + + โทเค็นการเข้าถึง API ส่วนบุคคลใหม่ + คำอธิบายโทเค็น + อ่านการตั้งค่า + เขียนการตั้งค่า + อ่านที่อยู่ของอีเมล + อ่านความท้าทายที่เข้ามา + ถูกสร้างเมื่อ %s + ใช้งานล่าสุด: %s + diff --git a/translation/dest/oauthScope/vi-VN.xml b/translation/dest/oauthScope/vi-VN.xml index ee4d2f1ee5b5e..6f091b4a6be87 100644 --- a/translation/dest/oauthScope/vi-VN.xml +++ b/translation/dest/oauthScope/vi-VN.xml @@ -8,47 +8,47 @@ Những gì mà Token có thể làm thay bạn: Token sẽ cấp quyền truy cập vào tài khoản của bạn. KHÔNG chia sẻ nó với bất kỳ ai! Đảm bảo rằng bạn đã sao chép mã truy cập cá nhân mới ngay bây giờ. Bạn sẽ không thể nhìn thấy nó lần nữa! - Tùy chọn đọc - Tùy chọn viết - Đọc địa chỉ email - Đọc các lời thách đấu được gửi đến - Gửi, chấp nhận và từ chối thách đấu + Tùy chọn đọc + Tùy chọn viết + Đọc địa chỉ email + Đọc các lời thách đấu được gửi đến + Gửi, chấp nhận và từ chối thách đấu Tạo nhiều ván đấu cùng lúc với nhiều người chơi - Đọc các bài học riêng và các chương trình phát sóng - Tạo, cập nhật, xóa các bài học và các chương trình phát sóng - Tạo, cập nhật và tham gia các giải đấu - Tạo và tham gia đua câu đố - Đọc các hoạt động câu đố - Đọc thông tin riêng của đội - Tham gia và rời khỏi đội + Đọc các bài học riêng và các chương trình phát sóng + Tạo, cập nhật, xóa các bài học và các chương trình phát sóng + Tạo, cập nhật và tham gia các giải đấu + Tạo và tham gia đua câu đố + Đọc các hoạt động câu đố + Đọc thông tin riêng của đội + Tham gia và rời khỏi đội Quản lý các đội bạn đứng đầu: gửi tin nhắn riêng, xóa thành viên - Đọc người chơi đã theo dõi - Theo dõi và bỏ theo dõi những người chới khác - Gửi tin nhắn riêng đến những người chơi khác - Chơi với bàn cờ API - Chơi cờ với bot API + Đọc người chơi đã theo dõi + Theo dõi và bỏ theo dõi những người chơi khác + Gửi tin nhắn riêng đến những người chơi khác + Chơi với bàn cờ API + Chơi cờ với bot API Xem và sử dụng động cơ máy tính bên ngoài Tạo và cập nhật các động cơ máy tính - Tạo các phiên trang web đã được xác thực (cấp toàn quyền truy cập!) - Sử dụng các công cụ quản trị (nằm trong quyền kiểm soát của bạn) + Tạo các phiên trang web đã được xác thực (cấp toàn quyền truy cập!) + Sử dụng các công cụ quản trị (nằm trong quyền kiểm soát của bạn) Khóa truy cập API cá nhân Bạn có thể thực hiện các yêu cầu OAuth mà không cần thông qua %s. - luồng mã ủy quyền + luồng mã ủy quyền Thay vào đó, %s mà bạn có thể sử dụng trực tiếp trong các yêu cầu API. - tạo khóa truy cập cá nhân + tạo khóa truy cập cá nhân Hãy bảo vệ những khóa này một cách cẩn thận! Chúng giống như mật khẩu. Ưu điểm của việc sử dụng mã thông báo thay vì đặt mật khẩu của bạn vào tập lệnh là mã thông báo có thể bị thu hồi và bạn có thể tạo nhiều mã thông báo. Đây là %1$s và %2$s. - ví dụ về ứng dụng khóa cá nhân - Tài liệu API + ví dụ về ứng dụng khóa cá nhân + Tài liệu API Khoá truy cập mới Khoá truy cập API Đã tạo %s - Lần cuối sử dụng %s + Lần cuối sử dụng %s Bạn đã từng chơi ván cờ rồi! Lưu ý chỉ dành cho nhà phát triển: Có thể điền trước biểu mẫu này bằng cách điều chỉnh các tham số truy vấn của URL. - Ví dụ như: %s - đánh dấu vào phạm vi %1$s và %2$s, đồng thời đặt mô tả khóa. - Mã phạm vi có thể được tìm thấy trong mã HTML của biểu mẫu. + Ví dụ như: %s + đánh dấu vào phạm vi %1$s và %2$s, đồng thời đặt mô tả khóa. + Mã phạm vi có thể được tìm thấy trong mã HTML của biểu mẫu. Việc cung cấp các URL điền sẵn này cho người dùng của bạn sẽ giúp họ có được phạm vi mã thông báo phù hợp. diff --git a/translation/dest/oauthScope/zh-CN.xml b/translation/dest/oauthScope/zh-CN.xml index f3e916d813b01..304db9302fd16 100644 --- a/translation/dest/oauthScope/zh-CN.xml +++ b/translation/dest/oauthScope/zh-CN.xml @@ -44,9 +44,9 @@ API 访问令牌 创建于 %s 上次使用:%s - 你已经有过对局了! + 你已经玩过对局了! 给开发者的提示: - 可以通过调整 URL 参数来预填此界面。 + 可以通过调整 URL 内参数以预填此界面。 例如:%s 将 %1$s 和 %2$s 作用域划勾,并设置令牌描述。 作用域代码可以在此表格的 HTML 代码中找到。 diff --git a/translation/dest/onboarding/aa-ER.xml b/translation/dest/onboarding/aa-ER.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/aa-ER.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/af-ZA.xml b/translation/dest/onboarding/af-ZA.xml new file mode 100644 index 0000000000000..5e514f33a3c36 --- /dev/null +++ b/translation/dest/onboarding/af-ZA.xml @@ -0,0 +1,17 @@ + + + Welkom! + Welkom by lichess.org! + Hierdie is jou profielblad. + Indien \'n kind hierdie rekening gaan gebruik mag jy %s wil kies. + Wat is volgende? Enkele voorstelle: + Leer die reëls van skaak + Verbeter m.b.v. taktiese skaakraaisels. + Speel teen Kunsmatige Intelligensie. + Speel teen mense vanoor die hele wêreld. + Volg jou vriende op Lichess. + Speel in toernooie. + Leer van %1$s en %2$s. + Stel Lichess op soos jy daarvan hou. + Verken die webtuiste en geniet jouself :) + diff --git a/translation/dest/onboarding/ak-GH.xml b/translation/dest/onboarding/ak-GH.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ak-GH.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/am-ET.xml b/translation/dest/onboarding/am-ET.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/am-ET.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/an-ES.xml b/translation/dest/onboarding/an-ES.xml new file mode 100644 index 0000000000000..1ccb3456fb368 --- /dev/null +++ b/translation/dest/onboarding/an-ES.xml @@ -0,0 +1,17 @@ + + + Bienveniu/da! + Bienveniu/da a lichess.org! + Esta ye la tuya pachina de perfil. + Esta cuenta la usará un nino u nina? Talment quiera activar lo %s. + Y agora, qué? Aquí tiens cualques sucherencias: + Aprende las reglas d\'os escaques + Millora con os problmeas de tacticas d\'escaques. + Chuga con una intelichencia artificial. + Chuga con oponents de tot lo mundo. + Sigue a los tuyos amigos en Lichess. + Chuga en torneyos. + Aprende de %1$s y %2$s. + Configura Lichess seguntes os tuyos gustos. + Explora este puesto y pasa-lo diverti-te :) + diff --git a/translation/dest/onboarding/ar-SA.xml b/translation/dest/onboarding/ar-SA.xml new file mode 100644 index 0000000000000..d28f7b3a3cb8d --- /dev/null +++ b/translation/dest/onboarding/ar-SA.xml @@ -0,0 +1,17 @@ + + + مرحبا! + مرحبًا بك في lichess.org! + هذه هي صفحتك الشخصية. + هل يستخدم هذا الحساب طفل؟ قد ترغب في تمكين %s. + ماذا الآن؟ فيما يلي بعض الاقتراحات: + تعلم قواعد الشطرنج + تحسن عن طريق ألغاز الشطرنج التكتيكية. + العب ضد الذكاء الصناعي. + العب ضد خصوم من جميع أنحاء العالم. + تابع اصدقائك على ليتشيس. + شارك في بطولات. + تعلم من %1$s و%2$s. + خصص ليتشيس حسب رغبتك. + استكشف الموقع و استمتع :) + diff --git a/translation/dest/onboarding/as-IN.xml b/translation/dest/onboarding/as-IN.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/as-IN.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/ast-ES.xml b/translation/dest/onboarding/ast-ES.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ast-ES.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/av-DA.xml b/translation/dest/onboarding/av-DA.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/av-DA.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/az-AZ.xml b/translation/dest/onboarding/az-AZ.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/az-AZ.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/ba-RU.xml b/translation/dest/onboarding/ba-RU.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ba-RU.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/be-BY.xml b/translation/dest/onboarding/be-BY.xml new file mode 100644 index 0000000000000..452d88dc48ea3 --- /dev/null +++ b/translation/dest/onboarding/be-BY.xml @@ -0,0 +1,4 @@ + + + Гуляйце ў турнірах. + diff --git a/translation/dest/onboarding/bg-BG.xml b/translation/dest/onboarding/bg-BG.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/bg-BG.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/bn-BD.xml b/translation/dest/onboarding/bn-BD.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/bn-BD.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/br-FR.xml b/translation/dest/onboarding/br-FR.xml new file mode 100644 index 0000000000000..ddffb8fd135d9 --- /dev/null +++ b/translation/dest/onboarding/br-FR.xml @@ -0,0 +1,7 @@ + + + Donemat! + Donemat war lichess.org! + Deskiñ reolennoù an echedoù + Kemer perzh e tournamantoù. + diff --git a/translation/dest/onboarding/bs-BA.xml b/translation/dest/onboarding/bs-BA.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/bs-BA.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/ca-ES.xml b/translation/dest/onboarding/ca-ES.xml new file mode 100644 index 0000000000000..e327ea6f6fb53 --- /dev/null +++ b/translation/dest/onboarding/ca-ES.xml @@ -0,0 +1,17 @@ + + + Benvingut/da! + Benvingut/da a lichess.org! + Aquesta és la teva pàgina de perfil. + Utilitzarà un nen aquest compte? Potser vols habilitar %s. + I ara què? Aquí tens alguns suggeriments: + Aprèn les regles dels escacs. + Millora amb els problemes de tàctiques d\'escacs. + Juga contra la Intel·ligència Artificial. + Juga contra oponents de tot el món. + Segueix als teus amics en Lichess. + Juga en tornejos. + Apreneu de %1$s i de %2$s. + Configura Lichess al teu gust. + Explora la web i diverteix-te :) + diff --git a/translation/dest/onboarding/ce-CE.xml b/translation/dest/onboarding/ce-CE.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ce-CE.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/ceb-PH.xml b/translation/dest/onboarding/ceb-PH.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ceb-PH.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/ckb-IR.xml b/translation/dest/onboarding/ckb-IR.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ckb-IR.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/co-FR.xml b/translation/dest/onboarding/co-FR.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/co-FR.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/cs-CZ.xml b/translation/dest/onboarding/cs-CZ.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/cs-CZ.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/cv-CU.xml b/translation/dest/onboarding/cv-CU.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/cv-CU.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/cy-GB.xml b/translation/dest/onboarding/cy-GB.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/cy-GB.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/da-DK.xml b/translation/dest/onboarding/da-DK.xml new file mode 100644 index 0000000000000..0f5529d08931d --- /dev/null +++ b/translation/dest/onboarding/da-DK.xml @@ -0,0 +1,17 @@ + + + Velkommen! + Velkommen til lichess.org! + Dette er din profilside. + Skal et barn bruge denne konto? Det kan være en god idé at aktivere %s. + Hvad nu? Her er et par forslag: + Lær reglerne for skak + Bliv bedre med taktiske skakopgaver. + Spil mod den kunstige intelligens. + Spil mod modstandere fra hele verden. + Følg dine venner på Lichess. + Spil i turneringer. + Lær af %1$s og %2$s. + Indstil Lichess, som du vil have det. + Gå på opdagelse på sitet og hav det sjovt :) + diff --git a/translation/dest/onboarding/de-DE.xml b/translation/dest/onboarding/de-DE.xml new file mode 100644 index 0000000000000..467873a18be25 --- /dev/null +++ b/translation/dest/onboarding/de-DE.xml @@ -0,0 +1,17 @@ + + + Willkommen! + Willkommen auf lichess.org! + Das ist deine Profilseite. + Wird ein Kind dieses Konto verwenden? Vielleicht möchtest du den %s aktivieren. + Was nun? Hier sind ein paar Vorschläge: + Schachregeln lernen + Verbessere dein Schach mit Taktik-Aufgaben. + Spiele gegen die künstliche Intelligenz. + Spiele gegen Gegner aus der ganzen Welt. + Folge deinen Freunden auf Lichess. + Spiele in Turnieren. + Lerne von %1$s und %2$s. + Konfiguriere Lichess nach deinen Wünschen. + Erkunde die Seite und hab\' Spaß :) + diff --git a/translation/dest/onboarding/el-GR.xml b/translation/dest/onboarding/el-GR.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/el-GR.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/en-US.xml b/translation/dest/onboarding/en-US.xml new file mode 100644 index 0000000000000..90760121355b3 --- /dev/null +++ b/translation/dest/onboarding/en-US.xml @@ -0,0 +1,17 @@ + + + Welcome! + Welcome to lichess.org! + This is your profile page. + Will a child use this account? You might want to enable %s. + What now? Here are a few suggestions: + Learn chess rules + Improve with chess tactics puzzles. + Play the AI. + Play opponents from around the world. + Follow your friends on Lichess. + Play in tournaments. + Learn from %1$s and %2$s. + Configure Lichess to your liking. + Explore the site and have fun :) + diff --git a/translation/dest/onboarding/eo-UY.xml b/translation/dest/onboarding/eo-UY.xml new file mode 100644 index 0000000000000..d996e402aacff --- /dev/null +++ b/translation/dest/onboarding/eo-UY.xml @@ -0,0 +1,17 @@ + + + Bonvenon! + Bonvenon al lichess.org! + Ĉi tiu estas via profilpaĝon. + Ĉu infano uzos ĉi tiun konton? Vi eble volas ebligi %s. + Kio nun? Jen kelkaj sugestoj: + Lernu ŝakajn regulojn. + Pliboniĝu per ŝakaj taktikaj puzloj. + Ludu kontraŭ la artefaritan intelekton. + Ludu kontraŭ kontraŭulojn de la tuta mondo. + Sekvu viajn geamikojn sur Lichess. + Ludu en turniroj. + Lernu de %1$s kaj %2$s. + Agordu Lichess laŭ via plaĉo. + Esploru la retpaĝon kaj amuziĝu :) + diff --git a/translation/dest/onboarding/es-ES.xml b/translation/dest/onboarding/es-ES.xml new file mode 100644 index 0000000000000..9904914561e50 --- /dev/null +++ b/translation/dest/onboarding/es-ES.xml @@ -0,0 +1,17 @@ + + + ¡Bienvenido! + ¡Bienvenido a lichess.org! + Esta es tu página de perfil. + ¿Un infante usará esta cuenta? Tal vez quieras habilitar %s. + ¿Y ahora? Aquí hay algunas sugerencias: + Aprender las reglas del ajedrez + Mejorar con ejercicios de tácticas. + Juega con la Inteligencia Artificial. + Juega con oponentes de todo el mundo. + Sigue a tus amigos en Lichess. + Juega en torneos. + Aprende de %1$s y %2$s. + Configura Lichess a tu gusto. + Explora el sitio y diviértete :) + diff --git a/translation/dest/onboarding/et-EE.xml b/translation/dest/onboarding/et-EE.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/et-EE.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/eu-ES.xml b/translation/dest/onboarding/eu-ES.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/eu-ES.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/fa-IR.xml b/translation/dest/onboarding/fa-IR.xml new file mode 100644 index 0000000000000..f235a5675e2e2 --- /dev/null +++ b/translation/dest/onboarding/fa-IR.xml @@ -0,0 +1,17 @@ + + + خوش آمدید! + به لیچس خوش آمدید! + این صفحه پروفایل شماست. + آیا یک کودک قرار است از این حساب استفاده کند؟ شاید بخواهید %s را فعال کنید. + حالا چی؟ این‌ها پیشنهاد ماست: + قوانین شطرنج را یاد بگیرید + با پازل‌های تاکتیکی بازی خود را بهتر کنید. + با هوش مصنوعی بازی کنید. + با حریف‌هایی از سراسر دنیا بازی کنید. + دوستان خود را روی لیچس دنبال کنید. + در تورنمنت‌ها بازی کنید. + از %1$s و %2$s یادبگیرید. + لیچس را طبق سلیقه خود تنظیم کنید. + سایت رو بگردید و لذت ببرید :) + diff --git a/translation/dest/onboarding/fi-FI.xml b/translation/dest/onboarding/fi-FI.xml new file mode 100644 index 0000000000000..6ffbd587a74b6 --- /dev/null +++ b/translation/dest/onboarding/fi-FI.xml @@ -0,0 +1,15 @@ + + + Tervetuloa! + Tervetuloa lichess.orgiin! + Tämä on profiilisivusi. + Käyttääkö lapsi tätä tunnusta? Harkitse siinä tapauksessa %sn päälle laittamista. + Mitä seuraavaksi? Tässä pari ehdotusta: + Opi shakin säännöt + Kehity ratkomalla taktiikkatehtäviä. + Pelaa tekoälyä vastaan. + Seuraa ystäviäsi Lichessissä. + Pelaa turnauksissa. + Konfiguroi Lichess mielesi mukaiseksi. + Tutustu sivustoon ja pidä hauskaa :) + diff --git a/translation/dest/onboarding/fo-FO.xml b/translation/dest/onboarding/fo-FO.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/fo-FO.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/fr-FR.xml b/translation/dest/onboarding/fr-FR.xml new file mode 100644 index 0000000000000..9b5dfcb8c2b7b --- /dev/null +++ b/translation/dest/onboarding/fr-FR.xml @@ -0,0 +1,17 @@ + + + Bienvenue! + Bienvenue sur lichess.org! + Voici votre profil. + Un enfant utilisera-t-il ce compte? Songez à activer le %s. + Et maintenant? Quelques suggestions : + Apprendre les règles des échecs + Améliorez-vous en résolvant des problèmes de tactique. + Affrontez l\'intelligence artificielle. + Jouez contre des adversaires du monde entier. + Suivez vos amis sur Lichess. + Participez à des tournois. + Apprenez de %1$s et %2$s. + Configurez Lichess à votre goût. + Explorez le site et amusez-vous :-) + diff --git a/translation/dest/onboarding/frp-IT.xml b/translation/dest/onboarding/frp-IT.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/frp-IT.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/fur-IT.xml b/translation/dest/onboarding/fur-IT.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/fur-IT.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/fy-NL.xml b/translation/dest/onboarding/fy-NL.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/fy-NL.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/ga-IE.xml b/translation/dest/onboarding/ga-IE.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ga-IE.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/gd-GB.xml b/translation/dest/onboarding/gd-GB.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/gd-GB.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/gl-ES.xml b/translation/dest/onboarding/gl-ES.xml new file mode 100644 index 0000000000000..9e33c9fcc1318 --- /dev/null +++ b/translation/dest/onboarding/gl-ES.xml @@ -0,0 +1,17 @@ + + + Benvido! + Benvido a lichess.org! + Esta é a túa páxina de perfil. + Un neno vai usar esta conta? Ó mellor deberías activar o %s. + E agora que? Velaquí algunhas suxestións: + Aprende as regras do xadrez. + Mellora cos exercicios de táctica. + Xoga coa Intelixencia Artificial. + Xoga con rivais de todo o mundo. + Sigue ós teus amigos en Lichess. + Xoga en torneos. + Aprende de %1$s e de %2$s. + Configura Lichess ó teu gusto. + Explora o sitio e pásao ben:) + diff --git a/translation/dest/onboarding/gn-PY.xml b/translation/dest/onboarding/gn-PY.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/gn-PY.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/gsw-CH.xml b/translation/dest/onboarding/gsw-CH.xml new file mode 100644 index 0000000000000..09ccf8820a4ca --- /dev/null +++ b/translation/dest/onboarding/gsw-CH.xml @@ -0,0 +1,17 @@ + + + Willkomme! + Wilkomme bi Lichess.org! + Das isch dini Profil Site. + Wird es Chind de Account benutze? Möchtsch villicht de %s aktiviere? + Was sofort? Da sind es paar Vorschläg: + Lern Schachregle + Verbesser dich, mit taktische Ufgabe. + Schpill die künschtlichi Intelligänz. + Schpill mit Gägner us de ganze Wält. + Folg dine Fründe uf Lichess. + Schpill bi Tunrier mit. + Lern vu %1$s und %2$s. + Konfigurier dir Lichess so, wie\'s dir g\'fallt. + Entdeck die ganzi Site und amüsier dich :) + diff --git a/translation/dest/onboarding/gu-IN.xml b/translation/dest/onboarding/gu-IN.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/gu-IN.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/ha-HG.xml b/translation/dest/onboarding/ha-HG.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ha-HG.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/he-IL.xml b/translation/dest/onboarding/he-IL.xml new file mode 100644 index 0000000000000..d6916e5cfbc04 --- /dev/null +++ b/translation/dest/onboarding/he-IL.xml @@ -0,0 +1,17 @@ + + + ברוכים הבאים! + ברוכים הבאים ל-lichess.org! + זה עמוד הפרופיל שלך. + אם החשבון הזה מיועד לילד/ה, מומלץ להפעיל את %s. + מה עכשיו? הנה כמה הצעות: + למדו את חוקי השחמט + השתפרו על ידי פתירת חידות שחמט. + שחקו נגד בינה מלאכותית. + שחקו נגד יריבים מרחבי העולם. + עקבו אחר חבריכם בליצ׳ס. + שחקו בטורנירים. + למדו מ%1$s ומ%2$s. + התאימו את ליצ׳ס להעדפותיכם. + שוטטו ברחבי האתר ותהנו :) + diff --git a/translation/dest/onboarding/hi-IN.xml b/translation/dest/onboarding/hi-IN.xml new file mode 100644 index 0000000000000..1b0e2e7c46a81 --- /dev/null +++ b/translation/dest/onboarding/hi-IN.xml @@ -0,0 +1,14 @@ + + + स्वागत! + Lichess.org में आपका स्वागत है! + यह आपका प्रोफ़ाइल पृष्ठ है. + क्या कोई बच्चा इस खाते का उपयोग करेगा? हो सकता है आप %s को सक्षम करना चाहें। + अब क्या? यहां कुछ सुझाव दिए गए हैं: + शतरंज के नियम सीखें + शतरंज की रणनीति पहेलियों में सुधार करें। + आर्टिफिशियल इंटेलिजेंस खेलें। + दुनिया भर के विरोधियों से खेलें। + Lichess पर अपने दोस्तों का अनुसरण करें। + टूर्नामेंट में खेलें। + diff --git a/translation/dest/onboarding/hr-HR.xml b/translation/dest/onboarding/hr-HR.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/hr-HR.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/hu-HU.xml b/translation/dest/onboarding/hu-HU.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/hu-HU.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/hy-AM.xml b/translation/dest/onboarding/hy-AM.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/hy-AM.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/ia-IA.xml b/translation/dest/onboarding/ia-IA.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ia-IA.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/id-ID.xml b/translation/dest/onboarding/id-ID.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/id-ID.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/ig-NG.xml b/translation/dest/onboarding/ig-NG.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ig-NG.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/io-EN.xml b/translation/dest/onboarding/io-EN.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/io-EN.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/is-IS.xml b/translation/dest/onboarding/is-IS.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/is-IS.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/it-IT.xml b/translation/dest/onboarding/it-IT.xml new file mode 100644 index 0000000000000..5daab81b17159 --- /dev/null +++ b/translation/dest/onboarding/it-IT.xml @@ -0,0 +1,17 @@ + + + Benvenuto! + Benvenuto su lichess.org! + Questa è la tua pagina profilo. + Questo account sarà usato da un bambino? In tal caso potresti voler attivare %s. + E adesso? Ecco alcuni suggerimenti: + Impara le regole degli scacchi + Migliora con esercizi di tattica degli scacchi. + Gioca contro un\'intelligenza artificiale. + Gioca contro avversari da tutto il mondo. + Segui i tuoi amici su Lichess. + Partecipa a tornei. + Impara da %1$s e %2$s. + Configura Lichess a tuo piacimento. + Esplora il sito e divertiti :) + diff --git a/translation/dest/onboarding/ja-JP.xml b/translation/dest/onboarding/ja-JP.xml new file mode 100644 index 0000000000000..2eabf3d0a3c49 --- /dev/null +++ b/translation/dest/onboarding/ja-JP.xml @@ -0,0 +1,16 @@ + + + ようこそ! + Lichess.org へようこそ! + これはあなたのプロフィールページです。 + このアカウントを使用するのは子供ですか? %s も使えます。 + では何をしましょう? たとえば… + チェスのルールを覚える。 + タクティクス問題で腕をきたえる。 + AI と対戦する。 + 世界中の人と対戦する。 + Lichess で友達をフォローする。 + トーナメントに参加する。 + Lichess の設定を好みに合わせて変える。 + あれこれ触ってお楽しみを :) + diff --git a/translation/dest/onboarding/jbo-EN.xml b/translation/dest/onboarding/jbo-EN.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/jbo-EN.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/jv-ID.xml b/translation/dest/onboarding/jv-ID.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/jv-ID.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/ka-GE.xml b/translation/dest/onboarding/ka-GE.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ka-GE.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/kaa-UZ.xml b/translation/dest/onboarding/kaa-UZ.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/kaa-UZ.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/kab-DZ.xml b/translation/dest/onboarding/kab-DZ.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/kab-DZ.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/kk-KZ.xml b/translation/dest/onboarding/kk-KZ.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/kk-KZ.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/km-KH.xml b/translation/dest/onboarding/km-KH.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/km-KH.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/kmr-TR.xml b/translation/dest/onboarding/kmr-TR.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/kmr-TR.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/kn-IN.xml b/translation/dest/onboarding/kn-IN.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/kn-IN.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/ko-KR.xml b/translation/dest/onboarding/ko-KR.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ko-KR.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/ky-KG.xml b/translation/dest/onboarding/ky-KG.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ky-KG.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/la-LA.xml b/translation/dest/onboarding/la-LA.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/la-LA.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/lb-LU.xml b/translation/dest/onboarding/lb-LU.xml new file mode 100644 index 0000000000000..efd2b043940c6 --- /dev/null +++ b/translation/dest/onboarding/lb-LU.xml @@ -0,0 +1,16 @@ + + + Wëllkomm! + Wëllkomm op lichess.org! + Dëst ass deng Profilsäit. + Soll dëse Kont vun emgem Kand benotzt ginn? Villäicht wëlls de de %s aktivéieren. + Wat elo? Hei sinn e puer Virschléi: + D\'Schachreegele léieren + Gëff besser am Schach, andeems de Taktik-Aufgabe léis. + Spill géint d\'kënschtlech Intelligenz. + Spill géint Géigner aus der ganzer Welt. + Spill op Turnéieren. + Léier vu(n) %1$s a(n) %2$s. + Konfiguréier Lichess esou, wéi et dir gefält. + Exploréier d\'Säit an ameséier dech dobäi :) + diff --git a/translation/dest/onboarding/lg-UG.xml b/translation/dest/onboarding/lg-UG.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/lg-UG.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/lo-LA.xml b/translation/dest/onboarding/lo-LA.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/lo-LA.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/lt-LT.xml b/translation/dest/onboarding/lt-LT.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/lt-LT.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/lv-LV.xml b/translation/dest/onboarding/lv-LV.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/lv-LV.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/mai-IN.xml b/translation/dest/onboarding/mai-IN.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/mai-IN.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/mdf-RU.xml b/translation/dest/onboarding/mdf-RU.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/mdf-RU.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/mg-MG.xml b/translation/dest/onboarding/mg-MG.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/mg-MG.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/mi-NZ.xml b/translation/dest/onboarding/mi-NZ.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/mi-NZ.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/mk-MK.xml b/translation/dest/onboarding/mk-MK.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/mk-MK.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/ml-IN.xml b/translation/dest/onboarding/ml-IN.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ml-IN.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/mn-MN.xml b/translation/dest/onboarding/mn-MN.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/mn-MN.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/mr-IN.xml b/translation/dest/onboarding/mr-IN.xml new file mode 100644 index 0000000000000..8188569b5969c --- /dev/null +++ b/translation/dest/onboarding/mr-IN.xml @@ -0,0 +1,6 @@ + + + सुस्वागतम! + Lichess.org वर स्वागत! + बुद्धीबळाचे नियम शिका + diff --git a/translation/dest/onboarding/ms-MY.xml b/translation/dest/onboarding/ms-MY.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ms-MY.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/mt-MT.xml b/translation/dest/onboarding/mt-MT.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/mt-MT.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/my-MM.xml b/translation/dest/onboarding/my-MM.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/my-MM.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/nb-NO.xml b/translation/dest/onboarding/nb-NO.xml new file mode 100644 index 0000000000000..3db68283c072f --- /dev/null +++ b/translation/dest/onboarding/nb-NO.xml @@ -0,0 +1,17 @@ + + + Velkommen! + Velkommen til lichess.org! + Dette er profilsiden din. + Skal et barn bruke denne kontoen? Du ønsker kanskje å aktivere %s. + Hva nå? Her er noen forslag: + Lær deg sjakkreglene + Bli bedre ved å knekke sjakknøtter. + Spill mot kunstig intelligens. + Spill mot motstandere fra hele verden. + Følg vennene dine på Lichess. + Spill i turneringer. + Lær av %1$s og %2$s. + Konfigurer Lichess etter din smak. + Utforsk nettstedet og ha det gøy :) + diff --git a/translation/dest/onboarding/ne-NP.xml b/translation/dest/onboarding/ne-NP.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ne-NP.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/nl-NL.xml b/translation/dest/onboarding/nl-NL.xml new file mode 100644 index 0000000000000..94c3f4253817b --- /dev/null +++ b/translation/dest/onboarding/nl-NL.xml @@ -0,0 +1,17 @@ + + + Welkom! + Welkom op lichess.org! + Dit is je profielpagina. + Gebruikt een kind dit account? Misschien wil je %s inschakelen. + Wat nu? Hier zijn een paar suggesties: + Leer schaakregels + Word beter met schaakpuzzels. + Speel tegen kunstmatige intelligentie. + Speel tegen tegenstanders van over de hele wereld. + Volg je vrienden op Lichess. + Speel in toernooien. + Leer van %1$s en %2$s. + Configureer Lichess naar uw smaak. + Verken de website en heb het naar je zin :) + diff --git a/translation/dest/onboarding/nn-NO.xml b/translation/dest/onboarding/nn-NO.xml new file mode 100644 index 0000000000000..63a66c1d90e91 --- /dev/null +++ b/translation/dest/onboarding/nn-NO.xml @@ -0,0 +1,17 @@ + + + Velkomen! + Velkomen til lichess.org! + Dette er profilsida di. + Skal eit barn bruka denne kontoen? Det kan være ein god idé å aktivera %s. + Kva no? Her er nokre forslag: + Lær deg sjakkreglane + Bli betre ved å løysa sjakkoppgåver. + Spel mot kunstig intelligens. + Spel mot sjakkspelarar frå heile verda. + Følg venene dine på Lichess. + Spel i turneringar. + Lær frå %1$s og %2$s. + Konfigurér Lichess etter dine preferansar. + Utforsk nettstaden og ha det morosamt :) + diff --git a/translation/dest/onboarding/ns-ZA.xml b/translation/dest/onboarding/ns-ZA.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ns-ZA.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/ny-MW.xml b/translation/dest/onboarding/ny-MW.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ny-MW.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/oc-FR.xml b/translation/dest/onboarding/oc-FR.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/oc-FR.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/om-ET.xml b/translation/dest/onboarding/om-ET.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/om-ET.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/or-IN.xml b/translation/dest/onboarding/or-IN.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/or-IN.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/os-SE.xml b/translation/dest/onboarding/os-SE.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/os-SE.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/pa-IN.xml b/translation/dest/onboarding/pa-IN.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/pa-IN.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/pi-IN.xml b/translation/dest/onboarding/pi-IN.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/pi-IN.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/pl-PL.xml b/translation/dest/onboarding/pl-PL.xml new file mode 100644 index 0000000000000..a6097e8eb840f --- /dev/null +++ b/translation/dest/onboarding/pl-PL.xml @@ -0,0 +1,17 @@ + + + Witaj! + Witaj na lichess.org! + To jest Twoja strona profilowa. + Czy tego konta będzie używało dziecko? Możesz włączyć %s. + Co teraz? Oto kilka sugestii: + Naucz się zasad gry + Rozwijaj się rozwiązując zadania szachowe. + Zagraj przeciwko sztucznej inteligencji. + Graj z ludźmi z całego świata. + Obserwuj swoich znajomych na Lichess. + Graj w turniejach. + Ucz się z %1$s i %2$s. + Skonfiguruj Lichess tak jak chcesz. + Odkrywaj nasz portal i baw się dobrze :) + diff --git a/translation/dest/onboarding/ps-AF.xml b/translation/dest/onboarding/ps-AF.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ps-AF.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/pt-BR.xml b/translation/dest/onboarding/pt-BR.xml new file mode 100644 index 0000000000000..41c1783a6bb61 --- /dev/null +++ b/translation/dest/onboarding/pt-BR.xml @@ -0,0 +1,17 @@ + + + Bem vindo(a)! + Bem-vindo(a) ao lichess.org! + Esta é sua página de perfil. + Alguma criança usará esta conta? Você pode querer habilitar %s. + E agora? Aqui estão algumas sugestões: + Aprenda regras do xadrez + Melhorar com problemas táticos de xadrez. + Jogar contra a Inteligência Artificial. + Jogar contra adversários de todo o mundo. + Seguir seus amigos no Lichess. + Jogar torneios. + Aprenda com %1$s e %2$s. + Configure o Lichess do seu modo. + Explore o site e divirta-se :) + diff --git a/translation/dest/onboarding/pt-PT.xml b/translation/dest/onboarding/pt-PT.xml new file mode 100644 index 0000000000000..f04ef1c86ebd0 --- /dev/null +++ b/translation/dest/onboarding/pt-PT.xml @@ -0,0 +1,17 @@ + + + Bem-vindo(a)! + Bem-vindo(a) ao lichess.org! + Esta é a tua página de perfil. + Uma criança usará esta conta? Podes querer habilitar %s. + E agora? Aqui estão algumas sugestões: + Aprender as regras do xadrez + Melhorar com problemas de táticas de xadrez. + Jogar contra a Inteligência Artificial. + Jogar contra adversários de todo o mundo. + Seguir os teus amigos no Lichess. + Jogar em torneios. + Aprender com %1$s e %2$s. + Configurar o Lichess ao teu gosto. + Explorar o site e divirta-te :) + diff --git a/translation/dest/onboarding/qu-PE.xml b/translation/dest/onboarding/qu-PE.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/qu-PE.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/rn-BI.xml b/translation/dest/onboarding/rn-BI.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/rn-BI.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/ro-RO.xml b/translation/dest/onboarding/ro-RO.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ro-RO.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/ru-RU.xml b/translation/dest/onboarding/ru-RU.xml new file mode 100644 index 0000000000000..df54d9f3f0bc0 --- /dev/null +++ b/translation/dest/onboarding/ru-RU.xml @@ -0,0 +1,17 @@ + + + Добро пожаловать! + Добро пожаловать на lichess.org! + Это ваша страница профиля. + Будет ли эту учётную запись использовать ребёнок? Возможно, вы захотите включить %s. + Что теперь? Вот несколько предложений: + Изучите шахматные правила + Улучшайте свой уровень решая шахматные задачи. + Играйте против искусственного интеллекта. + Играйте с соперниками со всего мира. + Следите за друзьями на Lichess. + Играйте в турнирах. + Изучайте %1$s и %2$s. + Настройте Lichess в соответствии с вашими предпочтениями. + Исследуйте сайт и получайте удовольствие :) + diff --git a/translation/dest/onboarding/rw-RW.xml b/translation/dest/onboarding/rw-RW.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/rw-RW.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/ry-UA.xml b/translation/dest/onboarding/ry-UA.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ry-UA.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/sa-IN.xml b/translation/dest/onboarding/sa-IN.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/sa-IN.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/sc-IT.xml b/translation/dest/onboarding/sc-IT.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/sc-IT.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/sco-GB.xml b/translation/dest/onboarding/sco-GB.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/sco-GB.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/sd-PK.xml b/translation/dest/onboarding/sd-PK.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/sd-PK.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/se-NO.xml b/translation/dest/onboarding/se-NO.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/se-NO.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/si-LK.xml b/translation/dest/onboarding/si-LK.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/si-LK.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/sk-SK.xml b/translation/dest/onboarding/sk-SK.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/sk-SK.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/sl-SI.xml b/translation/dest/onboarding/sl-SI.xml new file mode 100644 index 0000000000000..281e8f1757258 --- /dev/null +++ b/translation/dest/onboarding/sl-SI.xml @@ -0,0 +1,17 @@ + + + Dobrodošli! + Dobrodošli na lichess.org! + To je stran vašega profila. + Ali bo otrok uporabljal ta račun? Morda boste želeli omogočiti %s. + Kaj zdaj? Tukaj je nekaj predlogov: + Naučite se šahovskih pravil + Izboljšajte se z ugankami šahovske taktike. + Igrajte z umetno inteligenco. + Igrajte nasprotnike z vsega sveta. + Spremljajte svoje prijatelje na Lichess. + Igraj na turnirjih. + Učite se od %1$s in %2$s. + Konfigurirajte Lichess po svojih željah. + Raziščite stran in se zabavajte :) + diff --git a/translation/dest/onboarding/sn-ZW.xml b/translation/dest/onboarding/sn-ZW.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/sn-ZW.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/so-SO.xml b/translation/dest/onboarding/so-SO.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/so-SO.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/sq-AL.xml b/translation/dest/onboarding/sq-AL.xml new file mode 100644 index 0000000000000..ec8ffc06b804b --- /dev/null +++ b/translation/dest/onboarding/sq-AL.xml @@ -0,0 +1,17 @@ + + + Mirë se vini! + Mirë se vini në lichess.org! + Kjo është faqja e profilit tuaj. + A do ta përdorë këtë llogari ndonjë fëmijë? Mund të doni të aktivizoni %s. + Po më? Ja ndoca sugjerime: + Mësoni rregullat e shahut + Përmirësohuni, përmes puzzle-esh taktikash shahu. + Luani kundër Inteligjencës Artificiale. + Luani me kundërshtarë nga anembanë bota. + Ndiqni shokët tuaj në Lichess. + Luani në turne. + Mësoni nga %1$s dhe %2$s. + Formësojeni Lichess-in si t’ju pëlqejë. + Eksploroni sajtin dhe zbavituni :) + diff --git a/translation/dest/onboarding/sr-SP.xml b/translation/dest/onboarding/sr-SP.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/sr-SP.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/st-ZA.xml b/translation/dest/onboarding/st-ZA.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/st-ZA.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/sv-SE.xml b/translation/dest/onboarding/sv-SE.xml new file mode 100644 index 0000000000000..b48cfd9a9b57a --- /dev/null +++ b/translation/dest/onboarding/sv-SE.xml @@ -0,0 +1,17 @@ + + + Välkommen! + Välkommen till lichess.org! + Det här är din profilsida. + Kommer ett barn att använda detta konto? Du kanske vill aktivera %s. + Vad nu? Här är några förslag: + Lär dig schackregler + Förbättra med schacktaktikspussel. + Spela den Artificiella Intelligensen. + Spela motståndare från hela världen. + Följ dina vänner på Lichess. + Spela i Turneringar. + Lär av %1$s och %2$s. + Konfigurera Lichess efter dina önskemål. + Utforska sajten och ha kul :) + diff --git a/translation/dest/onboarding/sw-KE.xml b/translation/dest/onboarding/sw-KE.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/sw-KE.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/ta-IN.xml b/translation/dest/onboarding/ta-IN.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ta-IN.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/te-IN.xml b/translation/dest/onboarding/te-IN.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/te-IN.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/tg-TJ.xml b/translation/dest/onboarding/tg-TJ.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/tg-TJ.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/th-TH.xml b/translation/dest/onboarding/th-TH.xml new file mode 100644 index 0000000000000..09e2dd12e37c0 --- /dev/null +++ b/translation/dest/onboarding/th-TH.xml @@ -0,0 +1,17 @@ + + + ยินดีต้อนรับ! + ยินดีต้อนรับสู่ lichess.org + นี้คือหน้าโปรไฟล์ของคุณ + เด็กจะใช้บัญชีนี้หรือไม่ คุณอาจต้องการเปิดใช้งาน %s + แล้วอย่างไรต่อ? นี้คือคำแนะนำเล็กๆน้อยๆ + เรียนรู้กฎของหมากรุก + เก่งขึ้นด้วยปริศนากลยุทธ์ของหมากรุก + เล่นกับปัญญาประดิษฐ์ + เล่นกับคู่แข่งทั่วโลก + ติดตามเพื่อนของคุณบน Lichess + แข่งขันในทัวร์นาเมนต์ + เรียนรู้จาก %1$s และ %2$s + ตั้งค่า Lichess ตามความชอบของคุณ + สำรวจเว็บไซต์ให้สนุกนะ :) + diff --git a/translation/dest/onboarding/ti-ER.xml b/translation/dest/onboarding/ti-ER.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ti-ER.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/tk-TM.xml b/translation/dest/onboarding/tk-TM.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/tk-TM.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/tl-PH.xml b/translation/dest/onboarding/tl-PH.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/tl-PH.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/tlh-AA.xml b/translation/dest/onboarding/tlh-AA.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/tlh-AA.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/tn-ZA.xml b/translation/dest/onboarding/tn-ZA.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/tn-ZA.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/tp-TP.xml b/translation/dest/onboarding/tp-TP.xml new file mode 100644 index 0000000000000..e83f1a3bd65cb --- /dev/null +++ b/translation/dest/onboarding/tp-TP.xml @@ -0,0 +1,17 @@ + + + o kama pona! + o kama pona tawa lipu Lichess! + ni li lipu sina + jan lili li kepeken ala kepeken sijelo ni? kepeken la, ken la sina wile open e %s + tenpo ni la seme? ken la sina wile ni: + o kama sona e lawa pi musi ni + o wawa e sona sina kepeken musi lili + o utala e ilo + o utala e jan ante lon musi ni + o kama tawa poka pi jan pona sina + o musi lon utala suli a + o kama sona tan %1$s tan %2$s + o ante e ilo Lichess tawa wile sina + o musi pona a! + diff --git a/translation/dest/onboarding/tr-TR.xml b/translation/dest/onboarding/tr-TR.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/tr-TR.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/tt-RU.xml b/translation/dest/onboarding/tt-RU.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/tt-RU.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/ug-CN.xml b/translation/dest/onboarding/ug-CN.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ug-CN.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/uk-UA.xml b/translation/dest/onboarding/uk-UA.xml new file mode 100644 index 0000000000000..c69e5cd6c1490 --- /dev/null +++ b/translation/dest/onboarding/uk-UA.xml @@ -0,0 +1,17 @@ + + + Фу! + Ласкаво просимо на lichess.org! + Перевірте свою сторінку профілю. + Буде використовувати дочірній обліковий запис? Ви можете увімкнути %s. + Що зараз? Ось кілька пропозицій: + Вивчайте шахові правила + Покращуйтесь із шаховою тактичною головоломкою. + Зіграйте штучний інтелект. + Грайте з гравцями з усього світу + Слідкуйте за своїми друзями на Lichess. + Грайте в турнірах. + Вивчайте з %1$s та %2$s. + Налаштуйте Lichess на свій смак. + Досліджуйте сайт і веселіться :) + diff --git a/translation/dest/onboarding/ur-PK.xml b/translation/dest/onboarding/ur-PK.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/ur-PK.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/uz-UZ.xml b/translation/dest/onboarding/uz-UZ.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/uz-UZ.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/vi-VN.xml b/translation/dest/onboarding/vi-VN.xml new file mode 100644 index 0000000000000..e6d330a72a8c2 --- /dev/null +++ b/translation/dest/onboarding/vi-VN.xml @@ -0,0 +1,17 @@ + + + Chào mừng! + Chào mừng đến với lichess.org! + Đây là trang hồ sơ của bạn. + Một đứa trẻ sẽ sử dụng tài khoản này? Bạn có thể muốn kích hoạt %s. + Làm gì bây giờ? Dưới đây là một vài gợi ý: + Học luật chơi cờ vua + Cải thiện với các câu đố cờ vua chiến thuật. + Chơi với trí tuệ nhân tạo AI. + Chơi với đối thủ từ khắp nơi trên thế giới. + Theo dõi bạn bè của bạn trên Lichess. + Chơi trong các giải đấu. + Học từ %2$s và %1$s. + Tùy chỉnh Lichess theo ý thích của bạn. + Khám phá trang web và vui chơi :) + diff --git a/translation/dest/onboarding/wo-SN.xml b/translation/dest/onboarding/wo-SN.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/wo-SN.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/xh-ZA.xml b/translation/dest/onboarding/xh-ZA.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/xh-ZA.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/yo-NG.xml b/translation/dest/onboarding/yo-NG.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/yo-NG.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/zh-CN.xml b/translation/dest/onboarding/zh-CN.xml new file mode 100644 index 0000000000000..1aeaea2ca35ed --- /dev/null +++ b/translation/dest/onboarding/zh-CN.xml @@ -0,0 +1,7 @@ + + + 欢迎! + 欢迎来到 lichess.org。 + 这是你的个人资料页面。 + 未成年人会使用此帐户吗?您可能想要启用 %s。 + diff --git a/translation/dest/onboarding/zh-TW.xml b/translation/dest/onboarding/zh-TW.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/zh-TW.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/onboarding/zu-ZA.xml b/translation/dest/onboarding/zu-ZA.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/onboarding/zu-ZA.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/patron/ar-SA.xml b/translation/dest/patron/ar-SA.xml index 21a0e88ca01e5..6e0d4c5a3f1c3 100644 --- a/translation/dest/patron/ar-SA.xml +++ b/translation/dest/patron/ar-SA.xml @@ -69,6 +69,7 @@ الدفعة القادمة سيتم خصم %1$s في %2$s. قم بتبرع إضافي الآن + امنح أجنحة الراعي يتم إعطاء المتبرع شعار الجناح كهدية تحديث تغيير المبلغ الشهري (%s) diff --git a/translation/dest/patron/gsw-CH.xml b/translation/dest/patron/gsw-CH.xml index d3a80c3902c65..b067dd89bfcd5 100644 --- a/translation/dest/patron/gsw-CH.xml +++ b/translation/dest/patron/gsw-CH.xml @@ -17,8 +17,8 @@ Du häsch es läbelangs Gönnerkonto. Das isch grossartig! Du häsch es Gönnerkonto bis %s. Falls du nöd erneuere tuesch, wird dis Konto in es Regulärs umgwandlet. - Mir sind e \"Non-Profit-Organisation\", will mir glaubed, dass jede sött Zuegang zu ere gratis, \"Wältklass Schach Plattform\" ha. - Mir sind uf d\'Underschtützig vu so Lüt wie du agwise, zum das möglich z\'mache. Wänn dir Lichess g\'fallt, chasch du eus underschtütze, indem du schpändisch und so en Lichess Gönner wirsch! + Als \"Non-Profit-Organisation\" glaubed mir, dass jede sött Zuegang zunere gratis, \"Wältklass Schach Plattform\" ha. + Mir sind uf Underschtützig vu Lüt wie du agwise, um das z\'ermögliche. Wänn dir Lichess g\'fallt, chasch eus underschtütze, idem du schpändsch und en Lichess Gönner wirsch! Läbeslang Zahl eimal %s und wird für immer en Lichess Gönner! Läbeslange Lichess Gönner @@ -30,16 +30,16 @@ Bitte gib en Betrag in %s i Kreditcharte Amälde zum Schpände - Mir sind es chlises Team, drum macht dini Underschtützig en riese Underschid! - Die g\'firete Gönner, wo Lichess möglich mached + Mir sind es chlises Team, drum hät dini Underschtützig e grossi Würkig! + Gönner wo Lichess möglich mached Wohi flüsst das Gäld? Ganz zerscht in leischtigsstarchi Server. Dänn zahled mir en Vollzit-Entwickler: %s, de Gründer vu Lichess. - Da die detailliert Choschte Uufteilig + Da die detailliert Choschte-Ufteilig Isch Lichess e offizielli Non-Profit-Organisation? Ja, da isch d\'Gründigsurkund (uf französisch) Chann ich mini monetlichi Underschtützig ändere/chünde? - Ja - jederzii vu dere Site us oder + Ja - jederzit - vu dere Site us oder du chasch %s. de Lichess Support kontaktiere Anderi Schpändemöglichkeite? @@ -48,8 +48,8 @@ du chasch %s. Du erreichsch de Gönnerstatus aber nur mit dem obere Formular. Gits zuesätzlichi Funktione für en Gönner? - Nei - Lichess isch komplett gratis, für immer und für jedermann. Das isch es Verschpräche. -Aber en Lichess Gönner chann sich mit dem coole Flamme Icon sis Profil schmücke. + Nei - Lichess isch komplett gratis, für immer und für jedermann; das isch es Verschpräche. +Aber en Lichess-Gönner chann sis Profil mit dem coole Flügel-Icon schmücke. Check de Detailverglich vu de Funktione Lichess Gönner für 1 Monet diff --git a/translation/dest/patron/so-SO.xml b/translation/dest/patron/so-SO.xml index dd88402d840e4..92a7829323c84 100644 --- a/translation/dest/patron/so-SO.xml +++ b/translation/dest/patron/so-SO.xml @@ -1,6 +1,6 @@ - Deeqdo - Lichess Patron + Ku deeq + Macmiil Lichess Chess bilaash ah qof walba, weligiis! diff --git a/translation/dest/patron/th-TH.xml b/translation/dest/patron/th-TH.xml index 7b93839b56e11..ba6b12ca68280 100644 --- a/translation/dest/patron/th-TH.xml +++ b/translation/dest/patron/th-TH.xml @@ -18,6 +18,7 @@ รายครั้ง อื่นๆ บัตรเครดิต + เข้าสู่ระบบเพี่อบริจาคเงิน ติดต่อ Lichess support ไม่ เพราะ Lichess นั้นฟรี ตลอดไป และสำหรับทุกคน นั่นเป็นคำสัญญา อย่างไรก็ตาม ผู้อุปถัมภ์จะได้รับสิทธิ์ในการอวดด้วยไอคอนโปรไฟล์ใหม่สุดเจ๋ง diff --git a/translation/dest/patron/vi-VN.xml b/translation/dest/patron/vi-VN.xml index 47f82d12cdcf0..aeb6817398582 100644 --- a/translation/dest/patron/vi-VN.xml +++ b/translation/dest/patron/vi-VN.xml @@ -2,7 +2,7 @@ Ủng hộ Ủng hộ bằng tài khoản %s - Bảo trợ Lichess + Người bảo trợ Lichess Tài khoản miễn phí Trở thành một Người bảo trợ Lichess %s đã trở thành một Người bảo trợ Lichess @@ -10,9 +10,8 @@ %1$s là Người bảo trợ Lichess trong %2$s tháng Những Người bảo trợ mới - Cờ Vua Miễn Phí Cho Mọi Người! -Mãi Mãi! - Không có quảng cáo, không cần đóng tiền; nhưng có mã nguồn mở và sự đam mê. + Cờ Vua Miễn Phí Cho Mọi Người! Mãi Mãi! + Không có quảng cáo, không tốn tiền; nhưng có mã nguồn mở và sự đam mê. Cảm ơn sự ủng hộ của bạn! Bạn có một tài khoản Người bảo trợ trọn đời. Khá tuyệt vời đấy! Bạn có tài khoản người Bảo Trợ cho đến %s. @@ -46,9 +45,9 @@ Hoặc bạn có thể %s. Lichess được đăng ký với %s. Chúng tôi cũng chấp nhận chuyển khoản ngân hàng Xin lưu ý rằng chỉ có hình thức ủng hộ ở trên mới được cấp trạng thái Người bảo trợ. - Có tính năng nào được dành riêng cho những Người bảo trợ không? + Có những tính năng nào được dành riêng cho những Người bảo trợ không? Không, bởi vì Lichess hoàn toàn miễn phí, mãi mãi và dành cho tất cả mọi người. Đó là lời hứa của chúng tôi. -Tuy nhiên, Patron được quyền khoe khoang với những cánh Người bảo trợ mới thú vị hiển thị trên hồ sơ của bạn. +Tuy nhiên, Patron được quyền khoe khoang với những đôi cánh Người bảo trợ mới thú vị hiển thị trên hồ sơ của bạn. Xem so sánh các tính năng chi tiết Bảo trợ Lichess trong %s tháng @@ -59,8 +58,8 @@ Tuy nhiên, Patron được quyền khoe khoang với những cánh Người b Lần thanh toán tiếp theo Bạn sẽ trả %1$s vào ngày %2$s. Ủng hộ thêm ngay bây giờ - Tặng cánh Người bảo trợ - Tặng cánh Người bảo trợ cho kỳ thủ + Tặng đôi cánh Người bảo trợ + Tặng đôi cánh Người bảo trợ cho kỳ thủ Cập nhật Đổi số tiền hàng tháng (%s) Hủy sự ủng hộ của bạn diff --git a/translation/dest/perfStat/vi-VN.xml b/translation/dest/perfStat/vi-VN.xml index 3e40acacaf180..1f1b5ec45ad55 100644 --- a/translation/dest/perfStat/vi-VN.xml +++ b/translation/dest/perfStat/vi-VN.xml @@ -9,8 +9,8 @@ Giá trị thấp hơn nghĩa là Elo ổn định hơn. Ở ngưỡng trên %1$s, Elo được coi là tạm thời. Để được xếp trong bảng xếp hạng, giá trị này phải ở dưới ngưỡng %2$s (cờ tiêu chuẩn) hoặc %3$s (các biến thể). Tổng số ván cờ Các ván cờ có xếp hạng - Trò chơi trong giải đấu - Số ván cờ chơi Berserk + Số ván chơi trong giải đấu + Số ván chơi Berserk Thời gian đã chơi Đối thủ trung bình Thắng diff --git a/translation/dest/preferences/an-ES.xml b/translation/dest/preferences/an-ES.xml index 357caa1e75259..ffae017833f96 100644 --- a/translation/dest/preferences/an-ES.xml +++ b/translation/dest/preferences/an-ES.xml @@ -15,9 +15,11 @@ Inicial (en anglés) d\'a pieza (K, Q, R, B, N) Modo Zen Amostrar las puntuacions d\'o chugador + Amostrar los estilos d\'o chugador Esto permite d\'amagar totas las puntuaciosn d\'a pachina web pa centrar-se en os escaques. las partidas pueden estar puntuadas, nomás se cambia lo que se veye. Amostrar lo control de grandaria d\'o escaquero Nomás en posición inicial + Nomás en a partida Escaques a ciegas (piezas invisibles) Reloch d\'escaques Decenas de segundo @@ -39,6 +41,7 @@ Reclamar taulas per triple repetición Cuan lo tiempo restante sía de < 30 segundos Confirmación de movimiento + Se puet desactivar durant la partida en o menú d\'o taulero Partidas per correspondencia Correspondencia y sin limite de tiempo Confirmar abandono y ofiertas de taulas @@ -46,6 +49,7 @@ Movendo lo rei dos cuadros Movendo lo rei dica la torre Dentrada de movimientos con o teclau + Indica los movimientos con a tuya voz Flechas enta los movimientos validos Decir \"Buena partida, bien chugada\" en caso de redota u empaz Las tuyas preferencias s\'han alzau. diff --git a/translation/dest/preferences/ar-SA.xml b/translation/dest/preferences/ar-SA.xml index dfb7553dc73c1..254e53beb43a2 100644 --- a/translation/dest/preferences/ar-SA.xml +++ b/translation/dest/preferences/ar-SA.xml @@ -7,15 +7,16 @@ المؤثرات الحركية للقطعة الفرق المادي تميز معالم الرقعة (آخر نقلة والكش) - نقلات القطعة ( النقلات المتاحة والنقلات الاستباقية) + إظهار النقلات القانونية (النقلات المتاحة والنقلات الاستباقية) إحداثيات الرقعة (A-H, 1-8) قائمة النقلات خلال المباراة تدوين النقلة رمز قطعة الشطرنج حروف (K, Q, R, B, N) وضع التأمل - إظهار مستويات اللاعب - هذا يسمح بإخفاء جميع التقييمات من الموقع، للمساعدة في التركيز على الشطرنج. لا يزال من الممكن تقييم المباريات ، هذا فقط حول ما يمكنك رؤيته. + إظهار تقييمات اللاعب + إظهار ميول اللاعب + هذا يخفي جميع التقييمات من الموقع، للمساعدة في التركيز على مباراة الشطرنج. لا يزال من الممكن لعب مباريات مقيمة، هذا فقط يحدد ما تراه. أظهر زر تعديل حجم الرقعة خلال الوضع المبدئي فقط في اللعبة فقط @@ -24,8 +25,8 @@ أجزاء الثانية عندما يقل الوقت عن 10 ثوانٍ الشريط الأخضر للساعة - الصوت عندما يقارب الوقت على الإنتهاء - إعطاء مزيد من الوقت + إصدار صوت عندما يقارب الوقت الانتهاء + منح الوقت إعدادات اللعبة كيف يمكنك تحريك القطع؟ النقر فوق مربعين @@ -35,7 +36,7 @@ التراجع عن النقلات (بموافقة الخصم) في المباريات غير المقيمة فقط الترقية إلى وزير آلياً - اضغط مفتاح<ctrl> اثناء الترقية لتعطيل الترقية التلقائية مؤقتاً + اضغط مفتاح<ctrl> عند الترقية لتعطيل الترقية التلقائية مؤقتاً عند النقلة الاستباقية مطالبة بالتعادل لتكرار نفس النقلات ثلاث مرات بشكل تلقائي عندما يقل الوقت عن 30 ثانية diff --git a/translation/dest/preferences/da-DK.xml b/translation/dest/preferences/da-DK.xml index c5140717b9e2b..b3614f64dd5ec 100644 --- a/translation/dest/preferences/da-DK.xml +++ b/translation/dest/preferences/da-DK.xml @@ -15,7 +15,7 @@ Bogstaver (K, Q, R, B, N) Zentilstand Vis spilleres ratings - Vis spilleres flairs + Vis spilleres ikoner Dette gør det muligt at skjule alle ratings på hjemmesiden, så du kan fokusere på skakspillet. Partier kan stadig være ratede, det handler kun om, hvad du får at se. Vis brætstørrelse justering Kun ved indledende position diff --git a/translation/dest/preferences/de-DE.xml b/translation/dest/preferences/de-DE.xml index 46c01809f3f81..496fe17154732 100644 --- a/translation/dest/preferences/de-DE.xml +++ b/translation/dest/preferences/de-DE.xml @@ -15,7 +15,7 @@ Buchstaben (K, Q, R, B, N) Zen-Modus Wertungszahl von Spielern anzeigen - Spieler-Dekoration anzeigen + Spieler-Flairs anzeigen Versteckt alle Wertungen auf der Website, damit du dich voll auf das Schach zu konzentrieren kannst. Partien können immer noch gewertet sein, es geht nur darum, was du zu sehen bekommst. Regler zum Ändern der Brettgröße anzeigen Nur in der Anfangsstellung diff --git a/translation/dest/preferences/fa-IR.xml b/translation/dest/preferences/fa-IR.xml index 14cfaca14e1ad..69d26b4bd43cb 100644 --- a/translation/dest/preferences/fa-IR.xml +++ b/translation/dest/preferences/fa-IR.xml @@ -19,6 +19,7 @@ این یک تنظیمات بله/خیر است این تنظیمات به شما این اجازه را می دهد که انتخاب کنید که ریتینگ بازیکنان دیگر نشان داده شود یا نه. این تنظیمات برای این است که بعضی از بازیکنان، بازی خود را تحت تاثیر ریتینگ بقیه بازیکنان قرار می دهند + نمایش نشان بازیکنان این کار اجازه می دهد تمام امتیازها را از سایت پاک کنید، تا بتوانید روی شطرنج تمرکز کامل داشته باشید. بازی ها هنوز هم می توانند امتیازی باشند، فقط شما دیگر نمی توانید آن را ببینید. نمایش دستگیره برای تغییر اندازه صفحه فقط در آغاز بازی diff --git a/translation/dest/preferences/he-IL.xml b/translation/dest/preferences/he-IL.xml index 00eacae9899cb..2970e14c0dbc7 100644 --- a/translation/dest/preferences/he-IL.xml +++ b/translation/dest/preferences/he-IL.xml @@ -15,6 +15,7 @@ אות (K, Q, R, B, N) מצב זן הצג דירוג שחקנים + הצגת הסמלילים של השחקנים אם תבחר/י להסתיר את הדירוג, הדירוג של השחקן היריב לא יופיע כדי לאפשר לך להתרכז בשח, אך המשחק יהיה מדורג. הצג סמן להגדלת הלוח רק בעמדה ההתחלתית diff --git a/translation/dest/preferences/it-IT.xml b/translation/dest/preferences/it-IT.xml index ddaf733cca9ea..688c04da4351f 100644 --- a/translation/dest/preferences/it-IT.xml +++ b/translation/dest/preferences/it-IT.xml @@ -15,6 +15,7 @@ Lettera (K, Q, R, B, N) Modalità Zen Mostra punteggi giocatori + Mostra le icone del giocatore Questa funzionalità permette di nascondere i punteggi dei giocatori per aiutare a concentrarti sulla partita. Le partite possono comunque essere classificate, questa impostazione riguarda solo ciò che vedi. Mostra l\'icona di ridimensionamento della scacchiera Solo sulla posizione iniziale diff --git a/translation/dest/preferences/lb-LU.xml b/translation/dest/preferences/lb-LU.xml index 5f1b9968c00ba..742def57d8930 100644 --- a/translation/dest/preferences/lb-LU.xml +++ b/translation/dest/preferences/lb-LU.xml @@ -34,7 +34,7 @@ Virauszich (wärend dem Géigner sengem Zuch spillen) Zeréckhuelen (mat Zoustemmung vum Géigner) Just an ongewäerten Partien - Automatesch zur Damm ëmwandelen + Automatesch an eng Damm ëmwandelen Dréck ob deng <ctrl> Tasten während der Emwandlung fir temporär déi automatesch Emwandlung ze desaktivéieren Wann Virauszuch Remis duerch dräifach Stellungswidderhuelung reklaméieren diff --git a/translation/dest/preferences/nn-NO.xml b/translation/dest/preferences/nn-NO.xml index 532e172f4386b..5fcabc9c73836 100644 --- a/translation/dest/preferences/nn-NO.xml +++ b/translation/dest/preferences/nn-NO.xml @@ -15,6 +15,7 @@ Bokstav (K, Q, R, B, N) Zen-modus Vis ratingen til spelarane + Vis spelarikonar Denne innstillinga gjer det mogleg å gøyme alle ratingar frå nettsida. Dette for å hjelpa til med å fokusera på sjakken. Du kan framleis spela rangerte parti, dette handlar berre om kva du får sjå. Vis handtak for brettstorleik Berre for startstillinga diff --git a/translation/dest/preferences/ru-RU.xml b/translation/dest/preferences/ru-RU.xml index 9c0792607f907..ccbb553f9d099 100644 --- a/translation/dest/preferences/ru-RU.xml +++ b/translation/dest/preferences/ru-RU.xml @@ -15,7 +15,7 @@ Буква фигуры (K, Q, R, B, N) Режим Дзен Показывать рейтинг игрока - Показывать флаеры игроков + Показывать эмодзи игроков Позволяет скрыть все рейтинги на сайте, чтобы помочь сосредоточиться на игре. Сами партии останутся рейтинговыми, просто вы не будете это видеть. Показывать ручку изменения размера доски Только в начальном положении diff --git a/translation/dest/preferences/sl-SI.xml b/translation/dest/preferences/sl-SI.xml index 10204866486e4..9dcabb7aff9a8 100644 --- a/translation/dest/preferences/sl-SI.xml +++ b/translation/dest/preferences/sl-SI.xml @@ -57,6 +57,7 @@ Omenili so vas v komentarju na forumu Povabilo k študiji Novo v korespondenčnih partijah + Izzivi Turnir se bo kmalu začel Potekel vam bo čas Zvočno obvestilo znotraj Lichess diff --git a/translation/dest/preferences/sv-SE.xml b/translation/dest/preferences/sv-SE.xml index a0ef86178af38..7d4ba059771e9 100644 --- a/translation/dest/preferences/sv-SE.xml +++ b/translation/dest/preferences/sv-SE.xml @@ -15,7 +15,7 @@ Bokstav (K, Q, R, B, N) Zen-läge Visa spelarens rating - Visa spelarikoner + Visa spelarflairs Detta gör det möjligt att dölja all rating från webbplatsen, för att fokusera på schackspelet. Partiet kan fortfarande vara med rating, detta handlar bara om vad du får se. Visa handtag för att ändra brädets storlek Endast vid ursprunglig position diff --git a/translation/dest/preferences/vi-VN.xml b/translation/dest/preferences/vi-VN.xml index 8e91ff84fc200..0be1fc0766b0a 100644 --- a/translation/dest/preferences/vi-VN.xml +++ b/translation/dest/preferences/vi-VN.xml @@ -16,7 +16,7 @@ Chế độ tập trung Hiển thị hệ số của người chơi Hiển thị biểu tượng của người chơi - Điều này cho phép ẩn toàn bộ hệ số Elo từ trang web, giúp tập trung vào ván cờ. Ván đấu vẫn có thể tính Elo, điều này chỉ là về những thứ bạn muốn nhìn thấy. + Điều này sẽ ẩn toàn bộ hệ số Elo khỏi Lichess để giúp tập trung vào ván cờ. Ván đấu có xếp hạng vẫn ảnh hưởng đến hệ số Elo của bạn, đây chỉ là những thứ bạn có thể nhìn thấy. Hiện nút thay đổi kích cỡ bàn cờ Chỉ ở thế cờ ban đầu Chỉ trong ván cờ @@ -41,7 +41,7 @@ Tự động hoà khi lặp cờ ba lần Khi thời gian còn lại < 30 giây Xác nhận nước đi - Có thể bị vô hiệu hóa trong trò chơi với mục lục bàn cờ + Có thể bị vô hiệu hóa trong ván cờ với mục lục bàn cờ Cờ qua thư Cờ qua thư và không giới hạn Xác nhận chịu thua và đề nghị hòa @@ -54,7 +54,7 @@ Tự động nhắn \"Good game, well played\" (Ván cờ hay, chơi hay lắm) sau khi hòa hoặc thua Tùy chọn của bạn đã được lưu Cuộn con chuột trên bàn cờ để xem lại nước đi - Email thông báo hàng ngày sẽ bao gồm cả những ván cờ qua thư + Email thông báo hàng ngày sẽ bao gồm cả các ván cờ qua thư Streamer đang phát trực tiếp Tin nhắn mới Bạn được nhắc đến trong một bình luận trên diễn đàn diff --git a/translation/dest/puzzle/da-DK.xml b/translation/dest/puzzle/da-DK.xml index bd21f50517e68..6ab4e652a9123 100644 --- a/translation/dest/puzzle/da-DK.xml +++ b/translation/dest/puzzle/da-DK.xml @@ -18,18 +18,18 @@ Din opgave-rating vil ikke ændre sig. Bemærk at opgaver ikke er en konkurrence. Rating hjælper med at vælge de bedste opgaver i forhold til dine nuværende færdigheder. Find det bedste træk for hvid. Find det bedste træk for sort. - For at få personlige opgaver: - Opgave %s + For at få personlige taktikopgaver: + Taktikopgave %s Dagens opgave - Daglig opgave + Daglig taktikopgave Klik for at løse Godt træk Bedste træk! Bliv ved… Korrekt! - Opgave løst! + Taktikpgave løst! Efter åbninger - Opgaver efter åbninger + Taktikopgaver efter åbninger Åbninger du har spillet mest i ratede partier Brug \"Find på side\" i browsermenuen til at finde din foretrukne åbning! Brug Ctrl+f til at finde din foretrukne åbning! @@ -59,26 +59,26 @@ Eksempel Tilføj et andet tema - Næste opgave - Spring straks videre til næste opgave + Næste taktikopgave + Spring straks videre til næste taktikopgave Opgave-kontrolpanel Forbedringsområder Styrke Opgavehistorik løst mislykket - Løs opgaver af stigende sværhedsgrad og opbyg en sejrsstime. Der er intet ur, så tag dig god tid. Ét forkert træk og spillet er ovre! Men du kan springe ét træk over per session. - Din stime: %s + Løs taktikopgaver af stigende sværhedsgrad og opbyg en sejrsstime. Der er intet ur, så tag dig god tid. Ét forkert træk og spillet er ovre! Men du kan springe ét træk over per session. + Din stime: %s Spring dette træk over for at bevare din stime! Virker kun én gang per gennemløb. Fortsæt stimen Ny stime Fra mine partier - Søg opgaver fra en spillers partier - Opgaver fra %s\' partier - Søg opgaver - Du har ingen opgaver i databasen, men Lichess elsker dig alligevel. -Spil hurtige (rapid) og klassiske (classical) partier for at forøge chancerne for at en af dine opgave tilføjes! - %1$s opgaver fundet i %2$s partier + Søg taktikopgaver fra en spillers partier + Taktikopgaver fra %s\' partier + Søg taktikopgaver + Du har ingen taktikopgaver i databasen, men Lichess elsker dig alligevel. +Spil hurtige (rapid) og klassiske (classical) partier for at forøge chancerne for at en af dine taktikopgave tilføjes! + %1$s taktikopgaver fundet i %2$s partier Træn, analysér, forbedr %s spillet diff --git a/translation/dest/puzzle/fa-IR.xml b/translation/dest/puzzle/fa-IR.xml index 161d9753326dd..c1f180d9737c9 100644 --- a/translation/dest/puzzle/fa-IR.xml +++ b/translation/dest/puzzle/fa-IR.xml @@ -1,64 +1,64 @@ معماها - تم پازل ها - پیشنهادی - فاز ها - موضوع + موضوعات معما + توصیه + مرحله‌ها + بُن‌مایه‌ها پیشرفته - مدت - مات + تعداد حرکات + مات‌ها اهداف - منشا - حرکات مخصوص - شما این پازل را دوست داشتید؟ - برای بارگزاری بعدی رای دهید - پازل خوبی بود - پازل بدی بود - ریتینگ پازل شما تغییری نخواهد کرد. توجه کنید که پازل ها یک مسابقه نیستند. ریتینگ پازل شما به انتخاب پازل مناسب در سطح خودتان کمک می کند. + خاستگاه + حرکات ویژه + این معما را دوست داشتید؟ + موافقت برای بارگذاری معمای بعدی! + معمای خوبی بود + معمای بدی بود + امتیازبندی معمایی شما تغییری نخواهد کرد. توجه داشته باشید که معماها یک رقابت نیستند. امتیازبندی‌تان به انتخاب بهترین معماها برای سطح مهارت فعلی‌تان کمک می‌کند. بهترین حرکت برای سفید را پیدا کنید - بهترین حرکت را برای سیاه بیابید - برای دریافت پازل شخصی سازی شده + بهترین حرکت برای سیاه را پیدا کنید. + دریافت معماهای شخصی‌سازی‌شده: معمای %s - مسئله روز - پازل روزانه - برای جل کردن کلیک کنید - حرکتی خوب + معمای روز + معمای روزانه + برای جل کردن بزتیذ + حرکتِ خوب بهترین حرکت! ادامه دهید… - موفقیت! - پازل تمام شد! - به ترتیب شروع ها - پازل ها به ترتیب شروع - شروع هایی در بیشترین بازی های رتبه ای بازی کردید - با استفاده از قابلیت \"جستجو در صفحه\" مرورگر خود گشایش محبوب خود را پیدا کنید! - با استفاده از Ctrl+F گشایش محبوب خود را پیدا کنید! - ابن حرکت صحیح نیست + موفق شدید! + معما تکمیل شد! + بر اساس گشایش‌ها + معماها بر اساس گشایش‌ها + گشایش‌هایی که بیش از همه در بازی‌های امتیازی کرده‌اید + از گزینه «پیدا کردن در صفحه» مرورگر استفاده کنید تا گشایش مورد علاقه خود را پیدا کنید! + از Ctrl+f برای پیدا کردن گشایش مورد علاقه خود استفاده کنید! + ابن حرکت نیست! چیز دیگری پیدا کنید امتیاز: %s پنهان %s بار بازی شده است - این جدول%s بار بازی شده است + %s بار بازی شده برگرفته شده از بازی %s ادامه دادن تمرین میزان سختی معمولی - اسان تر + آسان‌تر آسان‌ترین - یک امتیاز پایین‌تر از امتیاز شما در معماها - %s امتیاز پایین‌تر از امتیاز شما در معماها + یک امتیاز پایین‌تر از امتیازبندی معمایی‌تان + %s امتیاز پایین‌تر از امتیازبندی معمایی‌تان - سخت تر + سخت‌تر سخت‌ترین - یک امتیاز بالاتر از امتیاز شما در معماها - %s امتیاز بالاتر از امتیاز شما در معماها + یک امتیاز بالاتر از امتیازبندی معمایی‌تان + %s امتیاز بالاتر از امتیازبندی معمایی‌تان مثال - تم دیگری را اضافه کنید. + افزودن موضوع دیگری پازل بعدی فورا به مسئله بعدی برورید با فعال کردن این تنظیمات ، صفحه به محض تکمیل پازلی که مشاهده می کنید به یک معمای جدید منتقل می شود. diff --git a/translation/dest/puzzle/so-SO.xml b/translation/dest/puzzle/so-SO.xml index e7421aea5a7e6..8c797b99bed3d 100644 --- a/translation/dest/puzzle/so-SO.xml +++ b/translation/dest/puzzle/so-SO.xml @@ -3,6 +3,8 @@ Halxidhaale Mawduucyada halxiraalaha Asalka + Hel dhaqaaqa ugu fiican cadaanka. + Hel dhaqaaqa ugu fiican madowga. Guul! qarsoon xaliyay diff --git a/translation/dest/puzzle/ta-IN.xml b/translation/dest/puzzle/ta-IN.xml index 6bf271799c8f9..f01f6cc52e561 100644 --- a/translation/dest/puzzle/ta-IN.xml +++ b/translation/dest/puzzle/ta-IN.xml @@ -7,7 +7,7 @@ கருக்கள் மேம்படுத்தபட்ட நீளங்கள் - மேட்ஸ் + முழுத்தடைகள் இலக்குகள் தோற்றம் சிறப்பு நகர்வுகள் @@ -53,6 +53,10 @@ கடினமானது மிகக்கடினமானது + + உங்கள் புதிர் மதிப்பீட்டைவிட ஒரு புள்ளி மேலே + உங்கள் புதிர் மதிப்பீட்டைவிட %s புள்ளிகள் அதிகம் + எடுத்துக்காட்டு வேறொரு வார்ப்புருவைச் சேர் அடுத்த புதிர் @@ -75,6 +79,14 @@ தரவுத்தளத்தில் உங்களுக்குரிய புதிர்கள் ஏதுமில்லை, ஆனாலும் லீசெஸ் உங்களை மிகவும் விரும்புகின்றது! %2$s விளையாட்டுக்களில் %1$s புதிர்கள் உள்ளன பயிற்சி, பகுப்பாய்வு, முன்னேற்றம் + + %s விளையாடியது + %s விளையாடியது + + + மீள விளையாட %s + மீள விளையாட %s + %s தீர்க்கப்பட்டது காட்சிப்படுத்துவதற்கு எதுவுமில்லை. முதலில் சில புதிர்களைத் தீர்க்கவும்! உங்கள் முன்னேற்றத்தை உகப்பாக்க இவற்றைப் பயிலவும்! diff --git a/translation/dest/puzzle/vi-VN.xml b/translation/dest/puzzle/vi-VN.xml index 3e962948af6be..1bf3f6a5b13f8 100644 --- a/translation/dest/puzzle/vi-VN.xml +++ b/translation/dest/puzzle/vi-VN.xml @@ -4,7 +4,7 @@ Chủ đề câu đố Đề xuất Giai đoạn - Chủ đề khác + Các mô-típ Nâng cao Thời lượng Chiếu hết @@ -75,7 +75,7 @@ Tìm câu đố Bạn không có câu đố nào trong dữ liệu, nhưng Lichess vẫn rất yêu mến bạn. -Hãy chơi thêm nhiều ván cờ nhanh và cờ chậm để có cơ hội có một câu đố từ ván đấu của riêng bạn! +Hãy chơi thêm nhiều ván cờ nhanh và cờ chậm để có cơ hội có một câu đố từ ván cờ của riêng bạn! Đã tìm được %1$s câu đố trong các ván đấu của %2$s Rèn luyện, phân tích, cải thiện diff --git a/translation/dest/puzzleTheme/da-DK.xml b/translation/dest/puzzleTheme/da-DK.xml index a8d6c2792c66b..1d082554ebfbd 100644 --- a/translation/dest/puzzleTheme/da-DK.xml +++ b/translation/dest/puzzleTheme/da-DK.xml @@ -62,9 +62,9 @@ Lang opgave Tre træk for at vinde. Mesterpartier - Opgaver fra partier af spillere med titel. + Taktikopgaver fra partier af spillere med titel. Mester mod mester partier - Opgaver fra partier mellem to spillere med titel. + Taktikopgaver fra partier mellem to spillere med titel. Mat Vind spillet med stil. Mat i 1 @@ -80,7 +80,7 @@ Midtspil En taktik i den anden fase af spillet. Et-træks opgave - En opgave der kun er ét træk lang. + En taktikopgave der kun er ét træk lang. Åbning En taktik i den første fase af spillet. Bondeslutspil @@ -101,19 +101,19 @@ Et slutspil med kun tårne og bønder. Offer En taktik der består i at opgive materiale på kort sigt for igen at få en fordel efter en tvungen træksekvens. - Kort opgave + Kort taktikopgave To træk for at vinde. Spid En manøvre hvor en brik af høj værdi angribes og må flyttes, hvorved en brik af lavere værdi bagved kan tages eller trues. Det omvendte af en binding. Kvalt mat En skakmat leveret af en springer, hvor den matte konge er ude af stand til at bevæge sig, fordi den er omgivet (eller kvalt) af sine egne brikker. Superstormester-partier - Opgaver fra partier spillet af verdens bedste spillere. + Taktikopgaver fra partier spillet af verdens bedste spillere. Fastlåste brikker En brik er ude af stand til at undslippe fangst, da den har begrænsede trækmuligheder. Underforvandling Forvandling til en springer, løber eller tårn. - Meget lang opgave + Meget lang taktikopgave Fire træk eller mere for at vinde. Røngtenangreb En brik angriber eller forsvarer et felt gennem en af modstanderens brikker. @@ -122,6 +122,6 @@ Sund blanding Lidt af hvert. Du kan ikke vide, hvad du skal forvente, så du skal være klar til alt! Præcis som i rigtige spil. Spiller-partier - Find opgaver lavet ud fra dine egne partier eller fra en anden spillers partier. + Find taktikopgaver lavet ud fra dine egne partier eller fra en anden spillers partier. Disse opgaver er i offentligt domæne og kan downloades fra %s. diff --git a/translation/dest/puzzleTheme/gl-ES.xml b/translation/dest/puzzleTheme/gl-ES.xml index 6c565dcb01872..936776748550c 100644 --- a/translation/dest/puzzleTheme/gl-ES.xml +++ b/translation/dest/puzzleTheme/gl-ES.xml @@ -47,7 +47,7 @@ Táctica que involucra a captura ao paso, onde un peón pode capturar a un peón opoñente que o deixou atrás usando o seu movemento inicial de dúas casas. Rei exposto Táctica que involucra a un rei con pouca defensa ó seu redor, a miúdo conducindo a xaque mate. - Pinza + Garfo Movemento no que a peza movida ataca a dúas pezas opoñentes á vez. Peza colgada Unha táctica que involucra unha peza do opoñente que non está suficientemente defendida e que por tanto se pode capturar. diff --git a/translation/dest/puzzleTheme/nn-NO.xml b/translation/dest/puzzleTheme/nn-NO.xml index 7b4ee69d7d5dd..c8166a1dceb2f 100644 --- a/translation/dest/puzzleTheme/nn-NO.xml +++ b/translation/dest/puzzleTheme/nn-NO.xml @@ -120,7 +120,7 @@ Trekktvang Ei stilling der alle moglege trekk skadar stillinga. Blanda drops - Litt av kvart. Du veit ikkje kva du blir møtt med, så du må vera førebudd på det meste. Nett som i verkelege parti. + Litt av alt. Du veit ikkje kva du blir møtt med, så du må vera førebudd på det meste. Nett som i verkelege parti. Spelar parti Finn oppgåver generert frå dine eller andre sine parti. Desse oppgåvene er offentleg eigedom og kan lastast ned frå %s. diff --git a/translation/dest/puzzleTheme/vi-VN.xml b/translation/dest/puzzleTheme/vi-VN.xml index e9095db4824bf..569f7ee58f9c3 100644 --- a/translation/dest/puzzleTheme/vi-VN.xml +++ b/translation/dest/puzzleTheme/vi-VN.xml @@ -17,7 +17,7 @@ Cờ tàn tượng Một thế cờ tàn chỉ có tượng và tốt. Chiếu hết kiểu Boden - Hai Tượng tấn công trên các đường chéo chéo nhau chiếu hết một quân vua bị kẹt bởi đồng đội của nó. + Hai quân Tượng tấn công trên các đường chéo chéo nhau chiếu hết một quân vua bị kẹt bởi đồng đội của nó. Nhập thành Chuyển quân vua đến vị trí an toàn và triển khai quân xe để tấn công. Ăn quân phòng thủ @@ -66,7 +66,7 @@ Ván đấu giữa 2 kiện tướng Câu đố từ các ván đấu giữa hai người chơi có danh hiệu. Chiếu hết - Chiến thắng trò chơi với phong cách. + Chiến thắng ván cờ với phong cách. Chiếu hết trong 1 nước Chiếu hết trong một nước cờ. Chiếu hết trong 2 nước @@ -107,7 +107,7 @@ Một mô típ liên quan tới việc một quân cờ có giá trị cao bị tấn công buộc phải di chuyển khỏi vị trí, dẫn tới một quân cờ giá trị thấp hơn ở phía sau bị tấn công hoặc ăn, ngược lại so với ghim. Chiếu kiểu kẹt Một nước chiếu hết với quân mã mà trong đó vua đối phương không thể di chuyển vì bị bao vây bởi chính các quân cờ khác của họ. - Ván đấu từ Siêu Đại Kiện Tướng + Các ván đấu từ Siêu Đại Kiện Tướng Câu đố từ những ván đấu đã được chơi bởi những kì thủ giỏi nhất trên thế giới. Quân bị bẫy Một quân cờ không thể thoát khỏi việc bị ăn vì nó bị giới hạn các nước đi. @@ -119,7 +119,7 @@ Một quân cờ tấn công hoặc phòng thủ một ô sau một quân cờ khác của đối phương. Cưỡng ép Đối phương bị giới hạn các nước mà họ có thể đi và tất cả các nước đi ấy đều hại họ. - Kết hợp lành mạnh + Phối hợp nhịp nhàng Mỗi thứ một chút. Bạn không biết được thứ gì đang chờ mình, vậy nên bạn cần phải sẵn sàng cho mọi thứ! Như một ván cờ thật vậy! Ván đấu của các người chơi Những câu đố từ các ván cờ của bạn hoặc từ các ván cờ của những người chơi khác. diff --git a/translation/dest/search/gsw-CH.xml b/translation/dest/search/gsw-CH.xml index e850f332d9f63..8ef2a8f80a053 100644 --- a/translation/dest/search/gsw-CH.xml +++ b/translation/dest/search/gsw-CH.xml @@ -12,11 +12,11 @@ Gägner Name Verlürer - Vum + vo bis Ob de Gägner en Mänsch oder en Computer gsi isch K.I. Stärchi - Quälle + Spielort Azahl Züg Ergäbnis Sieger Farb diff --git a/translation/dest/search/hi-IN.xml b/translation/dest/search/hi-IN.xml index 440eb4d841ee5..ff2f7d91c54e4 100644 --- a/translation/dest/search/hi-IN.xml +++ b/translation/dest/search/hi-IN.xml @@ -1,10 +1,10 @@ खोजें - उच्च खोज + उन्नत खोज %s शतरंज के खेल में खोजें - %s शतरंज के खेलो में खोजें + शतरंज के %s खेलो में खोजें एक खेल मिला @@ -28,7 +28,7 @@ मूल्यांकन अधिकतम संख्या अधिक्तम खेल​ - शामिल करे + शामिल करें अवरोही आरोही diff --git a/translation/dest/search/sl-SI.xml b/translation/dest/search/sl-SI.xml index aa267427f8811..99bc50b8a32ba 100644 --- a/translation/dest/search/sl-SI.xml +++ b/translation/dest/search/sl-SI.xml @@ -31,7 +31,7 @@ Barva Ocenjevanje Največje število - Največje število iger za vrnitev + Največje prikazano število iger Vključuje Padajoče Naraščajoče diff --git a/translation/dest/settings/da-DK.xml b/translation/dest/settings/da-DK.xml index 7da98d49c5ccd..f03d9a83b117b 100644 --- a/translation/dest/settings/da-DK.xml +++ b/translation/dest/settings/da-DK.xml @@ -4,8 +4,8 @@ Luk konto Din konto er under administration og kan ikke lukkes. Lukning er uigenkaldelig. Der er ingen fortrydelsesret. Er du sikker? - Du vil ikke få lov til at åbne en ny konto med det samme navn, selv hvis du ændrer store og små bogstaver. + Du vil ikke få lov til at åbne en ny konto med det samme navn, selv hvis du ændrer på store og små bogstaver. Jeg har skiftet mening, lad være med at lukke min konto - Er du sikker på, at du vil lukke din konto? Lukning af din konto er en permanent beslutning. Du vil ALDRIG NOGENSINDE kunne logge ind igen. + Er du sikker på, at du vil lukke din konto? Lukning af din konto er en permanent beslutning. Du vil ALDRIG kunne logge ind igen. Denne konto er lukket. diff --git a/translation/dest/site/af-ZA.xml b/translation/dest/site/af-ZA.xml index 252d8259dad80..18e287df4db5c 100644 --- a/translation/dest/site/af-ZA.xml +++ b/translation/dest/site/af-ZA.xml @@ -5,6 +5,7 @@ Nooi iemand vir \'n spel, gebruik hierdie URL Spel Verby Wag vir uitdager + Of laat jou opponent hierdie QR-kode skandeer Wagtend Jou beurt %1$s vlak %2$s @@ -98,8 +99,9 @@ Voeg dalk meer wedstryde van die voorkeure kieslys by? Openinge Opening ontdekker - Opening/eindspel ontdekker + Opening/eindspel-verkenner %s opening ontdekker + Speel eerste opening/eindspel-verkenner skuif Oorwinning gekelder deur 50-skuifreël Verloor voorkom deur 50-skuifreël Wen of 50 skuiwe deur vorige fout @@ -155,6 +157,10 @@ Sien al %s spelle Sien al %s spelle + + %1$s gradering oor %2$s spel + %1$s gradering oor %2$s spelle + %s boekmerk %s boekmerke @@ -267,7 +273,6 @@ In spel Tans besig om te speel Klaar - maak klaar %s Staak spel Spel gestaak Standaard @@ -324,7 +329,7 @@ Gee %s sekondes Hierdie rekening oortree die diensvoorwaardes van Lichess - Opening ontdekker & tafelbasis + Openings-verkenner & tafelbasis Terug-vat Stel terug-vat voor Terug-vat aanbod gestuur @@ -397,6 +402,7 @@ Plak \'n wedstryd PGN om dit deursoekbaar te herspeel, rekenaar analise, kletskamer en deelbare URL te kry. Variasies sal uitgevee word. Voer die PGN in d.m.v. \'n studie om hulle te behou. + Hierdie PGN is toeganklik vir die algemene publiek. Gebruik \'n studie om \'n spel privaat in te voer. %s ingevoerde spel %s het spelle ingetrek @@ -677,12 +683,21 @@ rekenaar analise, kletskamer en deelbare URL te kry. Sleutelbord kortpaaie skuif terug/vorentoe gaan na begin/einde + Wissel geselekteerde variasie vertoon/versteek kommentaar betree/verlaat variasie Vra rekenaaranalise aan, Leer uit jou foute Volgende (Leer uit jou foute) + Volgende flater Volgende fout Volgende onakkuraatheid + Vorige tak + Volgende tak + Wissel variasie pyle + Wissel vorige/volgende variasie + Wissel glyf-annotasies + Variasie pyle laat jou toe om te vaar sonder die skuif lys. + speel geselekteerde skuif Nuwe toernooi Skaaktoernooie met verskeie tydkontroles en variante Speel hoë-tempo skaak toernooie! Sluit aan by \'n amptelik geskeduleerde toernooi, of skep jou eie. Bullet, Blitz, klassiek, Skaak960, Heuwel Heerser, Trippelskaak, en meer beskikbare opsies vir oneindige skaak pret. @@ -725,6 +740,7 @@ rekenaar analise, kletskamer en deelbare URL te kry. Met vriende Met almal Kinder mode + Kindermodus is geaktiveer. Hierdie gaan oor veiligheid. In kindermodus, alle webwerf kommunikasie word afgeskakel. Skakel dit aan vir jou kinders en skoliere, om hulle te beskerm teen ander internet gebruikers. In kindermodus kry die Lichess handelsmerk \'n %s ikoon, sodat jy weet jou kinders is veilig. Jou rekening word bestuur deur jou skaak onderwyser. Vra hulle om kinder modus af te skakel. @@ -838,9 +854,10 @@ rekenaar analise, kletskamer en deelbare URL te kry. en stoor %s voorafskuif variasie en stoor %s voorafskuif variasies + Jy het \'n privaatboodskap van Lichess ontvang. + Klik hier om dit te lees Skies :( Jy moet vir \'n rukkie in die hoek sit. - Jy mag weer uitkom %s. Hoekom? Ons poog om \'n aangename ervaring aan alle spelers te gee. Om dit reg te kry, moet ons seker maak dat alle spelers goeie praktyke handhaaf. @@ -860,6 +877,7 @@ rekenaar analise, kletskamer en deelbare URL te kry. Ek stem in om al Lichess se beleide na te volg. Soek of begin \'n nuwe gesprek Pas aan + Bullet Blits Rapid Klassieke @@ -877,6 +895,8 @@ rekenaar analise, kletskamer en deelbare URL te kry. gebruik die meldingsformulier Om hulp te vra, %1$s probeer die kontak bladsy + Maak seker om %1$s te lees + die forum etiket Hierdie onderwerp is geargiveer en kan nie meer beantwoord word nie. Sluit aan by die %1$s om in hierdie forum te plaas %1$s span @@ -898,9 +918,6 @@ rekenaar analise, kletskamer en deelbare URL te kry. Tyd is byna op! [Klik om e-posadres te beskou] Laai af - Welkom! - Lichess is \'n liefdadigheidsorganisasie en heeltemal gratis/libre oopbron sagteware. -Alle bestuurskostes, ontwikkeling en inhoud word heeltemal gefinansier deur lede bydraes. Afrigter bestuurder Uitsender bestuurder Kanseleer die toernooi @@ -959,4 +976,8 @@ Laat dit leeg om elke spel vanaf die normale posisie te begin. Ruil kante Sluiting van jou rekening beteken terugtrekking van jou appèl Ons wenke om gebeurtenisse te organiseer + Instruksies + Wys vir my alles + Lichess is \'n liefdadigheidsorganisasie en heeltemal gratis/libre oopbron sagteware. +Alle bestuurskostes, ontwikkeling en inhoud word heeltemal gefinansier deur lede bydraes. diff --git a/translation/dest/site/an-ES.xml b/translation/dest/site/an-ES.xml index dcc3dee65e7b7..663e3a1eedc5a 100644 --- a/translation/dest/site/an-ES.xml +++ b/translation/dest/site/an-ES.xml @@ -70,6 +70,7 @@ Fer que siga la linia prencipal Borrar a partir d\'aquí Fer que siga variant + Copiar lo PGN d\'a variant Movimiento Variant perdedera Variant ganadera @@ -99,6 +100,7 @@ Explorador d\'obriduras Explorador d\'obriduras/finals Explorador d\'a obridura %s + Chugar lo primer movimiento de l\'explorador d\'obriduras/finals La regla d\'os 50 movimientos impide la victoria La regla d\'os 50 movimientos priva la redota Victora u taulas per 50 movimientos a causa d\'una error anterior @@ -114,12 +116,14 @@ Ubrir estudio Activar Indicar la millor chugada + Amostrar las flechas d\'as variants Indicador d\'avaluación Multiples linias Procesadors Memoria Analisi infinita Elimina lo limite de profundidat de l\'analisi y fa treballar de firme lo tuyo ordinador + Chestión de modulos Error grieu Error Imprecisión @@ -152,6 +156,10 @@ %s Partida %s Partidas + + puntuación %1$s en %2$s partida + puntuación %1$s en %2$s partidas + %s partida favorita %s partidas favoritas @@ -264,7 +272,6 @@ En chuego agora mesmo Se ye chugando agora Rematau - remata %s Cancelar partida Partida cancelada Standard @@ -341,6 +348,10 @@ %s estudio %s estudios + + %s simultania + %s simultanias + Veyer torneyo Tornar a lo torneyo No se puet ofrir taulas antes de fer 30 movimientos en os torneyos suizos. @@ -473,6 +484,7 @@ Movimientos Victorias blancas Victorias negras + Percentache de taulas Taulas Siguient torneyo %s: Oponent meyo @@ -493,7 +505,11 @@ Editar perfil Nombre Apellido + Define lo tuyo estilo + Estilo + I hai un achuste pa amagar toz es estilos d\'usuario en tot lo puesto web. Biografía + País u rechión Gracias! Vinclos a rez socials Una URL per linia. @@ -645,8 +661,10 @@ Exhibicions simultanias Anfitrión Color de l\'anfitrión: %s + Las tuyas simultanias pendients Nuevas simultanias creadas Crear una nueva simultania + Rechistra-te pa organizar u unir-te a una simultania Simultania no trobada Esta exhibición simultania no existe. Tornar a la pachina de simultanias @@ -671,8 +689,21 @@ Alcorces d\'o teclau movimiento enta zaga y enta adebant ir a l\'inicio/fin + Cambia la variant seleccionada amostrar/amagar comentarios dentrar/salir d\'a variant + Demanda una analisi d\'ordinador, aprende d\'as tuyas errors + Siguient (Aprende d\'as tuyas errors) + Siguient error grieu + Siguient error + Siguient imprecisión + Branca anterior + Siguient branca + Amuestra/amaga las flechas d\'as variants + Cambia a la variant anterior/siguient + Amuestra/amaga las anotacions con simbolos + Las flechas de variants te permiten navegar sin fer servir la lista de movimientos. + chuga lo movimiento triau Nuevo torneyo Torneyos d\'escaques con diferents variants y controls de tiempo Chuga torneyos d\'escaques rapidos! Une-te a un torneyo oficial programau u crea lo tuyo propio. Bala, Lampado, Clasico, Escaques960, Rei d\'o Pueyo, Tres escaques y muitas mas opcions pa que no remate la diversión con os escaques. @@ -688,6 +719,7 @@ La tuya puntuación en %1$s ye %2$s. Yes millor que lo %1$s d\'os chugadors de %2$s. %1$s ye millor que %2$s de %3$s chugadors. + Millor que lo %1$s d\'os chugadors de %2$s No tiens una puntuación de %s establida. La tuya puntuación Acumulau @@ -714,6 +746,7 @@ Con os míos amigos Con toz Modo infantil + Modo infantil activau. Per seguridat, en o modo infantil se desactivan totas las comunicacions con atros usuarios. Activa esto pa protecher a los menors d\'atros usuarios d\'Internet. En o modo infantil, lo logo de lichess tiene un icono de %s, indicando que los ninos son seguros. La tuya cuenta ye administrada. Demanda-le a lo tuyo mayestro abandonar lo modo pa ninos. @@ -770,6 +803,7 @@ FEN invalido Personalizada Notificacions + Notificacions: %1$s Puntuación: %s %s segundo pa fer lo primer movimiento @@ -826,9 +860,10 @@ y cabida %s linia de premovimiento y cabida %s linias de premovimiento + Has recibiu un mensache privau de Lichess. + Fe clic aquí pa leyer-lo Lo sentimos :( Hemos habiu de que suspender-te temporalment. - La suspensión expira en %s. Per qué? Lo nuestro obchectivo ye proporcionar una experiencia agradable a toz. Pa ixo, hemos d\'asegurar-nos que toz los chugadors se comportan como cal. @@ -848,6 +883,8 @@ Me comprometo a seguir las normas de Lichess. Buscar u empecipiar una nueva conversación Editar + Bullet + Blitz Rapidas Clasica Partidas increyiblement rapidas: menos de 30 segundos @@ -887,9 +924,6 @@ Cuasi ha rematau lo tiempo! [Fe clic pa veyer l\'adreza de correu-e] Descargar - Bienveniu/da! - Lichess ye una entidat sin animo de lucro y un software libre de tot y de codigo ubierto. -Los gastos de funcionamiento, desenrollo y contenius se financian exclusivament con as donacions d\'os usuarios. Configuración pa entrenadors Configuración pa streamers Cancelar lo torneyo @@ -944,4 +978,8 @@ Los gastos de funcionamiento, desenrollo y contenius se financian exclusivament Cambiar de color Si zarras la tuya cuenta s\'eliminará la tuya apelación Los nuestros consellos pa organizar eventos + Instruccions + Amuestra-me-lo tot + Lichess ye una entidat sin animo de lucro y un software libre de tot y de codigo ubierto. +Los gastos de funcionamiento, desenrollo y contenius se financian exclusivament con as donacions d\'os usuarios. diff --git a/translation/dest/site/ar-SA.xml b/translation/dest/site/ar-SA.xml index 180b9d0c28a27..a7307a9b8a3d1 100644 --- a/translation/dest/site/ar-SA.xml +++ b/translation/dest/site/ar-SA.xml @@ -17,7 +17,7 @@ مات مخنوق أبيض أسود - الأبيض + بالأبيض الأسود لون عشوائي إنشاء مباراة @@ -26,7 +26,7 @@ أنت تلعب بالقطع البيضاء أنت تلعب بالقطع السوداء إنه دورك! - ثم تحديد حالة غش + تحديد حالة غِشّ الملك في الوسط كش ملك ثلاثا نهاية السباق @@ -53,8 +53,8 @@ الأسود استسلم الأبيض ترك المباراة الأسود ترك المباراة - لم يقم الأبيض بالحركة - لم يقم الأبيض بالحركة + لم يلعب الأبيض بعد + لم يلعب الأسود بعد اطلب تحليل حاسب تحليل الحاسوب تحليل الحاسوب متاح @@ -63,7 +63,7 @@ عمق التحليل %s استخدام تحليل الخادم تحميل المحرك... - حساب النقلات... + جاري حساب النقلات... خطأ في تحميل المحرك تحليل سحابي تحليل أعمق @@ -71,9 +71,10 @@ باستخدام متصفحك التبديل للتحليل بالمتصفح رفع سلسلة الحركات - رفع الى التسلسل الرئيسي + كتابة التفريع الرئيسي احذف من هنا فرض التسلسل + انسخ التفريع بصيغة PGN التقلة خسارة بطريقة خاصة فوز بطريقة خاصة @@ -90,20 +91,20 @@ متوسط التقييم: %s أحدث المباريات أفضل الالعاب - اثنان مليون مباراة من قاعدة بيانات الاساتذة تقييم %1$s+ للاعبين من %2$s إلى %3$s + قاعدة بيانات مباريات الأساتذة تقييم %1$s+ للاعبين من %2$s إلى %3$s مات في %s نصف-نقلة مات في %s نصف-نقلة مات في %s نصف-نقلة مات في %s نصف-نقلة - مات في %s نصف-نقلة - مات في %s نصف-نقلة + كش مات في %s نقلة + كش مات في %s نقلة - DTZ50\'\' مع تقريب ، استنادًا إلى عدد من نصف التحركات حتى التقاط أو نقل بياض التالي + DTZ50\'\' هي عدد الحركات حتى حصول أخذ أو تحريك بيدق لم يتم العثور على مباريات تم الوصول إلى أقصى عمق! أترغب في ضم مباريات أكثر من قائمة التفضيلات؟ - الافتتاح + الافتتاحيات مستكشف الافتتاحيات مستكشف نهاية/بداية الدور مستكشف افتتاحيات %s @@ -123,6 +124,7 @@ فتح دراسة تفعيل سهم أفضل نقلة + أظهر سلسلة النقلات المرشحة مقياس التقييم عدد الخطوط المعالجات @@ -182,6 +184,14 @@ %s مباراة %s مباراة + + التصنيف للشطرنج %1$s بعد %2$s مباراة + التصنيف للشطرنج %1$s بعد %2$s مباراة واحدة + التصنيف للشطرنج %1$s بعد %2$s مباراتين + تصنيفك في %1$s بعد %2$s مباراة + تصنيفك في %1$s بعد %2$s مباراة + التصنيف للشطرنج %1$s بعد %2$s مباراة + %s مباراة مفضلة %s مباراة مفضلة @@ -322,7 +332,6 @@ يلعب الآن يلعب الآن انتهت - تنتهي %s إلغاء اللعبة اللعبة ألغيت عادي @@ -431,6 +440,14 @@ %s دراسات %s دراسة + + %s خصم + %s خصم واحد + %s خَصمان في الوقت نفسه + %s خصوم في الوقت نفسه + %s خَصم في الوقت نفسه + %s خَصم في الوقت نفسه + شاهد المسابقة عودة للمسابقة لا يمكنك التعادل قبل لعب 30 حركة في بطولة سويسرية. @@ -467,7 +484,7 @@ يجب أن تلعب %s مباراة مقيمة أخرى يجب أن تلعب %s مباراة مقيمة أخرى - تقييمك %s مؤقت + تقييمك في %s مؤقت تقييمك في %1$s وقدره %2$s عالي جدًا تقييمك الأسبوعي في %1$s وقدره %2$s عالي جدًا تقييمك في %1$s وقدره %2$s منخفض جدًا @@ -499,6 +516,7 @@ استورد مباراة عند لصق مباراة PGN تحصل على إمكانية كرار استعراضها وتحليل حاسوبي ودردشة للمباراة ورابط قابل للمشاركة. سيتم محو التغييرات. للحفاظ عليها، يرجى استيراد PGN (تنسيق لعبة الشطرنج المحمول) عبر تبويب دراسة. + يمكن لأي أحد الوصول إلى PGN، إذا أردت إنشاء تحليل خاص، استخدم قسم دراسة. مباراة مستوردة %s مباراة مستوردة %s @@ -599,6 +617,7 @@ نقلات اللعب فوز الأبيض فوز الأسود + معدل التعادل تعادل بطولة ال %s التالية: معدل الخصم @@ -619,7 +638,11 @@ حرر الملف الشخصي الاسم الأول اسم العائلة + اختيار الشارة + الشارة + يستخدم هذا الإعداد لإخفاء جميع شارات المستخدمين في الموقع. نبذة عنك + البلد أو المنطقة شكرًا لك! روابط وسائل التواصل الاجتماعي رابط واحد لكل سطر @@ -787,8 +810,10 @@ معارض التزامنيات المضيف لون المضيف: %s + مبارياتك المعلقة تزامنيات مُنشأة حديثاً استضف تزامنية جديدة + سجل لاستضافة أو الانضمام إلى محاكاة التزامنية غير موجودة معرض هذه التزامنية غير موجود. العودة لصفحة التزامنية @@ -813,6 +838,7 @@ اختصارات لوحة المفاتيح تحرك للخلف/للأمام اذهب للبداية/للنهاية + التفريع المحدد أظهر/أخفِ التعليقات متغير دخول/خروج اطلب تحليل الحاسوب وتعلم من أخطائك @@ -820,6 +846,13 @@ الخطأ الفادح التالي الخطأ التالي النقلة غير الدقيقة التالية + التفريع السابق + التفريع القادم + تبديل أسهم النقلات المرشحة + الدورة السابقة/التفريع التالي + تبديل الرموز التوضيحية + أسمهم النقلات تسمح لك بلعبها دون استخدام قائمة النقلات المرشحة. + لعب النقلة المحددة مسابقة جديدة مسابقات بانواع شطرنج مختلفة وساعات مختلفة العب مسابقات شطرنج بكل السرعات. انضم للمسابقات الرسمية المجدولة، أو ابدأ مسابقاتك الخاصة. @@ -866,6 +899,7 @@ مع الأصدقاء مع الجميع موقع الأطفال + وضع الطفل مفعل. هذا يتعلق بالأمان. في نمط الطفل، يتم تعطيل كافة اتصالات المواقع. تمكين هذا للأطفال والطلاب، لحمايتهم من مستخدمي الإنترنت الأخرين. في نمط الطفل، يكون لشعار ليتشيس رمز %s، لكي تعرف أن أطفالك آمنين. يتم إدارة حسابك. اسأل معلم الشطرنج الخاص بك عن رفع وضع الطفل. @@ -995,9 +1029,10 @@ واحفظ عدد %s تفريع نقلة مسبقة واحفظ عدد %s تفريع نقلة مسبقة + تسلّمتَ رسالة خاصة من ليتشيس. + اضغط هنا بمتابعة القراءة نأسف :( اضطررنا إلى حظرك لفترة قصيرة. - ينتهي الحظر خلال %s. لماذا ؟ نحن نهدف الى توفير تجربة شطرنج ممتعة للجميع. لهذا الغرض، علينا التأكد أن جميع اللاعبين يسلكون سلوكاً حسناً. @@ -1017,12 +1052,14 @@ أوافق على أني سأتبع سياسات الموقع. البحث أو بدء محادثة جديدة تعديل - سريع + الرصاصة + الخاطف + السريع تقليدي مباراةٌ جنونية السرعة: أقل من ثلاثين ثانية مباراةٌ سريعةٌ جداً: أقل من ٣ دقائق - مبارياتٌ سريعة: ٣ - ٨ دقائق - مباراياتٌ خاطفةٌ: ٨-٢٥ دقائق + مبارياتٌ خاطفة: ٣ - ٨ دقائق + مباريات سريعة: ٨-٢٥ دقائق مبارياتٌ كلاسيكيةٌ: ٢٥ دقيقة و أكثر ألعاب المراسلة: يوم أو عدة أيام لكل نقلة مدرب تكتيكات الشطرنج @@ -1056,9 +1093,6 @@ أوشك الوقت على الإنتهاء! [انقر للكشف عن عنوان البريد الإلكتروني] تحميل - مرحبا! - Lichess هو موقع خيري و مجاني بشكل كامل ومفتوح المصدر. -كافة التكاليف التشغيلية و التطويرية و المحتوى يتم الحصول عليه من قبل تبرعات المستخدمين. مدير المدرب مدير البث الغاء البطولة @@ -1117,4 +1151,8 @@ تبديل جهة اللعب إغلاق حسابك سوف تخسر تقدمك نصائحنا لتنظيم الأحداث + التعليمات + اظهر لي كل شيء + Lichess هو موقع خيري و مجاني بشكل كامل ومفتوح المصدر. +كافة التكاليف التشغيلية و التطويرية و المحتوى يتم الحصول عليه من قبل تبرعات المستخدمين. diff --git a/translation/dest/site/av-DA.xml b/translation/dest/site/av-DA.xml index 61bdabffa64ea..80b901c6871c8 100644 --- a/translation/dest/site/av-DA.xml +++ b/translation/dest/site/av-DA.xml @@ -270,7 +270,6 @@ ХӀай унеб буго гьабсагӀат ХӀай унеб буго гьабсагӀат ТӀубана - %s лъугӀула ХӀай лъугӀизабичӀого тезе ХӀай лъугӀизабичӀого тана ГӀадатаб @@ -831,7 +830,6 @@ %s гьабе ТӀаса лъугьа :( Дур хӀалтӀи заманалде гьоркьоб къотӀизабизе ккана. - Балагьун чӀеялъул заман %s лъугӀула. Щай? Нилъеца киназего шахматазда лъикӀаб хӀалбихьи щвезе жигар бахъула. Гьеб щвезе, нилъеда щивас къагӀидаби цӀунулел ругелали ракӀчӀун лъазе ккола. @@ -884,9 +882,6 @@ Заман лъугӀизехъин буго! [Электронияб почалъул адресс ричӀизе, тӀадецуй] РещтӀинабизе - ЛъикӀ щварав! - Lichess ккола лъикӀаб гьариги ругеб тӀубанго мухьги босулареб рагьараб кьучӀаб код бугеб программаялдалъун хьезари. -Садакъа кьолел хӀалтӀизабулез киналго харжал, разработка, контент гӀарцудалъун хьезарула. Мударибзабазе Стрим менежер Турнир гьабичӀого тезе @@ -943,4 +938,6 @@ Рахъ хизисе Дур аккаунт къаялъ дур хитӀаб нахъе ахӀула Тадбирал гьабизелъун нижер гӀакълаби + Lichess ккола лъикӀаб гьариги ругеб тӀубанго мухьги босулареб рагьараб кьучӀаб код бугеб программаялдалъун хьезари. +Садакъа кьолел хӀалтӀизабулез киналго харжал, разработка, контент гӀарцудалъун хьезарула. diff --git a/translation/dest/site/az-AZ.xml b/translation/dest/site/az-AZ.xml index 2f609716f65b5..62610ecd842fe 100644 --- a/translation/dest/site/az-AZ.xml +++ b/translation/dest/site/az-AZ.xml @@ -272,7 +272,6 @@ Hal-hazırda oyundadır Hal-hazırda oynayır Bitdi - %s sonra başa çatacaq Oyunu ləğv et Oyun ləğv olundu Standart @@ -818,7 +817,6 @@ Təəssüf :( Sizi bir müddətlik oyunlardan xaric etmək məcburiyyətində qaldıq. - Zaman aşımı %s bitəcək. Niyə? Biz hər kəs üçün xoş bir şahmat təcrübəsi təmin etməyi hədəfləyirik. Bunun üçün bütün oyunçuların düzgün təcrübəni izləməsini təmin etməliyik. @@ -875,7 +873,6 @@ Vaxt bitmək üzrədir! [E-poçt ünvanını görmək üçün klikləyin] Endir - Xoş Gəldiniz! Müsabiqəni ləğv et Turnir açıqlaması Yalnız qrup üzvləri diff --git a/translation/dest/site/be-BY.xml b/translation/dest/site/be-BY.xml index de0b04d4b77e3..71a92b734cc65 100644 --- a/translation/dest/site/be-BY.xml +++ b/translation/dest/site/be-BY.xml @@ -292,7 +292,6 @@ Граецца зараз Граецца зараз Скончана - завяршыцца %s Скасаваць гульню Гульня скасавана Стандартная @@ -556,6 +555,7 @@ Імя Прозвішча Біяграфія + Краіна або рэгіён Дзякуй! Спасылкі на сацсеткі Адзін URL на радок. @@ -911,7 +911,6 @@ Прабачце :( Нам прыйшлося на нейкі час выдаць вам тайм-аўт. - Вы зможаце вярнуцца праз %s. Чаму? Мы імкнемся даставіць задавальненне ад шахмат ўсім гульцам. Каб дамагчыся гэтага, мы павінны забяспечыць, каб усе гульцы вынікалі правілам добрага тону. @@ -971,9 +970,6 @@ Час амаль выйшаў! Націсніце, каб высветліць электроную пошту Спампаваць - Вітаем! - Lichess - гэта дабрачынная і цалкам бясплатная праграма з адкрытым зыходным кодам. -Усе аперацыйныя выдаткі, распрацоўка і шахматны кантэнт фінансуюцца выключна за кошт ахвяраванняў карыстальнікаў. Трэнерская старонка Стрымерская старонка Скасаваць турнір @@ -1032,4 +1028,6 @@ Змяніць бок Закрыцце вашага ўліковага запісу адкліча вашу апеляцыю Нашы парады па арганізацыі падзей + Lichess - гэта дабрачынная і цалкам бясплатная праграма з адкрытым зыходным кодам. +Усе аперацыйныя выдаткі, распрацоўка і шахматны кантэнт фінансуюцца выключна за кошт ахвяраванняў карыстальнікаў. diff --git a/translation/dest/site/bg-BG.xml b/translation/dest/site/bg-BG.xml index 717a91a694319..d7710a5697a77 100644 --- a/translation/dest/site/bg-BG.xml +++ b/translation/dest/site/bg-BG.xml @@ -271,7 +271,6 @@ Играещи сега Играещи сега Приключи - свършва след %s Отмени играта Играта е отменена Обикновен @@ -847,7 +846,6 @@ Съжалявам :( Наложи се да Ви сложим в принудителна почивка за известно време. - Принудителната почивка приключва %s. Защо? Стремим се да доставим приятно удоволствие от Шаха на всеки. С тази цел трябва да подсигурим, че всички играчи спазват добри практики. @@ -907,8 +905,6 @@ Времето почти изтече! [Щракнете, за да видите имейл адреса] Изтегли - Добре дошли! - Lichess е благотворителна организация и работи с напълно безплатен софтуер и отворен код. Всички разходи за опериране, разработка и съдържание са финансирани единствено от дарения от потребителите ни. Настройки за треньори Настройки за стриймъри Отмяна на турнира @@ -965,4 +961,5 @@ Смени страната Закриването на регистрацията Ви ще оттегли Вашето обжалване Нашите съвети за организиране на събития + Lichess е благотворителна организация и работи с напълно безплатен софтуер и отворен код. Всички разходи за опериране, разработка и съдържание са финансирани единствено от дарения от потребителите ни. diff --git a/translation/dest/site/bn-BD.xml b/translation/dest/site/bn-BD.xml index 6ee3bca09a079..0e7b09b0f7de0 100644 --- a/translation/dest/site/bn-BD.xml +++ b/translation/dest/site/bn-BD.xml @@ -245,7 +245,6 @@ এই মুহূর্তে খেলছেন এই মুহূর্তে খেলতেছে সমাপ্ত - শেষ করেছে %s খেলা বন্ধ করুন খেলা বন্ধ করা হয়েছে আদর্শ @@ -808,7 +807,6 @@ দুঃখিত :( আমাদের কিছুক্ষণের জন্য আপনাকে বিরতি হল। - সময় লাগবে %s. কেন আমাদের লক্ষ্য প্রত্যেকের জন্য একটি আনন্দদায়ক দাবা খেলার অভিজ্ঞতা দেয়া। সেই লক্ষ্যে আমাদের অবশ্যই নিশ্চিত করতে হবে যে সমস্ত খেলোয়াড় ভাল অভ্যাস চর্চা করে। @@ -865,9 +863,6 @@ সময় প্রায় শেষ! [ইমেইল অ্যাড্রেস দেখতে ক্লিক করুন] ডাউনলোড করুন - স্বাগতম! - লিচেস একটি অলাভজনক এবং সম্পূর্ণ বিনামূল্য/লিব্রা ওপেন সোর্স সফটওয়্যার। -সকল পরিচালনা খরচ, উন্নয়ন, এবং বিষয়বস্তু ব্যবহারকারীদের দানের মাধ্যমে সংগৃহীত হয়। প্রশিক্ষক ব্যবস্থাপনা সম্প্রচার নিয়ন্ত্রণ টুর্নামেন্ট বাতিল করুন @@ -914,4 +909,6 @@ Markdown links ব্যবহারযোগ্য: [name](https://url)লিচেস এর সকল খেলোয়াড়দের রেটেড খেলা থেকে সংগ্রহকৃত পক্ষ পরিবর্তন করুন একাউন্ট বন্ধ করলে আপনার আপিল প্রত্যাহার হয়ে যাবে + লিচেস একটি অলাভজনক এবং সম্পূর্ণ বিনামূল্য/লিব্রা ওপেন সোর্স সফটওয়্যার। +সকল পরিচালনা খরচ, উন্নয়ন, এবং বিষয়বস্তু ব্যবহারকারীদের দানের মাধ্যমে সংগৃহীত হয়। diff --git a/translation/dest/site/br-FR.xml b/translation/dest/site/br-FR.xml index 1dc4d58e8d30c..4613f338ef536 100644 --- a/translation/dest/site/br-FR.xml +++ b/translation/dest/site/br-FR.xml @@ -218,7 +218,7 @@ %s eur %s eur %s eur - %s eurvezhioù + %s eur %s vunutenn @@ -305,7 +305,6 @@ O c\'hoari bremañ O c\'hoari bremañ Echu - a vo echu e %s Nullañ ar c\'hrogad Krogad nullet Normal @@ -939,9 +938,10 @@ hag enrollit %s linennoù raktaolioù hag enrollit %s linennoù raktaolioù + Resevet ho peus ur gemennadenn brevez eus perzh Lichess. + Klikit amañ evit lenn anezhi Hon digarezit :( Ret e oa deomp ho skarzhañ e-pad ur prantad. - Ne veoc\'h ket mui forbannet a-benn %s. Perak? Plijout a rafe deomp e vourrfe pep hini c\'hoari echedoù war Lichess. Evit hen ober e dav deomp bezañ sur e vez graet gant doareoù dereat gant an holl. @@ -1000,7 +1000,6 @@ Ne chom ket kalz amzer! [Klikit da ziskouez ar chomlec’h postel] Pellgargañ - Degemer mat! Korn ar gourdoner Merañ ar stream Nullañ an tournamant diff --git a/translation/dest/site/bs-BA.xml b/translation/dest/site/bs-BA.xml index 401b42747fcb1..b7603b20cfc41 100644 --- a/translation/dest/site/bs-BA.xml +++ b/translation/dest/site/bs-BA.xml @@ -286,7 +286,6 @@ Upravo igraju Upravo igraju Završeno - završava %s Otkažite partiju Partija otkazana Standardna @@ -889,7 +888,6 @@ računarsku analizu, mogućnost dopisivanja i link za slanje drugima. Izvinjavamo se :( Morali smo Vam staviti privremenu zabranu na igranje. - Privremena zabrana ističe %s. Zašto? Naš cilj je da svima pružimo ugodno šahovsko iskustvo. Zbog toga, moramo osigurati da svi igrači ispravno postupaju. @@ -949,9 +947,6 @@ računarsku analizu, mogućnost dopisivanja i link za slanje drugima. Vrijeme je pri kraju! [Kliknite da otkrijete adresu e-pošte] Preuzmite - Dobro došli! - Lichess je dobrotvorni i potpuno besplatan softver otvorenog koda. -Svi operativni troškovi, razvoj i sadržaj finansiraju se isključivo od donacija korisnikâ. Postavke za trenera Postavke za strimera Otkaži turnir @@ -1011,4 +1006,6 @@ Ostavite prazno za početak igre sa normalne početne pozicije. Zamijenite strane Zatvaranje Vašeg računa povući će Vašu žalbu Naši savjeti za organiziranje događajâ + Lichess je dobrotvorni i potpuno besplatan softver otvorenog koda. +Svi operativni troškovi, razvoj i sadržaj finansiraju se isključivo od donacija korisnikâ. diff --git a/translation/dest/site/ca-ES.xml b/translation/dest/site/ca-ES.xml index db22c3eb9eac8..fbf042d209c18 100644 --- a/translation/dest/site/ca-ES.xml +++ b/translation/dest/site/ca-ES.xml @@ -5,6 +5,7 @@ Per convidar algú a jugar, envia-li aquest enllaç Partida finalitzada Esperant un adversari + O deixeu que el vostre oponent escanegi aquest codi QR Esperant El teu torn %1$s nivell %2$s @@ -272,7 +273,6 @@ En joc Jugant-se ara mateix Acabat - finalitza %s Avortar la partida Partida avortada Estàndard @@ -405,6 +405,7 @@ Importa una partida Enganxa el PGN d\'una partida per obtenir una repetició navegable, anàlisi computeritzada, xat de joc i enllaç per compartir. S\'esborraran les variants. Per mantenir-les, importeu el PGN mitjançant un estudi. + Aquest PGN és accessible públicament. Per a importar un joc de manera privada, utilitza un estudi. %s partides importades %s partides importades @@ -506,7 +507,8 @@ Edita el perfil Nom Cognoms - Defineix el teu estil: + Defineix el teu estil + Icona Existeix una configuració per amagar els estils dels jugadors a tot el lloc web. Biografia País o regió @@ -746,6 +748,7 @@ Amb amics Amb tothom Mode infantil + Mode nens activat. Es tracta de seguretat. En el mode nen, totes les comunicacions de la pàgina estan inhabilitades. Activa això als seus nens o alumnes, per protegir-los d\'altres usuaris d\'internet. En el mode nen, el logo de lichess té una icona de %s, per què sàpigues que els teus nens estan segurs. El vostre compte és gestionat. Demaneu al vostre professor deixar el mode per a nens. @@ -859,9 +862,10 @@ i guardar %s línia anticipada i guardar %s línies anticipades + Has rebut un missatge privat de Lichess. + Fes clic aquí per llegir-lo Ho sentim :( Hem hagut de penalitzar-te una estona. - La penalització acaba en %s. Perquè? Intentem donar una bona experiència d’escacs a tothom. Per això, hem de garantir que tots els jugadors segueixin bones pràctiques. @@ -881,6 +885,7 @@ Estic d’acord que seguiré totes les polítiques de Lichess. Cerca o inicia una nova discusió Edita + Bullet Blitz Ràpides Clàssic @@ -921,9 +926,6 @@ El temps quasi s\'ha exhaurit! [Clica per a mostrar l\'adreça de correu electrònic] Descarregar - Benvinguts! - Lichess és una entitat sense ànim de lucre i un programari totalment lliure i de codi obert. -Les despeses de funcionament, desenvolupament i continguts es financen exclusivament amb donacions d\'usuaris. Gestionament d\'entrenador Gestionament de retransmissor Cancel-lar el torneig @@ -984,4 +986,6 @@ Deixa-ho en blanc per començar els jocs de la posició inicial. Els nostres consells per organitzar esdeveniments Instruccions Mostrar tot + Lichess és una entitat sense ànim de lucre i un programari totalment lliure i de codi obert. +Les despeses de funcionament, desenvolupament i continguts es financen exclusivament amb donacions d\'usuaris. diff --git a/translation/dest/site/ckb-IR.xml b/translation/dest/site/ckb-IR.xml index 6d7504f7eccf7..b5d38948911e6 100644 --- a/translation/dest/site/ckb-IR.xml +++ b/translation/dest/site/ckb-IR.xml @@ -5,6 +5,7 @@ بۆ بانگهێشت کردنی کەسێک بۆ یاریکردن, ئەم بەستەرەیان پێ بدە یاریەکە کۆتایی هات چاوەڕوانی لایەینی بەرامبەر + یان با بەرامبەرەکەت ئەم QR کۆدە سکان بکات چاوەڕوانی سەرەی تۆیە %1$s ئاست %2$s @@ -70,6 +71,7 @@ کردن بە رستەی سەرەکی لێرە بیسڕەوە هێزی جۆراوجۆر + کۆپی گۆڕانکاری PGN جوڵە دۆران بەشێوەیەکی تایبەت براوەی هەمەجۆر @@ -271,7 +273,6 @@ یارییەکانی ئێستا ئێستا یاری دەکەن تەواوبوو - تەواوبوو %s هەڵوەشاندنەوەی یاری یارییەکە هەڵوەشایەوە ئاسایی @@ -484,6 +485,7 @@ جوڵەکانی یاری بردنەوەکانی سپی بردنەوەکانی ڕەش + رێژەی یەکسانبوون یەکسانبووەکان داهاتوو %s پاڵەوانیەتی: تێکڕای ڕکابەر @@ -504,6 +506,9 @@ دەستکاریکردنی پڕۆفایل ناو ناوی باپیرت + توانای خۆت دابنێ + بەهرە + ڕێکخستنێک هەیە بۆ شاردنەوەی هەموو تواناکانی بەکارهێنەر لە سەرانسەری ماڵپەڕەکەدا. ژیاننامە وڵات یان هەرێم سوپاس! @@ -846,9 +851,10 @@ بەم جۆرە %s ـی پێش جوڵە پاشەکەوتبکە بەم جۆرە %s ـی پێش جوڵە پاشەکەوتبکە + پەیامێکی تایبەتت لە لایەن لیچێسەوە پێگەیشتووە. + بۆ خوێندنەوەی کلیک لێرە بکە ببوورە :( ناچاربووین بۆ ماوەیەک دوورت بخەینەوە. - کاتە دیاریکراوەکەت %s ـیتربەسەر دەچێت. بۆ? ئامانجمان دابینکردنی ئەزموونێکی خۆشی شەترەنجە بۆ هەمووان. بۆ ئەو مەبەستەش دەبێت دڵنیابین لەوەی کە هەموو یاریزانەکان مەشقی باش پەیڕەو دەکەن. @@ -868,6 +874,7 @@ منیش لەگەڵ ئەوەدام کە هەموو سیاسەتەکانی لیچێس پەیڕەو دەکەم. گەڕان یان دەستپێکردنی گفتوگۆی نوێ دەستکاریکردن + بوڵێت \"Bullet\" ئاگرین\"بڵیتز\" خێرا کلاسیک @@ -908,9 +915,6 @@ خەریکە کاتت تەواودەبێت! [بۆ بینینی ناونیشانی ئیمەیڵ کلیک بکە] داگرتن - بەخێربێیت! - لیچێس وێبسایتێکی خێرخوازی و بەخۆڕایە، بە بێ هیچ رێکلامێک و بە تەواوەتی سەرچاوەی سیستەمی کراوەیە. -هەموو سەرچاوە داراییەکانی ئیشپێکردن و گەشەپێدان و بەڕێوەبردنی لەلایەن بەکارهێنەرانی خۆبەخش دابین دەکرێت. بەڕێوەبەری ڕاهێنەر بەڕێوەبەری ستریمەر هەڵوەشاندنەوەی پاڵەوانییەتی @@ -969,4 +973,6 @@ لای یاریکردن بگۆڕە داخستنی ئەکاونتەکەت تانەکەت دەکێشێتەوە پێشنیارەکانمان بۆ ڕێکخستنی ئیڤێنت + لیچێس وێبسایتێکی خێرخوازی و بەخۆڕایە، بە بێ هیچ رێکلامێک و بە تەواوەتی سەرچاوەی سیستەمی کراوەیە. +هەموو سەرچاوە داراییەکانی ئیشپێکردن و گەشەپێدان و بەڕێوەبردنی لەلایەن بەکارهێنەرانی خۆبەخش دابین دەکرێت. diff --git a/translation/dest/site/co-FR.xml b/translation/dest/site/co-FR.xml index e5f299c01c162..bb0f60e9bbd56 100644 --- a/translation/dest/site/co-FR.xml +++ b/translation/dest/site/co-FR.xml @@ -238,7 +238,6 @@ Si ghjoca avà Si ghjoca avà Finita - finisce %s Aburtì a partita Partita aburtita Classica @@ -793,7 +792,6 @@ Scusate :( Vi avemu messu fora pè un mumentu. - A suspensione finisce %s. Perchè? Vulemu prupone una stonda di scacchi piacevule pè tutti. A tale scopu, vulemu assicurà chì tutti i ghjucadori seguitinu e bone pratiche. @@ -850,8 +848,6 @@ U tempu hè guasgi finitu! [Clicheghja pè palisà l\' indirizzu mail] Telecaricà - Benvenutu! - Lichess hè un prugrammu infurmaticu in open source di rigalu in tuttu. Tutti i costi di sviluppu, di mantenanza è u cuntinutu sò pagati cù i dunazioni di l\' utilizatori. Respunsevule di l\' allinadori Responsevule di i streamers Cancellà u turneu @@ -908,4 +904,5 @@ Lasciate viotu pè principià a partita cù una pusizione nurmale. Partite classificate da tutti i Ghjucadori di Lichess Invertì a scacchera Chjude u vostru contu cancellerà a vostra chjama + Lichess hè un prugrammu infurmaticu in open source di rigalu in tuttu. Tutti i costi di sviluppu, di mantenanza è u cuntinutu sò pagati cù i dunazioni di l\' utilizatori. diff --git a/translation/dest/site/cs-CZ.xml b/translation/dest/site/cs-CZ.xml index 18c34797a97a3..33e929d581d47 100644 --- a/translation/dest/site/cs-CZ.xml +++ b/translation/dest/site/cs-CZ.xml @@ -301,7 +301,6 @@ Právě se hraje Právě se hraje Dokončeno - končí za %s Zrušit hru Hra byla zrušena Standard @@ -930,7 +929,6 @@ Omlouváme se :( Museli jsme tě odpojit na chvíli. - Odpojení skončí za %s. Proč? Snažíme se všem poskytnout příjemnou hru. Proto se musíme ujistit, že se všichni chovají správně. @@ -990,9 +988,6 @@ Čas vám běží! [Klikněte pro zobrazení e-mailové adresy] Stáhnout - Vítej! - Lichess je bezplatný a zcela svobodný/nezávislý softvér s otevřeným zdrojovým kódem. -Veškeré provozní náklady, vývoj a obsah jsou financovány výhradně z příspěvků uživatelů. Nastavení trenéra Nastavení streamera Zrušit turnaj @@ -1050,4 +1045,6 @@ Nechte toto pole prázdné, pokud chcete začínat z běžného základního pos Prohodit strany Uzavřením vašeho účtu stáhnete vaše odvolání Naše tipy pro organizování akcí + Lichess je bezplatný a zcela svobodný/nezávislý softvér s otevřeným zdrojovým kódem. +Veškeré provozní náklady, vývoj a obsah jsou financovány výhradně z příspěvků uživatelů. diff --git a/translation/dest/site/cv-CU.xml b/translation/dest/site/cv-CU.xml index fbcda45c2119e..16aaf33455fed 100644 --- a/translation/dest/site/cv-CU.xml +++ b/translation/dest/site/cv-CU.xml @@ -144,7 +144,6 @@ Сир Халлех пыракан Вӗҫленнисем - %s вӗҫленет Вӑййине пӑрахӑҫла Вӑййине пӑрахӑҫланӑ Стандартла @@ -486,7 +485,6 @@ %1$s сана \"%2$s\" чӗннӗ. Эсӗ \"%1$s\" хутшӑннӑ. %1$s vs %2$s - Хапӑл тӑватпӑр! Турнира пӑрахӑҫла Турнир пирки Турнир калаҫӑвӗ diff --git a/translation/dest/site/cy-GB.xml b/translation/dest/site/cy-GB.xml index 70ba07f127821..8eb19543db6d9 100644 --- a/translation/dest/site/cy-GB.xml +++ b/translation/dest/site/cy-GB.xml @@ -210,7 +210,6 @@ Ar ei hanner Chwarae ar hyn o bryd Wedi dod i ben. - yn gorffen mewn %s Erthylu gêm Gêm wedi\'i herthylu Normal @@ -688,7 +687,6 @@ Llongyfarchiadau, gwnaethoch chi ennill! [Cliciwch i ddangos cyfeiriad ebost] Lawrlwytho - Croeso! Gohirio\'r gystadleuaeth Disgrifiad cystadleuaeth diff --git a/translation/dest/site/da-DK.xml b/translation/dest/site/da-DK.xml index 5d2f7a1471f81..21eeca9e4272e 100644 --- a/translation/dest/site/da-DK.xml +++ b/translation/dest/site/da-DK.xml @@ -5,6 +5,7 @@ Invitér en til at spille ved at oplyse denne URL Spillet er slut Venter på modstander + Eller lad din modstander scanne denne QR-kode Venter Din tur %1$s niveau %2$s @@ -175,7 +176,7 @@ Forum %1$s skrev i forum %2$s Seneste debatindlæg - Skakspillere + Spillere Venner Samtaler I dag @@ -248,8 +249,8 @@ Ranking opdateres hvert %s minut - %s opgave - %s opgaver + %s taktikopgave + %s taktikopgaver Antal partier spillet @@ -272,7 +273,6 @@ Spilles lige nu Spilles lige nu Afsluttet - afsluttes %s Afbryd parti Partiet blev afbrudt Standard @@ -393,7 +393,7 @@ Nulstil Anvend Gem - Førertavle + Rangliste Tag et skræmbillede af den aktuelle stilling Parti som GIF Indsæt FEN-teksten her @@ -405,6 +405,7 @@ Importér parti Når du indsætter et partis PGN, får du en afspillelig gengivelse, en computeranalyse, en spilchat og en URL til deling. Varianter vil blive slettet. Hvis du vil beholde dem, skal du importere PGN\'en via et studie. + Denne PGN kan tilgås af offentligheden. For at importere et parti privat, skal du bruge et studie. %s importerede spil %s importerede spil @@ -506,8 +507,9 @@ Redigér profil Fornavn Efternavn - Indstil din flair: - Der er en indstilling til at skjule alle brugers flairs på tværs af hele webstedet. + Indstil dit ikon + Ikon + Der er en indstilling til at skjule alle brugerikoner på tværs af hele webstedet. Biografi Land eller region Tak! @@ -527,7 +529,7 @@ Automatisk videre til næste spil efter træk Autoskift - Opgaver + Taktikopgaver Turneringsvindere Navn Beskrivelse @@ -746,6 +748,7 @@ Med venner Med alle Børnetilstand + Børnetilstand er aktiveret. Dette angår sikkerhed. I børnetilstand er alt kommunikation deaktiveret. Aktivér dette for dine børn eller skoleelever for at beskytte dem mod andre internetbrugere. I børnetilstand vil Lichess-logoet få et %s ikon, så du ved, at dine børn er sikret. Din konto er under administration. Spørg din skaklærer om at ophæve børnetilstanden. @@ -859,9 +862,10 @@ og gem %s serie af forhåndstræk og gem %s serier af forhåndstræk + Du har modtaget en privat besked fra Lichess. + Klik her for at læse den Beklager :( Vi måtte give dig en timeout. - Timeouten udløber %s. Hvorfor? Vi tilstræber at give alle en behagelig skakoplevelse. I den forbindelse må vi sikre, at alle spillere følger god praksis. @@ -881,7 +885,8 @@ Jeg lover, at jeg vil overholde alle Lichess-politikker. Søg eller start ny diskussion Rediger - Lynskak + Bullet + Blitz Rapid Classical Vanvittigt hurtige spil: mindre end 30 sekunder @@ -921,9 +926,6 @@ Tiden er næsten udløbet! [Klik for at vise e-mailadresse] Download - Velkommen! - Lichess er en velgørenhedsorganisation og helt gratis/libre open source-software. -Alle driftsomkostninger, udvikling og indhold finansieres udelukkende af brugerdonationer. Træner-administration Streamer-administration Aflys turneringen @@ -984,4 +986,6 @@ Lad stå tomt for at starte partier fra den normale udgangsstilling. Tips til organisering af begivenheder Instruktioner Vis mig alt + Lichess er en velgørenhedsorganisation og helt gratis/libre open source-software. +Alle driftsomkostninger, udvikling og indhold finansieres udelukkende af brugerdonationer. diff --git a/translation/dest/site/de-DE.xml b/translation/dest/site/de-DE.xml index 81cf6e4f29d57..29d4ce8161ce4 100644 --- a/translation/dest/site/de-DE.xml +++ b/translation/dest/site/de-DE.xml @@ -5,6 +5,7 @@ Versende diese URL, um jemanden zum Spiel einzuladen Partie beendet Auf den Gegner warten + Oder lasse deinen Gegner diesen QR-Code scannen Warten Du bist am Zug %1$s Stufe %2$s @@ -272,7 +273,6 @@ Partie läuft Laufende Partien Beendet - endet in %s Partie abbrechen Partie abgebrochen Standard @@ -405,6 +405,7 @@ Partie importieren Wenn du ein PGN einfügst, hast du Zugriff auf eine Spielwiederholung, eine Computeranalyse, einen Spielchat und eine teilbare URL. Varianten werden gelöscht. Importiere die PGN mittels einer Studie, um sie zu behalten. + Diese PGN ist öffentlich zugänglich. Nutze eine Studie, um eine Partie nur für dich zu importieren. %s importierte Partie %s importierte Partien @@ -461,7 +462,7 @@ Erweiterte Einstellungen Wähle einen äußerst sicheren Namen für das Turnier. Sämtliche unangemessene Inhalte können zur Schließung deines Benutzerkontos führen. - Frei lassen, um das Turnier nach einem zufälligen Großmeister zu benennen. + Frei lassen, um das Turnier nach einem zufälligen Großmeister zu benennen. Wir empfehlen, diese nicht zu ändern. Falls du Teilnahmebedingungen setzt, wird das Turnier weniger Spieler haben. Zeige erweiterte Einstellungen @@ -506,8 +507,9 @@ Profil bearbeiten Vorname Nachname - Setze deine Dekoration: - Es gibt eine Einstellung, um alle persönlichen Stilelemente der Benutzer auf der gesamten Website zu verbergen. + Setze dein Flair + Flair + Es existiert eine Einstellungsmöglichkeit, um alle Benutzerflairs auf der gesamten Seite zu verbergen. Profiltext Land oder Region Vielen Dank! @@ -528,7 +530,7 @@ Nach dem Zug automatisch zur nächsten Partie gehen Automatischer Wechsel Aufgaben - Turniersieger + Turniersieger Name Beschreibung Interne Beschreibung @@ -661,10 +663,10 @@ Simultanschach Veranstalter Farbe des Ausrichters: %s - Deine ausstehenden Simultan-Vorstellungen + Deine ausstehenden Simultanpartien Neue Simultanspiele Ein Simultan veranstalten - Melde dich an, um eine Simultan-Vorstellung zu geben, oder einer beizutreten + Melde dich an, um eine Simultanpartie auszutragen oder sich einer anzuschließen Simultan nicht gefunden Dieses Simultan existiert nicht. Zurück zur Simultan Homepage @@ -689,7 +691,7 @@ Tastenkürzel Zug zurück/vor zum Anfang/Ende - Durch die ausgewählte Variation schalten + Durch die ausgewählte Variante schalten zeige/verberge Kommentare Variante wählen/verlassen Hole dir eine Computer-Analyse, lerne aus deinen Fehlern @@ -699,10 +701,10 @@ Die nächste Ungenauigkeit Vorherige Verzweigung Nächste Verzweigung - Variationspfeile an/auschalten + Variantenpfeile ein-/auschalten Durch vorherige/nächste Variante schalten - Glyph-Anmerkungen umschalten - Mit Variationspfeilen navigierst du durch die Zugliste. + Schalten der Zeichen-Anmerkungen + Mit Variantenpfeilen navigierst du durch die Zugliste. den ausgewählten Zug ausführen Neues Turnier Schachturnier mit verschiedenen Zeitkontrollen und Schachvarianten @@ -746,6 +748,7 @@ Mit deinen Freunden Mit jedem Kindermodus + Kindermodus ist aktiviert. Dies ist eine Sicherheitseinstellung. Im Kindermodus sind alle Kommunikationsmöglichkeiten deaktiviert. Aktiviere diese Option, um Kinder und Schüler vor anderen Internetbenutzern zu schützen. Im Kindermodus erhält das Lichess-Logo ein %s-Icon, an dem erkennen kannst, dass deine Kinder geschützt sind. Dein Konto wird verwaltet. Bitte deinen Schachlehrer den Kindermodus aufzuheben. @@ -859,9 +862,10 @@ und speichere %s Variante im Voraus und speichere %s Varianten im Voraus + Du hast eine private Nachricht von Lichess erhalten. + Hier klicken zum Lesen Entschuldigung :( Wir haben dich mit einer vorübergehenden Spielsperre belegt. - Die Sperre endet in %s. Warum? Wir möchten allen eine möglichst gute Schach-Erfahrung bieten. Um dies zu erreichen, müssen wir sicherstellen, dass sich alle Spieler korrekt verhalten. @@ -881,6 +885,7 @@ Ich stimme zu, dass ich allen Lichess-Richtlinien folgen werde. Suche oder beginne eine neue Konversation Bearbeiten + Bullet Blitz Schnellschach Klassisch @@ -921,9 +926,6 @@ Deine Zeit ist fast abgelaufen! [Klicke, um die E-Mail-Adresse anzuzeigen] Herunterladen - Herzlich Willkommen! - Lichess ist eine Wohltätigkeitsorganisation und eine völlig kostenlose/freie Open-Source-Software. -Alle Betriebskosten, Entwicklung und Inhalte werden ausschließlich durch Benutzerspenden finanziert. Trainerverwaltung Streamerverwaltung Turnier abbrechen @@ -982,6 +984,8 @@ Leer lassen, um Partien von der normalen Ausgangsstellung aus zu starten.andere Farbe Dein Benutzerkonto zu schließen wird auch deinen Einspruch zurückziehen Unsere Tipps für die Organisation von Veranstaltungen - Anweisungen + Anleitung Alles zeigen + Lichess ist eine Wohltätigkeitsorganisation und eine völlig kostenlose/freie Open-Source-Software. +Alle Betriebskosten, Entwicklung und Inhalte werden ausschließlich durch Benutzerspenden finanziert. diff --git a/translation/dest/site/el-GR.xml b/translation/dest/site/el-GR.xml index 07c6100cfdbfb..6343220af488a 100644 --- a/translation/dest/site/el-GR.xml +++ b/translation/dest/site/el-GR.xml @@ -270,7 +270,6 @@ Παιχνίδι σε εξέλιξη τώρα Σε εξέλιξη τώρα Ολοκληρωμένα - τελειώνει %s Εγκαταλείψτε το παιχνίδι Παιχνίδι εγκατελήφθη Κανονικό @@ -460,7 +459,7 @@ Ρυθμίσεις για προχωρημένους Διαλέξτε ένα πολύ ασφαλές όνομα για το τουρνουά. Οτιδήποτε ακόμα και ελάχιστα ακατάλληλο μπορεί να κλείσει τον λογαριασμό σας. - Αφήστε το κενό για να πάρει το όνομά του τυχαία από κάποιον γνωστό σκακιστή. + Αφήστε το κενό για να πάρει το όνομά του τυχαία από κάποιον γνωστό σκακιστή. Σας συνιστούμε να μην τα αλλάξετε αυτά. Εάν ορίσετε προϋποθέσεις εισόδου, το τουρνουά σας θα έχει λιγότερους παίκτες. Εμφάνιση ρυθμίσεων για προχωρημένους @@ -524,7 +523,7 @@ Προχωρήστε αυτόματα στο επόμενο παιχνίδι μετά την κίνηση Αυτόματη εναλλαγή Γρίφοι - Νικητές πρωταθλημάτων + Νικητές πρωταθλημάτων Ονομασία Περιγραφή Ιδιωτική περιγραφή @@ -847,7 +846,6 @@ Λυπούμαστε :( Πρέπει να σας αποκλείσουμε για λίγο. - Ο αποκλεισμός λήγει σε %s. Γιατί; Ο σκοπός μας είναι να προσφέρουμε μια ευχάριστη σκακιστική ατμόσφαιρα για όλους. Για να πραγματοποιηθεί αυτό, πρέπει να βεβαιωθούμε ότι όλοι οι παίκτες ακολουθούν τους κανόνες. @@ -867,6 +865,7 @@ Αποδέχομαι ότι θα συμμορφωθώ με όλες τις πολιτικές του Lichess. Αναζήτηση ή έναρξη νέας συνομιλίας Επεξεργασία + Bullet Blitz Rapid Κλασικό @@ -907,9 +906,6 @@ Ο χρόνος σας έχει σχεδόν τελειώσει! [Κάντε κλικ για να δείτε τη διεύθυνση ηλεκτρονικού ταχυδρομείου] Λήψη - Καλώς ήρθατε! - Το Lichess είναι ένα φιλανθρωπικό και εντελώς ελεύθερο λογισμικό ανοιχτού κώδικα. -Όλα τα εξοδα λειτουργίας, ανάπτυξης και περιεχομένου καλύπτοναι αποκλειστικά από δωρεές χρηστών. Διαχειριστής προπονητή Διαχειριστής streamer Ακύρωση τουρνουά @@ -968,4 +964,6 @@ Εναλλαγή πλευρών Αν απενεργοποιήσετε τον λογαριασμό σας θα χάσετε το δικαίωμα έφεσης Οι συμβουλές μας για τη διοργάνωση εκδηλώσεων + Το Lichess είναι ένα φιλανθρωπικό και εντελώς ελεύθερο λογισμικό ανοιχτού κώδικα. +Όλα τα εξοδα λειτουργίας, ανάπτυξης και περιεχομένου καλύπτοναι αποκλειστικά από δωρεές χρηστών. diff --git a/translation/dest/site/en-US.xml b/translation/dest/site/en-US.xml index 4a79caf342044..c35e490be8938 100644 --- a/translation/dest/site/en-US.xml +++ b/translation/dest/site/en-US.xml @@ -5,6 +5,7 @@ To invite someone to play, give this URL Game Over Waiting for opponent + Or let your opponent scan this QR code Waiting Your turn %1$s level %2$s @@ -272,7 +273,6 @@ Playing right now Playing right now Finished - finishes %s Abort game Game aborted Standard @@ -406,6 +406,7 @@ Paste a game PGN to get a browsable replay, computer analysis, game chat and shareable URL. Variations will be erased. To keep them, import the PGN via a study. + This PGN can be accessed by the public. To import a game privately, use a study. %s imported game %s imported games @@ -507,7 +508,8 @@ computer analysis, game chat and shareable URL. Edit profile First name Last name - Set your flair: + Set your flair + Flair There is a setting to hide all user flairs across the entire site. Biography Region or country @@ -747,6 +749,7 @@ computer analysis, game chat and shareable URL. With friends With everybody Kid mode + Child-mode is enabled. This is about safety. In kid mode, all site communications are disabled. Enable this for your children and school students, to protect them from other internet users. In kid mode, the lichess logo gets a %s icon, so you know your kids are safe. Your account is managed. Ask your chess teacher about lifting kid mode. @@ -860,9 +863,10 @@ computer analysis, game chat and shareable URL. and save %s premove line and save %s premove lines + You have received a private message from Lichess. + Click here to read it Sorry :( We had to time you out for a while. - The timeout expires %s. Why? We aim to provide a pleasant chess experience for everyone. To that effect, we must ensure that all players follow good practice. @@ -882,6 +886,7 @@ computer analysis, game chat and shareable URL. I agree that I will follow all Lichess policies. Search or start new discussion Edit + Bullet Blitz Rapid Classical @@ -922,9 +927,6 @@ computer analysis, game chat and shareable URL. Time is almost up! [Click to reveal email address.] Download - Welcome! - Lichess is a charity and entirely free/libre open source software. -All operating costs, development, and content are funded solely by user donations. Coach manager Streamer manager Cancel the tournament @@ -985,4 +987,6 @@ Leave empty to start games from the normal initial position. Our tips for organizing events Instructions Show me everything + Lichess is a charity and entirely free/libre open source software. +All operating costs, development, and content are funded solely by user donations. diff --git a/translation/dest/site/eo-UY.xml b/translation/dest/site/eo-UY.xml index 8385efdb6fcaf..0421064a723c7 100644 --- a/translation/dest/site/eo-UY.xml +++ b/translation/dest/site/eo-UY.xml @@ -70,6 +70,7 @@ Ĉefigi linion Forigi ekde tie ĉi Devigi variaĵon + Kopi varianto PGN Movo Malvenka variaĵo Venka variaĵo @@ -271,7 +272,6 @@ Ludate nun Ludanta Finite - finiĝos %s Ĉesigi ludon Ludo estis ĉesigita Normale @@ -505,6 +505,8 @@ Redakti profilon Persona nomo Familia nomo + Agordi viaj emoĝio + Emoĝio Biografio Dankon! Ligiloj al sociaj retejoj @@ -732,6 +734,7 @@ Kun amikoj Kun ĉiuj Infana reĝimo + Infana reĝimo estas enŝaltita. Ĉi tio temas pri sekureco. En infana reĝimo, ĉiuj retejaj komunikadoj estas malebligitaj. Aktivigu ĉi tiun por viaj infanoj kaj lernejaj studentoj, por protekti ilin de aliaj retaj uzantoj. En infana reĝimo, la lichess-emblemo ekhavas %s-emblemon, por ke vi sciu ke viaj infanoj sekuras. Via konto estas mastrumita. Petu vian ŝako-instruiston pri malaktivigado de \"infana reĝimo\". @@ -845,9 +848,10 @@ kaj savi %s antaŭmovan vicon kaj savi %s antaŭmovajn vicojn + Vi ricevis privatan mesaĝon de Lichess. + Alklaku ĉi tie por legi ĝin Bedaŭrinde :( Ni devis provizore suspendi vin. - La suspendo finiĝos %s. Kial? Nia celo estas provizi plaĉan ŝakan sperton al ĉiuj. Pro tio, ni devas certigi ke ĉiuj ludantoj sekvas bonna etiketon. @@ -867,6 +871,7 @@ Mi konsentas sekvi ĉiujn politikojn de Lichess. Serĉi aŭ komenci novan konversacion Redakti + Bullet Blitz Rapida Klasika @@ -907,9 +912,6 @@ La tempo preskaŭ finiĝis! [Klaku por malkaŝi retpoŝtadreson] Elŝuti - Bonvenon! - Lichess estas almoza kaj tute libera malfermitkoda programaro. -Ĉiu funkciada elspezo, ellaborado, kaj enhavo estas financita sole de uzantajn donacojn. Trejnista administranto Filmprezentista administranto Nuligi la turniron @@ -968,4 +970,6 @@ Lasi ĝin malplena por komenci ludojn de la norma komenca pozicio. Ŝanĝi flankojn Fermata de via konto estos eltiri vian apelacion Niaj konsiletoj por organizi eventojn + Lichess estas almoza kaj tute libera malfermitkoda programaro. +Ĉiu funkciada elspezo, ellaborado, kaj enhavo estas financita sole de uzantajn donacojn. diff --git a/translation/dest/site/es-ES.xml b/translation/dest/site/es-ES.xml index 97b6b6339646b..69081343f7a48 100644 --- a/translation/dest/site/es-ES.xml +++ b/translation/dest/site/es-ES.xml @@ -5,6 +5,7 @@ Para invitar a alguien a jugar, comparte este enlace Fin de la partida Esperando al oponente + O deja que tu oponente escanee este código QR Esperando Tu turno %1$s nivel %2$s @@ -272,7 +273,6 @@ En juego ahora mismo Se está jugando ahora Terminado - termina %s Cancelar partida Partida cancelada Estándar @@ -405,6 +405,7 @@ Importar partida Al pegar el PGN de una partida, se obtiene una repetición navegable, un análisis por ordenador, un chat de juego y un enlace para compartir. Las variaciones serán borradas. Para mantenerlas, importa el PGN a través de un estudio. + Este PGN es de acceso público. Para importar una partida de forma privada, utiliza un estudio. %s partida importada %s partidas importadas @@ -506,7 +507,8 @@ Editar perfil Nombre Apellido - Configura tu entorno: + Configura tu entorno + Entorno Existe una opción para ocultar la configuración de entorno en todo el sitio. Biografía País o región @@ -746,6 +748,7 @@ Con mis amigos Con todo el mundo Modo infantil + El modo infantil está activado. Por seguridad, en el modo infantil se desactivan todas las comunicaciones con otros usuarios. Activa esto para proteger a los menores de otros usuarios de Internet. En el modo infantil, el logo de lichess tiene un icono de %s, indicando que los niños están seguros. Tu cuenta está administrada. Pídele a tu profesor de ajedrez que desactive el modo para niños. @@ -859,9 +862,10 @@ y ahorra %s línea de premovimiento y ahorra %s líneas de premovimiento + Has recibido un mensaje privado de Lichess. + Haz clic aquí para leerlo Lo sentimos :( Hemos tenido que suspenderte temporalmente. - La suspensión expira en %s. ¿Por qué? Nuestro objetivo es proporcionar una experiencia agradable a todo el mundo. Para ello, debemos asegurarnos de que todos los jugadores se comportan como es debido. @@ -881,6 +885,7 @@ Me comprometo a seguir las normas de Lichess. Buscar o empezar una nueva conversación Editar + Bala Blitz Rápida Clásica @@ -921,9 +926,6 @@ ¡Te queda poco tiempo! [Haz clic para mostrar el correo electrónico] Descargar - ¡Bienvenido! - Lichess es una organización benéfica y un software totalmente libre y de código abierto. -Todos los gastos de funcionamiento, desarrollo y contenidos se financian exclusivamente mediante las donaciones de sus usuarios. Gestor de entrenador Gestor de emisiones Cancelar el torneo @@ -984,4 +986,6 @@ Déjalo vacío para empezar las partidas desde la posición inicial habitual.Nuestros consejos para organizar eventos Instrucciones Mostrarme todo + Lichess es una organización benéfica y un software totalmente libre y de código abierto. +Todos los gastos de funcionamiento, desarrollo y contenidos se financian exclusivamente mediante las donaciones de sus usuarios. diff --git a/translation/dest/site/et-EE.xml b/translation/dest/site/et-EE.xml index 71289d72aeb0d..d83c3c28b53e1 100644 --- a/translation/dest/site/et-EE.xml +++ b/translation/dest/site/et-EE.xml @@ -262,7 +262,6 @@ Mängimas praegu Praegu mängimas Lõpetatud - lõppeb %s Katkesta mäng Mäng katkestatud Standard @@ -826,7 +825,6 @@ arvutianalüüsi, mängu jututoa ning jagatava URL-i. Vabandust :( Me pidime su natukeseks ära blokeerima. - Blokeering aegub %s. Miks? Me tahame pakkuda häid malekogemusi kõigile. Selleks peame olema kindlad, et kõik meie kasutajad järgivad reegleid. @@ -886,9 +884,6 @@ arvutianalüüsi, mängu jututoa ning jagatava URL-i. Aeg on peaaegu otsas! [Vajuta e-maili aadressi nägemiseks] Lae alla - Tere tulemast! - Lichess on heategevuslik ja täiesti tasuta avatud lähtekoodiga tarkvara. -Kõik tegevuskulud, arendus ja sisu rahastatakse ainult kasutajate annetustest. Treeneri seaded Striimeri seaded Tühista turniir @@ -947,4 +942,6 @@ Jäta tühjaks, et alustada mänge tavalisest algpositsioonist. Vaheta pooli Konto sulgemine tühistab teie kaebuse Meie nõuanded ürituste korraldamiseks + Lichess on heategevuslik ja täiesti tasuta avatud lähtekoodiga tarkvara. +Kõik tegevuskulud, arendus ja sisu rahastatakse ainult kasutajate annetustest. diff --git a/translation/dest/site/eu-ES.xml b/translation/dest/site/eu-ES.xml index 783563e03e418..b9e3084921f31 100644 --- a/translation/dest/site/eu-ES.xml +++ b/translation/dest/site/eu-ES.xml @@ -272,7 +272,6 @@ Oraintxe jokatzen Oraintxe jokatzen Amaituta - bukaera: %s Partida geldiarazi Geldiarazitako partida Ohikoa @@ -405,6 +404,7 @@ Partida inportatu PGN partida bat itsastean ikusi daitekeen partida bat lortuko duzu, partidare eta analisiarekin, txatarekin eta elkarbanatu dezakezun helbide batekin. Aldaerak ezabatu egingo dira. Mantendu nahi badituzu inportatu PGNa azterlan gisa. + PGN hau edonork deskargatu dezake. Partida bat era pribatuan inportatzeko azterlan bat erabili behar duzu. %s inportatutako partidak %s inportatutako partidak @@ -461,7 +461,7 @@ Ezarpen aurreratuak Adeitasunezko izena hauta ezazu. Errespetua galduz gero, zure kontua itxiko dugu. - Ez baduzu betetzen, txapelketak Maisu Handi baten izena hartuko du, ausaz. + Ez baduzu betetzen, txapelketak Maisu Handi baten izena hartuko du, ausaz. Hobe ez ukitu hau. Parte hartzeko baldintzak jartzen badituzu, jokalari gutxiago sartuko dira txapelketan. Ezarpen aurreratuak @@ -506,7 +506,8 @@ Nire profila editatu Izena Abizena - Ezarri zure iruditxoa: + Ezarri zure iruditxoa + Iruditxoa Webgune guztian zehar erabiltzaile guztien iruditxoak ezkutatzeko ezarpen bat dago. Biografia Herrialdea @@ -528,7 +529,7 @@ Mugitu ondoren, hurrengo partidara joan Hurrengo partidara Ariketak - Txapelketaren irabazleak + Txapelketaren irabazleak Izena Deskribapena Deskribapen pribatua @@ -746,6 +747,7 @@ Lagunekin Edonorekin Haurren modua + Haur-modua aktibatuta dago. Hau segurtasunari buruzkoa da. Haurren moduan, webguneko komunikazio guztiak desaktibatuta daude. Aktibatu zure haur eta ikasleei beste Internet erabiltzaileengandik babesteko. Haurren moduan, lichess logoak %s ikonoa du, haurrak seguru daudela jakin dezazun. Zure kontua beste norbaitek kudeatzen du. Eskatu zure irakasleari haur modua desaktibatzeko. @@ -859,9 +861,10 @@ eta aurrejokaldi linea %s gorde eta %s aurrejokaldi linea gorde + Lichessek bidalitako mezu pribatu bat jaso duzu. + Egin klik hemen irakurtzeko Barkatu :( Denbora tarte baterako bota egin behar izan zaitugu. - Kanporaketaren amaiera: %s. Zergatik? Guztioi xakean jokatzeko aukera atsegina eskaintzea da gure helburua. Horretarako, jokalari guztiek praktika onak jarraitzen dituztela ziur izan behar dugu. @@ -881,6 +884,7 @@ Lichessek ezarritako politikak beteko ditut. Hizketaldia bilatu edo berria hasi Aldatu + Bullet Blitz Aktiboa Estandarra @@ -921,9 +925,6 @@ Denbora ia agortu da! [Egin klik eposta helbidea erakusteko] Deskargatu - Ongi etorri! - Lichess software librea da. -Garapen eta mantentze-kostu guztiak erabiltzaileen dohaintzekin ordaintzen dira. Entrenatzaileen kudeatzailea Zuzenekoen kudeatzailea Bertan behera utzi txapelketa @@ -983,4 +984,6 @@ Utzi hutsik partidak ohiko posizioarekin hasteko. Txapelketak antolatzeko gure aholkuak Jarraibideak Erakutsi guztia + Lichess software librea da. +Garapen eta mantentze-kostu guztiak erabiltzaileen dohaintzekin ordaintzen dira. diff --git a/translation/dest/site/fa-IR.xml b/translation/dest/site/fa-IR.xml index 6b4c52b736a5b..bce591dcf3c9f 100644 --- a/translation/dest/site/fa-IR.xml +++ b/translation/dest/site/fa-IR.xml @@ -5,6 +5,7 @@ برای دعوت کردن حریف این لینک را برای او بفرستید پایان بازی انتطار برای حریف + یا اجازه دهید حریف شما این QR کد را اسکن کند در حال انتظار نوبت شماست %1$s سطح %2$s @@ -29,7 +30,7 @@ تقلب تشخیص داده شد شاه در مرکز سه کیش - رقابت به اتمام رسید + مسابقه تمام شد اتمام بازی حریف جدید حریف شما می خواهد که دوباره با شما بازی کند @@ -48,7 +49,7 @@ بازیکن سفید تسلیم شد بازیکن سیاه تسلیم شد بازیکن سفید بازی را ترک کرد - بازیکن سیاه بازی را ترک کرد + سیاه بازی را ترک کرد سفید تکان نخورد مشکی تکان نخورد یک تحلیل رایانه‌ای درخواست بدهید @@ -66,7 +67,7 @@ تهدید را نمایش بدهید در مرورگر محلی ارزیابی به صورت محلی انتخاب شود - تنوع را یک سطح بالاتر ببرید + افزایش عمق شاخه اصلی خط کنونی را به خط اصلی تبدیل کنید از اینجا به بعد را پاک کنید نتیجه تحلیل را به عنوان یکی از تنوعهای بازی انتخاب نمایید @@ -100,6 +101,7 @@ جویشگر حرکت گشاینده بازی جویشگر حرکت گشاینده/پایان‌دهنده بازی حرکت گشاینده %s برای تنوع مورد بررسی + اولین حرکت اکسپلورر شروع/آخر بازی را بازی کن قانون پنجاه حرکت از پیروزی جلوگیری کرد قانون ۵۰ حرکت از شکست جلوگیری کرد برد یا ۵٠ حرکت بعد از اشتباه قبلی @@ -115,6 +117,7 @@ مطالعه را شروع نمایید فعال سازی فلش نشان دهنده بهترین حرکت + نمایش پیکان‌های شاخه اصلی درجه ی نشان دهنده برتری شاخه های متعدد پردازنده(ها) @@ -230,7 +233,7 @@ ما یک ایمیل به آدرس %s ارسال کرده ایم. ممکن است کمی طول بکشد تا برسد. 5 دقیقه صبر کنید و صندوق ورودی ایمیل خود را تازه کنید. - پوشه اسپم خود را نیز بررسی کنید، ممکن است ایمیل را آنجا بیابید. اگر چنین است، آن را به عنوان غیر اسپم (امن) علامت گذاری کنید. + پوشه هرزنامه خود را نیز بررسی کنید، ممکن است در آنجا باشد. اگر چنین است، آن را به عنوان غیر هرزنامه علامت‌گذاری کنید. اگر تمام موارد ناموفق بود، این ایمیل را برای ما ارسال کنید: متن بالا را کپی و پیست کرده و به آدرس زیر ارسال کنید %s ما به زودی با شما تماس خواهیم گرفت تا به شما کمک کنیم ثبت نام خود را تکمیل کنید. @@ -270,7 +273,6 @@ بازی در حال انجام بازی در حال انجام تمام شده - به پایان می‌رسد %s انصراف از بازی بازی لغو شد استاندارد @@ -403,6 +405,7 @@ بارگذاری بازی در صورت بارگذاری فایل PGN،آنالیز کامپیوتری و لینک قابل به اشتراک گذاری در اختیار شما قرار خواهد گرفت. تغییرات پاک خواهند شد. برای حفظ آنها، PGN را از طریق مطالعه وارد کنید. + این PGN برای عموم در دسترس است، برای وارد کردن خصوصی یک بازی، یک مطالعه ایجاد کنید. %s بارگزاری شده %s بارگزاری شده @@ -504,6 +507,9 @@ ویرایش پروفایل نام نام خانوادگی + تعیین کردن شکلک + نشان + تنظیماتی برای مخفی کردن همه شکلک‌های کاربر در کل ویگاه وجود دارد. زندگی نامه کشور یا منطقه ممنون! @@ -676,6 +682,7 @@ برای کمک به شما میتوانید برای خود زمان اضافی در نظر بگیرید. زمان اضافی میزبان به ازای پیوستن هر بازیکن، به زمان اولیه خود اضافه کنید. + زمان اضافه میزبان به ازای بازیکن مسابقات لی چس سوالات متداول مسابقات زمان باقی مانده به شروع مسابقه @@ -684,6 +691,7 @@ میانبر های صفحه کلید حرکت به عقب/جلو رفتن به آغاز/پایان + چرخه شاخه اصلی انتخاب‌شده نمایش/ پنهان کردن نظرات ورود / خروج به شاخه یک تحلیل کامپیوتری درخواست کنید، از اشتباهات خود درس بگیرید @@ -693,6 +701,10 @@ بی‌دقتی بعدی شاخه قبلی شاخه بعدی + کلید پیکان‌های شاخه اصلی + چرخه قبلی/بعدی شاخه اصلی + کلید نماد حاشیه‌نویسی + پیکان های شاخه اصلی به شما امکان می‌دهد بدون استفاده از فهرست حرکت، پیمایش کنید. حرکت انتخاب شده را بازی کن مسابقه جدید مسابقات شطرنج با ویژگی های مختلف مثل کنترل زمان و غیره @@ -736,6 +748,7 @@ با دوستان با همه حالت کودکان + حالت کودک فعال است. این گزینه،امنیتی است.با فعال کردن حالت ((کودکانه))،همه ی ارتباطات(چت کردن و...)غیر فعال می شوند.با فعال کردن این گزینه،کودکان خود را محافطت کنید. در حالت کودکانه،به نماد لیچس،یک %s اضافه می شود تا شما از فعال بودن آن مطلع شوید. اکانت شما مدیریت شده است. برای برداشتن حالت کودک از معلم شطرنج خود درخواست کنید. @@ -849,9 +862,10 @@ و پیش حرکت %s را حفظ کنید و پیش حرکت های %s را حفظ کنید + شما یک پیام خصوصی از لیچس دریافت کرده‌اید. + برای خواندن آنها اینجا کلیک کنید متاسفم :( شما برای مدتی مسدود شدید. - تعداد %s به پایان رسیدن زمان. چرا؟ هدف ما مهیا ساختن تجربه لذت بخش شطرنج به همه افراد است. به همین منظور، ما باید اطمینان حاصل کنیم که تمام بازیکنان تمرین خوب را دنبال میکنند. @@ -871,6 +885,7 @@ من تضمین میکنم که به تمام قوانین و خط مشی های لیچس پایبند باشم . جستجو یا شروع کردن مکالمه جدید ویرایش + بولت برق‌آسا سریع کلاسیک @@ -911,8 +926,6 @@ زمان تقریباً تمام شده است! جهت مشاهده ایمیل کلیک کنید دانلود - خوش آمدید! - لایچس یک خیریه و کاملا رایگان و نرم افزاری متن باز است. تمام هزینه های اجرا، توسعه و محتوا تنها بر پایه هدایای کاربران بنا شده است. تنظیمات مربی مدیریت پخش لغو مسابقه @@ -972,4 +985,5 @@ پیشنهادهای ما برای برگزاری رویدادها راهنما همه چیز را به من نشان بده + لایچس یک خیریه و کاملا رایگان و نرم افزاری متن باز است. تمام هزینه های اجرا، توسعه و محتوا تنها بر پایه هدایای کاربران بنا شده است. diff --git a/translation/dest/site/fi-FI.xml b/translation/dest/site/fi-FI.xml index 3de21baac65fc..3b66dfd40d9f0 100644 --- a/translation/dest/site/fi-FI.xml +++ b/translation/dest/site/fi-FI.xml @@ -5,6 +5,7 @@ Lähetä tämä linkki kaverillesi, jonka haluat kutsua pelaamaan Peli ohi Odotetaan vastustajaa + Tai anna vastustajasi skannata tämä QR-koodi Odotetaan Sinun vuorosi %1$s taso %2$s @@ -272,7 +273,6 @@ Pelaamassa juuri nyt Parhaillaan menossa Päättynyt - päättyy %s Keskeytä peli Peli keskeytetty Tavallinen @@ -405,6 +405,7 @@ Tuo peli Liitä pelin PGN, niin voit selata peliä ja saat sille tietokoneanalyysin, keskusteluhuoneen sekä URL:n, jonka voit jakaa. Muunnelmat poistetaan. Jos haluat säilyttää ne, tuo PGN ensin tutkielmaan. + Tähän PGN:ään on julkinen pääsy. Käytä tutkielmaa halutessasi tuoda pelin ja pitää sen yksityisenä. %s Tuotua peliä %s Tuotua peliä @@ -461,7 +462,7 @@ Lisäasetukset Valitse asiallinen nimi turnaukselle. Hiemankin epäasiallinen nimi voi johtaa käyttäjätunnuksesi sulkemiseen. - Jos jätät tyhjäksi, turnaus nimetään jonkun suurmestarin mukaan. + Jos jätät tyhjäksi, turnaus nimetään jonkun suurmestarin mukaan. Emme suosittele näihin koskemista. Jos asetat liittymisehtoja, turnauksessasi tulee olemaan vähemmän pelaajia. Näytä lisäasetukset @@ -506,7 +507,8 @@ Muokkaa profiilia Etunimi Sukunimi - Valitse tyylisi: + Valitse tyylisi + Tyyli On olemassa asetus, jolla voit piilottaa kaikkien käyttäjien tyylit koko sivustolla. Kuvaus Maa tai alue @@ -528,7 +530,7 @@ Avaa heti seuraavan pelisi tehtyäsi siirron Automaattinen siirtyminen Tehtävät - Turnausvoittajat + Turnausvoittajat Nimi Kuvaus Yksityinen kuvaus @@ -745,6 +747,7 @@ Kavereille Kaikille Lapsitila + Lapsitila on käytössä. Turvallisuusasia. Lapsitilassa kaikki kommunikointi sivustolla on pois käytöstä. Käytä tätä suojaamaan lasta tai koululaista muilta internetin käyttäjiltä. Lapsitilassa Lichessin logoon liitetään %s-kuvake, josta tiedät lastesi olevan turvassa. Käyttäjätunnuksesi on hallinnassa. Lapsitilan poistoa voit pyytää shakkiopettajaltasi. @@ -858,9 +861,10 @@ ja tallenna %s esisiirtolinja ja tallenna %s esisiirtolinjaa + Olet saanut henkilökohtaisen viestin Lichessiltä. + Lue se napsauttamalla tästä Pahoittelumme :( Meidän täytyi komentaa sinut jäähylle hetkeksi. - Jäähy päättyy %s. Miksi? Haluamme tarjota mukavan shakkielämyksen kaikille. Siksi meidän on huolehdittava siitä, että kaikki käyttäytyvät asiallisesti. @@ -880,6 +884,7 @@ Vakuutan että noudatan Lichessin sääntöjä. Hae tai aloita uusi keskustelu Muokkaa + Bullet Pikapeli Nopea Klassinen @@ -918,11 +923,8 @@ Olet hävinnyt Lichessin käyttöehtoja rikkoneelle henkilölle Hyvitys: %1$s %2$s vahvuuslukupistettä. Aika on melkein lopussa! - [Paljasta sähköpostiosoite napsauttamalla tätä] + [Paljasta sähköpostiosoite napsauttamalla tästä] Lataa - Tervetuloa! - Lichess on hyväntekeväisyysjärjestö ja täysin ilmainen avoimen lähdekoodin ohjelmisto. -Kaikki toimintakustannukset, kehitystyö ja sisältö rahoitetaan yksinomaan käyttäjien lahjoituksilla. Valmentaja-asetukset Striimausasetukset Peruuta turnaus @@ -983,4 +985,6 @@ Jätä kenttä tyhjäksi, jos haluat pelien alkavan normaalista alkuasemasta.Meidän vinkkimme tapahtumien järjestämiseen Ohjeet Näytä kaikki + Lichess on hyväntekeväisyysjärjestö ja täysin ilmainen avoimen lähdekoodin ohjelmisto. +Kaikki toimintakustannukset, kehitystyö ja sisältö rahoitetaan yksinomaan käyttäjien lahjoituksilla. diff --git a/translation/dest/site/fo-FO.xml b/translation/dest/site/fo-FO.xml index b7f163d39aa3d..f0b1e0913c315 100644 --- a/translation/dest/site/fo-FO.xml +++ b/translation/dest/site/fo-FO.xml @@ -231,7 +231,6 @@ Verður telvað beint nú Telvað beint nú Liðugt - endar %s Enda talvið Talvið varð brotið av Vanligt @@ -769,7 +768,6 @@ Orsaka :( Vit noyddust at geva tær leikbrá eina tíð. - Leikbráið gongur út %s. Hví? Vit miða ímóti at veita øllum eina góða talvuppliving. Tískil mugu vit vissa okkum, at allir telvarar sýna góðan atburð. @@ -819,7 +817,6 @@ %1$s móti %2$s Tíðin er skjótt úti! Tak niður - Vælkomin! Einki kjatt Bert liðleiðarar Bert limir av liðnum diff --git a/translation/dest/site/fr-FR.xml b/translation/dest/site/fr-FR.xml index 28fd8026149d2..b1b56105932b1 100644 --- a/translation/dest/site/fr-FR.xml +++ b/translation/dest/site/fr-FR.xml @@ -5,6 +5,7 @@ Pour inviter quelqu\'un à jouer, donnez-lui ce lien Partie terminée En attente de votre adversaire + Ou laissez votre adversaire scanner ce code QR En attente À votre tour %1$s niveau %2$s @@ -272,7 +273,6 @@ En cours Parties en cours Terminé - se termine dans %s Annuler la partie Partie annulée Standard @@ -405,6 +405,7 @@ Importer une partie Quand vous collez une partie en PGN vous pouvez la rejouer, consulter l\'analyse de l\'ordinateur, utiliser le tchat et partager le lien. Les variantes seront effacées. Pour les conserver, importez le PGN dans une étude. + Cette partie en format PGN n\'est pas privée. Pour importer une partie en privé, utilisez une étude. %s partie importée %s parties importées @@ -506,7 +507,8 @@ Modifier le profil Prénom Nom - Choisissez votre émoji : + Choisir votre émoji + Émoji Un paramètre permet de cacher les émojis d\'un utilisateur sur tout le site. Biographie Pays ou région @@ -689,6 +691,7 @@ Raccourcis clavier avancer/reculer aller au début/à la fin + Changer de variante montrer/cacher les annotations entrer dans/sortir d\'une variante Demandez une analyse informatique, apprenez de vos erreurs @@ -745,6 +748,7 @@ Avec mes amis Avec tout le monde Mode enfants + Le mode enfant est activé. Cela concerne la sécurité. Dans le mode enfants, toutes les communications du site sont désactivées. Activez ce mode pour vos enfants et pour les écoliers, afin de les protéger des autres utilisateurs. Dans le mode enfants, l\'icône %s se rajoute au logo lichess pour que vous sachiez que les enfants sont en sécurité. Votre compte est géré. Demandez à votre professeur d\'échecs de désactiver le mode enfant. @@ -858,9 +862,10 @@ et enregistrer %s variante de précoups et enregistrer %s variantes de précoups + Vous avez reçu un message privé de Lichess. + Cliquez ici pour le lire. Désolé :( Nous avons dû temporairement vous suspendre. - Le délai d\'attente expire dans %s. Pourquoi ? Nous souhaitons procurer à chacun une agréable expérience du jeu d\'échecs. Dans ce but, nous devons veiller à ce que tous les joueurs adoptent les bonnes pratiques. @@ -880,6 +885,7 @@ Je m\'engage à respecter toutes les règles de Lichess. Rechercher ou démarrer une nouvelle conversation Éditer + Bullet Blitz Rapide Classique @@ -920,9 +926,6 @@ Vous n\'avez presque plus de temps! [Cliquer pour révéler l\'adresse courriel] Télécharger - Bienvenue ! - Lichess est une association à but non lucratif et un logiciel open source entièrement libre. -Tous les coûts d\'exploitation, le développement et le contenu sont financés uniquement par les dons des utilisateurs. Configuration des paramètres Coach Configuration des paramètres Streamer Annuler le tournoi @@ -983,4 +986,6 @@ Laissez vide pour commencer les parties à partir de la position initiale normal Nos conseils pour l\'organisation d\'événements Instructions Tout afficher + Lichess est une association à but non lucratif et un logiciel open source entièrement libre. +Tous les coûts d\'exploitation, le développement et le contenu sont financés uniquement par les dons des utilisateurs. diff --git a/translation/dest/site/fy-NL.xml b/translation/dest/site/fy-NL.xml index f536138464c94..a808837195d1b 100644 --- a/translation/dest/site/fy-NL.xml +++ b/translation/dest/site/fy-NL.xml @@ -193,7 +193,6 @@ No oan it spylje No dwaande Dien - dien %s Ferlit it spul Spul ferlitte Standert diff --git a/translation/dest/site/ga-IE.xml b/translation/dest/site/ga-IE.xml index abfce05d93e87..9ed7c99e9f037 100644 --- a/translation/dest/site/ga-IE.xml +++ b/translation/dest/site/ga-IE.xml @@ -306,7 +306,6 @@ Á imirt anois Á imirt anois Críochnaithe - ag críochnú i %s Éirigh as Éiríodh as an chluiche Caighdeán @@ -949,7 +948,6 @@ anailís ríomhaire, comhrá cluiche agus URL inroinnte. Tá brón orainn :( Bhí orainn tú a chur ar fionraí ar feadh tréimhse. - Beidh an tréimhse fionraithe thart %s. Cén fáth? Ba mhaith linn atmaisféar maith fichille a chur ar fáil do chách. De bhrí sin, ní mór dúinn a chinntiú go leanann gach ficheallaí dea-chleachtas. @@ -1008,9 +1006,6 @@ anailís ríomhaire, comhrá cluiche agus URL inroinnte. Tá an t-am beagnach suas! [Cliceáil chun seoladh ríomhphoist a nochtadh] Íoslódáil - Fáilte! - Is carthanas é Lichess, agus agus bogearraí foinse oscailte go hiomlán saor in aisce. -Maoinítear na costais oibriúcháin, na forbartha agus an t-ábhar go léir trí thabhartais úsáideora amháin. Socruithe cóitseál a bhainistiú Socruithe sruthanna a bhainistiú Cuir an comórtas ar ceal @@ -1069,4 +1064,6 @@ Fág folamh chun cluichí a thosú ón ngnáthshuíomh tosaigh. Athraigh taobhanna Má dhúnann tú do chuntas tarraingeofar siar d’achomharc Ár leideanna chun imeachtaí a eagrú + Is carthanas é Lichess, agus agus bogearraí foinse oscailte go hiomlán saor in aisce. +Maoinítear na costais oibriúcháin, na forbartha agus an t-ábhar go léir trí thabhartais úsáideora amháin. diff --git a/translation/dest/site/gl-ES.xml b/translation/dest/site/gl-ES.xml index 19d21e3361ce9..4c7d21d9cde7e 100644 --- a/translation/dest/site/gl-ES.xml +++ b/translation/dest/site/gl-ES.xml @@ -5,6 +5,7 @@ Para invitar a alguén a xogar, dálle este URL Partida rematada Agardando un rival + Ou deixa que o teu rival escanee este código QR Agardando A túa quenda %1$s nivel %2$s @@ -272,7 +273,6 @@ Xogando agora mesmo Xogando agora mesmo Finalizado - remata %s Abortar partida Partida abortada Estándar @@ -393,7 +393,7 @@ Restablecer Aplicar Gardar - Lista de líderes + Listaxe de líderes Fai unha captura de pantalla da posición actual Gardar a partida en formato GIF Pega o texto FEN aquí @@ -403,8 +403,9 @@ Continuar dende aquí Estudar Importar partida - Pega o PGN dunha partida para obter unha versión navegable, análise por ordenador, sala de conversa do xogo e unha ligazón para compartila. + Pega o PGN dunha partida para obter unha versión navegable, análise por ordenador, sala de conversa e unha ligazón pública para compartila. As variantes borraranse. Pra conservalas, importa o PGN mediante un estudo. + Este PGN é de acceso público. Para importar unha partida de xeito privado, emprega un estudo. %s partida importada %s partidas importadas @@ -461,7 +462,7 @@ Axustes avanzados Escolle un nome seguro para o torneo. Calquera comportamento minimamente inadecuado podería levar ao peche da túa conta. - Deixar en branco para poñerlle ó torneo o nome dun Grande Mestre notable. + Deixar en branco para poñerlle ó torneo o nome dun Grande Mestre notable. Non recomendamos cambiar estes axustes. Se estableces condicións de entrada, o teu torneo terá menos xogadores. Amosar axustes avanzados @@ -506,7 +507,8 @@ Editar perfil Nome Apelido(s) - Escolle a túa habelencia: + Escolle a túa habelencia + Habelencia Nas preferencias podes agochar por completo as habelencias dos xogadores en todo o sitio. Biografía País ou rexión @@ -528,7 +530,7 @@ Pasar automaticamente á seguinte partida despois de mover Auto-cambio Problemas - Gañadores do torneo + Gañadores do torneo Nome Descrición Descrición privada @@ -689,6 +691,7 @@ Atallos do teclado mover atrás/adiante Ir ó comezo/remate + Cambia a variante seleccionada mostrar/ocultar comentarios Entrar/saír da variante Solicita unha análise por computador, Aprende dos teus erros @@ -696,7 +699,10 @@ Seguinte metida de zoca Seguinte erro Seguinte imprecisión + Rama anterior + Rama seguinte Activar/desactivar as frechas das variantes + Variante anterior/seguinte Activar/desactivar as anotacións con símbolos As frechas das variantes permítenche navegar sen usar a lista de movementos. facer a xogada seleccionada @@ -742,6 +748,7 @@ Cos teus amigos Con todo o mundo Modo infantil + O modo infantil está activado. Por seguridade, no modo infantil desactívanse tódalas comunicacións. Activa isto para protexer aos teus nenos ou alumnos de outros usuarios de Internet. En modo infantil, o logo de Lichess ten unha icona de %s, indicando que os nenos están seguros. A túa conta é xestionada. Pídelle ó teu mestre que desactive o modo infantil. @@ -769,7 +776,7 @@ Análise da partida %1$s crea %2$s %1$s únese a %2$s - a %1$s gústalle %2$s + A %1$s gústalle %2$s Emparellamento rápido Retos Anónimo @@ -855,9 +862,10 @@ e garda %s variante de premovementos e garda %s variantes de premovementos + Recibiches unha mensaxe privada de Lichess. + Fai clic aquí para lela Sentímolo :( Tivemos que suspenderte temporalmente. - A suspensión expira %s. Por que? O noso obxectivo é proporcionar unha experiencia amena no xadrez pra todo o mundo. Para iso, debemos asegurarnos de que todos os xogadores se comportan como é debido. @@ -877,6 +885,7 @@ Comprométome a seguir as normas de Lichess. Busca ou comeza unha nova conversa Editar + Bala Lóstrego Rápidas Clásicas @@ -917,9 +926,6 @@ Quédache pouco tempo! [Pincha para ver o correo electrónico] Descarga - Benvid@! - Lichess é unha organización benéfica e un programa totalmente libre e de código aberto. -Todos os custos de funcionamento, desenvolvemento e contidos fináncianse unicamente mediante as doazóns dos usuarios. Administrador de adestradores Administrador de presentadores Cancelar o torneo @@ -980,4 +986,6 @@ Deixa en branco para comezar as partidas dende a posición inicial normal.Os nosos consellos para organizar eventos Instrucións Amósamo todo + Lichess é unha organización benéfica e un programa totalmente libre e de código aberto. +Todos os custos de funcionamento, desenvolvemento e contidos fináncianse unicamente mediante as doazóns dos usuarios. diff --git a/translation/dest/site/gsw-CH.xml b/translation/dest/site/gsw-CH.xml index b85c846e36252..e7838e92cd64e 100644 --- a/translation/dest/site/gsw-CH.xml +++ b/translation/dest/site/gsw-CH.xml @@ -3,8 +3,9 @@ Schpill mit eme Fründ Schpill mit em Computer Wottsch öpper zum Schpille ilade - schick die URL - Schpiil verbii + Schpiel verbi Uf de Gägner warte + Oder lass din Gägner de QR-Code skänne Am Warte Du bisch dra %1$s Schtufe %2$s @@ -273,7 +274,6 @@ Au vum Erschtelle vu mehrere Konte wird dringend abgrate - übermässigs Multiko Partie isch am laufe Lauft jetzt Beändet - ändet %s Partie abbräche Partie abbroche Standard @@ -406,6 +406,7 @@ Au vum Erschtelle vu mehrere Konte wird dringend abgrate - übermässigs Multiko Partie importiere Füeg e Schpiel-PGN i, für Zuegriff uf Schpielwiderholig, Computeranalyse, Chat und e teilbari URL. d\'Variazione werded glöscht. Zums b\'halte, muesch d\'PGN mit ere Schtudie importiere. + De PGN isch öffentlich zuegänglich. Zum es Schpiel privat importiere, nimmsch e Schtudie. %s importierti Partie %s importierti Partie @@ -463,7 +464,7 @@ zum bewise dass du en Mänsch bisch. Erwiterti Ischtellige Wähl en möglichscht sichere Name fürs Turnier. Alles - au liecht Unagmässes - cha zur Schlüssig vu dim Konto fühere. - Frei la zum s\'Turnier nach eme namhafte Schachschpiller z\'benänne. + Frei la zum s\'Turnier nach eme namhafte Schachschpiller z\'benänne. Mir empfehled, das nöd z\'ändere. Wänn du Teilnahmebedingige verlangsch, wird dis Turnier weniger Schpiller ha. Zeig die erwiterete Ischtellige @@ -508,7 +509,8 @@ zum bewise dass du en Mänsch bisch. Profil bearbeite Vorname Nachname - Wähl dis Emoji: + Wähl dis Emoji + Emoji Alli Benutzer-Emojis chönnd - uf de ganze Site - usbländet werde. Biografie Land, Region oder Kanton @@ -530,7 +532,7 @@ zum bewise dass du en Mänsch bisch. Nach em Zug automatisch zur nächschte Partie Automatische Wächsel Ufgabe - Turnier Sieger + Turnier Sieger Name Beschribig Privati Beschribig @@ -755,6 +757,7 @@ gits nöd und es isch immer ungwertet. Mit Fründe Mit allne Chinder-Modus + De Chindermodus isch aktiviert. Sicherheitsiischtellig: Im Chinder-Modus isch die ganzi Kommunikation deaktiviert. Aktivier de Modus und schütz dini Chind vor andere Internetbenutzer. Im Chinder-Modus erschint s\'Lichess-Logo mit %s-Icon, was dir zeigt, dass dini Chind gschützt sind. Dis Konto wird verwaltet, frög din Schachlehrer für d\'Ufhebig vum Chindermodus. @@ -868,9 +871,10 @@ gits nöd und es isch immer ungwertet. und speicher %s Voruszug Zile und speicher %s Voruszug Zile + Lichess hät dir e privati Nachricht g\'schickt. + Klick da zum läse Äxgüsi :( Mir händ dich für es Zitli müesse schperre. - Die Schperrig ändet in %s. Wieso? Mir wänd allne e möglichscht gueti Schach-Erfahrig büte. Zum dem Zwäck müend mir sicher si, dass sich all Schpiller korräkt verhalted. @@ -890,6 +894,7 @@ gits nöd und es isch immer ungwertet. Ich schtimme zue, dass ich alli Lichess-Richtlinie befolge. Suech e Underhaltig oder fang e Neui a Bearbeite + Bullet Blitz Schnällschach Klassisch @@ -930,9 +935,6 @@ gits nöd und es isch immer ungwertet. D\'Zit isch fascht abgloffe! [Klick, zum die E-Mail-Adrässe azeige] Abelade - Willkomme! - Lichess isch e Wohltätigkeitsorganisation und e völlig choschtelosi/freii Open-Source-Software. -Alli Betriebs-Choschte, d\'Entwicklig und d\'Inhält werded usschliesslich dur Benutzerschpände finanziert. Trainer Verwalter Streamer Verwalter Brich das Turnier ab @@ -994,4 +996,6 @@ Fäld leer lah und all Schpiel normal schtarte. Eusi Tipps, fürs Organisiere vu Events Awisige Zeig mer alles + Lichess isch e Wohltätigkeitsorganisation und e völlig choschtelosi/freii Open-Source-Software. +Alli Betriebs-Choschte, d\'Entwicklig und d\'Inhält werded usschliesslich dur Benutzerschpände finanziert. diff --git a/translation/dest/site/gu-IN.xml b/translation/dest/site/gu-IN.xml index 5775ea0e763b2..99f176efb76e4 100644 --- a/translation/dest/site/gu-IN.xml +++ b/translation/dest/site/gu-IN.xml @@ -123,6 +123,7 @@ મેમરી અનંત વિશ્લેષણ ગહનતાનિ મર્યાદા હટાવે, અને તમારા કમ્પ્યુટરને ગરમ રાખે છે + એન્જિન મેનેજર મોટી ભૂલ ભૂલ ચૂક @@ -150,6 +151,7 @@ પૂર્ણ કદ જુઓ સાઇન આઉટ કરો સાઇન ઇન કરો + મને લોગીન કરેલો રાખો તમારે તે કરવા માટે એક ખાતાનિ જરૂર છે નોંધણી કોમ્પ્યૂટરો અને કોમ્પ્યુટર ની મદદ લેનારા ખેલાડીઓને રમવા માટે મંજૂરી નથી. રમતી વખતે મહેરબાની કરીને શતરંજના મશીનો, ડેટાબેઝો, અથવા બીજા ખેલાડીઓ દ્વારા મદદ ના લેવી. એ પણ નોંધ લેશો કે બહુવિધ ખાતા બનાવવાનું સખત નિરુત્સાહ છે અને અતિશય બહુવિધ ખાતા બનાવવા પર પ્રતિબંધ મૂકવામાં આવશે. @@ -178,6 +180,10 @@ %s કલાક %s કલાક + + %s મિનિટ + %s મિનિટ + સમય રેટિંગ રેટિંગ નો આલેખ @@ -194,6 +200,15 @@ ઇમેઇલ પાસવર્ડ રીસેટ પાસવર્ડ ભૂલી ગયાં? + આ પાસવર્ડ અત્યંત સામાન્ય છે અને અનુમાન લગાવવા માટે ખૂબ જ સરળ છે. + કૃપા કરીને તમારા વપરાશકર્તા નામનો ઉપયોગ તમારા પાસવર્ડ તરીકે કરશો નહીં. + તમે બીજી સાઇટ પર સમાન પાસવર્ડનો ઉપયોગ કર્યો છે અને તે સાઇટ સાથે ચેડા કરવામાં આવ્યા છે. તમારા લિચેસ એકાઉન્ટની સલામતી સુનિશ્ચિત કરવા માટે, અમારે તમારે નવો પાસવર્ડ સેટ કરવાની જરૂર છે. તમારી સમજ બદલ આભાર. + તમે લિચેસ છોડી રહ્યા છો + અન્ય સાઇટ પર તમારો લિચેસ પાસવર્ડ ક્યારેય ટાઇપ કરશો નહીં! + %s પર આગળ વધો + કોઈ બીજા દ્વારા સૂચવવામાં આવેલ પાસવર્ડ સેટ કરશો નહીં. તેઓ તેનો ઉપયોગ તમારું એકાઉન્ટ ચોરી કરવા માટે કરશે. + કોઈ બીજા દ્વારા સૂચવવામાં આવેલ ઈમેલ સરનામું સેટ કરશો નહીં. તેઓ તેનો ઉપયોગ તમારું એકાઉન્ટ ચોરી કરવા માટે કરશે. + ઈમેલની ખાતરીમાં મદદ કરશો શ્રેણી શ્રેણી %s રમતો રમ્યા @@ -210,7 +225,6 @@ અત્યારે રમાય છે અત્યારે રમાય છે સમાપ્ત થઈ - %s માં પૂરી થાય છે રમતનો ત્યાગ કરો રમતનો ત્યાગ કર્યો સામાન્ય diff --git a/translation/dest/site/he-IL.xml b/translation/dest/site/he-IL.xml index a36ee955b33c3..e2b255f1c80ac 100644 --- a/translation/dest/site/he-IL.xml +++ b/translation/dest/site/he-IL.xml @@ -5,6 +5,7 @@ כדי להזמין מישהו לשחק, שתפו את הכתובת הזאת המשחק הסתיים ממתין ליריב + אפשר גם לתת ליריבך לסרוק את קוד ה־QR הזה ממתין תורך %1$s רמה %2$s @@ -302,7 +303,6 @@ מתקיים עכשיו מתקיים עכשיו הסתיים - נגמר %s ביטול המשחק המשחק בוטל רגיל @@ -461,6 +461,7 @@ ייבוא משחק כשמדביקים משחק בפורמט PGN מקבלים אפשרות לצפות במשחק ולדפדף בו, ניתוח ממוחשב, צ׳אט וקישור לשיתוף. וריאציות — כלומר רצפי מהלכים שאינם המסעים הראשיים (mainline) — יימחקו. כדי לשמור אותן, ייבאו את ה־PGN כלוח למידה. + ה־PGN הזה זמין לציבור. כדי לייצא את המשחק באופן פרטי, השתמשו בלוח למידה. משחק מיובא %s %s משחקים מיובאים @@ -551,6 +552,7 @@ מהלכים ששוחקו ניצחונות כלבן ניצחונות כשחור + שיעור תוצאות התיקו תוצאות תיקו טורניר ה%s הבא: יריב ממוצע @@ -571,6 +573,9 @@ עריכת פרופיל שם פרטי שם משפחה + הגדירו את הסמליל שלכם + סמליל + ישנה הגדרה שמאפשרת להסתיר את כל הסמלילים באתר. ביוגרפיה מדינה או אזור תודות מקרב לב @@ -760,6 +765,7 @@ קיצורי מקלדת גלול אחורה/קדימה מעבר להתחלה/לסיום + מחזור הוריאציה שנבחרה הצג/הסתר הערות כנס לגרסה או צא ממנה בקשו ניתוח ממוחשב, למדו מטעויותיכם @@ -767,6 +773,13 @@ הטעות הגסה הבאה הטעות הבאה אי־הדיוק הבא + הענף הקודם + הענף הבא + הפעלת חצי הווריאנטים + מחזור הוריאנט הקודם/הבא + הפעלת סמלי ההערות + חצי הווריאנטים מאפשרים ניווט קל ללא צורך ברשימת המסעים. + שחקו את המהלך שנבחר טורניר חדש טורנירי שחמט הכוללים משחקים עם מגבלות זמן וסוגי שחמט מגוונים שחק בטורנירי שחמט מהירים! הצטרף לטורניר רשמי ומתוכנן, או צור אחד משלך. Bullet, Blitz, Threecheck, Chess960, King of the Hill, Classical, ואפשרויות נוספות של משחקי שחמט מהנים. @@ -811,6 +824,7 @@ עם חברים עם כולם מצב ילדים + מצב ילדים מופעל. בשביל הבטיחות. במצב ילדים, כל אמצעי התקשורת באתר מבוטלים. הפעילו אופציה זו עבור ילדיכם ועבור תלמידי בית ספר. זאת כדי להגן עליהם מפני משתמשים אחרים. במצב ילדים הסמל של ליצ\'ס מקבל אייקון %s, כדי שתדעו שילדיכם מוגנים. החשבון שלך מנוהל. תוכל/י לבקש מהמורה שלך לשחמט להסיר את מצב הילדים. @@ -932,9 +946,10 @@ ושמרו %s המשכים מוגדרים מראש ושמרו %s המשכים מוגדרים מראש + קיבלתם הודעה פרטית מ-Lichess. + לחצו כאן כדי לקרוא אותה מצטערים :( נאלצנו להשעות אותך לזמן מה. - ההשעיה תסתיים %s. למה? אנחנו מנסים לספק חווית שח נעימה לכולם. בעקבות זאת, אנחנו חייבים לוודא שכל השחקנים ינהגו בכבוד. @@ -954,6 +969,7 @@ אני מסכימ/ה לציית לכל מדיניות של Lichess. חפשו את התחילו שיחה חדשה עריכה + Bullet Blitz Rapid Classical @@ -994,9 +1010,6 @@ הזמן הולך ואוזל! לחץ/י כדי לחשוף את כתובת הדוא\"ל הורדה - ברוכים הבאים! - ליצ\'ס הוא ארגון לטובת הכלל ותוכנת קוד פתוח חינמית. -כל עלויות התפעול, הפיתוח והתוכן ממומנות אך ורק על ידי תרומות משתמשים. הגדרות עבור מאמנים אזור ניהול משדר ביטול הטורניר @@ -1056,4 +1069,8 @@ הפוך צד סגירת החשבון תבטל את פנייתך הטיפים שלנו לארגון אירועים + הוראות + הראו לי הכל + ליצ\'ס הוא ארגון לטובת הכלל ותוכנת קוד פתוח חינמית. +כל עלויות התפעול, הפיתוח והתוכן ממומנות אך ורק על ידי תרומות משתמשים. diff --git a/translation/dest/site/hi-IN.xml b/translation/dest/site/hi-IN.xml index 65a71bd4bdc03..7a9a01d96a3a7 100644 --- a/translation/dest/site/hi-IN.xml +++ b/translation/dest/site/hi-IN.xml @@ -49,20 +49,20 @@ काले ने हार मान ली सफेद खेल छोड़ कर चला गया काला खेल छोड़ कर चला गया - सफेद ने चाल नही चली - काले ने चाल नही चली + सफेद ने चाल नहीं चली + काले ने चाल नहीं चली कंप्यूटर विश्लेषण का अनुरोध करें कंप्यूटर विश्लेषण कंप्यूटर विश्लेषण उपलब्ध है - कंप्युटर विश्लेषण निष्क्रिय है - विश्लेषण मंडल + कंप्यूटर विश्लेषण निष्क्रिय है + विश्लेषण पट मध्यमार्ग %s सर्वर विश्लेषण का उपयोग किया जा रहा है इंजन लोड हो रहा है ... चालों की गणना की जा रही है... इंजन लोड करने में समस्या क्लाउड विश्लेषण - भीतर जाओ + गहराई में जाओ खतरे को दिखाएं स्थानीय ब्राउज़र में स्थानीय मूल्यांकन को टॉगल करें @@ -88,7 +88,7 @@ शीर्ष खेल %2$s से %3$s के दशक के %1$s+ FIDE- रेटेड खिलाड़ियों के दो मिलियन ओटीबी खेल - %s हाफ मूव में मैच + %s हाफ मूव में मेट %s आधे-कदम में चेकमैट DTZ50\'\' गोलाई के साथ, अगले कब्ज़ा या प्यादा की चाल तक आधे-चालों की संख्या के आधार पर @@ -264,7 +264,6 @@ अभी खेला जा रहा अभी खेला जा रहा समाप्त - %s में खत्म खेल रद्द करें खेल रद्द किया गया साधारण @@ -831,7 +830,6 @@ खेद :( हमें आपको कुछ समय के लिए प्रतिबंधित करना पड़ा। - आपका प्रतिबंध %s समाप्त हो जाएगा। क्यों? हम सभी के लिए एक सुखद शतरंज अनुभव प्रदान करना चाहते हैं। इसलिए, हमें यह सुनिश्चित करना होगा कि सभी खिलाड़ी अच्छे अभ्यास का पालन करें। @@ -889,9 +887,6 @@ समय लगभग समाप्त है! [ईमेल देखने के लिए क्लिक करें] डाउनलोड करें - आपका स्वागत है! - Lichess एक चैरिटी और पूरी तरह से फ्री/लिबर ओपन सोर्स सॉफ्टवेयर है। -सभी परिचालन लागत, विकास और सामग्री पूरी तरह से उपयोगकर्ता दान द्वारा वित्त पोषित हैं। कोच मनेजर स्ट्रीमर मैनेजर टूर्नामेंट रद्द करें @@ -949,4 +944,6 @@ पार्श्व बदलना आपका खाता बंद करने से आपकी अपील वापस ले ली जाएगी कार्यक्रम आयोजित करने कि सलाह + Lichess एक चैरिटी और पूरी तरह से फ्री/लिबर ओपन सोर्स सॉफ्टवेयर है। +सभी परिचालन लागत, विकास और सामग्री पूरी तरह से उपयोगकर्ता दान द्वारा वित्त पोषित हैं। diff --git a/translation/dest/site/hr-HR.xml b/translation/dest/site/hr-HR.xml index fbe105061a5fa..a38e3c960d881 100644 --- a/translation/dest/site/hr-HR.xml +++ b/translation/dest/site/hr-HR.xml @@ -278,7 +278,6 @@ Upravo igraju Upravo igraju Završeno - završava %s Prekini igru Igra prekinuta Standardno @@ -868,7 +867,6 @@ računalnu analizu, chat partije i URL za dijeljenje. Oprosti :( Trebali smo te na neko vrijeme izbaciti. - Vrijeme izbačaja ističe za %s. Zašto? Nama je u cilju da pružimo ugodno šahovsko iskustvo. Stoga moramo osigurati da svi igrači dobro postupaju. @@ -928,9 +926,6 @@ računalnu analizu, chat partije i URL za dijeljenje. Vrijeme uskoro ističe! [Klikni za prikaz e-mail adrese] Preuzmi - Dobrodošli! - Lichess je dobrotvorni i potpuno besplatan softver otvorenog koda. -Svi operativni troškovi, razvoj i sadržaj financiraju se isključivo donacijama korisnika. Postavke za trenera Postavke za strimera Otkaži turnir @@ -989,4 +984,6 @@ Ostavite prazno za početak igre s normalne početne pozicije. Promijeni strane Zatvaranje računa će povući vašu žalbu Naši savjeti za organizaciju događaja + Lichess je dobrotvorni i potpuno besplatan softver otvorenog koda. +Svi operativni troškovi, razvoj i sadržaj financiraju se isključivo donacijama korisnika. diff --git a/translation/dest/site/hu-HU.xml b/translation/dest/site/hu-HU.xml index add8e9a6b0802..6b5649c9a4e5a 100644 --- a/translation/dest/site/hu-HU.xml +++ b/translation/dest/site/hu-HU.xml @@ -271,7 +271,6 @@ Játszma folyamatban Éppen zajlik Befejezett - véget ér: %s Játszma elvetése Játszma elvetve Hagyományos @@ -846,7 +845,6 @@ Sajnáljuk Kénytelenek vagyunk egy kis időre visszatartani. - Feloldás %s múlva. Miért? Célunk mindenki számára jó felhasználói élményt biztosítani. Ennek érdekében minden játékosnak követnie kell megfelelő magatartást. @@ -906,9 +904,6 @@ Hamarosan lejár az idő! [Kattints az email cím megtekintéséhez] Letöltés - Üdvözlünk! - A Lichess egy jótékonysági szervezet és teljesen ingyenes/szabad nyílt forrású szoftver. -Minden működési költséget, fejlesztést és tartalmat felhasználói adományokból fedezünk. Edzői vezérlőpult Közvetítői vezérlőpult Versenykiírás törlése @@ -967,4 +962,6 @@ Hagyd üresen és a játszmák a kezdőállásból indulnak. Oldal megfordítása A fiókod lezárása visszavonja a fellebbezésed Tippjeink események szervezéséhez + A Lichess egy jótékonysági szervezet és teljesen ingyenes/szabad nyílt forrású szoftver. +Minden működési költséget, fejlesztést és tartalmat felhasználói adományokból fedezünk. diff --git a/translation/dest/site/hy-AM.xml b/translation/dest/site/hy-AM.xml index d904cec97e31a..4161b205fa811 100644 --- a/translation/dest/site/hy-AM.xml +++ b/translation/dest/site/hy-AM.xml @@ -271,7 +271,6 @@ Այս պահին խաղում են Խաղում են այս պահին Ավարտվել է - ավարտվում է %s Կասեցնել խաղը Խաղը կասեցված է Ստանդարտ @@ -845,7 +844,6 @@ Ներողություն :( Մենք ստիպված ենք անջատել Ձեզ որոշ ժամանակով։ - Դուք կարող եք վերադառնալ %s անց։ Ինչու՞ Մեր նպատակը շախմատը բոլորի համար հետաքրքիր դարձնելն է։ Դրան հասնելու համար մենք պետք է այնպես անենք, որ բոլոր խաղացողները հետևեն բարեկրթության կանոններին։ @@ -905,9 +903,6 @@ Ժամանակը գրեթե սպառվել է [Սեղմեք՝ էլեկտրոնային փոստի հասցեն բացելու համար] Ներբեռնել - Բարի՜ գալուստ։ - Lichess-ը բարեգործական կազմակերպություն է, որը տրամադրում է բաց նախնական կոդով ազատ և անվճար ծրագրային ապահովում։ -Օպերացիոն բոլոր ծախսերը, մշակումները և կոնտենտը ֆինանսավորվում են բացառապես օգտատերերի նվիրաբերությունների հաշվին։ Մարզիչների համար Սթրիմերների համար Չեղարկել մրցաշարը @@ -965,4 +960,6 @@ Փոխել կողմը Ձեր մասնակցային հաշվի փակումը կչեղարկի Ձեր դիմումը Մեր խորհուրդները միջոցառումներ կազմակերպելու հարցում + Lichess-ը բարեգործական կազմակերպություն է, որը տրամադրում է բաց նախնական կոդով ազատ և անվճար ծրագրային ապահովում։ +Օպերացիոն բոլոր ծախսերը, մշակումները և կոնտենտը ֆինանսավորվում են բացառապես օգտատերերի նվիրաբերությունների հաշվին։ diff --git a/translation/dest/site/ia-IA.xml b/translation/dest/site/ia-IA.xml index 3303a90060748..0579318b3d6e0 100644 --- a/translation/dest/site/ia-IA.xml +++ b/translation/dest/site/ia-IA.xml @@ -203,7 +203,6 @@ Jocante ora mesmo Jocante ora mesmo Finite - fini %s Cancellar partita Partita cancellate Standard diff --git a/translation/dest/site/id-ID.xml b/translation/dest/site/id-ID.xml index d2593d1c80e43..05bbfd1f3825a 100644 --- a/translation/dest/site/id-ID.xml +++ b/translation/dest/site/id-ID.xml @@ -250,7 +250,6 @@ Bermain saat ini Mainkan sekarang Selesai - berakhir dalam %s Batalkan permainan Permainan dibatalkan Standar @@ -791,7 +790,6 @@ Maaf :( Kami harus mengatur waktu Anda untuk sementara waktu. - Batas waktu berakhir %s lagi. Mengapa? Kami bertujuan dan menjaga untuk memberikan pengalaman catur yang menyenangkan bagi semua orang. Untuk itu, kami harus memastikan bahwa semua pemain harus mengikuti perlakuan yang baik. @@ -850,9 +848,6 @@ Waktu hampir habis! [Klik untuk memunculkan alamat email] Unduh - Wilujeng sumping! - Lichess adalah sebuah amal dan semuanya merupakan perangkat lunak sumber terbuka yang gratis/bebas. -Semua biaya operasi, pengembangan, dan konten didanai sepenuhnya oleh donasi pengguna. Manajer pelatih Manajer stream Batalkan turnamen @@ -910,4 +905,6 @@ Kosongkan untuk memulai permainan dari posisi awal yang normal. Tukar Sisi Menutup akun anda akan menarik permohonan banding Tips dari kami terkait penyelenggaraan acara + Lichess adalah sebuah amal dan semuanya merupakan perangkat lunak sumber terbuka yang gratis/bebas. +Semua biaya operasi, pengembangan, dan konten didanai sepenuhnya oleh donasi pengguna. diff --git a/translation/dest/site/is-IS.xml b/translation/dest/site/is-IS.xml index a0eab46bafd5d..37200563c228f 100644 --- a/translation/dest/site/is-IS.xml +++ b/translation/dest/site/is-IS.xml @@ -236,7 +236,6 @@ Spilandi núna Spilandi þessa stundina Lokið - lýkur eftir %s Hverfa frá leik Hætt við skák Staðlað @@ -738,7 +737,6 @@ Leika %s Afsakaðu :( Við þurftum að setja þig í stutt leikbann. - Leikbannið rennur út eftir: %s. Afhverju? Hvernig er hægt að komast hjá þessu? Tefldu allar skákir sem þú byrjar. diff --git a/translation/dest/site/it-IT.xml b/translation/dest/site/it-IT.xml index d07b5200107b6..79f434ace96cb 100644 --- a/translation/dest/site/it-IT.xml +++ b/translation/dest/site/it-IT.xml @@ -5,6 +5,7 @@ Per invitare qualcuno a giocare, dagli questo URL Partita terminata In attesa dell\'avversario + O fai scansionare il Codice QR al tuo avversario In attesa Tocca a te %1$s livello %2$s @@ -272,7 +273,6 @@ Partita in corso In corso Terminati - finisce %s Interrompi la partita Partita interrotta Standard @@ -406,6 +406,7 @@ Quando incolli una partita tramite PGN potrai rivederla, analizzarla con il computer, commentarla in chat, e condividerla tramite un indirizzo URL. Le varianti saranno cancellate. Per salvarle, importa il PGN in uno studio. + Questo PGN è accessibile pubblicamente. Per importare una partita privatamente, utilizza uno studio. %s partita importata %s partite importate @@ -486,6 +487,7 @@ analizzarla con il computer, commentarla in chat, e condividerla tramite un indi Mosse giocate Il Bianco vince Il Nero vince + Tasso di pareggio Patte Prossimo torneo %s: Punteggio medio degli avversari @@ -506,6 +508,9 @@ analizzarla con il computer, commentarla in chat, e condividerla tramite un indi Modifica profilo Nome Cognome + Imposta la tua icona + Icona + Esiste un\'impostazione per nascondere le icone di tutti gli utenti, sull\'intero sito. Biografia Nazione o regione Grazie! @@ -744,6 +749,7 @@ analizzarla con il computer, commentarla in chat, e condividerla tramite un indi Con gli amici Con tutti Modalità bambino + La modalità bambini è attiva. Questa modalità riguarda la sicurezza: in modalità bambino tutte le comunicazioni sono disabilitate. Si consiglia di attivare questa modalità per bambini e studenti, in modo da proteggerli dagli altri utenti. In \"modalità bambino\", al logo di lichess viene aggiunto %s, in questo modo sai che il bambino è sicuro. Il tuo account è gestito esternamente. Chiedi al tuo istruttore di disattivare la \"modalità bambino\". @@ -857,9 +863,10 @@ analizzarla con il computer, commentarla in chat, e condividerla tramite un indi e salva %s linea pre-mossa e salva %s linee pre-mossa + Hai ricevuto un messaggio privato da Lichess. + Clicca qui per leggerlo Ci dispiace :( Abbiamo dovuto bloccarti per un po\' di tempo. - Sarai sbloccato %s. Perché? Vogliamo offrire a tutti una esperienza di scacchi piacevole. A tal fine, dobbiamo assicurarci che tutti i giocatori si comportino bene. @@ -879,6 +886,7 @@ analizzarla con il computer, commentarla in chat, e condividerla tramite un indi Dichiaro di acconsentire a tutte le politiche di Lichess. Cerca o inizia una nuova conversazione Modifica + Bullet Blitz Rapid Classical @@ -919,9 +927,6 @@ analizzarla con il computer, commentarla in chat, e condividerla tramite un indi Il tempo è quasi finito! [Clicca per mostrare l\'indirizzo email] Download - Benvenuto! - Lichess è un software open source completamente gratuito e libero -Tutti i costi operativi, lo sviluppo e i contenuti sono finanziati esclusivamente dalle donazioni degli utenti. Gestore allenatore Gestore streamer Annulla il torneo @@ -982,4 +987,6 @@ Lascia vuoto per avviare le partite dalla posizione iniziale normale. I nostri consigli per organizzare eventi Istruzioni Mostra tutto + Lichess è un software open source completamente gratuito e libero +Tutti i costi operativi, lo sviluppo e i contenuti sono finanziati esclusivamente dalle donazioni degli utenti. diff --git a/translation/dest/site/ja-JP.xml b/translation/dest/site/ja-JP.xml index 130b692f337b9..f02b38c35eeb4 100644 --- a/translation/dest/site/ja-JP.xml +++ b/translation/dest/site/ja-JP.xml @@ -5,6 +5,7 @@ 誰かを招待する時はこのURLを送ってください 終局 相手を待っています + または相手にこの QR コードをスキャンさせてください 待機中 あなたの手番です %1$s レベル %2$s @@ -257,7 +258,6 @@ 対局中 対局中 終了したトーナメント - 終了は %s 対局を中止する 対局を中止しました スタンダード @@ -378,6 +378,7 @@ ゲームの PGN を貼りつけると、ブラウザ上でのリプレイ、 コンピュータ解析、ゲームチャット、共有可能 URL が得られます。 変化手順は消えます。残したい場合は研究を経由して PGN をインポートしてください。 + この PGN はすべての人に公開されます。非公開の状態で棋譜をインポートするには「研究」機能でどうぞ。 %s 局をインポート @@ -474,7 +475,8 @@ プロフィールの編集 - フレアを設定: + フレアを設定 + フレア サイト全体でフレアを非表示にする設定があります。 自己紹介 国・地域 @@ -707,6 +709,7 @@ 友達にだけ 誰にでも キッズモード + キッズモードが有効です。 これは安全対策です。「キッズモード」ではサイト上の会話がすべて無効になります。子供や生徒のアカウントでこのモードを有効にしておけば、彼らを他のユーザーから守ることができます。 キッズモードでは Lichess のロゴに %s のアイコンが付き、安全であることを示します。 あなたのアカウントは管理されています。キッズモードの停止は講師に依頼してください。 @@ -816,9 +819,10 @@ %s 種のコンディショナルムーブを設定 + Lichess からプライベートメッセージが来ました。 + ここをクリックして読む 残念です :( しばらく対局を禁止します。 - 禁止は %s 後に解けます。 どうして? Lichess ではすべての人に楽しいチェス体験を提供しています。 そのためには、すべての人にマナーを守っていただく必要があります。 @@ -838,6 +842,7 @@ 私は Lichess のすべてのポリシーに従うことに同意します。 検索または新しいトピックを始める 編集 + ブレット ブリッツ ラピッド クラシカル @@ -878,9 +883,6 @@ 残り時間わずか! [ クリックでメールアドレスを表示 ] ダウンロード - ようこそ! - Lichess は非営利組織であり、完全に無料/自由なオープンソースソフトウェアです。 -運営費、開発、コンテンツを支えているのはすべてユーザーの寄付です。 コーチ用設定 配信者用設定 トーナメントをキャンセル @@ -941,4 +943,6 @@ チェスイベント開催のアドバイス 使用法 すべてを表示 + Lichess は非営利組織であり、完全に無料/自由なオープンソースソフトウェアです。 +運営費、開発、コンテンツを支えているのはすべてユーザーの寄付です。 diff --git a/translation/dest/site/ka-GE.xml b/translation/dest/site/ka-GE.xml index 3c55a96633271..d5dad7b58dfaa 100644 --- a/translation/dest/site/ka-GE.xml +++ b/translation/dest/site/ka-GE.xml @@ -271,7 +271,6 @@ თამაშობს ამ დროს თამაშობს ამ დროს დამთავრებული - რჩება %s თამაშის შეწყვეტა თამაში შეწყვეტილია სტანდარტი @@ -846,7 +845,6 @@ ბოდიში :( გარკვეული დროით უნდა შეგიჩეროთ თამაში. - დროის ამოწურვა მთავრდება %s-ში. რატომ? ჩვენი მიზანია შევქმნათ სასიამოვნო გამოცდილება ჭადრაკის სათამაშოდ ყველასათვის. ამიტომაც, ჩვენ უნდა დავრწმუნდეთ, რომ ყველა მოთამაშე იცავს თამაშის ეტიკეტს. @@ -906,9 +904,6 @@ დრო თითქმის ამოიწურა! [დააჭირეთ ელფოსტის სანახავად] ჩამოტვირთვა - მოგესალმებით! - Lichess -ი არის საქველმოქმედო და სრულიად უფასო/თავისუფალი ღია კოდის მქონე პროგრამა. -ყველა საოპერაციო ხარჯი, დეველოპმენტი და კონტენტი ფინანსდება მხოლოდ მომხმარებლის შემოწირულობებით. მასწავლებლის მენეჯერი სტრიმერის მენეჯერი ტურნირის გაუქმება @@ -967,4 +962,6 @@ მხარეების შეცვლა თქვენი ექაუნთის დახურვა გამოიწვევს თქვენი აპელაციის ამოღებას ჩვენი რჩევები ღონისძიებების ორგანიზებისთვის + Lichess -ი არის საქველმოქმედო და სრულიად უფასო/თავისუფალი ღია კოდის მქონე პროგრამა. +ყველა საოპერაციო ხარჯი, დეველოპმენტი და კონტენტი ფინანსდება მხოლოდ მომხმარებლის შემოწირულობებით. diff --git a/translation/dest/site/kaa-UZ.xml b/translation/dest/site/kaa-UZ.xml index 88af6681af962..6836d86fd41a7 100644 --- a/translation/dest/site/kaa-UZ.xml +++ b/translation/dest/site/kaa-UZ.xml @@ -339,7 +339,6 @@ Tez Jazılıw Júklep alıw - Xosh keldińiz! Sheklewsiz Bas oyınshınıń hár bir oyın ushın taslar reńi Boljawlı baslanıw waqtı diff --git a/translation/dest/site/kk-KZ.xml b/translation/dest/site/kk-KZ.xml index f0039bb867523..c34755e393f4e 100644 --- a/translation/dest/site/kk-KZ.xml +++ b/translation/dest/site/kk-KZ.xml @@ -272,7 +272,6 @@ Қазір ойнап отыр Қазір болып жатыр Аяқталды - %s аяқталады Ойынды тоқтату Ойын тоқтатылды Классикалық @@ -850,7 +849,6 @@ Өкінішті-ақ :( Сізді уақытша шектеуге мәжбүрміз. - Шектеудің аяқталуына %s қалды. Себебі қандай? Біз әрбіреудің ойны жайлы өтуін мақсат етеміз. Сол үшін барлық ойыншылардың тәртібін қадағалауға тура келеді. @@ -910,9 +908,6 @@ Уақыт таусылғалы тұр! [поштаны көрсету үшін шертіңіз] Жүктеп алу - Қол келдіңіз! - Личес – толығымен тегін/еркін, қайырымдылық негізінде жасалған програм. -Бүкіл жұмыс шығыны, әзірлеу, мазмұны пайдаланушылардың ақшалай демеуінен өтеледі. Бапкер басқармасы Стример басқармасы Жарысты болдырмау @@ -971,4 +966,6 @@ Түсті ауыстыру Тіркелгі жабылса, қарсы шағымдарыңыз жойылады Шара ұйымдастыру туралы ақыл-кеңес + Личес – толығымен тегін/еркін, қайырымдылық негізінде жасалған програм. +Бүкіл жұмыс шығыны, әзірлеу, мазмұны пайдаланушылардың ақшалай демеуінен өтеледі. diff --git a/translation/dest/site/kmr-TR.xml b/translation/dest/site/kmr-TR.xml index 4557ab1c0647f..4e6ab49b1c4e9 100644 --- a/translation/dest/site/kmr-TR.xml +++ b/translation/dest/site/kmr-TR.xml @@ -228,7 +228,6 @@ Vê gavê tê lîstin Vê gavê tê lîstin Qedîya - xelas dibe nav %s de Lîstikê betal bike Lîstik hate betalkirin Standard @@ -756,7 +755,6 @@ Bibore :( Em ji bo demekê te bi dûr xistin. - Xelasbûna wextê bidûrxistinê: %s. Çima? Em armanc dikin ku ji bo herkesî serpêhatiyeke bi xweşî pêşkêş bikin. Ji bo vê yekê, divê em piştrast bibin ku hemû lîzer mêtodeke baş dişopînin. @@ -805,7 +803,6 @@ Dem heme hema qedîya! [Ji bo nîşandana adresa epeyamê bitikîne] Daxîne - Bi xêr hatî! Ev wê tenê carekê bixebite. Ger tu hesabê xwe cara duyem bigirî, wê ti rêyeke xilaskirinê nîn be. Adresa epeyamê yê girêdayî hesabê diff --git a/translation/dest/site/kn-IN.xml b/translation/dest/site/kn-IN.xml index ff87e1306e09f..714c3299c3b98 100644 --- a/translation/dest/site/kn-IN.xml +++ b/translation/dest/site/kn-IN.xml @@ -270,7 +270,6 @@ ಆಟ ಪ್ರಗತಿಯಲ್ಲಿದೆ ಆಟಗಳು ಪ್ರಗತಿಯಲ್ಲಿವೆ ಮುಕ್ತಾಯಗೊಂಡಿದೆ - %s ಗಳಲ್ಲಿ ಮುಕ್ತಾಯಗೊಳ್ಳಲಿದೆ ಆಟವನ್ನು ತ್ಯಜಿಸು ಆಟವನ್ನು ತ್ಯಜಿಸಲಾಗಿದೆ ಸಾಮಾನ್ಯ @@ -845,7 +844,6 @@ ಕ್ಷಮಿಸಿ :( ನಿಮ್ಮ ಪ್ರವೇಶವನ್ನು ಸ್ವಲ್ಪ ಹೊತ್ತು ತಡೆಹಿಡಿಯಬೇಕಾಗಿ ಬಂದಿದೆ. - ನಿಮ್ಮ ನಿಷೇಧ ಕೊನೆಗೊಳ್ಳಲು ಇರುವ ಬಾಕಿ ಸಮಯ %s. ಯಾಕೆ? ಎಲ್ಲರಿಗೂ ಆನಂದದಾಯಕವಾದ ಚೆಸ್ ಅನುಭವವನ್ನು ಕೊಡಬೇಕು ಎಂಬುದು ನಮ್ಮ ಆಶಯವಾಗಿದೆ. ಅದರ ಸಲುವಾಗಿ, ಎಲ್ಲಾ ಆಟಗಾರರೂ ಒಳ್ಳೆಯ ಅಭ್ಯಾಸಗಳನ್ನು ಅನುಸರಿಸುತ್ತಿದ್ದಾರೆ ಎಂಬುದನ್ನು ನಾವು ಖಚಿತಪಡಿಸಬೇಕಾಗಿದೆ. @@ -904,9 +902,6 @@ ಸಮಯ ಮುಗಿಯುತ್ತಾ ಬರುತ್ತಿದೆ! [ಇಮೇಲ್ ವಿಳಾಸ ತೋರಿಸಲು ಇಲ್ಲಿ ಒತ್ತಿ] ಡೌನ್ಲೋಡ್ - ಸುಸ್ವಾಗತ! - ಲೀಚೆಸ್ ಒಂದು ದಾನ-ಆಧಾರಿತ ಹಾಗೂ ಸಂಪೂರ್ಣ ಉಚಿತ/ ಸ್ವತಂತ್ರ, ಮುಕ್ತ ಸಾಫ್ಟ್ವೇರ್ ತಂತ್ರಜ್ಞಾನ. -ಎಲ್ಲಾ ನಿರ್ವಹಣಾ ವೆಚ್ಚಗಳು, ಸಾಫ್ಟ್ವೇರ್ ಬೆಳವಣಿಗೆ, ಹಾಗೂ ವಸ್ತು/ವಿಷಯಗಳು ಕೇವಲ ದೇಣಿಗೆಯಿಂದ ಮಾತ್ರ ನಡೆಸಲಾಗುತ್ತಿದೆ. ಕೋಚ್ ಪ್ರೊಫೈಲ್ ಸಲಕರಣೆಗಳು ನೇರಪ್ರಸಾರ ವ್ಯವಸ್ಥಾಪಕ ಪಂದ್ಯಾವಳಿಯನ್ನು ರದ್ದುಪಡಿಸಿ @@ -964,4 +959,6 @@ ತಂಡ ಬದಲಾಯಿಸಿ ಖಾತೆ ಮುಚ್ಚಿದರೆ ನಿಮ್ಮ ಮನವಿಯನ್ನು ಹಿಂಪಡೆಯಲಾಗುವುದು ಈವೆಂಟ್‌ಗಳನ್ನು ಆಯೋಜಿಸಲು ನಮ್ಮ ಸಲಹೆಗಳು + ಲೀಚೆಸ್ ಒಂದು ದಾನ-ಆಧಾರಿತ ಹಾಗೂ ಸಂಪೂರ್ಣ ಉಚಿತ/ ಸ್ವತಂತ್ರ, ಮುಕ್ತ ಸಾಫ್ಟ್ವೇರ್ ತಂತ್ರಜ್ಞಾನ. +ಎಲ್ಲಾ ನಿರ್ವಹಣಾ ವೆಚ್ಚಗಳು, ಸಾಫ್ಟ್ವೇರ್ ಬೆಳವಣಿಗೆ, ಹಾಗೂ ವಸ್ತು/ವಿಷಯಗಳು ಕೇವಲ ದೇಣಿಗೆಯಿಂದ ಮಾತ್ರ ನಡೆಸಲಾಗುತ್ತಿದೆ. diff --git a/translation/dest/site/ko-KR.xml b/translation/dest/site/ko-KR.xml index 45fb2edb4ff80..58d39616b3f0f 100644 --- a/translation/dest/site/ko-KR.xml +++ b/translation/dest/site/ko-KR.xml @@ -254,7 +254,6 @@ 대국 중 지금 대국 중 종료 - %s 남음 게임 중단 게임 중단됨 표준 @@ -799,7 +798,6 @@ 죄송합니다 :( 짧은 시간동안 정지를 받으셨습니다. - 타임아웃은 %s 후에 후에 만료됩니다. 왜 그런가요? 우리는 모두에게 즐거운 체스 경험을 제공하는 것을 목표로 합니다. 그러기 위해서는, 우리는 모든 플레이어가 좋은 관행을 따르도록 보장해야 합니다. @@ -859,9 +857,6 @@ 시간이 거의 다 되었습니다! [이메일 주소를 보려면 클릭] 다운로드 - 환영합니다! - Lichess는 비영리 기구이며 완전한 무료/자유 오픈소스 소프트웨어입니다. -모든 운영 비용, 개발, 컨텐츠 조달은 전적으로 사용자들의 기부로 이루어집니다. 코치 설정 스트리머 설정 토너먼트 취소 @@ -921,4 +916,6 @@ FEN 포지션을 생성하기 위해 %s를 사용할 수 있습니다. 계정을 폐쇄하면 이의 제기는 자동으로 취소됩니다 이벤트 준비를 위한 팁 설명 + Lichess는 비영리 기구이며 완전한 무료/자유 오픈소스 소프트웨어입니다. +모든 운영 비용, 개발, 컨텐츠 조달은 전적으로 사용자들의 기부로 이루어집니다. diff --git a/translation/dest/site/la-LA.xml b/translation/dest/site/la-LA.xml index 2a272a0990c79..29bfafc494109 100644 --- a/translation/dest/site/la-LA.xml +++ b/translation/dest/site/la-LA.xml @@ -225,7 +225,6 @@ Nunc ludendus Nunc ludendus Perfectus - %s ceteri Lusionem tollere Sublata lusio Communis diff --git a/translation/dest/site/lb-LU.xml b/translation/dest/site/lb-LU.xml index 86ef34998b0d8..a4945a49acfcd 100644 --- a/translation/dest/site/lb-LU.xml +++ b/translation/dest/site/lb-LU.xml @@ -5,6 +5,7 @@ Fir een an dës Partie z\'invitéieren, gëff ëm dëse Link Game Over Op de Géigner waarden + Oder looss däi Géigner dëse QR-Code scannen Waarden Du bass drun %1$s Level %2$s @@ -51,21 +52,21 @@ Schwaarz huet d\'Partie verlooss Wäiss huet net gezunn Schwaarz huet net gezunn - Eng Computer Analyse ufroen + Eng Computeranalys ufroen Computeranalys - Computer Analyse disponibel - Computer Analyse desaktivéiert + Computeranalys disponibel + Computeranalys desaktivéiert Analysebriet Déift %s - Server Analyse gëtt benotzt + Serveranalys gëtt benotzt Engine luet... Zich ginn gerechent... Feeler beim Lueden - Cloud Analyse - Déift erhiewen - Bedrohung weisen + Cloudanalys + Déift eropsetzen + Bedroung weisen am lokalen Browser - Lokal Computer Analyse aktivéieren/desaktivéieren + Lokal Computeranalys aktivéieren/desaktivéieren Variant opwäerten Haaptvariant maachen Vun hei läschen @@ -110,7 +111,7 @@ PGN importéieren Läschen Importéiert Partie läschen? - Replay Modus + Replay-Modus Realzäit No CPL Studie opmaachen @@ -118,10 +119,10 @@ Beschten Zuch Feil Variantefeiler weisen Evaluatioun weisen - Puer Linnen + Méi Varianten CPUs Aarbechtsspäicher - Endlos Analyse + Endlos Analys Entfernt d\'Déifenbegrenzung an hält däin Computer waarm Engineverwaltung Gaffe @@ -272,7 +273,6 @@ Partie leeft Partien lafen Fäerdeg - ass fäerdeg %s Partie ofbriechen Partie ofgebrach Standard @@ -485,17 +485,18 @@ Zich gespillt Wäiss Victoiren Schwaarz Victoiren + Remisquot Remis Nächsten %s Turnéier: Duerschnëttlechen Géigner - Briet Editor + Briet-Editor Briet opbauen - Beléift Eröffnungen + Beléift Erëffnungen Endspill Positiounen Chess960 Start Positioun: %s - Start Positioun + Ausgangsstellung Briet opraumen - Positioun lueden + Stellung lueden Privat %s den Moderatoren mellen Profil vollstänneg zu: %s @@ -506,6 +507,7 @@ Virnumm Numm Biographie + Land oder Regioun Merci! Sozial Medien Links Eng URL pro Zeil. @@ -586,7 +588,7 @@ Außerhalb vum Briet An luesen Partien Ëmmer - Nie + Ni %1$s hëlt deel bei %2$s Victoire Defaite @@ -685,7 +687,7 @@ bei Start/Schluss goen Kommentarer weisen/verstoppen Variant wiehlen/verloossen - Computer-Analyse ufroen, léier aus dengen Feeler + Computeranalys ufroen, léier aus denge Feeler Nächst (Aus dengen Feeler léieren) Nächst Gaffe Nächsten Feeler @@ -732,6 +734,7 @@ Mat Kolleegen Mat jidderengem Kannermodus + De Kannermodus ass aktiv. Eng Sécherheetsastellung. Am Kannermodus sin all Kommunikatiounsweeër blockéiert. Aktivéier dës Optioun fir Kanner an Schüler virun Internetbenotzer ze schützen. Am Kannermodus huet den Lichess logo en %s Icon, sou weess du dass däin Kand sécher ass. Däin Konto gëtt verwalt. Fro däin Schachtrainer fir den Kannermodus opzehiewen. @@ -756,7 +759,7 @@ Disponibel an %s Sprooch! Disponibel an %s Sproochen! - Spill Analyse + Analys vun der Partie %1$s huet %2$s kreéiert %1$s mëscht mat bäi %2$s %1$s gefällt %2$s @@ -772,9 +775,9 @@ Mam Gerät synchroniséieren URL vum Hannergrondbild: Briet Geometrie - Briet Design + Brietdesign Briet Gréisst - Figuren Set + Figurestil An Websäit anbetten Dësen Benotzernumm gëtt schonn benotzt, wiel wannechgelift een aneren. De Benotzernumm muss mat engem Buschtaf ufänken. @@ -820,7 +823,7 @@ Probéier een aneren Zuch fir Wäiss Probéier een aneren Zuch fir Schwaarz Léisung - Waarden op Analyse + Waarden op d\'Analys Keng Feeler vun Wäiss fonnt Keng Feeler vun Schwaarz fonnt Wäiss Feeler all nogekuckt @@ -845,9 +848,10 @@ an späicher %s bedingten Virauszuch an späicher %s bedingt Virauszich + Du krus eng privat Noriicht vu Lichess. + Klick hei fir se ze liesen Sorry :( Mir missten dir eng Auszeit ginn. - D\'Auszeit ass riwwer %s. Firwat? Mir wëllen jidderengem eng méiglechst gudd Schacherfahrung offréieren. Fir dat ze erreechen mussen mer sécherstellen dass all Spiller sech korrekt verhalen. @@ -867,6 +871,7 @@ Ech stëmmen zou dass ech den Lichess-Richtlinnen folgen wäert. Sichen oder nei Konversatioun starten Änneren + Bullet Blitz Rapid Klassesch @@ -907,9 +912,6 @@ Deng Zäit ass baal ofgelaf! [Klick fir Email Adress ze weisen] Download - Wëllkomm! - Lichess ass eng Wohltätegkeetsorganisatioun an eng komplett kostenfrei/open source Software. -All Betriebskäschten, Entwécklung an Inhalter ginn ausschließlech vun Benotzerspenden finanzéiert. Coach Manager Stream Manager Turnéier annuléieren @@ -944,10 +946,10 @@ Looss et eidel, fir Partie aus der normaler Ausgangspositioun ze starten.Just Ekippenmemberen Duerch den Zuchbam navigéieren Maus Tricks - Lokal Computer Analyse aktivéieren/desaktivéieren - All Computer Analyse aktivéieren/desaktivéieren + Lokal Computeranalys aktivéieren/desaktivéieren + All Computeranalysen aktivéieren/desaktivéieren Beschten Computer Zuch spillen - Analyse Optiounen + Analysoptiounen Chat fokusséieren Dësen Hëllefdialog weisen Konto nei opmaachen @@ -968,4 +970,8 @@ Looss et eidel, fir Partie aus der normaler Ausgangspositioun ze starten.Faarf wiesselen Däin Benotzerkonto zou ze maachen wäert och däin Asproch zeréckzéihen Eis Tipps fir d\'Organiséieren vun Turnéier + Instruktiounen + Alles weisen + Lichess ass eng Wohltätegkeetsorganisatioun an eng komplett kostenfrei/open source Software. +All Betriebskäschten, Entwécklung an Inhalter ginn ausschließlech vun Benotzerspenden finanzéiert. diff --git a/translation/dest/site/lt-LT.xml b/translation/dest/site/lt-LT.xml index 196a7ec10b462..8237e11d6dfaa 100644 --- a/translation/dest/site/lt-LT.xml +++ b/translation/dest/site/lt-LT.xml @@ -302,7 +302,6 @@ Vyksta šiuo metu Vyksta šiuo metu Baigėsi - baigiasi %s Nutraukti partiją Partija nutraukta Standartinis @@ -935,7 +934,6 @@ kompiuterinę analizę, partijos pokalbį bei URL dalinimuisi. Atsiprašome :( Turėjome jus laikinai apriboti. - Apribojimas baigiasi %s. Kodėl? Mes stengiamės suteikti galimybę visiems patirti šachmatų malonumą. Dėl to turime užtikrinti, kad visi žaidėjai laikytųsi gerųjų praktikų. @@ -995,9 +993,6 @@ kompiuterinę analizę, partijos pokalbį bei URL dalinimuisi. Laikas beveik baigėsi! [spustelėkite norėdami pamatyti el. pašto adresą] Atsisiųsti - Sveiki atvykę! - Lichess yra labdara ir pilnai atviro kodo/libre projektas. -Visos veikimo išlaidos, programavimas ir turinys yra padengti išskirtinai tik vartotojų parama. Trenerių valdymas Transliuotojų valdymas Atšaukti turnyrą @@ -1056,4 +1051,6 @@ Palikite tuščią norėdami pradėti žaidimą nuo įprastos pradinės pozicijo Pakeisti puses paskyras Mūsų patarimai organizuojant renginius + Lichess yra labdara ir pilnai atviro kodo/libre projektas. +Visos veikimo išlaidos, programavimas ir turinys yra padengti išskirtinai tik vartotojų parama. diff --git a/translation/dest/site/lv-LV.xml b/translation/dest/site/lv-LV.xml index 086461d982618..f27472475cbcb 100644 --- a/translation/dest/site/lv-LV.xml +++ b/translation/dest/site/lv-LV.xml @@ -285,7 +285,6 @@ Šobrīd spēlē Šobrīd notiek Beidzies - beidzas %s Atcelt spēli Spēle atcelta Standarta @@ -886,7 +885,6 @@ Lūdzu piedodiet :( Mums nācās jūs apturēt uz laiku. - Pārtraukums beigsies %s. Kāpēc? Mēs cenšamies visiem piedāvāt patīkamu šaha pieredzi. Līdz ar to, mums jānodrošina, lai visi spēlētāji pieturas pie labas prakses. @@ -946,9 +944,6 @@ Laiks gandrīz beidzies! [Noklikšķiniet, lai atklātu e-pasta adresi] Lejupielādēt - Laipni lūdzam! - Lichess ir labdarības organizācija un pilnībā bezmaksas/brīva atvērtā koda programmatūra. -Visas izmaksas, izstrādāšanu un saturu finansē vienīgi lietotāju ziedojumi. Trenera iestatījumi Straumētāja iestatījumi Atcelt turnīru @@ -1006,4 +1001,6 @@ Atstājiet tukšu, lai spēles sāktos no parastās pozīcijas. Mainīties ar pusēm Konta slēgšana atsauks jūsu iesniegumu Pasākumu organizēšanas ieteikumi + Lichess ir labdarības organizācija un pilnībā bezmaksas/brīva atvērtā koda programmatūra. +Visas izmaksas, izstrādāšanu un saturu finansē vienīgi lietotāju ziedojumi. diff --git a/translation/dest/site/mg-MG.xml b/translation/dest/site/mg-MG.xml index c26d1872c875b..0a7f0811bb291 100644 --- a/translation/dest/site/mg-MG.xml +++ b/translation/dest/site/mg-MG.xml @@ -166,7 +166,6 @@ Milalao amin\'izao fotoana Milalao amin\'izao fotoana Vita - tapitra afaka %s Ajanona ny lalao Tapaka ny lalao Standard diff --git a/translation/dest/site/mk-MK.xml b/translation/dest/site/mk-MK.xml index b7b496c0318f3..08d3bf4932b6f 100644 --- a/translation/dest/site/mk-MK.xml +++ b/translation/dest/site/mk-MK.xml @@ -266,7 +266,6 @@ Моментално игра Моментално игра Завршено - Завршува за %s Откажи ја играта Играта е откажана Стандарден @@ -841,7 +840,6 @@ Извини :( Моравме да те исклучиме на кратко. - Исклучувањето престанува %s. Зошто? Се стремиме да пружиме пријатно искуство за сите. Поради тоа, мораме да се осигуриме дека сите играчи се чесни. @@ -900,9 +898,6 @@ Времето е пред истекување! [Кликнете за да се прикаже е-мајл адресата] Преземете - Добредојде! - Lichess е добротворна организација и целосно бесплатен/слободен софтвер со отворен код. -Сите оперативни трошоци, развој и содржина се финансираат исклучиво од донации на корисници. Поставки за тренер Поставки за стример менаџер Откажи го турнирот @@ -961,4 +956,6 @@ Променете страна Затворањето на Вашиот профил ќе ја повлече жалбата Наши совети за организирање настани + Lichess е добротворна организација и целосно бесплатен/слободен софтвер со отворен код. +Сите оперативни трошоци, развој и содржина се финансираат исклучиво од донации на корисници. diff --git a/translation/dest/site/ml-IN.xml b/translation/dest/site/ml-IN.xml index 1e30946dd3554..ed490bf1616e3 100644 --- a/translation/dest/site/ml-IN.xml +++ b/translation/dest/site/ml-IN.xml @@ -228,7 +228,6 @@ ഇപ്പോള്‍ കളിച്ചുക്കൊണ്ടിരിക്കുന്നു ഇപ്പോൾ കളിച്ചുകൊണ്ടിരിക്കുന്നു പൂര്‍ത്തിയായി - %s-ൽ തീരുന്നു മത്സരം ഉപേക്ഷിക്കുക മത്സരം ഉപേക്ഷിച്ചു സ്റ്റാന്‍ഡേര്‍ഡ് @@ -761,7 +760,6 @@ ക്ഷമിക്കുക :( ഞങ്ങൾക്ക് നിങ്ങളെ കുറച്ചു സമയം പുറത്തു നിർത്തേണ്ടി വന്നു. - ടൈം ഔട്ട് %s ഉള്ളിൽ കഴിയും. എന്തുകൊണ്ട്? ഞങ്ങൾ എല്ലാവര്ക്കും സുഗമമായ ചെസ്സ് അനുഭവം നൽകാൻ ലക്ഷ്യമിടുന്നു. അതിനാൽ, എല്ലാ കളിക്കാരും നല്ല രീതിയിൽ പ്രവർത്തിക്കുന്നു എന്ന് ഞങ്ങൾക്ക് ഉറപ്പു വരുത്തണം. diff --git a/translation/dest/site/mn-MN.xml b/translation/dest/site/mn-MN.xml index c71978af015bb..46088a74eddbf 100644 --- a/translation/dest/site/mn-MN.xml +++ b/translation/dest/site/mn-MN.xml @@ -225,7 +225,6 @@ Яг одоо тоглож байна Энэ мөчид тэмцээн, нэгэн зэрэг үзэсгэлэн буюу үйл явдал үргэлжилж байгааг харуулж байна. Өндөрлөсөн тэмцээнүүд - %s дуусна Өргийг цуцлах Өрөг цуцлагдлаа Стандарт @@ -723,7 +722,6 @@ Дэмжихээ болих Баяр хүргье мундаг аа, чи ялчихлаа! Татаж суулгах - Тавтай морил! Хударсан бол тэмцээнийг цуцлах Тэмцээний тайлбар Зөвхөн багийн хүмүүс шүү diff --git a/translation/dest/site/mr-IN.xml b/translation/dest/site/mr-IN.xml index 1835cf7c09848..de57862969a8f 100644 --- a/translation/dest/site/mr-IN.xml +++ b/translation/dest/site/mr-IN.xml @@ -243,7 +243,6 @@ आता खेळत आहे आता खेळत आहेत समाप्त - समाप्ती %s डाव बंद करा डाव बंद केला गेला मानक @@ -811,7 +810,6 @@ माफ करा आम्हाला आपल्याला काही वेळ खेळू देता येणार नाही. - आपला प्रतिबंध समाप्त होईल %s. कारण? प्रत्येकासाठी एक सुखद बुद्धिबळ अनुभव देण्याचे आमचे ध्येय आहे. त्या दृष्टीने, आम्ही हे सुनिश्चित केले पाहिजे की सर्व खेळाडूंनी चांगल्या कार्यपद्धतीचे अनुसरण केले आहे. @@ -868,9 +866,6 @@ वेळ संपत आलेली आहे! [ईमेल पाहण्यासाठी क्लिक करा] डाउनलोड करा - सुस्वागतम! - Lichess एक चॅरिटी आहे आणि संपूर्णपणे मुक्त / नि: शुल्क मुक्त स्रोत सॉफ्टवेअर आहे. -सर्व ऑपरेटिंग खर्च, विकास आणि सामग्री केवळ वापरकर्त्याच्या देणग्याद्वारे अनुदानित केली जाते. प्रशिक्षक व्यवस्थापक स्ट्रीमर व्यवस्थापक स्पर्धा रद्द करा @@ -929,4 +924,6 @@ मानांकीत खेळ सर्व lichess खेळाडूंकडून नमुने घेऊन ठरवले आहेत रंग बदला खाते बंद केल्यास आवाहन मागे घेतले जाईल + Lichess एक चॅरिटी आहे आणि संपूर्णपणे मुक्त / नि: शुल्क मुक्त स्रोत सॉफ्टवेअर आहे. +सर्व ऑपरेटिंग खर्च, विकास आणि सामग्री केवळ वापरकर्त्याच्या देणग्याद्वारे अनुदानित केली जाते. diff --git a/translation/dest/site/ms-MY.xml b/translation/dest/site/ms-MY.xml index e3066be3a3c8d..ccb6ea67706f0 100644 --- a/translation/dest/site/ms-MY.xml +++ b/translation/dest/site/ms-MY.xml @@ -249,7 +249,6 @@ Sedang bermain sekarang Sedang bermain sekarang Tamat - tamat dalam %s Batalkan permainan Permainan dibatalkan Standard @@ -719,7 +718,6 @@ analisis komputer, perbualan dalam permainan dan URL kongsi bersama. Maaf :( Kami terpaksa memberikan time out buat sementara. - Timeout berakhir %s. Kenapa? Matlamat kami adalah untuk memberikan pengalaman catur yang baik untuk semua. Dengan itu, kami perlu memastikan semua pemain mengikuti amalan yang baik. @@ -741,7 +739,6 @@ analisis komputer, perbualan dalam permainan dan URL kongsi bersama. Tahniah, anda menang! Masa hampir habis! Muat turun - Selamat Datang! Anggaran masa bermula Dalam zon waktu anda Ruangan sembang pertandingan diff --git a/translation/dest/site/nb-NO.xml b/translation/dest/site/nb-NO.xml index 67234d73b55bc..56b482c23b3be 100644 --- a/translation/dest/site/nb-NO.xml +++ b/translation/dest/site/nb-NO.xml @@ -5,6 +5,7 @@ For å invitere noen til å spille, gi dem denne lenken Partiet er avsluttet Venter på motstander + Eller få motstanderen din til å skanne denne QR-koden Venter Ditt trekk %1$s på nivå %2$s @@ -272,7 +273,6 @@ Pågår Pågår Ferdig - avsluttes %s Avbryt partiet Partiet er avbrutt Standard @@ -405,6 +405,7 @@ Importer parti Lim inn PGN for gjennomblaing, maskinanalyse, partisamtale og delbar URL. Varianter importeres ikke. Bruk en studie for å importere PGN med varianter. + Denne PGN-en er offentlig tilgjengelig. Bruk en studie for å importere et parti privat. %s importert parti %s importerte partier @@ -506,7 +507,8 @@ Rediger profil Fornavn Etternavn - Velg flair: + Velg flair + Flair Det finnes en innstilling for å skjule alle brukerflairer på hele nettstedet. Biografi Land eller region @@ -746,6 +748,7 @@ Med venner Med alle Barnemodus + Barnemodus er aktivert. Dette handler om sikkerhet. I barnemodus blir all kommunikasjon skrudd av. Bruk dette for å skjerme barn og skole-elever mot brukere på Internett. I barnemodus får lichess-logoen et %s symbol, så du vet at barna dine er trygge. Kontoen din er forvaltet. Be sjakklæreren din fjerne barnemodus. @@ -859,9 +862,10 @@ og lagre %s linje med forhåndstrekk og lagre %s linjer med forhåndstrekk + Du har mottatt en privat melding fra Lichess. + Klikk her for å lese den Beklager :( Vi måtte gi deg en timeout. - Timeouten utløper %s. Hvorfor? Vi forsøker å gi alle en god sjakkopplevelse. For å oppnå det må vi sikre at alle spillere følger god praksis. @@ -881,6 +885,7 @@ Jeg lover å respektere alle Lichess\' retningslinjer. Søk eller start en ny diskusjon Rediger + Bullet Blitz Hurtigsjakk Klassisk @@ -921,9 +926,6 @@ Tiden er nesten ute! [Klikk for å vise e-postadresse] Last ned - Velkommen! - Lichess er en ideell forening, basert på fri programvare med åpen kildekode. -Alle kostnader for drift, utvikling og innhold finansieres utelukkende av brukerbidrag. Innstillinger for trenere Innstillinger for strømmere Avlys turneringen @@ -984,4 +986,6 @@ La feltet stå tomt for å begynne partiene fra den normale utgangsstillingen.Tips for arrangementer Instruksjoner Vis meg alt + Lichess er en ideell forening, basert på fri programvare med åpen kildekode. +Alle kostnader for drift, utvikling og innhold finansieres utelukkende av brukerbidrag. diff --git a/translation/dest/site/ne-NP.xml b/translation/dest/site/ne-NP.xml index 26061a1ce0e73..b54e056e1b6ca 100644 --- a/translation/dest/site/ne-NP.xml +++ b/translation/dest/site/ne-NP.xml @@ -203,7 +203,6 @@ खेल चालु छ खेल चालु छ समाप्त भयो - समापन हुदैं %s खेल रद्द गरौ खेल रद्द गरियो सामान्य @@ -723,7 +722,6 @@ माफ पाउँ :( तपाइलाई केही समयको लागि रोक लगाउन पर्ने भयो। - बाकि समय: %s। किन? हामी सम्पूर्ण खेलाडीहरुको लागि बुद्धिचालको सुमधुर अनुभव श्रीजना गर्न चाहान्छौं। त्यसको लागि सम्पूर्ण खेलाडीहरुको चालचलन राम्रो हुन जरुरी छ। diff --git a/translation/dest/site/nl-NL.xml b/translation/dest/site/nl-NL.xml index c99279685bc98..d70f17ea730ff 100644 --- a/translation/dest/site/nl-NL.xml +++ b/translation/dest/site/nl-NL.xml @@ -5,6 +5,7 @@ Deel deze link als u iemand wil uitnodigen om met u te spelen Partij afgelopen Wachten op een tegenstander + Of laat je tegenstander deze QR-code scannen Even geduld a.u.b. U bent aan zet %1$s niveau %2$s @@ -272,7 +273,6 @@ Nu aan het spelen Nu aan het spelen Afgelopen - klaar in %s Partij afbreken Partij afgebroken Standaard @@ -405,6 +405,7 @@ Importeer partij Als je een PGN in het venster plakt, krijg je een doorzoekbare replay, een computeranalyse, een chatbox bij de partij en een deelbare URL. Variaties worden gewist. Om ze te behouden, importeer de PGN via een studie. + Deze PGN kan toegankelijk zijn voor iedereen. Gebruik een studie om een partij privé te importeren. %s Geïmporteerde partij %s Geïmporteerde partijen @@ -506,7 +507,8 @@ Pas profiel aan Voornaam Achternaam - Stel je flair in: + Stel je flair in + Symbool Er bestaat een instelling om alle gebruikersflairs over de hele site te verbergen. Biografie Land of regio @@ -746,6 +748,7 @@ Met vrienden Met iedereen Kindvriendelijke modus + Kindvriendelijke modus is ingeschakeld. Dit gaat over veiligheid. In kindvriendelijke modus, worden alle communicatiemogelijkheden op de website uitgeschakeld. Activeer dit voor kinderen en scholieren, om hen te beschermen tegen andere internetgebruikers. In kindvriendelijke modus, krijgt het Lichess-logo een %s-icoontje, zodat je weet dat je kinderen veilig zijn. Je account wordt beheerd. Vraag je schaakdocent om de kindermodus uit te zetten. @@ -859,9 +862,10 @@ en bewaar %s premove-variant en bewaar %s premove-varianten + Je hebt een privébericht van Lichess ontvangen. + Klik hier om het te bekijken Sorry :( We moesten je voor een korte tijd een time-out geven. - The time-out verloopt %s. Waarom? Wij streven ernaar om iedereen een prettige schaakervaring te bieden. Daarom moeten we ervoor zorgen dat alle spelers goede praktijken volgen. @@ -881,6 +885,7 @@ Ik ga ermee akkoord dat ik de Lichess-regels zal volgen. Zoek of start een nieuwe discussie Wijzigen + Bullet Blitz Rapid Klassiek @@ -921,9 +926,6 @@ De tijd is bijna om! [Klik om het e-mailadres te tonen] Downloaden - Welkom! - Lichess is een organisatie zonder winstoogmerk en is volledig open en gratis (libre). -Alle exploitatiekosten, ontwikkeling en inhoud worden enkel gefinancierd door donaties van gebruikers. Coach manager Streamer manager Toernooi annuleren @@ -984,4 +986,6 @@ Laat leeg om partijen te starten vanaf de normale beginstelling. Onze tips voor het organiseren van evenementen Instructies Alles tonen + Lichess is een organisatie zonder winstoogmerk en is volledig open en gratis (libre). +Alle exploitatiekosten, ontwikkeling en inhoud worden enkel gefinancierd door donaties van gebruikers. diff --git a/translation/dest/site/nn-NO.xml b/translation/dest/site/nn-NO.xml index 4bfa618095f66..99704042f9f75 100644 --- a/translation/dest/site/nn-NO.xml +++ b/translation/dest/site/nn-NO.xml @@ -5,6 +5,7 @@ Send denne lekkja til den du utfordrar Partiet er avslutta Ventar på motspelar + Eller la motspelaren din skanna denne QR-koden Ventar Ditt trekk %1$s på nivå %2$s @@ -272,7 +273,6 @@ Pågår Pågår Slutt - endar %s Avbryt partiet Partiet blei avbrote Standard @@ -406,6 +406,7 @@ Når du limer inn eit PGN-parti kan du bla gjennom partiet, få en computeranalyse, chatte eller dele ein URL. Variantar vert sletta. Vil du behalde dei kan du importere PGN\'en via ein studie. + Denne PGN-fila er offentleg tilgjengeleg. Bruk ein studie for å importere eit parti berre for deg. %s importert parti %s importerte parti @@ -507,6 +508,9 @@ få en computeranalyse, chatte eller dele ein URL. Rediger profilen Førenamn Etternamn + Vel ikonet ditt + Ikon + Det finst ei innstilling for å skjule alle brukarikonar på heile nettstaden. Biografi Land eller region Takk! @@ -669,7 +673,7 @@ få en computeranalyse, chatte eller dele ein URL. Returner til simultanheimesida Simultanframsyning inneber at ein spelar møter fleire motspelarar samstundes. Mot 50 motspelarar, fekk Fischer 47 sigrar, to remisar og eit tap. - Konseptet liknar på verkelege simultansjakkframsyningar, der verten for arrangementet går frå bord til bord og spelar mot fleire motstandarar samstundes. + Konseptet er henta frå verkelege framsyningar der verten for arrangementet går frå bord til bord og spelar mot fleire motstandarar samstundes. Når simultanframsyninga byrjar, startar alle deltakarane eit parti mot den som er vert. Verten får spela med dei kvite brikkene. Simultanoppvisinga endar når alle partia er ferdigspelte. Simultanframsyningar er alltid urangerte. Omstart, attendetrekk og \"meirtid\" er slått av. Opprett @@ -745,6 +749,7 @@ få en computeranalyse, chatte eller dele ein URL. Med vener Med alle Barnemodus + Barnemodus er aktivert. Dette handlar om tryggleik. I barnemodus er all kommunikasjon avskrudd. Bruk dette for å verne barn og skoleelevar mot andre Internett-brukarar. I barnemodus får lichess-logoen eit %s symbol, så du kan sjå at barna dine er trygge. Kontoen din er under administrasjon. Spør sjakklæraren din om det er mogleg å få oppheva barnemodusen. @@ -760,7 +765,7 @@ få en computeranalyse, chatte eller dele ein URL. Annonsefri Alle funksjonar Mobiltelefon og nettbrett - kvikksjakk, lynsjakk, saktesjakk + Bullet, lynsjakk, saktesjakk Fjernsjakk Spel online og offline Vis løysinga @@ -858,9 +863,10 @@ få en computeranalyse, chatte eller dele ein URL. og lagra %s line med førehandstrekk og lagra %s liner med førehandstrekk + Du har fått ei privat melding frå Lichess. + Klikk her for å lesa den Beklagar :( Vi måtte gje deg ein timeout. - Timeouten tek slutt %s. Kvifor? Vi freistar å gje alle ei god sjakk-oppleving. For å oppnå det må vi sikre at alle spelarane respekterar god praksis. @@ -880,6 +886,7 @@ få en computeranalyse, chatte eller dele ein URL. Eg lovar at eg vil følge alle reglane til Lichess. Søk eller start ein ny diskusjon Rediger + Bullet Blitz Snøggsjakk Langsjakk @@ -920,9 +927,6 @@ få en computeranalyse, chatte eller dele ein URL. Tida er nesten ute! [Klikk for å visa epost-adresse] Last ned - Velkomen! - Lichess er ein velgjerdsorganisasjon basert på fritt tilgjengeleg open-kjeldekode-programvare. -Alle kostnader for drift, utvikling og innhald vert finansiert eine og åleine av brukardonasjonar. Trenarleiar Strøymingsleiar Avlys turneringa @@ -983,4 +987,6 @@ La feltet stå tomt for å starte partia frå den normale utgangsstillinga.Tips for å organisere arrangement Instruksjonar Vis alt + Lichess er ein velgjerdsorganisasjon basert på fritt tilgjengeleg open-kjeldekode-programvare. +Alle kostnader for drift, utvikling og innhald vert finansiert eine og åleine av brukardonasjonar. diff --git a/translation/dest/site/os-SE.xml b/translation/dest/site/os-SE.xml index 303629d1614d0..1552c25e55942 100644 --- a/translation/dest/site/os-SE.xml +++ b/translation/dest/site/os-SE.xml @@ -215,7 +215,6 @@ Хъазт цæуы Ныртæккæ цæуы Фæудгонд - фæуы %s Хъазт аивын Хъазт аивд у Классикон шахмæттæ diff --git a/translation/dest/site/pa-IN.xml b/translation/dest/site/pa-IN.xml index 7338813e4fc1b..c1df8af6d6f61 100644 --- a/translation/dest/site/pa-IN.xml +++ b/translation/dest/site/pa-IN.xml @@ -75,7 +75,6 @@ ਹੁਣੇ ਖੇਡ ਰਿਹਾ ਹੈ ਹੁਣੇ ਖੇਡ ਰਿਹਾ ਹੈ ਸਮਾਪਤ ਹੋਇਆ - ਮੁਕੰਮਲ %s ਖੇਡੋ ਭੇਜੋ ਮੁਫਤ ਦਾ ਸ਼ਤਰੰਜ diff --git a/translation/dest/site/pl-PL.xml b/translation/dest/site/pl-PL.xml index 265138fbf8188..a643f5781dbd8 100644 --- a/translation/dest/site/pl-PL.xml +++ b/translation/dest/site/pl-PL.xml @@ -5,6 +5,7 @@ Przekaż ten adres, by zaprosić kogoś do wspólnej gry Partia zakończona Oczekiwanie na ruch przeciwnika + Lub pozwól przeciwnikowi zeskanować ten kod QR Oczekiwanie Twój ruch %1$s poziom %2$s @@ -302,7 +303,6 @@ W toku W toku Zakończone - kończy się %s Przerwij partię Partia została przerwana Standard @@ -438,10 +438,10 @@ Musisz należeć do klubu %s Nie należysz do klubu %s Wróć do partii - Darmowe szachy online. Zagraj w szachy o przyjaznym interfejsie. Bez rejestracji, bez reklam, bez wtyczek. Zagraj w szachy z komputerem, znajomymi lub losowo wybranymi przeciwnikami. + Proste w obsłudze szachy online dla wszystkich. Nie trzeba zakładać konta, nie ma reklam, bez instalacji. Możesz grać z komputerem, z kimś, kogo znasz - albo z nieznajomymi. %1$s dołączył do klubu %2$s %1$s założył klub %2$s - rozpoczął streaming + rozpoczyna nadawanie na żywo %s rozpoczął streaming Średni ranking Miejsce @@ -461,6 +461,7 @@ Importuj partię Wklejenie PGN partii daje możliwość jej odtworzenia, analizy komputerowej, rozmowy i udostępnienia. Warianty zostaną usunięte. Aby je zachować, zaimportuj plik PGN jako opracowanie. + Ten zapis PGN będzie dostępny publicznie. Aby zaimportować partię tylko dla siebie, stwórz prywatne opracowanie. %s zaimportowana partia %s zaimportowana partie @@ -572,7 +573,8 @@ Edycja profilu Imię Nazwisko - Ustaw swój emblemat: + Ustaw swój emblemat + Emblemat Istnieje ustawienie, pozwalające ukryć wszystkie emblematy użytkowników na lichess. O mnie Kraj lub region @@ -821,6 +823,7 @@ Tylko znajomym Każdemu Tryb dla dzieci + Tryb dla dzieci jest włączony. Chodzi o bezpieczeństwo. W trybie dla dzieci możliwość komunikacji w serwisie jest wyłączona. Włącz go na rzecz swoich dzieci, czy uczniów, by chronić ich przed możliwymi zagrożeniami ze strony innych użytkowników internetu. W trybie dla dzieci logo lichess ma dodatkową ikonę %s, wiesz wtedy, że Twoje dziecko jest chronione. Twoim kontem zarządza nauczyciel. Poproś go o przekazanie zarządzania Tobie. @@ -942,9 +945,10 @@ i zapisz %s wariantów warunkowych i zapisz %s wariantów warunkowych + Otrzymałeś prywatną wiadomość od Lichess. + Kliknij tutaj, aby ją odczytać Przykro nam :( Na pewien czas musieliśmy wykluczyć Cię z gry. - Wykluczenie skończy się %s. Dlaczego? Naszym celem jest zapewnienie wszystkim przyjemności z gry. W tym celu musimy zapewnić przestrzeganie dobrych praktyk przez wszystkich graczy. @@ -964,6 +968,7 @@ Zgadzam się przestrzegać wszystkich zasad Lichess. Szukaj lub rozpocznij nową rozmowę Edytuj + Bullet Blitz Szybkie Klasyczne @@ -1004,9 +1009,6 @@ Czas prawie minął! [Kliknij, aby ujawnić adres e-mail] Pobierz - Witaj! - Lichess jest organizacją niedochodową i całkowicie darmowym otwartym oprogramowaniem. -Wszystkie koszty operacyjne, rozwój i treści są finansowane wyłącznie z darowizn użytkowników. Menedżer trenera Menedżer streamera Anuluj turniej @@ -1067,4 +1069,6 @@ Pozostaw puste, aby rozpoczynać partie z normalnej pozycji początkowej.Nasze wskazówki dotyczące organizacji wydarzeń Instrukcje Pokaż mi wszystko + Lichess jest organizacją niedochodową i całkowicie darmowym otwartym oprogramowaniem. +Wszystkie koszty operacyjne, rozwój i treści są finansowane wyłącznie z darowizn użytkowników. diff --git a/translation/dest/site/pt-BR.xml b/translation/dest/site/pt-BR.xml index 8560f9f09a457..5ba2e384fe8da 100644 --- a/translation/dest/site/pt-BR.xml +++ b/translation/dest/site/pt-BR.xml @@ -5,6 +5,7 @@ Para convidar alguém para jogar, envie este URL Fim da partida Aguardando oponente + Ou deixe seu oponente ler este QR Code Aguardando Sua vez %1$s nível %2$s @@ -272,7 +273,6 @@ Jogando agora Jogando agora Terminado - termina %s Cancelar partida Partida cancelada Padrão @@ -405,6 +405,7 @@ Importar partida Após colar uma partida em PGN você poderá revisá-la interativamente, consultar uma análise de computador, utilizar o chat e compartilhar um link. As variantes serão apagadas. Para salvá-las, importe o PGN em um estudo. + Este PGN pode ser acessado publicamente. Use um estudo para importar um jogo privado. %s de partidas importadas %s de partidas importadas @@ -506,7 +507,8 @@ Editar perfil Primeiro nome Sobrenome - Escolha seu emote: + Escolha seu emote + Estilo Você pode esconder todos os emotes de usuário no site. Biografia País ou região @@ -689,6 +691,7 @@ Atalhos de teclado retroceder/avançar lance ir para início/fim + Alternar entre as variantes mostrar/ocultar comentários entrar/sair da variante Solicite análise do computador, aprenda com seus erros @@ -696,6 +699,9 @@ Próximo erro grave Próximo erro Próxima imprecisão + Ativar/desativar setas + Variante seguinte/anterior + Ativar/desativar anotações Novo torneio Torneios de xadrez com diversos controles de tempo e variantes Jogue xadrez em ritmo acelerado! Entre em um torneio oficial agendado ou crie seu próprio. Bullet, Blitz, Clássico, Chess960, King of the Hill, Três Xeques e outras modalidades disponíveis para uma ilimitada diversão enxadrística. @@ -738,6 +744,7 @@ Com amigos Com todos Modo infantil + O modo infantil está ativado. Isto diz respeito à segurança. No modo infantil, todas as comunicações do site são desabilitadas. Habilite isso para seus filhos e alunos, para protegê-los de outros usuários da Internet. No modo infantil, a logo do lichess tem um ícone %s, para que você saiba que suas crianças estão seguras. Sua conta é gerenciada. Para desativar o modo infantil, peça ao seu professor. @@ -851,9 +858,10 @@ e salvar a linha de pré-lance de %s e salvar as linhas de pré-lance de %s + Você recebeu uma mensagem privada do Lichess. + Clique aqui para ler Desculpa :( Tivemos de bloqueá-lo por um tempo. - O tempo expirará %s. Por quê? Buscamos oferecer uma experiência agradável de xadrez para todos. Para isso, precisamos assegurar que nossos jogadores sigam boas práticas. @@ -873,6 +881,7 @@ Eu concordo que seguirei todas as normas do Lichess. Procurar ou iniciar nova conversa Editar + Bullet Blitz Rápida Clássico @@ -913,8 +922,6 @@ O tempo está quase acabando! [Clique para revelar o endereço de e-mail] Baixar - Bem-vindo! - Lichess é um software de código aberto, totalmente grátis e sem fins lucrativos. Todos os custos operacionais, de desenvolvimento, e os conteúdos são financiados unicamente através de doações de usuários. Configurações para professores Configurações para streamers Cancelar o torneio @@ -975,4 +982,5 @@ Deixe em branco para começar as partidas na posição inicial padrão. Nossas dicas para organização de eventos Instruções Mostrar tudo + Lichess é um software de código aberto, totalmente grátis e sem fins lucrativos. Todos os custos operacionais, de desenvolvimento, e os conteúdos são financiados unicamente através de doações de usuários. diff --git a/translation/dest/site/pt-PT.xml b/translation/dest/site/pt-PT.xml index 8145237ae2c48..6634a748c784a 100644 --- a/translation/dest/site/pt-PT.xml +++ b/translation/dest/site/pt-PT.xml @@ -5,6 +5,7 @@ Para convidares alguém para jogar, envia este URL Fim da partida Aguardando oponente + Ou deixa o teu oponente ler este código QR A aguardar É a tua vez %1$s nível %2$s @@ -272,7 +273,6 @@ A jogar agora A decorrer agora Terminado - termina %s Cancelar a partida Partida cancelada Padrão @@ -406,6 +406,7 @@ Coloca aqui o PGN de um jogo, para teres acesso a navegar pela repetição, análise de computador, sala de chat do jogo e link de partilha. As variações serão apagadas. Para mantê-las, importe o PGN através de um estudo. + Este PGN pode ser acessada pelo público. Para importar um jogo de forma privada, use um estudo. %s partida importada %s partidas importadas @@ -507,7 +508,8 @@ análise de computador, sala de chat do jogo e link de partilha. Editar o perfil Nome próprio Apelido - Defina o teu estilo: + Defina o teu estilo + Estilo Há uma opção para ocultar todos os estilos dos utilizadores em todo o site. Biografia País ou região @@ -703,6 +705,7 @@ análise de computador, sala de chat do jogo e link de partilha. Ativar/desactivar seta da variante Ciclo anterior/próxima variante Ativar/desativar anotações com símbolos + Setas de variação permitem navegar sem usar a lista de movimentos. jogar o movimento selecionado Novo torneio Torneios de xadrez com diversos ritmos de jogo e variantes @@ -746,6 +749,7 @@ análise de computador, sala de chat do jogo e link de partilha. Com amigos Com todos Modo infantil + Modo infantil está ativado. Iso é sobre segurança. No modo infantil, todas as comunicações do site ficam desactivadas. Activa esta opção para os teus filhos ou alunos, para protegê-los de outros utilizadores da internet. No modo criança, o logótipo do Lichess fica com um ícone %s para que saibas que as tuas crianças estão seguras. A sua conta é gerida. Peça ao seu professor de xadrez para retirar o modo infantil. @@ -859,9 +863,10 @@ análise de computador, sala de chat do jogo e link de partilha. e guarda %s variante de movimentos antecipados e guarda %s variantes de movimentos antecipados + Recebestes uma mensagem privada do Lichess. + Clica aqui para ler Desculpa :( Tivemos de te banir por algum tempo. - O banimento expira %s. Porquê? Tencionamos proporcionar uma experiência de xadrez agradável a todos. Para isso, temos de nos assegurar que todos os jogadores seguem boas práticas. @@ -881,6 +886,7 @@ análise de computador, sala de chat do jogo e link de partilha. Concordo que seguirei todas as políticas do Lichess. Pesquisa ou começa uma nova conversa Editar + Bullet Rápidas Semi-rápidas Clássicas @@ -921,9 +927,6 @@ análise de computador, sala de chat do jogo e link de partilha. O tempo está quase a terminar! [Clique para revelar o endereço de e-mail] Transferir - Bem-vindo(a)! - Lichess é uma instituição de caridade e software de código aberto totalmente livre. -Todos os custos operacionais, de desenvolvimento e conteúdo são financiados exclusivamente por doações de usuários. Gestor de treinadores Gestor do streamer Cancelar o torneio @@ -984,4 +987,6 @@ Deixe em branco para iniciar jogos da posição inicial normal. Os nossos conselhos para organizar eventos Instruções Mostra-me tudo + Lichess é uma instituição de caridade e software de código aberto totalmente livre. +Todos os custos operacionais, de desenvolvimento e conteúdo são financiados exclusivamente por doações de usuários. diff --git a/translation/dest/site/ro-RO.xml b/translation/dest/site/ro-RO.xml index ef561222c1f6a..14156ebdf6236 100644 --- a/translation/dest/site/ro-RO.xml +++ b/translation/dest/site/ro-RO.xml @@ -287,7 +287,6 @@ În desfășurare... În desfășurare Terminat - se termină %s Abandonați partida Partidă abandonată Standard @@ -433,6 +432,7 @@ Importați partida Copiați o partidă în format PGN pentru a putea apoi sa o rejucati, sa cereti o analiză a computerului, sa folositi functia de chat și sa obtineti un URL pentru distribuire. Variațiile vor fi șterse. Pentru a le păstra, importați PGN-ul printr-un studiu. + Acest PGN poate fi accesat public. Pentru a importa un joc în mod privat, folosește un studiu. %s partidă importată %s partide importate @@ -494,7 +494,7 @@ Setări avansate Alege un nume foarte sigur pentru turneu. Orice nume care este chiar și ușor nepotrivit poate cauza închiderea contului tău. - Lăsați necompletat pentru a numi turneul după un jucător bun de șah. + Lăsați necompletat pentru a numi turneul după un jucător bun de șah. Iți recomandăm să nu modifici aceste setări. Dacă stabilești condiții de intrare, turneul tău va avea mai puțini jucători. Afișează setările avansate @@ -560,7 +560,7 @@ Du-te automat la partida următoare după mutare Schimbă automat Probleme de șah - Câștigători de turnee + Câștigători de turnee Nume Descriere Descriere privată @@ -900,7 +900,6 @@ Scuze :( Am fost nevoiți să suspendăm activitatea pentru puțin timp. - Suspendarea se termină %s. De ce? Dorim să oferim o experiență plăcută tuturor jucătorilor de șah. Ca acest lucru să se întâmple, trebuie să ne asigurăm că toți jucătorii respectă bunele practici. @@ -960,9 +959,6 @@ Timpul se apropie de sfârșit! [Click pentru a dezvălui adresa de e-mail] Descărcare - Bine ați venit! - Lichess este o asociație non-profit și un software gratuit și open-source. -Toate costurile de operare și de dezvoltare sunt finanțate doar din donațiile utilizatorilor. Setări pentru antrenor Setări pentru streamer Anulează turneul @@ -1023,4 +1019,6 @@ Lăsați gol pentru a începe jocurile din poziția inițială normală.Sfaturile noastre pentru organizarea evenimentelor Instrucțiuni Afișează-mi tot + Lichess este o asociație non-profit și un software gratuit și open-source. +Toate costurile de operare și de dezvoltare sunt finanțate doar din donațiile utilizatorilor. diff --git a/translation/dest/site/ru-RU.xml b/translation/dest/site/ru-RU.xml index e5996fe6ac842..4836a61155fc4 100644 --- a/translation/dest/site/ru-RU.xml +++ b/translation/dest/site/ru-RU.xml @@ -5,6 +5,7 @@ Чтобы пригласить друга, отправьте ему эту ссылку Партия окончена Ожидание соперника + Или дайте вашему сопернику отсканировать этот QR-код Ожидание Ваш ход %1$s уровня %2$s @@ -302,7 +303,6 @@ Идёт игра Идёт прямо сейчас Завершён - завершится %s Отменить игру Игра отменена Классические шахматы @@ -461,6 +461,7 @@ Импортировать партию Вставьте запись партии в формате PGN, и вы получите возможность переигрывать партию, выполнять компьютерный анализ, общаться в чате и делиться ссылкой на эту игру. Варианты будут удалены. Чтобы их сохранить, импортируйте PGN в студии. + Этот PGN-файл может быть доступен публично. Чтобы импортировать игру приватно, используйте студию. %s импортированная %s импортированные @@ -572,8 +573,9 @@ Редактировать профиль Имя Фамилия - Задайте свой флаер: - Эта настройка скрывает все флаеры пользователей на всём сайте. + Задайте свой эмодзи + Эмодзи + Эта настройка скрывает все эмодзи пользователей на всём сайте. О себе Страна или регион Спасибо! @@ -822,6 +824,7 @@ С друзьями Со всеми Детский режим + Детский режим включён. Это для безопасности. В детском режиме отключены все коммуникации на сайте. Включите его для ваших детей и учеников, чтобы защитить их от других пользователей интернета. В детском режиме к логотипу lichess добавляется значок в виде %s, чтобы вы знали, что ваши дети находятся в безопасности. Ваш аккаунт находится под управлением. Спросите своего учителя по шахматам об удалении детского режима. @@ -943,9 +946,10 @@ и сохранить %s последовательностей и сохранить %s последовательностей + Вы получили личное сообщение от Lichess. + Нажмите здесь, чтобы прочитать его Извините :( Мы вынуждены прервать вас на время. - Вы можете вернуться через %s. Почему? Наша цель — сделать шахматы интересными для всех. Чтобы этого добиться, мы должны сделать так, чтобы все игроки следовали правилам хорошего тона. @@ -965,6 +969,7 @@ Подтверждаю, что я буду следовать всем правилам Lichess. Найти обсуждение или начать новое Изменить + Пуля Блиц Рапид Классика @@ -1005,9 +1010,6 @@ Время почти истекло! [Нажмите, чтобы раскрыть адрес электронной почты] Загрузить - Добро пожаловать! - Lichess - это благотворительное и полностью бесплатное программное обеспечение с открытым исходным кодом. -Все эксплуатационные расходы, разработка и контент финансируются исключительно за счет пожертвований пользователей. Для тренеров Управление стримом Отменить турнир @@ -1068,4 +1070,6 @@ Наши советы по организации мероприятий Руководство Показать всё + Lichess - это благотворительное и полностью бесплатное программное обеспечение с открытым исходным кодом. +Все эксплуатационные расходы, разработка и контент финансируются исключительно за счет пожертвований пользователей. diff --git a/translation/dest/site/ry-UA.xml b/translation/dest/site/ry-UA.xml index dba441f3f6d4c..35a5c99585833 100644 --- a/translation/dest/site/ry-UA.xml +++ b/translation/dest/site/ry-UA.xml @@ -277,7 +277,6 @@ Бавит ся тепирь Бавит ся тепирь Довершеный - кончит ся %s Одмінити бавку Бавка одмінена Прості шахматы @@ -606,9 +605,6 @@ Вы прыйграли, тому тко нарушив правила Lichess Верненя: %1$s %2$s рейтінґовых балув. Стерьхати - Вітаєме! - Lichess - то незысковоє ай докус задарноє проґрамноє застаченя из одкрытым зачаточным кодом. -Вшыткі кельтункы, розробкы ай контент ся фінанзувут лем бо вы нас поддіржуєте. Хочете дашто уповісти хосновачам? Будьте курті у свойих словах. Приступні одкликованя у форматови Markdown:[name](https://url) Мінімум рейтінґу Максімум тыждньвого рейтінґу @@ -648,4 +644,6 @@ До Рейтінґові партії вшыткых бавлячув Lichess Перемінити бук + Lichess - то незысковоє ай докус задарноє проґрамноє застаченя из одкрытым зачаточным кодом. +Вшыткі кельтункы, розробкы ай контент ся фінанзувут лем бо вы нас поддіржуєте. diff --git a/translation/dest/site/sco-GB.xml b/translation/dest/site/sco-GB.xml index ade789bc3c27f..44c0335884500 100644 --- a/translation/dest/site/sco-GB.xml +++ b/translation/dest/site/sco-GB.xml @@ -230,7 +230,6 @@ Spielin richt noo Spielin richt noo Feenisht - feenshes %s Misgae gemm Gemm misgane Ordinar diff --git a/translation/dest/site/si-LK.xml b/translation/dest/site/si-LK.xml index 2819f80cb0251..8d3201661b3ef 100644 --- a/translation/dest/site/si-LK.xml +++ b/translation/dest/site/si-LK.xml @@ -251,7 +251,6 @@ PlayFirstOpeningEndgameExplorerMove දැන් තරඟ කරයි දැන් තරඟ කරයි අවසානයි - %sකින් ඉවරවේ තරඟය නවත්වන්න තරගය අත්හැර දැමුනි සම්මත diff --git a/translation/dest/site/sk-SK.xml b/translation/dest/site/sk-SK.xml index 016d775b36f6e..59b2f7a60381b 100644 --- a/translation/dest/site/sk-SK.xml +++ b/translation/dest/site/sk-SK.xml @@ -5,6 +5,7 @@ Zdieľaním tejto adresy môžete niekoho pozvať k partii Koniec partie Čaká sa na súpera + Alebo nech Váš súper naskenuje tento QR kód Čaká sa Ste na ťahu %1$s úrovne %2$s @@ -302,7 +303,6 @@ Práve hrajú Práve sa hrá Ukončené - končí %s Zrušiť hru Hra bola zrušená Štandard @@ -461,6 +461,7 @@ Importovať partiu Vložením partie vo formáte PGN získate možnosť jej prehrania, počítačovú analýzu, chat k partii ako aj URL pre jej zdieľanie. Variácie sa vymažú. Ak ich chcete zachovať, importujte PGN prostredníctvom štúdie. + Toto PGN je verejne dostupné. Ak chcete partiu importovať súkromne, použite štúdiu! %s importovaná partia %s importované partie @@ -572,7 +573,8 @@ Upraviť profil Meno Priezvisko - Nastavte si svoju ikonku štýlu: + Nastavte si svoju ikonku štýlu + Ikonka štýlu V nastaveniach je možné skryť všetky ikonky štýlu používateľov na celej stránke. Životopis Krajina alebo región @@ -936,7 +938,6 @@ Prepáčte :( Na chvíľu sme Vás museli odstaviť. - Vaša odstávka skončí o %s. Prečo? Naším cieľom je poskytovať všetkým príjemný šachový zážitok. Aby sme to docielili, musíme sa uistiť že sa všetci hráči správajú športovo. @@ -956,6 +957,7 @@ Súhlasím, že budem dodržiavať všetky pravidlá Lichessu. Prehľadávať alebo začať novú konverzáciu Upraviť + Bullet Blitz Rapid Klasický šach @@ -996,8 +998,6 @@ Čas čoskoro vyprší! [Kliknite pre zobrazenie e-mailovej adresy] Stiahnuť - Vitajte! - Lichess je bezplatný a úplne slobodný/nezávislý softvér s otvoreným zdrojovým kódom. Všetky prevádzkové náklady, vývoj a obsah sú financované výlučne z darov používateľov. Nastavenia trénera Správca streamov Zrušiť turnaj @@ -1058,4 +1058,5 @@ Nechajte prázdne pre začatie partií zo základnej pozície! Naše tipy ohľadom organizovania podujatí Inštrukcie Ukázať všetko + Lichess je bezplatný a úplne slobodný/nezávislý softvér s otvoreným zdrojovým kódom. Všetky prevádzkové náklady, vývoj a obsah sú financované výlučne z darov používateľov. diff --git a/translation/dest/site/sl-SI.xml b/translation/dest/site/sl-SI.xml index d0209cb1ef883..d767260bc12c8 100644 --- a/translation/dest/site/sl-SI.xml +++ b/translation/dest/site/sl-SI.xml @@ -5,6 +5,7 @@ Kopiraj URL in povabi prijatelja k igri Konec igre Čakam nasprotnika + Ali pa naj vaš nasprotnik skenira to QR kodo Čakam Ti si na potezi %1$s, stopnja: %2$s @@ -301,7 +302,6 @@ Trenutno se igra Pravkar poteka Končano - končal %s Opusti igro Igra je opuščena Običajno @@ -460,6 +460,7 @@ Uvozi igro Ko prilepite PGN partijo imate na voljo brskanje partije, računalniško analizo, klepet o igri in povezavo, ki jo lahko delite. Variante bodo izbrisane. Če jih želite obdržat, uvozite PGN kot študijo. + Ta PGN je javno dostopen. Za zasebni uvoz igre uporabite študijo. %s uvožena igra %s uvoženi igri @@ -571,7 +572,8 @@ Uredi profil Ime Priimek - Določite svoj okus: + Določite svoj okus + Simbol Obstaja nastavitev za skrivanje vseh uporabniških čustev na celotnem spletnem mestu. Biografija Država ali regija @@ -813,6 +815,7 @@ S prijatelji Z vsemi Način za otroke + Otroški način je omogočen. To je o varnosti. V načinu za otroke so vsi pogovori onemogočeni. Omogočite ta način, da otroke in šolarje zaščitite pred drugimi uporabniki na internetu. V načinu za otroke ima lichess ikono %s in označuje da so otroci varni. Vaš račun je upravljan. Vprašajte svojega učitelja šaha o dvigovalnem otroškem načinu. @@ -934,9 +937,10 @@ in shranite %s predpotezne variante in shranite %s predpoteznih variant + Prejeli ste zasebno sporočilo od Lichess. + Kliknite tukaj, če ga želite prebrati Oprostite :( Morali smo vas za nekaj časa onemogočiti. - Omejitev poteče čez %s. Zakaj? Želimo, da imajo z igranjem vsi prijetno izkušnjo. Da bi to dosegli, moramo zagotoviti, da se vsi igralci držijo dobre prakse. @@ -956,6 +960,7 @@ Strinjam se, da bom spoštoval vsa pravila Lichess strani. Poišči ali prični nov pogovor Uredi + Hitri šah Hitropotezni šah Pospešeni Klasični šah @@ -996,9 +1001,6 @@ Čas je skoraj pošel! [Kliknite, da razkrijete elektronski naslov] Prenos - Dobrodošli! - Lichess je dobrodelna in popolnoma brezplačna odprtokodna programska oprema. -Vsi operativni stroški, razvoj in vsebina se financirajo izključno iz donacij uporabnikov. Nastavitve za trenerja Nastavitve upravitelja pretočnega predvajanja Prekini turnir @@ -1057,4 +1059,6 @@ Pustite prazno, da začnete igre iz običajnega začetnega položaja. Zamenjaj strani Z zaprtjem računa bo vaša pritožba umaknjena Naši nasveti za organizacijo dogodkov + Lichess je dobrodelna in popolnoma brezplačna odprtokodna programska oprema. +Vsi operativni stroški, razvoj in vsebina se financirajo izključno iz donacij uporabnikov. diff --git a/translation/dest/site/so-SO.xml b/translation/dest/site/so-SO.xml index 65432f976e12d..c048a26c5b609 100644 --- a/translation/dest/site/so-SO.xml +++ b/translation/dest/site/so-SO.xml @@ -1,106 +1,385 @@ - Laciyaar saaxiibkaa - La ciyaar computer - Saa codsi ugu dirto qof kula ciyara, see URLkaan - Ciyaartii waa dhamaatay - Waxaad sugaysa qof kula dheela - Sugitaan + La ciyaar saaxiib + La ciyaar komyuutar + Si aad u casuumtid qof kula ciyaara, u dir laynkan + Ciyaarti way dhamaatay + Sugaya herjeede cusub + Sugaya Waa markaagii - %1$sheer%2$s - Heerarka - Awoodaada - Chess - Sheekaysi - Is casil - Ismarinwaa + %1$s heerka %2$s + Heerka + Adeyg + Fur luuqa + U qor + Is dhiib + Jeg-meyd + Ismariwaa Cadaan - Madoow - Abuur ciyaar cusub + Madow + caddaan ahaan + madow ahaan + Qori tuur + Bilow ciyaar cusub Cadaankaa badiyey - Madoowbaa badiyey - Waxaad ku ciyaaraysaa kuwa cadaanka - Waxaad ku ciyaraysaa kuwa madoowga + Madowgaa badiyey + Waxaad tahay kooxda cad + Waxaad tahay kooxda madow Waa markaagii! - Tartankii waa dhamaaday + Dareen khiyaamo + Dhexdu boqran + Sadex jeg + Baratanki wuu dhamaaday + Dhamaadka noocan + Horjeede cusub + Horjeedahaagu ciyaar cusub buu kaa rabaa Kubiir ciyaarta Cadaanbaa baa ciyaaraya Madowbaa ciyaaraya + + Horjeedahaagi wuu ka baxay ciyaarta. Waad badin kartaa %s sikin gudihii. + Horjeedahaagi wuu ka baxay ciyaarta. Waad badin kartaa %s sikin gudohood. + + Horjeedahaagi wuu ka baxay ciyaarta. Waxad kala dooran kartaa badis, baraaje ama sug. Guusha qaado - Bareejo dalbo - Fadlan si wanaagsan u isticmal luqa! - caadaan wuu iscasilay - Madow wa iscasilay - Cadaan wuu ka baxay ciyaarta - Madow wuu ka baxay ciyaarta - Cadaan waxba ma dhaqaaqijin - Madow waxba ma dhaqaajin - Dalbo in computer falanqeeyo - Falanqaynta computerka - falanqaynta computerka wa diyaar - falanqayn computerka maleh + Baraaje dalbo + Fadlan si wanaagsan u isticmal luuqa! + Qofka ugu horreeya ee laynkan soo raacaa kula ciyaaraya. + Caddaankaa isdhiibay + Madowgaa isdhiibay + Cadaankaa ka baxay ciyaarta + Madowgaa ka baxay ciyaarta + Cadaanku muu dhaqaaqin + Madowgu muu dhaqaaqin + Dalbo falanqayn komyuutar + Falanqayn komyuutar + Falanqayn komyuutar baa jirta + Falanqayn komyuutar ma furna Looxa falanqaynta - Gudaha usii gal - Itus khatarta + Mug %s + Miciinsi falanqayn adeege + Shaqayn adeege... + Cabirid dhaqdhaqaaq... + Qalad adeege + Falanqayn hawo + Sii gudagal + Tus khatarta + aaladdaada + Dooro flanqaynta aaladdaada + Hore u wad faracan + Sida ugu caansan ka dhig + Ka tuur halkan + Khasab faracan + Koobi garee PGNka faracan Dhaqaaq + Guukdarro nooc + Nooc badis + Qalab yari + Dhaqaaq askari + Qabasho Xidh Guuleysta Lumin Barbardhac Lama garanayo + Keydka weyn + Caddaan / Baraaje / Madow + Qiimaynta isku celceliska ah: %s + Ciyaarihii ugu bambeeyey + Ciyaaraha ugu sareeya + Ciyaaraha MD ee %1$s+ ciyaartoyda FIDE ee %2$s ilaa %3$s + + Mayd %s badh-dhaqaaq gudihii + Mayd %s badh-dhaqaaq gudohood + + DTZ50\" la soo gaabiyey, kuna salaysan tirada badh-dhaqaaq ee ka hadhay qabashada ama dhaqaaqa askari ee xiga + Ciyaari ma jirto + Muggii ugu weynaa la gaadh! + Malaha kaga soo dar ciyaaro kale meesha doorashada? + Furitaanada + Furitaan baadhaha + Furitaan/dhamaad baadhaha + Furitaan baadhaha %s + Ciyaar dhaqaaqa ugu horeeya ee furitaan/dhamaad-baadhaha + Xeerka 50 tallaabo ayaa diiday guul + Xeerka 50 tallaabo ayaa diiday guuldarro + Guul ama 50 tallaabo khalad hore dartii + Guuldarro ama 50 tallaabo khalad hore dartii + Diyaar! + Soo geli PGN + Tuur + Tuur ciyaartan la soo geliyey? + Qaabka ku soo celinta Waqtiga dhabta ah + Inta muhiinka ah + Fur casharka Fur - CPU + Falaadha tallaabada ugu fiican + Tus falaadhaha faracyada + Qiinqaynta tallaabada + Shaxo badan + CPUyada + Baaxadda + Falanqayn bilaa xad ah + Mug bilaa xad ah oo komyuutarkaaga kululaynaya Qalad Qalad Khalad + + %s qalad + %s qaladaad + + + %s halmaam + %s halmaamyo + + + %s aan fiicnayn + %s aan fiicnayn + + Wakhtiyada tallaabo + Rog looxa + Ku celin sadex jeer + Ku dhawaaq baraaje + Codso baraaje + Baraaje + + %s ciyaartow + %s ciyaartoy + + Heshiis baraaje + Konton dhaqaaq bilaa natiijo + Ciyaaraha hadda + + %s ciyaar + %s ciyaarood + + + Darajada %1$s ee %2$s ciyaar + Darajada %1$s ee %2$s ciyaarood + + + %s calaamad + %s calaamadood + + Weynee daaqadda + Ka bax + Gal + Ha ka bixin + Akoon baad u baahanahay + Is diwaangeli + Komyuutar iyo qof qish komyuutar isticmaalaya lama ogola. Fadlan ha isticmaalin qish komyuutar ama caawin qof kale markad ciyaaraysid. Iyana ogow in samayska akoono badan aad looga soo hor jeedo, oo haddii ad sidaa samaysid akoonkaaga la xidhi karo. Ciyaaraha - Madal + Barta bulshada + %1$s ayaa ku soo qoray sheekada %2$s + Qoraalkii u dambeeyay ee barta bulshada Ciyaartoyda Saaxiibo + Sheekaysiyada Maanta Shalay + Mirirada dhinaciiba + Faraca + Faracyada + Xadka wakhtiga + Imika + Maalinle + Maalmood doorkiiba + Hal maalin + + %s maalin + %s maalmood + + + %s saacad + %s saacadood + + + %s mirir + %s mirir + Waqtiga + Qiimaynta + Daraasada qiimaynta + Magaca-akoon + Magac-akoon ama iimayl + Beddel magac-akoonka + Weynida xarfaha oo kaliiyaa is bedeli kara. Tusaale ahaan \"hebel\" iyo \"Hebel\". + Magac-koonkaaga beddel. Mar keliyaa kuu banana oo aad bedeli karto weynida xarfaha uun. + Magac-akoon edeb leh dooro. Ma bedeli kartid mar dambe waana la xidhi doonaa magicii edeb darro ah! + Waxan u isticmaalaynaa cusbaynta furahaaga oo keli ah. Erey sir + Beddel furaha + Beddel iimaylka + Iimayl + Cusboonaysii furaha + Ma ilowday furaha? + Furahani waa caan, si fidud baa loo nasiibin karaa. + Fadlan magaca-akoonka boggaaga ha ka dhigan fure. + Fure la mid ah ayaad u isticmaashay bog kale. Bogaasna waa la jabsaday. Si ad u ilaaliso nabada akoonkaaga Lichess, waxad u baahantahay fure cusub. Waad ku mahadsantay in ad na fahantay. + Waad ka baxaysaa Lichess + Weligaa ha ku qorin furaha Lichess bog kale! + Ku sii soco %s + Fure laguu sheegay ha ka dhigan fure. Waxay Kaa xadi karaan akoonka. + Wax laguu sheegay ha ka dhigan iimayl. Waxa lagaa xadi karaan akoonka. + Caawin hubinta iimaylka + Maad helin iimaylka hubinta diwaangelin ka dib? + Magac-akoonkee baad u isticmaashay diwaangelinta? + Maanu helin akoon magacan leh: %s. + Magacan-akoonkan waxad ku samayn kartaa akoon cusub + Iimayl baan u dirnay %s. + Wakhti yar sii si ay kuu soo gaadho. + Sug 5 mirir oo ka dib baadh iimaylkaaga. + Sidoo kale eeg spam folderkaaga, halkaasaad ka heli kartaa. Haddii ay sidaa tahay, u beddel \"not spam\". + Haddii ay waxba shaqayn waayaan, noosoo dir iimaylkan: + Koobi garee qoraalka sare oo u soo dir %s + Waanu kuu soo laaban in yar ka dib si an kaaga caawinno diwaangelinta. + Akoonka %s waa la hubiyay si guul leh. + Hadda waxad ku geli kartaa %s. + Uma baahnid iimayl hubsasho ah. + Akoonka %s wuu xidhanyahay. + Akoonka %s wuxuu diwaangashanyay bilaa iimayl. Darajo Darajo: %s + + Qiimayntu waxay cusboonaataa mirir kasta + Qiimayntu waxay cusboonaataa %s mirir oo kasta + - %s halxiraalaha - %s xujooyinka + %s xujooyinka + %s xujo + Ciyaaraha dhamaaday + + %s mar buu kula kulmay + %s mar buu kula kulmay + + Ka noqo + Wakhtigii ba ka dhamaaday cadaanka + Wakhtigaa ka dhacay madowga + La dir dalbasho baraaje + Waa la aqbalay baraaje + Waa laga noqday dalbasho baraaje + Cadaankaa dalbaday baraaje + Madowgaa dalbaday baraaje + Caddaanku wudu diiday baraaje + Madowgu wuu diiday baraaje + Horjeedahaagu wuxu kaa dalbaday baraaje Aqbal Diid Ciyaarta hadda Ciyaarta hadda Dhameystiray + Tuur ciyaartan + Ciyaartan waa la tuuray Heerka - Xadidneyn + Bilaa xad + Nooca + Aan qiimaysnayn + Qiimaysan + Aan qiimaysnayn + Qiimaysan + Ciyaartani way qiimaysantay + Ku celi + La dir codsi ku celin ah + Waa la aqbalay codsi ku celin ah + Waa laga noqday codsi ku celin ah + Waa la diiday codsi ku celin ah + Ka noqo codsi ku celin ah + Eeg ciyaarta ku celinta ah + Hubso tallaabada Ciyaar Sanduuqa + Luuqa + Gal luuqa + Wakhtigii baa kaa dhamaaday. + Qolka daawadaha + Farriin samee + Hordhac Dir + Taran ah sikino + + %s ciyaaraya + %s ciyaaraya + + Qiimayntayda + Horjeedahaagu wuxuu kaa dalbaday ka noqosho + + %s cashar + %s cashar + + Ku noqo tartanka + Ku noqo ciyaarta + Ciyaarta jesta oo onlayn ah bilaashna ah. Ku ciyaar jes madal nadiif ah. Uma baahna diwaangelin, ma leh xayeysiis, umana baahna adeeg kale. La ciyaar jes komyuutarka, asxaabtaada amma dadka hawada ku jira. + %1$s wuxu ku biiray kooxda %2$s Goobta Dib u dajin Badbaadiyo + Horjeedayaasha aad jeceshay + La soco %s + Ka hadh %s Ka badan Ciyaaryahan Ku biir Dhibco + Horjeedaha celceliska ah Gaar ah - Halxiraalaha + + %s ciyaar baa socota + %s ciyaarood baa socda + + Xujooyin + Waxa soo geliyey %s + Warbixino + Qoraalada + Halkan ku qor qoraalo kuu gooni ah + Magacan-akoonkan ama iimayl khaldan + Taranka saacadda Cod Marna Guulayso + Horjeede + Baro Bulshada Qalabka + Taran + Daawo ciyaaro + Daawo + 50 ciyaarood Fischer wuxuu ka keenay 47 guulood, 2 baraaje iyo 1 guuldarro. Abuur + Jes maalinle ah + Xalka eeg + Kulan degdeg ah + Dooro kulan Qarsoodi + Dhibcahaaga: %s Luqadda Iftiin Madow Hufan + Magacan-akoonkan waa la haystaa, fadlan tijaabi mid kale. + Magacan-akoonku waa inuu ku bilaabmo xaraf. + Magacan-akoonku waa inuu ku dhamaado xaraf ama tiro. + Magacan-akoonka waxa u banana xarfo, tiro, xarriiq hoose, iyo xarriiq isku xidhe ah. Xarriiquhu iskuma xigi karaan. + Magacan-akoonkan lama aqbali karo. + Aasaaska jesta Tababarayaal Xiga Xalka Waan ka xumahay :( - Maxaa sababay? + Waayo? + Dhibcaha abid + Jes maalinle ah: hal ama dhowr cisho dhaqaaqiiba + Tababaraha xeeladaha jesta + Ciyaarta >< %1$s + Caawintan tus + Is deji! + Waxaad la ciyaaraysaa hadda %s. + Tuur ciyaarta + Is dhiib ciyaartan + Ciyaar kale ma bilaabi kartid inta aanay tani fhamaan. + Ka dib + Ka hor + Ciyaaraha qiimaysan ee ka dhacay Lichess + Beddel kooxda diff --git a/translation/dest/site/sq-AL.xml b/translation/dest/site/sq-AL.xml index 0402e80293583..ff0db42b4902b 100644 --- a/translation/dest/site/sq-AL.xml +++ b/translation/dest/site/sq-AL.xml @@ -271,7 +271,6 @@ Po luhet tani Po luhet tani Përfundoi - përfundon %s Ndërprite lojën Loja u ndërpre Standard @@ -462,7 +461,7 @@ loje dhe URL për ta ndarë me të tjerë. Rregullime të thelluara Zgjidhni një emër shumë të sigurt për turneun. Çfarëdo gjëje qoftë edhe pakëz e papërshtatshme mund të sjellë mbylljen e llogarisë tuaj. - Për ta emërtuar turneun me emrin e një lojtari të njohur shahu, lëreni të zbrazët. + Për ta emërtuar turneun me emrin e një lojtari të njohur shahu, lëreni të zbrazët. Rekomandojmë të mos i prekni këto. Nëse ujdisni domosdoshmëri pjesëmarrjeje, turneu juaj do të ketë më pak lojtarë. Shfaq rregullimet të thelluara @@ -526,7 +525,7 @@ loje dhe URL për ta ndarë me të tjerë. Kalo automatikisht në lojën tjetër pas lëvizjes Kalim automatik Ushtrime - Fitues të turneve + Fitues të turneve Emër Përshkrim Përshkrim privat @@ -738,6 +737,7 @@ loje dhe URL për ta ndarë me të tjerë. Me shokët Me gjithkënd Mënyra për fëmijë + Mënyra për fëmijë është e aktivizuar. Kjo është për sigurinë. Nën mënyrën për fëmijë, krejt komunikimet në sajt janë të çaktivizuara. Aktivizojeni këtë për fëmijët dhe nxënësit tuaj të shkollave, për t’i mbrojtur ata nga të tjerë përdorues të internetit. Nën mënyrën për fëmijë, stemës së Lichess-it i vihet një ikonë %s, që ta dini se fëmijët tuaj janë të parrezik. Llogaria juaj administrohet. Rreth heqjes së mënyrës “fëmijë” pyetni mëuesin tuaj të shahut. @@ -851,9 +851,10 @@ loje dhe URL për ta ndarë me të tjerë. edhe ruaj %s linja premove edhe ruaj %s linja premove + Morët një mesazh privat nga Lichess. + Klikoni këtu që ta lexoni Na ndjeni :( Na u desh t’ju ndalnim për ca kohë. - Ndalesa skadon më %s. Pse? Synojmë të ofrojmë përvojë të këndshme me shahun për këdo. Për këtë qëllim, duhet të garantojmë që krejt tarët të ndjekin praktikat e mira. @@ -912,9 +913,6 @@ loje dhe URL për ta ndarë me të tjerë. Koha gati mbaroi! [Klikoni që të zbulohet adresa email] Shkarkoje - Mirë se vini! - Lichess është një program bamirësie dhe krejtësisht falas/libre, me burim të hapët. -Krejt kostot operative, zhvillimi dhe lënda financohen vetëm me dhurime nga përdoruesit. Trajner Përgjegjës transmetimesh Anulojeni turneun @@ -975,4 +973,6 @@ Që lojërat të fillojnë nga pozicioni fillestar normal, lëreni të zbrazët. Këshillat tona për organizim veprimtarish Udhëzime Shfaqmë gjithçka + Lichess është një program bamirësie dhe krejtësisht falas/libre, me burim të hapët. +Krejt kostot operative, zhvillimi dhe lënda financohen vetëm me dhurime nga përdoruesit. diff --git a/translation/dest/site/sr-SP.xml b/translation/dest/site/sr-SP.xml index 82e6725176753..3e80054bcf9d1 100644 --- a/translation/dest/site/sr-SP.xml +++ b/translation/dest/site/sr-SP.xml @@ -230,7 +230,6 @@ Управо игра Управо игра Завршен - завршава се %s Прекините партију Партија прекинута Стандардно @@ -792,7 +791,6 @@ Извините :( Морали смо Вас привремено избацити. - Тајм аут истиче %s. Зашто? Желимо да свима обезбедимо пријатно искуство у шаху. Баш због тога, ми се морамо потрудити да се сви играчи држе добре праксе. @@ -839,5 +837,4 @@ %1$s против %2$s Време ускоро изтиче! Преузми - Добродошли! diff --git a/translation/dest/site/sv-SE.xml b/translation/dest/site/sv-SE.xml index ecb40b78c16f1..655982e649ed3 100644 --- a/translation/dest/site/sv-SE.xml +++ b/translation/dest/site/sv-SE.xml @@ -272,7 +272,6 @@ Spelas just nu Spelas just nu Slut - slutar %s Avbryt partiet Partiet avbröts Standard @@ -405,6 +404,7 @@ Importera parti Klistra in ett partis PGN-kod så får du en bläddringsbar uppspelning, en datoranalys, en spel-chatt och en delbar URL. Variationer kommer att raderas. För att behålla dem, importera PGN:en via en studie. + Denna PGN kan nås av allmänheten. För att importera ett parti privat, använd en studie. %s Importerat parti %s Importerade partier @@ -506,8 +506,9 @@ Ändra profil Förnamn Efternamn - Ställ in din ikon: - Det finns en inställning för att dölja alla användarikoner över hela webbplatsen. + Ställ in din flair + Flair + Det finns en inställning för att dölja alla användarflairs över hela webbplatsen. Biografi Land eller region Tack! @@ -746,6 +747,7 @@ Med vänner Med alla Barnsäkert läge + Barnsäkert läge är aktiverat. Detta är en säkerhetsinställning. I barnsäkert läge är all kommunikation inaktiverad. Använd detta för dina barn och skolelever för att skydda dem från andra internetanvändare. I barnsäkert läge får lichess-logotypen en %s ikon, så att du vet att dina barn är säkra. Ditt konto hanteras. Fråga din schacklärare om att ta bort barnläget. @@ -859,9 +861,10 @@ och spara %s linje av förhandsdrag och spara %s linje av förhandsdrag + Du har fått ett privat meddelande från Lichess. + Klicka här för att läsa den Beklagar :( Vi är tvugna att stänga av dig en stund. - Avstängningen upphör om %s. Varför? Vårt mål är att tillhandahålla alla en behaglig schackupplevelse till alla. Därför måste vi försäkra oss om att alla spelare följer god sed. @@ -881,6 +884,7 @@ Jag instämmer med att jag kommer att följa alla Lichess-regler. Sök eller starta ny konversation Redigera + Bullet Blitz Snabbschack Classical @@ -921,9 +925,6 @@ Tiden är nästan slut! [Klicka för att visa mailadress] Ladda ner - Välkommen! - Lichess är en välgörenhet och helt gratis/fri programvara med öppen källkod. -Alla driftskostnader, utveckling och innehåll finansieras enbart av användardonationer. Coach-hanterare Stream-hanterare Avbryt turneringen @@ -984,4 +985,6 @@ Lämna tomt för att starta spel från den normala startpositionen. Våra tips för anordnande av evenemang Instruktioner Visa mig allt + Lichess är en välgörenhet och helt gratis/fri programvara med öppen källkod. +Alla driftskostnader, utveckling och innehåll finansieras enbart av användardonationer. diff --git a/translation/dest/site/sw-KE.xml b/translation/dest/site/sw-KE.xml index eaf3ffdea45ab..789b7dae29b07 100644 --- a/translation/dest/site/sw-KE.xml +++ b/translation/dest/site/sw-KE.xml @@ -120,6 +120,5 @@ Inachezwa Inaendelea Imekamilika - itakamilika ndani ya %s cheza tena diff --git a/translation/dest/site/ta-IN.xml b/translation/dest/site/ta-IN.xml index 60d89fe709aae..cd171df462e73 100644 --- a/translation/dest/site/ta-IN.xml +++ b/translation/dest/site/ta-IN.xml @@ -272,7 +272,6 @@ இப்பொழுது ஆடுகின்றது இப்பொழுது ஓடிக்கொண்டிருக்கின்றன முடிந்தது - %s வினாடிகளில் முடியும் ஆட்டத்தைக் கலை ஆட்டம் கலைந்தது மரபு @@ -494,7 +493,7 @@ தகவல்களை மாற்று முதல் பெயர் கடைசி பெயர் - உங்கள் திறமையை அமைக்கவும்: + உங்கள் திறமையை அமைக்கவும் தளம் முழுவதும் அனைத்து பயனர் திறமைகளையும் மறைக்க ஒரு அமைப்பு உள்ளது. வாழ்க்கை சரித்திரம் நாடு அல்லது வட்டாரம் @@ -797,7 +796,6 @@ மன்னிக்கவும் :( நாங்கள் உங்களை சிறிது நேரம் வெளியேற்ற வேண்டியிருந்தது. - நேரம் முடிந்தது %s. ஏன்? அனைவருக்கும் இனிமையான சதுரங்க அனுபவத்தை வழங்குவதை நோக்கமாகக் கொண்டுள்ளோம். அந்த வகையில், அனைத்து வீரர்களும் நல்ல பயிற்சியைப் பின்பற்றுவதை உறுதி செய்ய வேண்டும். diff --git a/translation/dest/site/te-IN.xml b/translation/dest/site/te-IN.xml index 3a9ceeb7ec222..42aaf7a680bfe 100644 --- a/translation/dest/site/te-IN.xml +++ b/translation/dest/site/te-IN.xml @@ -225,7 +225,6 @@ ఆట ఆడుతున్నారు ఆట ఆడుతున్నారు పూర్తయింది - మిగిలిన సమయం %s ఆట ఆగిపోయింది గేమ్ ఆగిపోయినది ప్రామాణికం diff --git a/translation/dest/site/th-TH.xml b/translation/dest/site/th-TH.xml index a9e9c5a95ce4f..59c65076aefc1 100644 --- a/translation/dest/site/th-TH.xml +++ b/translation/dest/site/th-TH.xml @@ -5,6 +5,7 @@ ให้ URL นี้เพื่อเชิญคนมาเล่น จบเกม กำลังรอคู่แข่ง + หรือให้คู่แข่งคุณสแกน QR code นี้ กำลังรอ ตาคุณ %1$s ระดับ %2$s @@ -101,6 +102,8 @@ เล่นการเปิดเกม/จบเกม-ค้นหาตาเดิน พลาดชัยชนะเนื่องจากกฎการเดิน 50 รอบ รอดจากการแพ้ด้วยกฎการเดิน 50 รอบ + ชนะหรือเดิน 50 ตาโดยไม่ได้ตั้งใจ + แพ้หรือเดิน 50 ตาโดยไม่ได้ตั้งใจ พร้อมลุย! ป้อน PGN ลบ @@ -253,7 +256,6 @@ กำลังเล่น กำลังแข่ง จบแล้ว - เสร็จสิ้น%s ยกเลิกเกม เกมถูกยกเลิก มาตรฐาน @@ -373,6 +375,7 @@ นำเข้าเกม เมื่อวาง PGN ของเกมแล้ว คุณจะได้รับความสามารถเรียกดูการเล่นซ้ำ, การวิเคราะห์ด้วยคอมพิวเตอร์, แชทของเกม และ URL ที่สามารถแชร์ได้ การเดินรูปแบบต่างๆ จะถูกลบทิ้ง หากต้องการเก็บไว้ ให้นำเข้า PGN ผ่านการศึกษา + PGN นี้เป็นสาธารณะ หากอยากจะนำเข้าเกมแบบส่วนตัว โปรดใช้หน้ากรณีศึกษา %s เกมที่ถูกนำเข้า @@ -470,6 +473,7 @@ ชื่อจริง นามสกุล ตั้งค่ารูปตกแต่ง + รูปตกแต่ง มันมีการตั้งค่าที่ทำให้ไม่สามารถเห็นรูปตกแต่งของผู้ใช้ได้ทั้งเวบไซต์ ชีวประวัติ ประเทศหรือภูมิภาค @@ -704,6 +708,7 @@ กับเพื่อนๆ กับทุกคน โหมดเด็ก + โหมดเด็กถูกเปิดใช้งาน สิ่งนี้เป็นเรื่องเกี่ยวกับความปลอดภัย ในโหมดสำหรับเด็ก การสื่อสารของไซต์ทั้งหมดจะถูกปิด จงใช้งานสิ่งนี้สำหรับเด็กและนักเรียนของคุณ เพื่อปกป้องพวกเขาจากผู้ใช้อินเทอร์เน็ตอื่นๆ ในโหมดสำหรับเด็ก โลโก้ lichess จะมีไอคอน %s เพื่อให้คุณรู้ว่าเด็กๆของคุณปลอดภัย บัญชีของคุณได้รับการจัดการ ถามครูหมากรุกของคุณเกี่ยวกับการยกระดับโหมดเด็ก @@ -813,9 +818,10 @@ และบันทึก %s เส้นทางเดินล่วงหน้า + คุณได้รับข้อความส่วนตัวจาก Lichess + คลิกที่นี้เพื่ออ่าน ขออภัย :( เราจำเป็นต้องให้คุณรอคอยสักช่วงหนึ่ง - เวลารอคอย สิ้นสุดใน %s ทำไม? เรามุ่งหวังจะมอบประสบการณ์เกมหมากรุกที่น่าพอใจให้กับทุกคน เพื่อให้ได้ผล เราต้องแน่ใจว่าผู้เล่นทุกคนได้ปฏิบัติตามเป็นอย่างดี @@ -835,6 +841,7 @@ ฉันยอมรับว่า ฉันจะปฏิบัติตามทุกนโยบายของ Lichess ค้นหา หรือเริ่มการสนทนาใหม่ แก้ไข + บุลเล็ต บลิตซ์ แรปพิด คลาสสิก @@ -875,9 +882,6 @@ เวลาใกล้จะหมดแล้ว [คลิกเพื่อเผยที่อยู่อีเมล] ดาวน์โหลด - ยินดีต้อนรับ! - Lichess เป็นมูลนิธิ และเป็นซอฟต์แวร์โอเพนซอร์สที่ฟรีอย่างสิ้นเชิง -ค่าบริการ การพัฒนา และเนี้อหาของ Lichess ได้รับทุนสนับสนุนจากการบริจาคเงินของผู้ใช้เว็บไซต์อย่างเดียว ตัวจัดการผู้ฝึกสอน ตัวจัดการผู้สตรีม ยกเลิกทัวร์นาเมนต์ @@ -933,4 +937,6 @@ คำแนะนำในการจัดการอีเว้นท์นี้ คำแนะนำ แสดงให้ฉันทุกอย่าง + Lichess เป็นมูลนิธิ และเป็นซอฟต์แวร์โอเพนซอร์สที่ฟรีอย่างสิ้นเชิง +ค่าบริการ การพัฒนา และเนี้อหาของ Lichess ได้รับทุนสนับสนุนจากการบริจาคเงินของผู้ใช้เว็บไซต์อย่างเดียว diff --git a/translation/dest/site/tk-TM.xml b/translation/dest/site/tk-TM.xml index d8559422824a2..c577e55fc56e3 100644 --- a/translation/dest/site/tk-TM.xml +++ b/translation/dest/site/tk-TM.xml @@ -198,7 +198,6 @@ Häzir oýnalýar Progressiýada Tamamlanan - gutarýar %s Oýny abort et Oýun abort edildi Standart diff --git a/translation/dest/site/tl-PH.xml b/translation/dest/site/tl-PH.xml index c1b495af641c5..2f913a2ec859f 100644 --- a/translation/dest/site/tl-PH.xml +++ b/translation/dest/site/tl-PH.xml @@ -220,7 +220,6 @@ Maglalaro ngayon Naglalaro ngayon Tapos na - matatapos ng %s Itigil ang laro Ang laro ay natigil Pamantayan @@ -773,7 +772,6 @@ Pasensya na :( Kinailangan naming tapusin ang oras mo pansamantala. - Ang pagtatapos ng oras ay magwawakas sa loob ng %s. Bakit? Nilalayon naming makapagbigay ng isang nakalulugod na karanasan sa ahedres para sa lahat. Sa ganoong kahulugan, kailangan naming siguruhin na ang lahat ng mga manlalaro ay susunod sa mabuting gawi. @@ -830,9 +828,6 @@ Yung oras mo ay malapit na matapos! [Mag-click upang ipakita ang email address] Download - Maligayang pagdating! - Ang Lichess ay isang charity at buong libre / libre open source software. -Ang lahat ng mga gastos sa pagpapatakbo, pag-unlad, at nilalaman ay pinopondohan lamang ng mga donasyon ng gumagamit. Tagapamahala ng coach Tagapamahala ng streamer Kanselahin ang paligsahan @@ -890,4 +885,6 @@ Mag-iwan ng walang laman upang magsimula ng mga laro mula sa normal na paunang p Na-rate na mga laro na pinakita mula sa lahat ng manlalaro ng Lichess Lumipat ng sides Ang pagsasara ng iyong account ay babawiin ang iyong apela + Ang Lichess ay isang charity at buong libre / libre open source software. +Ang lahat ng mga gastos sa pagpapatakbo, pag-unlad, at nilalaman ay pinopondohan lamang ng mga donasyon ng gumagamit. diff --git a/translation/dest/site/tlh-AA.xml b/translation/dest/site/tlh-AA.xml index 79a1558f80f9b..47fd7b9e7ff6a 100644 --- a/translation/dest/site/tlh-AA.xml +++ b/translation/dest/site/tlh-AA.xml @@ -139,5 +139,4 @@ qatlh? choH lI\' - qavan! diff --git a/translation/dest/site/tp-TP.xml b/translation/dest/site/tp-TP.xml index 1e6fdff7af390..db8d4f39fea41 100644 --- a/translation/dest/site/tp-TP.xml +++ b/translation/dest/site/tp-TP.xml @@ -255,7 +255,6 @@ musi li lon musi lon tenpo ni ni li pini - pini %s o pini e musi musi li pini nasin pi ante ala @@ -486,6 +485,7 @@ o musi lon lipu pona. sina wile ala pana e nimi li wile ala e ilo. sitelen esun ante e sona jan nimi jan nimi mama + sitelen namako sona jan sina pona tawa sina! nimi nasin pi lipu toki jan @@ -816,9 +816,10 @@ sina wile e ni la, o pali e tawa ken pi jan musi tu. sina pali e ni la, tawa %s pi tenpo kama ken li awen sina pali e ni la, tawa %s pi tenpo kama ken li awen + jan Lichess li toki len tawa sina + sina wile lukin e toki la o luka e ni pakala a :( sina wile awen lon tenpo lili. - tenpo kama la sina ken %s lon sin. tan seme? mi wile e musi pona tawa jan ale. tan ni la, mi wile e ni: jan ale li pona tawa nasin pi musi pona. @@ -838,6 +839,7 @@ sina wile e ni la, o pali e tawa ken pi jan musi tu. mi toki wawa e ni: mi pali ala e ijo ni: ona li ike tawa nasin lawa pi ilo Lichess. o lukin anu open e toki o ante + musi pi nasin Bullet nasin Rapid nasin Classical musi pi tenpo lili lili lili (tenpo Sekunta 30 li suli) @@ -877,9 +879,6 @@ sina wile e ni la, o pali e tawa ken pi jan musi tu. tenpo lili li lon a! [pilin la sina ken lukin e ma pi lipu kon] o pana e musi tawa ilo sona mi - o kama pona! - ilo Lichess li mani ala li pona. ale li ken lukin e lipu ilo pi ilo Lichess li ken ante e ilo Lichess. -ona li wile e mani la jan li wile pana e mani tawa ona. lipu pi jan pi pana sona nasin pali pi sitelen tawa o moli e musi @@ -936,4 +935,6 @@ sina wile kepeken e nasin ijo pi ante ala, o sitelen ala. poki pini tenpo musi ale pi nanpa wawa tan jan musi ale pi ilo \"Lichess\" o lukin tan poka ante + ilo Lichess li mani ala li pona. ale li ken lukin e lipu ilo pi ilo Lichess li ken ante e ilo Lichess. +ona li wile e mani la jan li wile pana e mani tawa ona. diff --git a/translation/dest/site/tr-TR.xml b/translation/dest/site/tr-TR.xml index 7c3b3ae9c9280..bf56f07d4ed5e 100644 --- a/translation/dest/site/tr-TR.xml +++ b/translation/dest/site/tr-TR.xml @@ -272,7 +272,6 @@ Şu an oynanıyor Şu anda oynanıyor Bitti - %s sona erecek Oyunu iptal et Oyun iptal edildi Standart @@ -405,6 +404,7 @@ Oyun yükle Göz atılabilir bir oyun tekrarı, bilgisayar analizi, oyun sohbeti ve paylaşılabilir bir URL edinmek için bir oyun PGN\'si yapıştırın. Varyasyonlar silinecek. Varyasyonları saklamak için bir çalışma aracılığıyla PGN\'yi içe aktarın. + Bu PGN herkes tarafından erişilebilir. Bir oyunu özel olarak yüklemek istiyorsanız bir çalışma kullanın. %s yüklenen oyun %s yüklenen oyun @@ -659,8 +659,10 @@ Eş zamanlı gösteriler Ev Sahibi Ev sahibi rengi: %s + Bekleyen simultane oyunlarınız Yeni oluşturulan eş zamanlı gösteriler Eş zamanlı gösteri düzenle + Bir simultane oyuna katılmak veya ev sahipliği yapmak için kayıt olun Eş zamanlı gösteri bulunamadı Eş zamanlı gösteri yok. Eş zamanlı gösteri sayfasına geri dön @@ -685,6 +687,7 @@ Klavye kısayolları önceki/sonraki hamle başa/sona git + Seçilen varyasyonu değiştir yorumları gizle/göster varyasyona gir/çık Bilgisayar analizi talep et, Hatalarından ders al @@ -695,6 +698,8 @@ Önceki dal Sıradaki dal Varyasyon oklarını aç/kapat + Önceki/sonraki varyasyona geç + Varyasyon okları, hamle listesini kullanmadan gezinmenizi sağlar. seçili hamleyi oyna Yeni turnuva Çeşitli zaman kontrolleri ve varyantları içeren satranç turnuvası @@ -738,6 +743,7 @@ Yalnızca arkadaşlarımla Herkes ile Çocuk modu + Çocuk modu etkin. Bu mod çocuğunuzun güvenliği ile ilgilidir. Tüm site bağlantıları devre dışı bırakılır. Eğer çocuklarınızı veya okul öğrencilerinizi diğer internet kullanıcılarına karşı korumak istiyorsanız, bu modu etkinleştirin. Çocuk modunda, lichess.org logosu yerine %s belirir, bilin ki çocuğunuz güvendedir :) Hesabınız kısıtlandı. Çocuk modunu kaldırmak için satranç öğretmeninize danışın. @@ -851,9 +857,10 @@ ve %s önceki varyantları kaydedin ve %s önceki varyantları kaydedin + Lichess size bir özel mesaj gönderdi. + Okumak için buraya tıklayın Üzgünüz :( Sizi bir süreliğine oyunlardan men etmek zorunda kaldık. - Men süresi %s dolacak. Neden? Herkese keyifli bir satranç deneyimi sunmayı amaçlıyoruz. Bu nedenle, bütün oyuncuların doğru davranışlar sergilemesine özen gösteriyoruz. @@ -873,6 +880,7 @@ Lichess kurallarını takip edeceğim. Tartışma ara veya yenisini başlat Düzenle + Bullet Yıldırım Hızlı Klasik @@ -913,8 +921,6 @@ Zaman dolmak üzere! [E-posta adresini göstermek için tıklayın] İndir - Hoş geldiniz! - Lichess bir yardım kuruluşudur ve tamamen özgür/açık kaynak kodlu bir yazılımdır. Tüm işletme maliyetleri, geliştirmeler ve içerikler yalnızca kullanıcı bağışları ile finanse edilmektedir. Eğitmen ayarları Yayıncı ayarları Turnuvayı iptal et @@ -974,4 +980,5 @@ Başlangıç pozisyonundan oynamak için boş bırakınız. Etkinlik düzenlemek için ipuçlarımız Talimatlar Bana her şeyi göster + Lichess bir yardım kuruluşudur ve tamamen özgür/açık kaynak kodlu bir yazılımdır. Tüm işletme maliyetleri, geliştirmeler ve içerikler yalnızca kullanıcı bağışları ile finanse edilmektedir. diff --git a/translation/dest/site/tt-RU.xml b/translation/dest/site/tt-RU.xml index cc069c1bcb625..ab0c7076f9e09 100644 --- a/translation/dest/site/tt-RU.xml +++ b/translation/dest/site/tt-RU.xml @@ -206,7 +206,6 @@ Хәзер уйнала Хәзер уйнала Тәмамланган - %s сон бетә Уенны туктату Уен туктатылды Стандартлы @@ -712,7 +711,6 @@ Юаныч :( Без сезне бераз вакытка чыгарырга тиеш идек. - Тоту вакыты %s. Нигә? Без һәркемгә шаһмат тәҗрибәсен тәкъдим итәбез. Моның өчен без барлык уенчыларның да яхшы практиканы үтәргә тиеш. @@ -768,7 +766,6 @@ Кире кайтару %1$s %2$s рейтинг накате. Вакыт тугады диярлек! Йөкләп алу - Рәхим итегез! Ярыш тасвирламасы Туктагыз! diff --git a/translation/dest/site/uk-UA.xml b/translation/dest/site/uk-UA.xml index 79253be2066be..633ae45c3583b 100644 --- a/translation/dest/site/uk-UA.xml +++ b/translation/dest/site/uk-UA.xml @@ -5,6 +5,7 @@ Щоб запросити когось до гри, дайте це посилання Гру завершено Очікування на суперника + Або дайте вашому супернику просканувати цей QR-код Очікування Ваш хід %1$s, рівень %2$s @@ -302,7 +303,6 @@ Грається зараз Грається просто зараз Завершено - завершиться %s Скасувати гру Гру скасовано Стандартний @@ -462,6 +462,7 @@ Вставте PGN гри щоб отримати повтор в браузері, комп\'ютерний аналіз, ігровий чат та посилання, яким можна поділитися. Варіації будуть видалені. Для збереження імпортуйте PGN через дослідження. + Цей PGN може бути у вільному доступі. Для імпорту гри в приватному режимі використовуйте студії. %s імпортована гра %s імпортовані гри @@ -573,7 +574,8 @@ Редагувати профіль Ім\'я Прізвище - Оберіть свій тотем: + Оберіть свій тотем + Аватар Це налаштування вимикає аватари всіх користувачів сайту. Біографія Країна чи область @@ -818,6 +820,7 @@ Друзям Будь-кому Дитячий режим + Дитячий режим активовано. Це заради безпеки. У дитячому режимі усе спілкування на сайті вимкнено. Увімкніть цю функцію для ваших дітей та учнів, щоб захистити їх від інших користувачів Інтернету. В дитячому режимі логотип Lichess замінюється на %s, щоб ви знали, що ваші діти в безпеці. Ваш обліковий запис керується. Попросіть вашого вчителя шахів вимкнути дитячий режим. @@ -939,9 +942,10 @@ і зберегти %s послідовностей і зберегти %s послідовностей + Ви отримали особисте повідомлення від Lichess. + Натисніть тут, щоб прочитати Вибачте :( Нам довелося забанити вас на певний час. - Вам залишилося відпочивати ще %s. Чому? Ми хочемо, щоб усім було приємно грати у нас в шахи. Щоб домігтися цього ефекту, ми повинні впевнитися, що всі гравці добре поводяться. @@ -1001,8 +1005,6 @@ Час майже скінчився! [Натисніть щоб побачити електронну адресу] Завантаження - Вітаємо! - Lichess - це благодійне і абсолютно безкоштовне програмне забезпечення з відкритим кодом. Усі витрати на обслуговування, розробка та вміст фінансуються виключно за рахунок пожертвувань користувачів. Тренерські налаштування Стрімерські налаштування Скасувати турнір @@ -1060,4 +1062,5 @@ Змінити сторону Закриття облікового запису призведе до скасування вашої апеляції Наші поради щодо організації подій + Lichess - це благодійне і абсолютно безкоштовне програмне забезпечення з відкритим кодом. Усі витрати на обслуговування, розробка та вміст фінансуються виключно за рахунок пожертвувань користувачів. diff --git a/translation/dest/site/ur-PK.xml b/translation/dest/site/ur-PK.xml index 03124d50d4816..3804cc48ae832 100644 --- a/translation/dest/site/ur-PK.xml +++ b/translation/dest/site/ur-PK.xml @@ -221,7 +221,6 @@ فی الحال کھیل میں مصروف ہے فی الحال کھیل میں مصروف ہے ختم شد - ختم ہو گا %s کھیل منسوخ کريں منسوخ شد معروف @@ -744,7 +743,6 @@ معذرت :( ہمیں آپ پر کچھ عرصے کے لیے پابندی لگانا پڑی. - پابندی %s کے بعد ختم ہو گی. کیوں? ہم سب کو خوشگوار شطرنج کا تجربہ دینا چاہتے ہیں. اس مقصد کے لیے، ہمیں یقینی بنانا چاہئے کہ تمام کھلاڑی اچھی پریکٹس کا مظاہرہ کریں. diff --git a/translation/dest/site/uz-UZ.xml b/translation/dest/site/uz-UZ.xml index caf6f6c6e4461..ff17b514a6c86 100644 --- a/translation/dest/site/uz-UZ.xml +++ b/translation/dest/site/uz-UZ.xml @@ -270,7 +270,6 @@ Xozirni o\'zidayoq o\'ynash Xozirni o\'zidayoq o\'ynash Tugadi - %s tugaydi O\'yinni bekor qilish O\'yin bekor qilindi Standart @@ -845,7 +844,6 @@ Kechirasiz :( Biz sizga bir muncha vaqtga taym aut berishimiz kerak edi. - Taym aut %s dan keyin tugaydi. Nima uchun? Biz har bir kishiga shahmat malakasini oshirishni maqsad qilganmiz. Buning uchun biz barcha o\'yinchilar yaxshi amaliyot ortidan ketishi uchun imkoniyat yaratishimiz kerak. @@ -904,9 +902,6 @@ Vaqt deyarli yakunlandi! [Email manzilni ochiqlash uchun bosing] Yuklab oling - Хуш келибсиз! - Lichess - bu xayriyali va ochiq kodli bepul dasturiy ta‘minot. -Ishlab chiqish, saytni ma‘lumotlar bilan to‘ldirish kabi barcha boshqa amallar harajatlari xayriya jamg‘armasi hisobidan bajariladi. Murabbiylarga Strimmerlarga Turnirni yakunlash @@ -965,4 +960,6 @@ Normal boshlang‘ich holatdan boshlash uchun uni bo‘sh qoldiring. Tomonlarni almashtirish Akkountingizni yopishingiz sizni murojaatlaringizni bekor qiladi Tadbirlar o‘tkazish yuzasidan bizning maslahatlarimiz + Lichess - bu xayriyali va ochiq kodli bepul dasturiy ta‘minot. +Ishlab chiqish, saytni ma‘lumotlar bilan to‘ldirish kabi barcha boshqa amallar harajatlari xayriya jamg‘armasi hisobidan bajariladi. diff --git a/translation/dest/site/vi-VN.xml b/translation/dest/site/vi-VN.xml index 893608714e93c..c8d6dee8a7f28 100644 --- a/translation/dest/site/vi-VN.xml +++ b/translation/dest/site/vi-VN.xml @@ -5,6 +5,7 @@ Để mời ai đó chơi, hãy gửi URL này Ván cờ kết thúc Đang chờ đối thủ + Hoặc để đối thủ của bạn quét mã QR này Đang chờ Đến lượt bạn %1$s cấp độ %2$s @@ -15,12 +16,12 @@ Chịu thua Chiếu hết Hòa pat - Trắng - Đen + Quân Trắng + Quân Đen khi chơi quân trắng khi chơi quân đen Chọn màu quân ngẫu nhiên - Tạo ván cờ mới + Tạo một ván cờ Bên Trắng thắng Bên Đen thắng Bạn chơi quân trắng @@ -70,7 +71,7 @@ Xoá từ đây Đổi biến Sao chép biến PGN - Các nước đi + Nước đi Nước đi dẫn đến hết cờ Nước chiếu hết theo luật Thiếu quân để chiếu hết @@ -101,9 +102,9 @@ Chơi nước đầu tiên của người khám phá khai cuộc/tàn cuộc Chiến thắng bị ngăn cản bởi luật 50 nước Ván đấu được cứu thua bởi luật 50 nước - Giành chiến thắng hoặc 50 lần di chuyển do sai lầm trước + Giành chiến thắng hoặc 50 nước đi do sai lầm trước Thua hoặc 50 lần nước đi do nhầm lẫn trước đó - Chỉ được đảm bảo thắng/thua nếu dòng tàn cuộc được đề xuất đã được tuân theo kể từ lần bắt hoặc ăn quân cuối cùng, do có thể làm tròn các giá trị DTZ trong sách tàn cuộc Syzygy. + Thắng/thua chỉ được đảm bảo nếu dòng tàn cuộc được đề xuất đã được tuân theo kể từ lần ăn quân hoặc tiến tốt cuối cùng, do có thể làm tròn các giá trị DTZ trong sách tàn cuộc Syzygy. Đã xong! Nhập PGN Xóa @@ -145,7 +146,7 @@ Hòa do đồng ý hai bên 50 nước không có tiến triển - Những ván cờ đang chơi + Các ván cờ đang diễn ra %s ván cờ @@ -257,12 +258,11 @@ Đang diễn ra Đang diễn ra Hoàn thành - kết thúc trong %s Hủy ván cờ Ván cờ đã bị hủy bỏ Tiêu chuẩn Không giới hạn - Chế độ + Thể loại Không xếp hạng Có xếp hạng Không xếp hạng @@ -283,7 +283,7 @@ Bạn đã bị tạm dừng trò chuyện. Phòng khán giả Soạn tin nhắn - Chủ đề + Tiêu đề Gửi Gia tăng theo giây Chơi Cờ Vua Trực Tuyến Miễn Phí @@ -377,8 +377,9 @@ Sự Kiện Cờ Đồng Loạt Nghiên cứu Nhập ván cờ Dán PGN của ván đấu để xem lại trên trình duyệt, phân tích bằng máy tính, -trò chuyện trong ván đấu và có một URL có thể chia sẻ được. +trò chuyện trong ván đấu và có một URL có thể chia sẻ công khai. Các biến sẽ bị xóa. Để giữ chúng, hãy nhập PGN thông qua một nghiên cứu. + Ai cũng có thể truy cập PGN này. Để nhập ván cờ một cách riêng tư, hãy sử dụng nghiên cứu. %s ván cờ đã nhập @@ -430,7 +431,7 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ được.< Thiết lập nâng cao Hãy chọn tên chuẩn mực cho giải đấu. Một hành động dù chỉ một chút không thích hợp, tài khoản của bạn có thể bị khoá. - Hãy để trống để lấy tên theo tên một người chơi đáng chú ý. + Hãy để trống để lấy tên theo tên một người chơi đáng chú ý. Chúng tôi khuyên bạn không nên thay đổi. Nếu bạn thiết lập điều kiện tham gia, giải của bạn sẽ có ít người chơi hơn. Hiện thiết lập nâng cao @@ -475,7 +476,8 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ được.< Chỉnh sửa thông tin cá nhân Tên Họ - Đặt biểu tượng của bạn: + Đặt biểu tượng của bạn + Biểu tượng Có một cài đặt để ẩn tất cả biểu tượng của người dùng trên toàn bộ trang web. Tiểu sử Quốc gia hoặc khu vực @@ -486,8 +488,8 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ được.< Để bảo vệ và chia sẻ an toàn, hãy xem xét thực hiện một nghiên cứu. Xóa nước cờ Ván trước được lên Lichess TV - Các kỳ thủ đang trực tuyến - Các kỳ thủ tích cực + Các kỳ thủ trực tuyến + Những kỳ thủ tích cực Lưu ý, ván cờ có xếp hạng nhưng không tính thời gian! Thành công @@ -496,7 +498,7 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ được.< Tự động chuyển đến ván tiếp theo sau khi thực hiện nước đi Tự động chuyển Câu đố - Các nhà vô địch giải + Các nhà vô địch giải Tên Mô tả Mô tả riêng tư @@ -509,7 +511,7 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ được.< Số bài viết Bài đăng gần đây nhất Lượt xem - Bình luận + Số bình luận Bình luận chủ đề này Trả lời Tin nhắn @@ -567,7 +569,7 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ được.< %1$s %2$s trong %3$s Dòng thời gian Bắt đầu đấu: - Tất cả thông tin đều được công khai và không bắt buộc. + Tất cả thông tin đều công khai và không bắt buộc. Giới thiệu gì đó về bạn như sở thích, bạn thích gì ở cờ, khai cuộc yêu thích, ván cờ yêu thích, thần tượng, ... Tối đa: %s kí tự. @@ -615,10 +617,10 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ được.< Thư viện video Các Streamer Ứng dụng Điện thoại - Các nhà phát triển web + Nhà phát triển web Về chúng tôi Giới thiệu về %s - %1$s là một máy chủ cờ vua miễn phí (%2$s), không có quảng cáo, mã nguồn mở. + %1$s là một máy chủ cờ vua miễn phí (%2$s), có mã nguồn mở và không có quảng cáo. thật sự Đóng góp Điều khoản Dịch vụ @@ -635,9 +637,9 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ được.< Trở về trang chủ cờ đồng loạt Cờ đồng loạt gồm một người duy nhất chơi cùng lúc với nhiều người khác. Trong số 50 đối thủ, Fischer thắng 47, hoà 2 và thua 1. - Ý tưởng được lấy từ các sự kiện có thật. Trong đời thực, một người chủ trì cờ đồng loạt sẽ di chuyển từ bàn này qua bàn khác và đánh một nước mỗi bàn. + Ý tưởng được lấy từ những sự kiện có thật. Trong đời thực, một người chủ trì cờ đồng loạt sẽ di chuyển từ bàn này qua bàn khác và đánh một nước mỗi bàn. Khi cờ đồng loạt bắt đầu, mỗi người chơi sẽ bắt đầu ván cờ với người chủ trì. Cờ đồng loạt kết thúc khi tất cả các ván cờ hoàn tất. - Cờ đồng loạt luôn không tính Elo. Việc tái đấu, đi lại hay cho thêm thời gian đều bị vô hiệu. + Cờ đồng loạt luôn không tính xếp hạng. Việc tái đấu, đi lại hay cho thêm thời gian đều bị vô hiệu. Tạo Khi bạn tạo một sự kiện cờ đồng loạt, bạn sẽ chơi với nhiều người cùng một lúc. Nếu bạn chọn nhiều biến thể, mỗi người chơi sẽ được lựa chọn chơi biến thể nào. @@ -671,7 +673,7 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ được.< chơi nước đi đã chọn Giải đấu mới Giải đấu cờ vua với nhiều thiết lập thời gian và biến thể phong phú - Chơi các giải đấu cờ vua nhịp độ nhanh! Tham gia một giải đấu chính thức hoặc tự tạo giải đấu của bạn. Cờ Siêu Chớp, cờ Chớp, cờ Nhanh, cờ Chậm, Chess960, King of the Hill, Threecheck và nhiều lựa chọn khác cho niềm vui đánh cờ vô tận. + Chơi các giải đấu cờ vua nhịp độ nhanh! Tham gia một giải đấu chính thức hoặc tự tạo giải đấu của bạn. Cờ Đạn, cờ Chớp, cờ Nhanh, cờ Chậm, Chess960, King of the Hill, Threecheck và nhiều lựa chọn khác cho niềm vui đánh cờ vô tận. Không tìm thấy giải đấu Giải đấu này không tồn tại. Giải đấu có thể đã bị huỷ, nếu tất cả người chơi rời giải trước khi giải đấu bắt đầu. @@ -701,7 +703,7 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ được.< Tải tệp đã được nhập Bảng tổng điểm Bạn cũng có thể cuộn chuột trên bàn cờ để xem các nước đi của ván. - Cuộn qua các biến thể máy tính để xem trước chúng. + Cuộn qua các biến máy tính để xem trước chúng. Nhấn Shift+click hoặc nhấp chuột phải để vẽ vòng tròn, mũi tên trên bàn cờ. Cho phép người chơi khác gửi tin nhắn cho bạn Nhận thông báo khi được đề cập trong diễn đàn @@ -710,6 +712,7 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ được.< Với bạn bè Với mọi người Chế độ trẻ em + Chế độ trẻ em đã được bật. Điều này là để an toàn. Trong chế độ trẻ em, tất cả mọi giao tiếp trên trang web đều bị tắt. Kích hoạt điều này cho con của bạn và học sinh trong lớp để bảo vệ chúng khỏi những người dùng khác trên Internet. Trong chế độ trẻ em, biểu tượng Lichess có một biểu tượng %s, từ đó bạn biết con bạn được an toàn. Tài khoản của bạn đang bị quản lý. Hỏi giáo viên dạy cờ của bạn để lấy lại quyền điều khiển. @@ -748,7 +751,7 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ được.< Trong suốt Giao diện thiết bị URL ảnh nền: - Hình dạng của bàn cờ + Hình dáng bàn cờ Chủ đề bàn cờ Kích cỡ bàn cờ Bộ cờ @@ -758,7 +761,7 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ được.< Tên người dùng phải kết thúc với một chữ cái hoặc một số. Tên người dùng chỉ được chứa chữ cái, số, dấu gạch nối và dấu gạch dưới. Dấu gạch dưới và dấu gạch nối không được liên tiếp nhau. Tên người dùng này không được chấp nhận. - Chơi cờ vua phong cách + Chơi cờ vua theo phong cách Cờ cơ bản Huấn luyện viên PGN không hợp lệ @@ -819,9 +822,10 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ được.< và lưu %s nước đi trước + Bạn đã nhận được một tin nhắn riêng từ Lichess. + Nhấn vào đây để đọc nó Rất tiếc :( Chúng tôi phải ngừng bạn lại một thời gian. - Hết hiệu lực trong %s. Tại sao? Mục tiêu của chúng tôi là cung cấp trải nghiệm chơi cờ vui vẻ cho mọi người. Để đạt được mục đích, chúng tôi phải chắc chắn rằng mọi người phải tuân thủ tốt. @@ -841,6 +845,7 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ được.< Tôi đồng ý rằng, tôi sẽ luôn tuân thủ các chính sách của Lichess. Tìm hoặc bắt đầu một cuộc trò chuyện Chỉnh sửa + Cờ đạn Cờ chớp Cờ nhanh Cờ chậm @@ -881,18 +886,15 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ được.< Thời gian sắp hết! [Nhấp để tiết lộ địa chỉ email] Tải xuống - Chào mừng! - Lichess là một tổ chức phi lợi nhuận và là một phần mềm mã nguồn mở/hoàn toàn miễn phí. -Tất cả chi phí vận hành, phát triển và nội dung được tài trợ bởi sự đóng góp của người dùng. Quản lý huấn luyện viên Quản lý trang Streamer Hủy giải đấu Mô tả giải đấu - Có điều gì đặc biệt bạn muốn nói với những người tham gia không? Cố gắng viết ngắn gọn. Các liên kết cấu trúc Markdown có sẵn: [name](https://url) - Các ván đấu có tính Elo và tác động đến hệ số Elo của người chơi + Có điều gì đặc biệt bạn muốn nói với những người tham gia không? Cố gắng viết ngắn gọn. Các liên kết cấu trúc Markdown có sẵn: [Văn bản](https://url) + Các ván đấu có xếp hạng và ảnh hưởng đến hệ số Elo của người chơi Chỉ thành viên trong đội Không giới hạn - Số ván đã tính Elo tối thiểu + Số ván đã xếp hạng tối thiểu Elo tối thiểu Elo cao nhất trong tuần Chỉ những kỳ thủ có danh hiệu @@ -943,4 +945,6 @@ Bỏ trống để bắt đầu tất cả ván đấu bằng thế trận ban Lời khuyên của chúng tôi để tổ chức các sự kiện Hướng dẫn Cho tôi xem mọi thứ nào + Lichess là một tổ chức phi lợi nhuận và là một phần mềm mã nguồn mở/hoàn toàn miễn phí. +Tất cả chi phí vận hành, phát triển và nội dung được tài trợ bởi những đóng góp của người dùng. diff --git a/translation/dest/site/zh-CN.xml b/translation/dest/site/zh-CN.xml index 0c7568b6919e4..1f95ffe0a2453 100644 --- a/translation/dest/site/zh-CN.xml +++ b/translation/dest/site/zh-CN.xml @@ -257,7 +257,6 @@ 正在对局 正在进行 已结束 - 结束:%s 中止本局 棋局已中止 标准国际象棋 @@ -473,7 +472,7 @@ 编辑个人资料 - 设置你的图标: + 设置你的图标 有一个设置可以隐藏整个站点上的所有用户图标。 个人简介 国家或地区 @@ -815,7 +814,6 @@ 抱歉 :( 我们必须将你停止一段时间。 - 剩余时间:%s 为什么? 我们致力于为所有人提供一个愉悦的下棋体验。 因此,我们必须确保每个玩家都要遵循规范。 @@ -875,8 +873,6 @@ 时间快到了! [点击显示电子邮件地址] 下载 - 欢迎! - Lichess 是一个非盈利、完全免费自由的开源软件。所有的运维成本、开发以及内容完全来自用户捐赠。 教练管理 直播管理 取消锦标赛 @@ -935,4 +931,6 @@ 更换所持颜色 关闭账户将撤回你的上诉 举办赛事的小建议 + 全部展示 + Lichess 是一个非盈利、完全免费自由的开源软件。所有的运维成本、开发以及内容完全来自用户捐赠。 diff --git a/translation/dest/site/zh-TW.xml b/translation/dest/site/zh-TW.xml index e1359bff47c77..abc288c787b8a 100644 --- a/translation/dest/site/zh-TW.xml +++ b/translation/dest/site/zh-TW.xml @@ -69,6 +69,7 @@ 將這步棋導入主要流程中 從這處開始刪除 移除變化 + 複製變體 PGN 走棋 您因特殊規則而輸了 您因特殊規則而贏了 @@ -85,7 +86,7 @@ 平均評分: %s 最近的棋局 評分最高的棋局 - 兩百萬局來自%2$s到%3$s年國際棋聯積分%1$s以上的棋手對局棋譜 + 來自%2$s到%3$s年國際棋聯積分%1$s以上的棋手對局棋譜 在%s步內將死對手 @@ -97,6 +98,7 @@ 開局瀏覽器 開局與終局瀏覽器 %s開局瀏覽器 + 在開局/殘局瀏覽器走第一步棋 在不違反50步和局規則下贏得這局棋 藉由50步和局規則來避免輸掉棋局 贏棋或因先前錯誤50步作和 @@ -112,6 +114,7 @@ 打開研究視窗 開啟 最佳移動的箭頭 + 顯示變體箭頭 棋力估計表 路線分析線 CPU @@ -254,7 +257,6 @@ 正在對局 正在進行 已結束 - %s 後結束 中止本局 棋局已中止 標準 @@ -449,6 +451,7 @@ 步數 白方獲勝 黑方獲勝 + 和棋率 和棋 下一個%s錦標賽 平均對手評分 @@ -469,7 +472,11 @@ 編輯資料 + 設置你的圖標 + 圖標 + 有一個設置可以隱藏整個網站上所有用户圖標。 個人簡介 + 國家或地區 謝謝! 官方社群連結 每行一個網址 @@ -617,8 +624,10 @@ 車輪戰 主持 主持者使用旗子顏色:%s + 你待處理的車輪戰 最近开始的同步赛 主持新同步赛 + 註冊以舉辦或參與車輪戰 找不到该同步赛 此車輪戰不存在。 返回表演赛主页 @@ -643,6 +652,7 @@ 快捷键 后退/前进 跳到开始/结束 + 循環已選取的變體 显示/隐藏评论 进入/退出变体 請求引擎分析,從你的失誤中學習 @@ -650,6 +660,13 @@ 下一個漏著 下一個錯著 下一個疑著 + 上一個分支 + 下一個分支 + 切換變體箭頭 + 循環上一個/下一個變體 + 切換圖形標註 + 變體箭頭讓你不需棋步列表導航 + 走已選的棋步 新比赛 国际象棋赛事均设有不同的时间控制和变体 加入快節奏的國際象棋比賽!加入定時賽事,或創建自己的。子彈,閃電,經典,菲舍爾任意制,王到中心,三次將軍,並提供更多的選擇為無盡的國際象棋樂趣。 @@ -691,6 +708,7 @@ 與好友分享 與所有人分享 兒童模式 + 已啓用兒童模式 考量安全,在兒童模式中,網站上全部的文字交流將會被關閉。開啟此模式來保護你的孩子及學生不被網路上的人傷害。 在兒童模式下,Lichess的標誌會有一個%s圖示,讓你知道你的孩子是安全的。 你的帳戶被管理,詢問你的老師解除兒童模式。 @@ -800,9 +818,10 @@ 以儲存%s列預走的棋步 + 你收到一個來自 Lichess 的私人信息。 + 點擊閱讀 抱歉:( 您被封鎖了,在一陣子的時間內將不能下棋 - 封鎖結束至%s 為什麼? 我們的目的在於為所有人提供愉快的國際象棋體驗 為此,我們必須確保所有參與者都遵循良好做法 @@ -822,9 +841,10 @@ 我同意我將會遵守Lichess的規則 尋找或開始聊天 編輯 + 快棋 快速模式 經典 - 瘋狂速度模式:低於30秒 + 瘋狂速度模式: 低於30秒 非常速度模式:低於3分鐘 快速模式:3到8分鐘 一般模式:8到25分鐘 @@ -861,9 +881,6 @@ 時間快到了! [按下展示電郵位置] 下載 - 歡迎! - Lichess是個慈善、完全免費之開源軟件。 -一切營運成本、開發和內容皆來自用戶之捐贈。 教練管理 直播管理 取消錦標賽 @@ -922,4 +939,8 @@ 更換所持顏色 關閉帳戶將會收回你的上訴 舉辦賽事的小建議 + 說明 + 全部顯示 + Lichess是個慈善、完全免費之開源軟件。 +一切營運成本、開發和內容皆來自用戶之捐贈。 diff --git a/translation/dest/site/zu-ZA.xml b/translation/dest/site/zu-ZA.xml index e00f6e50b7d49..8d4c9990c3d2d 100644 --- a/translation/dest/site/zu-ZA.xml +++ b/translation/dest/site/zu-ZA.xml @@ -199,7 +199,6 @@ Iyadlala manje Iyadlala manje Kuqediwe - kuqeda %s Lahla umdlalo Umdlalo uhoxisiwe Okujwayelekile diff --git a/translation/dest/storm/ar-SA.xml b/translation/dest/storm/ar-SA.xml index 5aef1e99c94df..a240eb7f144eb 100644 --- a/translation/dest/storm/ar-SA.xml +++ b/translation/dest/storm/ar-SA.xml @@ -1,12 +1,12 @@ حَرِك لتبدأ - أنت تلعب القطع البيضاء في جميع الألغاز - أنت تلعب القطع السوداء في جميع الألغاز - الألغاز المحلولة - أعلى نتيجة يومية جديدة! - أعلى نتيجة أسبوعية جديدة! - أعلى نتيجة شهرية جديدة! + أنت تلعب بالقطع البيضاء في جميع الألغاز + أنت تلعب بالقطع السوداء في جميع الألغاز + الألغاز التي حللتها سابقا + حققت نتيجة يومية جديدة! + حققت نتيجة أسبوعية جديدة! + حققت نتيجة شهرية جديدة! أعلى مستوى جديد على الإطلاق! النتيجة العالية السابقة كانت %s إلعب مرة أخرى @@ -34,12 +34,12 @@ الوقت الوقت لكل نقلة تقييم أصعب لغز تم حله - الألغاز الملعوبة + الألغاز التي لعبتها سابقا سباق جديد انهاء السباق أعلى النتائج اعرض أفضل الجولات - أفضل جولة في اليوم + أفضل جولة لك اليوم جولات استعد! ينتظر انضمام مزيد من الاعبين... @@ -60,7 +60,7 @@ تخطى هذه الحركة للحفاظ على سلسلة الانتصارات. الألغاز التي فشلت في حلها ألغاز بطيئة - الالغاز التي تم تخطيها + الألغاز التي تخطيتها هذا الأسبوع هذا الشهر كل الأوقات diff --git a/translation/dest/storm/da-DK.xml b/translation/dest/storm/da-DK.xml index 37861789f16cb..75bb1adc834c3 100644 --- a/translation/dest/storm/da-DK.xml +++ b/translation/dest/storm/da-DK.xml @@ -3,7 +3,7 @@ Ryk for at starte Du har de hvide brikker i alle opgaver Du har de sorte brikker i alle opgaver - opgaver løst + taktikopgaver løst Ny dagsrekord! Ny ugerekord! Ny månedsrekord! @@ -26,7 +26,7 @@ Tid Tid per træk Sværeste løst - Spillede opgaver + Spillede taktikopgaver Ny runde (genvejstast: Mellemrum) Afslut runde (genvejstast: Enter) Rekorder @@ -50,9 +50,9 @@ spring over NYT! Du kan springe ét træk over pr. race: Spring dette træk over for at bevare din kombo! Virker kun én gang pr. race. - Mislykkede opgaver - Langsomme opgaver - Oversprunget opgave + Mislykkede taktikopgaver + Langsomme taktikopgaver + Oversprunget taktikopgave Denne uge Denne måned Alle tiders diff --git a/translation/dest/storm/ta-IN.xml b/translation/dest/storm/ta-IN.xml index 3ea04e700dfa8..e905658221d6e 100644 --- a/translation/dest/storm/ta-IN.xml +++ b/translation/dest/storm/ta-IN.xml @@ -1,2 +1,62 @@ - + + தொடங்குவதற்கு நகர்த்தவும் + நீங்கள் அனைத்து புதிர்களிலும் வெள்ளை துண்டுகளை விளையாடுகிறீர்கள் + நீங்கள் அனைத்து புதிர்களிலும் கருப்பு துண்டுகளை விளையாடுகிறீர்கள் + புதிர்கள் தீர்க்கப்பட்டன + புதிய தினசரி அதிகபட்ச மதிப்பெண்! + புதிய வாராந்திர உயர் மதிப்பெண்! + புதிய மாதாந்திர உயர் மதிப்பெண்! + புதிய அனைத்து நேர உயர் மதிப்பெண்! + முந்தைய அதிகபட்ச மதிப்பெண் %s + மீண்டும் ஆடு + + 1 ஓட்டம் + %s ஓட்டங்கள் + + + %2$s ஒரு ஓட்டம் ஓடினார் + %2$s இல் %1$s ஓட்டங்கள் ஓடினார் + + உயர் மதிப்பெண்கள்: %s + மதிப்பெண்கள் + நகர்த்தல்கள் + துல்லியம் + சேர்க்கை + நேரம் + ஒரு நகர்வுக்கு நேரம் + மிக உயர்ந்த தீர்வு + விளையாடப்பட்ட புதிர்கள் + புது ஓட்டம் (hotkey: Space) + இறுதி ஓட்டம் (hotkey: Enter) + உயர் மதிப்பெண்கள் + சிறந்த ஓட்டங்களைக் காண்க + நாளின் சிறந்த ஓட்டம் + ஓட்டங்கள் + தயாராய் இருங்கள்! + மேலும் வீரர்கள் சேர காத்திருக்கிறோம்... + பந்தயம் முடிந்தது! + பார்க்கபடுகிறது + பந்தயத்தில் சேருங்கள்! + பந்தயத்தைத் தொடங்குங்கள் + உங்கள் தரவரிசை: %s + மறு போட்டிக்காக காத்திருங்கள் + அடுத்த பந்தயம் + மறு போட்டியில் சேரவும் + தொடங்க காத்திருக்கிறது + புதிய விளையாட்டை உருவாக்கவும் + பொது பந்தயத்தில் சேரவும் + உங்கள் நண்பர்களை பந்தயம் செய்யுங்கள் + தவிர்க்கவும் + ஒரு பந்தயத்திற்கு ஒரு நகர்வை நீங்கள் தவிர்க்கலாம்: + உங்கள் சேர்க்கையைப் பாதுகாக்க இந்த நடவடிக்கையைத் தவிர்க்கவும்! ஒரு பந்தயத்திற்கு ஒரு முறை மட்டுமே வேலை செய்கிறது. + தோல்வியுற்ற புதிர்கள் + மெதுவான புதிர்கள் + தவிர்க்கப்பட்ட புதிர் + இந்த வாரம் + இந்த மாதம் + எல்லா நேரமும் + மீளேற்றுவதற்கு சொடுக்கவும் + இந்த ஓட்டம் காலாவதியானது! + இந்த ஓட்டம் மற்றொரு தாவலில் திறக்கப்பட்டது! + diff --git a/translation/dest/storm/vi-VN.xml b/translation/dest/storm/vi-VN.xml index 6b4d58ca51837..d6c100c35a138 100644 --- a/translation/dest/storm/vi-VN.xml +++ b/translation/dest/storm/vi-VN.xml @@ -2,7 +2,7 @@ Đi quân để bắt đầu Bạn chơi quân trắng ở tất cả các câu đố - Bạn chơi quân đen ở tất cả các câu đố + Bạn cầm quân đen ở tất cả các câu đố câu đố đã giải Điểm cao hàng ngày mới! Điểm cao hàng tuần mới! diff --git a/translation/dest/streamer/be-BY.xml b/translation/dest/streamer/be-BY.xml index 128eb010ebaa0..8e11d4b329df3 100644 --- a/translation/dest/streamer/be-BY.xml +++ b/translation/dest/streamer/be-BY.xml @@ -4,8 +4,8 @@ Стрымер на Lichess У ЭФІРЫ! АФЛАЙН - Зараз стрыміць: %s - Апошні стрым %s + Зараз стрыміць: %s + Апошні стрым %s Стаць страмерам на Lichess У вас ёсць каналы на Twitch або YouTube? Пачынаем! @@ -29,9 +29,10 @@ Ваш стрым разглядаецца мадэратарамі. Калі ласка, запоўніце інфармацыю наконт стрыму і загрузіце выяву. Калі будзіце гатовы быць адлюстраваным як стрымер Lichess, %s - запрасіць прагляд мадэратара - Ваша імя карыстальніка/спасылка на Twich + запрасіць прагляд мадэратара + Ваша імя карыстальніка/спасылка на Twich Апцыянальна. Калі няма, пакіньце пустым + ID вашага YouTube канала Ваша стрымерскае імя на Lichess Сцісла: максімальна %s сімвал diff --git a/translation/dest/streamer/gsw-CH.xml b/translation/dest/streamer/gsw-CH.xml index d21e6c3e847e2..6e773bdd8f8d1 100644 --- a/translation/dest/streamer/gsw-CH.xml +++ b/translation/dest/streamer/gsw-CH.xml @@ -4,8 +4,8 @@ Lichess Streamer LIVE! OFFLINE - Streamt grad: %s - Letschtä Stream %s + Streamt grad: %s + Letschte Stream %s Wird en Lichess Streamer Häsch du än Twitch- oder än YouTube-Kanal? Los geht\'s! @@ -29,11 +29,11 @@ Din Stream wird vo Moderatorä überprüäft. Bitte füll dini Streamer-Informatione us und lad es Bild ufe. Wänn du parat bisch, als Streamer azeigt z\'werde, %s - verlang e Überprüefig dur en Moderator + verlang e Überprüefig dur en Moderator D\'Lichess-Streamer-Site richtet sich a ihres Publikum mit dere Schprach, wo vu dinere Streaming-Platform bereit gschtellt wird. Wähl die richtig Schtandard-Schprach, für dini Schach-Streams, i de App oder i dem Dienscht, wo du für d\'Überträgig verwändsch. - Din Twitch-Benutzername oder d\'URL + Din Twitch-Benutzername oder d\'URL Optional. Leer lassen falls nicht verfügbar - Dini YouTube-Kanal-ID + Dini YouTube-Kanal-ID Din Streamername uf Lichess Churz fasse: Maximal %s Zeiche diff --git a/translation/dest/streamer/ta-IN.xml b/translation/dest/streamer/ta-IN.xml index 58a9bf0ac87e1..8a19d77a7ea22 100644 --- a/translation/dest/streamer/ta-IN.xml +++ b/translation/dest/streamer/ta-IN.xml @@ -1,7 +1,51 @@ - லிச்சஸ் ஸ்ட்ரீமர்கள் - லிச்சஸ் ஸ்ட்ரீமர் - லைவ்! - ஆஃப்லைன் + லிசெஸ் இணைய ஒளிபரப்பாளர்கள் + லிசெஸ் இணைய ஒளிபரப்பாளர் + நேரலை! + அகல்நிலை + தற்போதைய இணைய ஒளிபரப்புகள்: %s + கடைசி இணைய ஒளிப்பரப்பு %s + லிசெஸ் இணைய ஒளிபரப்பாளர் ஆகுங்கள் + உங்களிடம் Twitch அல்லது YouTube சேனல் உள்ளதா? + இதோ! + அனைத்து இணைய ஒளிபரப்பாளர்கள் + இணைய ஒளிபரப்பாளர் பக்கத்தைத் திருத்து + உங்கள் இணைய ஒளிபரப்பாளர் பக்கம் + இணைய ஒளிபரப்பு சாதனங்களைப் பதிவிறக்கு + %s இணைய ஒளிபரப்பு செய்கிறார் + இணைய ஒளிபரப்புவதற்கான விதிமுறைகள் + உங்கள் இணைய ஒளிபரப்பு தலைப்பில் \"lichess.org\" என்ற முக்கிய சொல்லைச் சேர்த்து, நீங்கள் Lichess இல் இணைய ஒளிபரப்பு செய்யும்போது \"Chess\" வகையைப் பயன்படுத்தவும். + லிசெஸ் அல்லாதவற்றை நீங்கள் இணைய ஒலிபரப்பு செய்யும்போது முக்கிய சொல்லை அகற்றவும். + லிசெஸ் உங்கள் இணைய ஒளிபரப்பை தானாகவே கண்டறிந்து பின்வரும் சலுகைகளை இயக்கும்: + உங்கள் இணைய ஒளிபரப்பின்போது அனைவருக்கும் நியாயமான விளையாட்டை உறுதிசெய்ய எங்கள் %sஐப் படிக்கவும். + இணைய ஒளிபரப்பு நியாயமான விளையாட்டு அதிகம் கேட்க்கபட்ட கேள்விகள் + முக்கிய வார்த்தையுடன் இணைய ஒளிபரப்பின் நன்மைகள் + உங்கள் லிசெஸ் சுயவிவரத்தில் தீப்பிடிக்கும் இணைய ஒளிபரப்பாளர் சின்னம் பெறுங்கள். + இணைய ஒளிபரப்பாளர்கள் பட்டியலில் முதலிடம் பெறுங்கள். + உங்கள் லிசெஸ் பின்தொடர்பவர்களுக்கு தெரிவிக்கவும். + உங்கள் விளையாட்டுகள், போட்டிகள் மற்றும் படிப்புகளில் உங்கள் இணைய ஒளிபரப்பை காட்டுங்கள். + உங்கள் இணைய ஒளிபரப்பு அங்கீகரிக்கப்பட்டது. + உங்கள் இணைய ஒளிபரப்பு மதிப்பீட்டாளர்களால் மதிப்பாய்வு செய்யப்படுகிறது. + உங்கள் இணைய ஒளிபரப்பாளர் தகவலைப் பூர்த்தி செய்து, படத்தைப் பதிவேற்றவும். + நீங்கள் லிசெஸ் இணைய ஒளிபரப்பாளராகப் பட்டியலிட தயாராக இருக்கும்போது, %s + மதிப்பீட்டாளர் மதிப்பாய்வைக் கோருங்கள் + லிசெஸ் இணைய ஒளிபரப்பாளர் பக்கம் உங்கள் இணைய ஒளிபரப்பாளர் மேடை வழங்கிய மொழிமூலம் உங்கள் பார்வையாளர்களைக் குறிவைக்கிறது. நீங்கள் ஒளிபரப்புவதற்குப் பயன்படுத்தும் செயலிகள் அல்லது சேவையில் உங்கள் சதுரங்க இணைய ஒளிபரப்புகளுக்கான சரியான இயல்புநிலை மொழியை அமைக்கவும். + உங்கள் Twitch பயனர்பெயர் அல்லது URL + விருப்பமானது. இல்லை என்றால் காலியாக விடவும் + உங்கள் YouTube அலைவரிசை அடையாளம் + லிசெஸ் இல் உங்கள் இணைய ஒளிபரப்பாளர் பெயர் + + சுருக்கமாக வைக்கவும்: %s எழுத்து அதிகபட்சம் + சுருக்கமாக வைத்திருங்கள்: %s எழுத்துகள் அதிகபட்சம் + + இணைய ஒளிபரப்பாளர் பக்கத்தில் தெரியும் + மதிப்பீட்டாளர்களால் அங்கீகரிக்கப்படும்போது + தலைப்பு + உங்கள் இணைய ஒலிபரப்பு பற்றி ஒரே வாக்கியத்தில் சொல்லுங்கள் + நீண்ட விளக்கம் + %s இணைய ஒளிபரப்பாளர் படம் + உங்கள் படத்தை மாற்றவும்/நீக்கவும் + ஒரு படத்தைப் பதிவேற்றவும் + அதிகபட்ச அளவு: %s diff --git a/translation/dest/streamer/th-TH.xml b/translation/dest/streamer/th-TH.xml index 56422fd0cd78f..f214efaeb2ca9 100644 --- a/translation/dest/streamer/th-TH.xml +++ b/translation/dest/streamer/th-TH.xml @@ -1,9 +1,16 @@ สตรีมเมอร์ของ Lichess + สตรีมเมอร์ของ Lichess สด ออฟไลน์ กำลังถ่ายทอดสดอยู่ขณะนี้: %s ถ่ายทอดสดครั้งล่าสุด %s ไปกันเลย! + สตรีมเมอร์ทั้งหมด + แก้ไขหน้าสตรีมเมอร์ + หน้าสครีมเมอร์ของคุณ + %s กำลังสตรีม + กฎในการสตรีม + อัพโหลดรูปภาพ diff --git a/translation/dest/streamer/vi-VN.xml b/translation/dest/streamer/vi-VN.xml index 04d13d4e3cc5f..ee2aefec82a6a 100644 --- a/translation/dest/streamer/vi-VN.xml +++ b/translation/dest/streamer/vi-VN.xml @@ -1,6 +1,6 @@ - Những Streamer của Lichess + Các Streamer của Lichess Streamer của Lichess TRỰC TIẾP! NGOẠI TUYẾN @@ -30,10 +30,10 @@ Hãy điền thông tin về luồng của bạn và tải lên một hình ảnh. Khi bạn sẵn sàng trờ thành Streamer của Lichess, %s yêu cầu các quản trị viên đánh giá - Trang streamer Lichess hướng đến người xem là những người dùng ngôn ngữ bằng ngôn ngữ do nền tảng phát trực tuyến của bạn cung cấp. Đặt ngôn ngữ mặc định chính xác cho luồng stream cờ vua của bạn trong ứng dụng hoặc dịch vụ mà bạn sử dụng để phát sóng. - Tên người dùng hoặc đường dẫn đến trang Twitch của bạn + Trang Streamer Lichess hướng đến người xem là những người dùng ngôn ngữ bằng ngôn ngữ do nền tảng phát trực tuyến của bạn cung cấp. Đặt ngôn ngữ mặc định chính xác cho luồng cờ vua của bạn trong ứng dụng hoặc dịch vụ mà bạn sử dụng để phát sóng. + Tên người dùng hoặc đường dẫn đến trang Twitch của bạn Tùy chọn. Bỏ trống nếu không có - ID kênh YouTube của bạn + ID kênh YouTube của bạn Tên luồng của bạn trên Lichess Để tên ngắn gọn: tối đa %s ký tự diff --git a/translation/dest/study/da-DK.xml b/translation/dest/study/da-DK.xml index 7c7a8e72da777..0cafbc6cd5781 100644 --- a/translation/dest/study/da-DK.xml +++ b/translation/dest/study/da-DK.xml @@ -22,8 +22,8 @@ %s kapitler - %s Parti - %s Partier + %s parti + %s partier Tilføj medlemmer diff --git a/translation/dest/study/fi-FI.xml b/translation/dest/study/fi-FI.xml index 980e1a5c7eefa..12e9cd8953c6b 100644 --- a/translation/dest/study/fi-FI.xml +++ b/translation/dest/study/fi-FI.xml @@ -62,7 +62,7 @@ Tutkielman PGN Lataa kaikki pelit Luvun PGN - Kopioi PGN + Kopioi PGN Kopioi tämän luvun PGN leikepöydälle. Lataa peli Tutkielman URL @@ -131,7 +131,7 @@ Haluatko poistaa tutkielman keskusteluhistorian? Et voi palauttaa sitä enää! Poista tutkielma Poistetaanko koko tutkielma? Et voi palauttaa sitä enää. Vahvista poisto kirjoittamalla tutkielman nimen: %s - Missä haluat tutkia? + Missä haluat tutkia tätä? Hyvä siirto Virhe Loistava siirto diff --git a/translation/dest/study/gsw-CH.xml b/translation/dest/study/gsw-CH.xml index 5ea7f88582bba..ddaedd93b8ded 100644 --- a/translation/dest/study/gsw-CH.xml +++ b/translation/dest/study/gsw-CH.xml @@ -62,7 +62,7 @@ Schtudie PGN Lad alli Partie abe Kapitel PGN - PGN kopiere + PGN kopiere PGN-Kapitel id Zwüscheablag kopiere. Lad die Partie abe Schtudie URL @@ -109,8 +109,8 @@ URL vu de Partie Partie vu %1$s oder %2$s lade Kapitäl ärschtelä - Schtuudiä ärschtelä - Schtuudiä bearbeitä + Schtudie erschtelle + Schtudie bearbeite Sichtbarkeit Öffentlich Unglischtet @@ -128,10 +128,10 @@ Schtart Schpeichärä Tschätt löschä - Tschättverlauf vu de Schtudie lösche? Das chann nüme rückgängig gmacht werde! - Schtuudiä löschä + Chatverlauf vu de Schtudie lösche? Das chann nüme rückgängig gmacht werde! + Schtudie lösche Die ganz Schtudie lösche? Es git keis Zrugg! Gib zur Beschtätigung de Name vu de Schtudie i: %s - Weli Schtuudiä wöttsch bruuchä? + Welli Schtudie wottsch bruche? Guete Zug Fähler Briliantä Zug diff --git a/translation/dest/study/ta-IN.xml b/translation/dest/study/ta-IN.xml index c02c09bf224fc..4713bcc697bb5 100644 --- a/translation/dest/study/ta-IN.xml +++ b/translation/dest/study/ta-IN.xml @@ -17,7 +17,19 @@ மிகவும் பிரபலமானவை அகரவரிசைப்படி புதிய அத்தியாயத்தைச் சேர்க்க + + %s அத்தியாயம் + %s அத்தியாயங்கள் + + + %s விளையாட்டு + %s விளையாட்டுக்கள் + உறுப்பினர்களை இணைக்க + + %s உறுப்பினர் + %s உறுப்பினர்கள் + கற்கைக்கான அழைப்பு உங்களுக்குத் தெரிந்த, இந்தக் கற்கையை ஊக்கத்துடன் கற்க விரும்புபவர்களை மட்டுமே அழையுங்கள். பயனர்பெயரில் தேட @@ -36,17 +48,21 @@ காய்களின் குறியீட்டுடன் சிறுகுறிப்புக்கள் நகர்வுகள் குறைவாக உள்ளதால் இந்த அத்தியாயத்தைப் பகுப்பாய்வு செய்ய இயலாதுள்ளது. இந்தக் கற்கையில் பங்களிப்பவர் மட்டுமே கணினிப் பகுப்பாய்வைக் கோரலாம். + முதன்மை வரியின் முழு கணினி பகுப்பாய்வைச் சேவை வழங்கியிடமிருந்து பெறலாம். இந்த அத்தியாயம் முடிவடைந்ததும், நீங்கள் பகுப்பாய்வை ஒருமுறை கோரலாம். + அனைத்து SYNC உறுப்பினர்களும் ஒரே நிலையில் உள்ளனர் + பார்வையாளர்களுடன் மாற்றங்களைப் பகிர்ந்து அவற்றைச் சேவையகத்தில் சேமிக்கவும் விளையாடுகிறார் முதலாவது முந்தைய அடுத்து இறுதி + பகிர் & ஏற்றுமதி நகல் PGN கற்கை எல்லா விளையாட்டுக்களையும் பதிவிறக்க அத்தியாயத்திற்குரிய PGN - PGN நகல் + PGN நகல் அத்தியாயத்திற்குரிய PGN ஐக் கிளிப்போட்டிற்கு பிரதிபண்ண. விளையாட்டைப் பதிவிறக்க கற்கை URL @@ -58,6 +74,7 @@ உட்பொதிப்பதைப் பற்றி மேலும் அறிய பொதுக் கற்கைகளை மாத்திரமே உட்பொதிக்கலாம்! திற + %1$s, %2$s மூலம் உங்களிடம் கொண்டு வரப்பட்டது கற்கையைக் காணவில்லை அத்தியாயத்தைத் திருத்த புதிய அத்தியாயம் @@ -67,6 +84,7 @@ பலகையில் கீழ் எப்போதும் வரும் குறிப்புரை அத்தியாயத்தைச் சேமிக்க குறிப்புரைகளை நீக்க + மாறுபாடு வரிகளை நீக்கு அத்தியாயத்தை நீக்க இந்த அத்தியாயத்தை அழிக்கவும். அழித்தால் மீள்விக்க இயலாது! இந்த அத்தியாயத்திற்குரிய எல்லாக் குறிப்புரைகளையும் குறியீடுகளையும் வரையப்பட்ட உருவங்களையும் நீக்கவும் @@ -74,11 +92,46 @@ எதுவுமில்லை சாதாரண பகுப்பாய்வு அடுத்த நகர்வை ஒழிக்கவும் + ஊடாடும் பாடம் %s ஆம் அத்தியாயம் + காலியாக கற்கையை இந்த இடத்திலிருந்து ஆரம்பிக்கவும் + திருத்து + தனிப்பயன் நிலையிலிருந்து தொடங்கவும் + URLகள் மூலம் ஆட்டங்களை ஏற்றவும் + FEN இலிருந்து ஒரு நிலையை ஏற்றவும் + PGN இலிருந்து ஆட்டங்களை ஏற்றவும் + தானியங்கி + + %s ஆட்டம்வரை உங்கள் PGN உரையை இங்கே ஒட்டவும் + %s ஆட்டங்கள்வரை உங்கள் PGN உரையை இங்கே ஒட்டவும் + + ஆட்டங்களின் URL, ஒரு வரிக்கு ஒன்று + %1$s அல்லது %2$s இலிருந்து ஆட்டங்களை ஏற்றவும் + அத்தியாயத்தை உருவாக்கவும் + படிப்பை உருவாக்குங்கள் + படிப்பைத் திருத்தவும் + தெரிவுநிலை + பொது + பட்டியலிடப்படாதது + அழைப்பு மட்டும் + நகலெடுக்க அனுமதிக்கவும் + யாரும் இல்லை + நான் மட்டும் + பங்களிப்பாளர்கள் + உறுப்பினர்கள் + அனைவரும் + ஒத்திசைவை இயக்கு + ஆம்: அனைவரையும் ஒரே நிலையில் வைத்திருங்கள் + இல்லை: மக்கள் சுதந்திரமாக உலாவ அனுமதிக்கவும் + பொருத்தப்பட்ட ஆய்வுக் கருத்து + தொடங்கு சேமி கருத்துரைகளை நீக்க + ஆய்வு அரட்டை வரலாற்றை நீக்கவா? திரும்பிப் போவதில்லை! கற்கையை அழிக்க + முழு ஆய்வையும் நீக்கவா? திரும்பிப் போவதில்லை! உறுதிப்படுத்த ஆய்வின் பெயரை உள்ளிடவும்: %s + நீங்கள் அதை எங்கே படிக்க விரும்புகிறீர்கள்? சிறந்த நகர்வு தவறு சிறப்பான நகர்வு @@ -97,6 +150,21 @@ கறுப்பு வெல்கின்றது புதுமை அபிவிருத்தி + முயற்சி தாக்கு + எதிர்விளையாட்டு + நேர பிரச்சனை + இழப்பீட்டுடன் + யோசனையுடன் + அடுத்த அத்தியாயம் + முந்தைய அத்தியாயம் + ஆய்வு நடவடிக்கைகள் + தலைப்புகள் + எனது தலைப்புகள் + பிரபலமான தலைப்புகள் + தலைப்புகளை நிர்வகிக்கவும் + பின்செல் மீண்டும் விளையாட + இந்த நிலையில் நீங்கள் என்ன விளையாடுவீர்கள்? + வாழ்த்துகள்! இந்தப் பாடத்தை முடித்துவிட்டீர்கள். diff --git a/translation/dest/study/vi-VN.xml b/translation/dest/study/vi-VN.xml index 72f5da67091bb..eff216f54b36d 100644 --- a/translation/dest/study/vi-VN.xml +++ b/translation/dest/study/vi-VN.xml @@ -28,7 +28,7 @@ %s Thành viên Mời vào nghiên cứu - Vui lòng chỉ mời những người bạn biết và muốn tham gia. + Vui lòng chỉ mời những người bạn biết và những người tích cực muốn tham gia nghiên cứu này. Tìm kiếm theo tên người dùng Khán giả Người đóng góp @@ -59,7 +59,7 @@ PGN nghiên cứu Tải xuống tất cả ván đấu PGN chương - Sao chép PGN + Sao chép PGN Sao chép PGN chương vào bảng nhớ tạm. Tải xuống ván cờ URL nghiên cứu @@ -97,12 +97,12 @@ Bắt đầu từ thế cờ tùy chỉnh Tải ván cờ bằng URL Tải thế cờ từ chuỗi FEN - Tải thế cờ từ PGN + Tải ván cờ từ PGN Tự động Dán PGN ở đây, tối đa %s ván - Đường dẫn của các ván, một đường dẫn mỗi dòng + URL của các ván, một URL mỗi dòng Tải ván cờ từ %1$s hoặc %2$s Tạo chương Tạo nghiên cứu @@ -141,7 +141,7 @@ Bên trắng có một chút lợi thế Bên đen có một chút lợi thế Lợi thế bên trắng - Lợi thế bên đen + Bên đen lợi thế hơn Bên trắng đang thắng dần Bên đen đang thắng dần Nước cờ mới diff --git a/translation/dest/swiss/ar-SA.xml b/translation/dest/swiss/ar-SA.xml index 1eea702aeb60d..e606cfa288f51 100644 --- a/translation/dest/swiss/ar-SA.xml +++ b/translation/dest/swiss/ar-SA.xml @@ -89,34 +89,39 @@ كم من البايت يستطيع اللاعب الحصول عليها؟ اللاعب يحصل على نقطة واحدة في كل مرة لا يستطيع فيها نظام الإقران العثور على إقران لهم. بالإضافة إلى ذلك، يتم منحهم نصف نقطة عندما ينضم لاعب متأخر إلى البطولة. + ماذا سيحدث في حالة التعادلات المبكرة؟ + في البطولات السويسرية، لا يمكن للاعبين أن يتفقوا على التعادل قبل إكمال 30 نقلة. ورغم أن هذا التدبير لا يمنع الاتفاق المسبق على التعادل، لكنه يزيد من صعوبة حدوثها. ماذا يحدث إذا لم يلعب اللاعب المباراة؟ سيمر وقتهم ويخسرون بسبب الوقت. وعندها سيقوم النظام بإخراجهم من البطولة لكي لا يخسروا مزيداً من الجولات. يمكنهم إعادة الانضمام للبطولة متى ما شاؤوا. + ماذا يحدث للبطولات غير المعروضة؟ + اللاعبون الذين يسجلون في البطولات السويسرية، ولكنهم لا يلعبون مبارياتهم قد يسببون مشكلة. +لذا، يُمنع اللاعبين الذين يفعلون ذلك من الانضمام إلى حدث سويسري جديد لفترة زمنية معينة، لكن يمكن لمنشئ البطولة السويسرية أن يقرر ضمهم إليها مع ذلك. هل يمكن للاعب الانضمام متأخراً؟ نعم، إلى أن يبدأ أكثر من نصف المباريات؛ على سبيل المثال في إحدى عشرة جولة يمكن للاعبين أن ينضموا قبل أن تبدأ الجولة السادسة وفي 12 جولة قبل بدء الجولة السابعة. الانضمام المتأخر يحصل على جزء واحد، حتى لو فاتهم عدة جولات. هل ستحل البطولات السويسرية محل البطولات العادية؟ لا، هذه ميزات تكميلية. ماذا عن جولة روبين؟ - نود أن نضيفه، لكن لسوء الحظ أن جولة روبين لا تعمل على الإنترنت. -السبب هو أنه ليس لديها طريقة عادلة للتعامل مع الأشخاص الذين يغادرون المسابقة مبكرا. لا يمكننا أن نتوقع أن يلعب جميع اللاعبين كل ألعابهم في حدث على الانترنت. لن يحدث ذلك فقط، ونتيجة لذلك فإن معظم بطولات روبين ستكون معيبة وغير عادلة. -أقرب ما يمكنك الوصول إلى جولة روبن على الإنترنت هو لعب بطولة سويسرية بعدد كبير جدا من الجولات. ثم سيتم تشغيل جميع الأزواج الممكنة قبل انتهاء البطولة. - ماذا عن أنظمة البطولة الأخرى؟ - نحن لا نخطط لإضافة المزيد من أنظمة البطولة إلى ليشيس في الوقت الحالي. - في بطولة سويسرية %1$s، لا يلعب كل منافس بالضرورة جميع الاعبين الآخرين. يلتقي المنافسون الواحد تلو الآخر في كل جولة من الجولات ويقترنون باستخدام مجموعة من القواعد المصممة لضمان أن يلعب كل منافس معارضين بنفس الدرجة من التشغيل. ولكن ليس نفس الخصم أكثر من مرة. الفائز هو المنافس ذو أعلى مجموع النقاط المكتسبة في جميع الجولات. كل المنافسين يلعبون في كل جولة ما لم يكن هناك عدد فردي من اللاعبين.\" - لا يمكن إنشاء البطولات السويسرية إلا من قبل قادة الفريق، ولا يمكن لعبها إلا من قبل أعضاء الفريق. -%1$s لبدء اللعب في البطولات. + نود أن نضيفها، لكن لسوء الحظ أن لا يمكن لعب جولة روبين على الإنترنت. +وذلك لعدم توفر طريقة عادلة للتعامل مع الأشخاص الذين يغادرون المسابقة مبكرا. فلا يمكننا أن نضمن أن يلعب جميع اللاعبين كل مبارياتهم في بطولة على الانترنت، ونتيجة لذلك فإن معظم بطولات روبين ستكون معيبة وغير عادلة. +إذا أردت لعب جولة روبن على الإنترنت فيمكنك لعب بطولة سويسرية بعدد كبير جدا من الجولات، بحيث تلعب مع كل المشاركين في البطولة. + ماذا عن أنماط البطولات الأخرى؟ + نحن لا نخطط لإضافة المزيد من أنماط البطولات إلى ليتشيس في الوقت الحالي. + في البطولات السويسرية %1$s، لا يلعب كل لاعب بالضرورة مع جميع اللاعبين الآخرين، وإنما يلتقي خصما واحدا في كل جولة من الجولات، يحدد بواسطة مجموعة من القواعد لضمان أن يلعب كل لاعب مع خصوم من مستواهم نفسه، ولكن لا يمكن أن يلعب ضد الخصم نفسه أكثر من مرة. الفائز هو اللاعب الذي جمع أكبر عدد من النقاط في البطولة، سيلعب جميع اللاعبين في كل الجولات ما لم يكن عددهم فرديا. + لا يمكن إنشاء البطولات السويسرية إلا من قبل قادة الفرق، ولا يمكن المشاركة فيها إلا من قبل أعضاء الفريق. +سجل في فريق%1$s لبدء اللعب في البطولات. انضم أو أنشأ فريق - الملعوبة حالياً + البطولات الملعوبة الآن ستبدأ قريباً المقارنة مدة المسابقة مدة محددة مسبقاً في دقائق الحد الأقصى للجولات المحددة مسبقاً، ولكن المدة غير معروفة عدد المباريات - أكبر عدد ممكن من اللعب في الفترة المخصصة - تقرر مسبقاً، نفس الشيء لجميع اللاعبين + أكبر عدد ممكن من المباريات في المدّة المختارة + تقرر مسبقاً، يطبق على جميع اللاعبين نظام الاقتران أي خصم متاح حيث يكون الخصم مقارب بالتصنيف افضل ربط بين اللاعبين اعتماداً على النقاط و نتيجة كسر التعادل @@ -134,5 +139,6 @@ مشابه للبطولات التي تقام على الرقعة غير محدود و مجاني السماح فقط للمستخدمين المحددين مسبقاً بالانضمام - إذا كانت هذه القائمة غير فارغة، فسيتم منع أسماء المستخدمين المتغيبين من هذه القائمة من الانضمام. اسم مستخدم واحد لكل سطر. + إذا كانت هذه القائمة غير فارغة، فسيمنع اللاعبون غير الواردة أسماءهم في هذه القائمة من الانضمام إلى البطولة. اسم مستخدم واحد لكل سطر. + ألعب مبارياتك diff --git a/translation/dest/swiss/gsw-CH.xml b/translation/dest/swiss/gsw-CH.xml index 05031a9cc0ef7..025ec7a1482f4 100644 --- a/translation/dest/swiss/gsw-CH.xml +++ b/translation/dest/swiss/gsw-CH.xml @@ -65,16 +65,16 @@ als villicht vu \"Krethi und Plethi\", wänn so es Turnier öffentlich isch.Wänn für en Schpiller kei Paarig gfunde wird, gits es \"bye\" - das bedütet, dass er i dere Rundi nöd schpille muess, aber trotzdem en Punkt überchunnt. Zuesätzlich gits au en halbe Punkt, wänn er erscht schpöter is Turnier ischtigt. - Was passiert bi frühe Uslosige? + Was passiert bime schnälle Remis? Bi Partie, in Turnier nach \"Schweizer System\", chann mer keis Remis veribare, bevor nöd mindeschtens 30 Züg gschpillt worde sind. Die Regelig chann zwar nöd verhindere, dass es Remis scho vor em Schpiel abgmacht wird, aber sie erschwerts es bizli, sich schpontan uf es Remis z\'einige. Was passiert, wänn en Schpiller nöd zur Partie ahtritt? Sini Zit lauft und wänn sie um isch, verlürt er das Schpiel. De Schpiller wird vum Syschtem us em Tunrier gnah, dass er nöd no meh verlürt. -Doch er chann jederzieit wieder is Turnier ischtige. - Was passiert bi Nöd-Erschine? - Schpiller, wo sich für es \"Schweizer Turnier\" amälded, aber nöd schpilled, chönd Problem bereite. -Um das Problem z\'entschärfe, laht Lichess so Schpiller für e beschtimmti Zit nöd ame neue \"Schweizer Turnier\" mitschpille. -En Organisator vume \"Schweizer Turnier\" chann aber entscheide, ob er so Schpiller trotzdem akzeptiert. +Doch er chann jederzit wieder is Turnier ischtige. + Was passiert bi nöd am Turnier erschine? + Schpiller, wo sich für es Turnier nach \"Schweizer System\" amälded, aber nöd schpilled, chönd Problem bereite. +Um das Problem z\'entschärfe, laht Lichess so Schpiller für e beschtimmti Zit nöd ame neue Turnier nach \"Schweizer System\" mitschpille. +En Organisator vume Turnier nach \"Schweizer System\" chann aber entscheide, ob er so Schpiller trotzdem akzeptiert. Chönd Schpiller au schpöter is Turnier ischtige? Ja - sogar dänn no, wänn bereits die halb Azahl Runde gschpillt isch! So cha me in es Turnier mit 11 Runde no ischtige vor Rundi 6 afangt oder bi 12 Runde, bevor Rundi 7 schtartet. diff --git a/translation/dest/swiss/ta-IN.xml b/translation/dest/swiss/ta-IN.xml index ae1db01235653..208b0467c186e 100644 --- a/translation/dest/swiss/ta-IN.xml +++ b/translation/dest/swiss/ta-IN.xml @@ -5,4 +5,113 @@ இந்த சுற்றைக் காண்க எல்லா சுற்றுகளையும் %s காண்க + + %s சுற்று + %s சுற்றுகள் + + + %s சுற்று சுவிஸ் + %s சுற்றுகள் சுவிஸ் + + + ஒரு நாளைக்கு ஒரு சுற்று + ஒவ்வொரு %s நாட்களுக்கும் ஒரு சுற்று + + சுற்றுகள் கைமுறையாகத் தொடங்கப்படுகின்றன + + சுற்றுகளுக்கு இடையே %s வினாடி + சுற்றுகளுக்கு இடையே %s வினாடிகள் + + + சுற்றுகளுக்கு இடையே %s நிமிடம் + சுற்றுகளுக்கு இடையே %s நிமிடங்கள் + + இல் தொடங்குகிறது + அடுத்த சுற்று + + நடந்துகொண்டிருக்கும் விளையாட்டு + நடந்துகொண்டிருக்கும் விளையாட்டுகள் + + போட்டி தொடங்கும் தேதி + சுற்றுகளின் எண்ணிக்கை + ஒற்றைப்படை எண்ணிக்கையிலான சுற்றுகள் உகந்த வண்ண சமநிலையை அனுமதிக்கிறது. + சுற்றுகளுக்கு இடையிலான இடைவெளி + தடைசெய்யப்பட்ட இணைத்தல் + ஒன்றாக விளையாடக் கூடாத வீரர்களின் பயனர் பெயர்கள் (உதாரணமாக, உடன்பிறந்தவர்கள்). ஒரு வரிக்கு இரண்டு பயனர் பெயர்கள், இடைவெளியால் பிரிக்கப்பட்டிருக்கும். + புதிய சுவிஸ் போட்டி + களங்களுக்குப் பதிலாகச் சுவிஸ் போட்டிகளை எப்போது பயன்படுத்த வேண்டும்? + சுவிஸ் போட்டியில், அனைத்து பங்கேற்பாளர்களும் ஒரே எண்ணிக்கையிலான ஆட்டங்களை விளையாடுகிறார்கள், மேலும் ஒருவருக்கொருவர் ஒருமுறை மட்டுமே விளையாட முடியும். +சங்கங்கள் மற்றும் அதிகாரப்பூர்வ போட்டிகளுக்கு இது ஒரு நல்ல தேர்வாக இருக்கும். + புள்ளிகள் எவ்வாறு கணக்கிடப்படுகின்றன? + ஒரு வெற்றி ஒரு புள்ளி மதிப்புடையது, ஒரு சமநிலை ஒரு அரை புள்ளி, மற்றும் தோல்வி பூச்சியம் புள்ளிகள். +ஒரு சுற்றின்போது ஒரு வீரரை ஜோடி சேர்க்க முடியாதபோது, அவர்கள் ஒரு புள்ளி மதிப்புடன் அடுத்த சுற்றில் இடம் பெறுவார்கள். + போட்டி சமநிலை தடை எவ்வாறு கணக்கிடப்படுகின்றன? + Sonneborn-Berger மதிப்பெண் + %s உடன். +வீரர் வெற்றி கொள்ளும் ஒவ்வொரு எதிராளியின் மதிப்பெண்ணையும் மற்றும் சமன் செய்த ஒவ்வொரு எதிராளியின் மதிப்பெண்ணில் பாதியையும் சேர்க்கவும். + எவ்வாறு இணைத்தல் செயல்படுகிறது? + டச்சு அமைப்பு + FIDE கையேடு + %1$s உடன், %3$sக்கு இணங்க, %2$s ஆல் செயல்படுத்தப்பட்டது. + போட்டியில் வீரர்களைவிட அதிக சுற்றுகள் இருந்தால் என்ன நடக்கும்? + சாத்தியமான அனைத்து ஜோடிகளும் விளையாடியதும், போட்டி முடிவடைந்து வெற்றியாளர் அறிவிக்கப்படும். + இது ஏன் அணிகளுக்கு மட்டுப்படுத்தப்பட்டது? + சுவிஸ் போட்டிகள் நிகல்நிலை சதுரங்கத்திற்காக வடிவமைக்கப்படவில்லை. அவர்கள் வீரர்களிடமிருந்து நேரம் தவறாமை, அர்ப்பணிப்பு மற்றும் பொறுமையைக் கோருகின்றனர். +இந்த நிலைமைகள் உலகளாவிய போட்டிகளைவிட ஒரு குழுவிற்குள் சந்திக்கும் வாய்ப்புகள் அதிகம் என்று நாங்கள் நினைக்கிறோம். + ஒரு வீரர் எத்தனை முறை அடுத்த சுற்றிற்கு கடத்த முடியும்(byes) பெற முடியும்? + ஒவ்வொரு முறையும் இணைத்தல் அமைப்பால் அவருக்கான ஜோடியைக் கண்டுபிடிக்க முடியாத ஒவ்வொரு முறையும் ஒரு வீரர் ஒரு புள்ளியிலிருந்து விடை பெறுகிறார். +கூடுதலாக, ஒரு ஆட்டக்காரர் ஒரு போட்டியில் தாமதமாகச் சேரும்போது அரைப் புள்ளியுடன் அடுத்த சுற்றுக்குக் கடத்தப்படுவார். + ஆரம்பகட்ட சமநிலையினால் என்னல் என்ன நடக்கும்? + சுவிஸ் விளையாட்டுகளில், வீரர்கள் 30 நகர்வுகள் விளையாடுவதற்கு முன்பு சமன் செய்துகொள்ள முடியாது. இந்த நடவடிக்கையால் முன் ஏற்பாடு செய்யப்பட்ட சமனிலைகளை தடுக்க முடியாது என்றாலும், குறைந்தபட்சம் வெளியேறும்போது ஒரு சமநிலையை ஒப்பந்தத்தை இது கடினமாக்குகிறது. + ஒரு வீரர் விளையாட்டை விளையாடவில்லை என்றால் என்ன நடக்கும்? + அவர்களின் கடிகாரம் ஓடும், அவர்கள் நேரம் இல்லாமல் போவார்கள் மற்றும் விளையாட்டை இழக்கிறார்கள். +பின்னர் போட்டி அமைப்பிலிருந்து வீரர் வெளியேற்றப்படுவார், அதனால் அவர்கள் அதிக விளையாட்டுகளை இழக்க மாட்டார்கள். + அவர்கள் எந்த நேரத்திலும் போட்டியில் மீண்டும் சேரலாம். + நிகழ்ச்சிகள் இல்லாதது தொடர்பாக என்ன செய்யப்படுகிறது? + சுவிஸ் நிகழ்வுகளுக்கு பதிவு செய்யும் வீரர்கள் ஆனால் தங்கள் ஆட்டங்களை விளையாடாதவர்கள் சிக்கலாக இருக்கலாம். +இந்தச் சிக்கலைத் தணிக்க, ஒரு ஆட்டத்தை விளையாடத் தவறிய வீரர்களை ஒரு குறிப்பிட்ட நேரத்திற்கு புதிய சுவிஸ் நிகழ்வில் சேர்வதை லிசெஸ் தடுக்கும். +சுவிஸ் நிகழ்வை உருவாக்கியவர் அவர்களை எப்படியும் நிகழ்வில் சேர அனுமதிக்கலாம். + வீரர்கள் தாமதமாகச் சேர முடியுமா? + ஆம், பாதிக்கும் மேற்பட்ட சுற்றுகள் தொடங்கும் வரை; எடுத்துக்காட்டாக, 11-சுற்றுகள் சுவிஸ்ஸில், வீரர்கள் சுற்று 6 தொடங்குவதற்கு முன்பும், 7-வது சுற்று தொடங்குவதற்கு முன்பு 12-சுற்றுகளிலும் சேரலாம். +தாமதமாகச் சேருபவர்கள் பல சுற்றுகளைத் தவறவிட்டாலும், ஒரு சுற்று கடத்தபடுவார்கள். + சுவிஸ், கோதா போட்டிகளை மாற்றுமா? + இல்லை. அவை நிரப்பு அம்சங்கள். + தொடர் சுழல்முறை பற்றி என்ன? + நாங்கள் அதைச் சேர்க்க விரும்புகிறோம், ஆனால் துரதிர்ஷ்டவசமாக தொடர் சுழல்முறை நிகல்நிலையில் வேலை செய்யவில்லை. +காரணம், போட்டியிலிருந்து முன்கூட்டியே வெளியேறும் நபர்களைக் கையாள்வதில் நியாயமான வழி இல்லை. ஆன்லைன் நிகழ்வில் அனைத்து வீரர்களும் தங்கள் அனைத்து ஆட்டங்களையும் விளையாடுவார்கள் என்று எதிர்பார்க்க முடியாது. இது நடக்காது, இதன் விளைவாக பெரும்பாலான தொடர் சுழல்முறை போட்டிகள் குறைபாடுள்ளதாகவும் நியாயமற்றதாகவும் இருக்கும், இது இருப்பதற்கான காரணத்தையே தோற்கடிக்கிறது. +தொடர் சுழல்முறைக்கு நிகல்நிலையில் நீங்கள் நெருங்கி வரக்கூடியது, அதிக எண்ணிக்கையிலான சுற்றுகளுடன் சுவிஸ் போட்டியை விளையாடுவதாகும். போட்டி முடிவதற்குள் சாத்தியமான அனைத்து ஜோடிகளும் விளையாடப்படும். + மற்ற போட்டி அமைப்புகளைப் பற்றி என்ன? + தற்சமயம் லிசெஸ்க்கு அதிகமான போட்டி அமைப்புகளைச் சேர்க்க நாங்கள் திட்டமிடவில்லை. + %1$s சுவிஸ் போட்டியில், ஒவ்வொரு போட்டியாளரும் மற்ற அனைத்து போட்டியாளர்களையும் விளையாட வேண்டிய அவசியமில்லை. போட்டியாளர்கள் ஒவ்வொரு சுற்றிலும் ஒருவரையொருவர் சந்திக்கிறார்கள், மேலும் ஒவ்வொரு போட்டியாளரும் ஒரே மாதிரியான மதிப்பெண்களுடன் எதிராளிகளை விளையாடுவதை உறுதிசெய்ய வடிவமைக்கப்பட்ட விதிகளின் தொகுப்பைப் பயன்படுத்தி ஜோடியாக இணைக்கப்படுகிறார்கள், ஆனால் ஒரே எதிரியை ஒன்றுக்கு மேற்பட்ட முறை அல்ல. வெற்றியாளர் அனைத்து சுற்றுகளிலும் பெறப்பட்ட அதிக மொத்த புள்ளிகளுடன் போட்டியாளர் ஆவார். ஒற்றைப்படை எண்ணிக்கையிலான வீரர்கள் இல்லாவிட்டால் அனைத்து போட்டியாளர்களும் ஒவ்வொரு சுற்றிலும் விளையாடுவார்கள். + சுவிஸ் போட்டிகளைக் குழு தலைவர்களால் மட்டுமே உருவாக்க முடியும், மேலும் குழு உறுப்பினர்களால் மட்டுமே விளையாட முடியும். +சுவிஸ் போட்டிகளில் விளையாட %1$s. + ஒரு குழுவில் சேரவும் அல்லது உருவாக்கவும் + தற்பொழுது விளையாடப்படுகிறது + விரைவில் தொடங்கும் + ஒப்பீடு + போட்டியின் காலம் + நிமிடங்களில் முன் வரையறுக்கப்பட்ட காலம் + முன் வரையறுக்கப்பட்ட அதிகபட்ச சுற்றுகள், ஆனால் கால அளவு தெரியவில்லை + விளையாட்டுகளின் எண்ணிக்கை + ஒதுக்கப்பட்ட காலத்தில் விளையாடலாம் + அனைத்து வீரர்களுக்கும் ஒரே மாதிரியாக முன்கூட்டியே முடிவு செய்யப்பட்டது + இணைத்தல் அமைப்பு + ஒரே மாதிரியான தரவரிசையில் கிடைக்கக்கூடிய எந்த எதிரியும் + புள்ளிகள் மற்றும் சமன்முறி ஆட்டங்களின் அடிப்படையில் சிறந்த இணைத்தல் + இணைத்தல் நேரம் காத்திருப்பு + வேகமாக: எல்லா வீரர்களுக்கும் காத்திருக்காது + மெதுவாக: அனைத்து வீரர்களுக்கும் காத்திருக்கிறது + ஒரே மாதிரியான இணைத்தல் + சாத்தியம், ஆனால் தொடர்ச்சியாக இல்லை + தடை செய்யப்பட்டுள்ளது + தாமதமாகச் சேர்தல் + ஆம் பாதிக்கு மேல் சுற்றுகள் தொடங்கும் வரை + Pause + ஆம் ஆனால் சுற்றுகளின் எண்ணிக்கையைக் குறைக்கலாம் + தொடர் மற்றும் மூர்க்கம் + OTB போட்டிகளைப் போன்றது + வரம்பற்ற மற்றும் இலவசம் + முன் வரையறுக்கப்பட்ட பயனர்களை மட்டுமே சேர அனுமதிக்கவும் + இந்தப் பட்டியல் காலியாக இல்லை என்றால், இந்தப் பட்டியலில் இல்லாத பயனர்கள் சேர தடை விதிக்கப்படும். ஒரு வரிக்கு ஒரு பயனர் பெயர். + உங்கள் ஆட்டங்களை விளையாடுங்கள் diff --git a/translation/dest/swiss/tp-TP.xml b/translation/dest/swiss/tp-TP.xml index aba00d0963526..b72ea21b2875b 100644 --- a/translation/dest/swiss/tp-TP.xml +++ b/translation/dest/swiss/tp-TP.xml @@ -1,6 +1,10 @@ utala musi pi nasin Suwasi + + musi %s + musi %s + lipu lawa pi kulupu FIDE o awen nanpa sin pi tenpo lili en nanpa sin pi pona mute poka diff --git a/translation/dest/swiss/vi-VN.xml b/translation/dest/swiss/vi-VN.xml index d684216505290..a9dd5e8435eb6 100644 --- a/translation/dest/swiss/vi-VN.xml +++ b/translation/dest/swiss/vi-VN.xml @@ -30,7 +30,7 @@ Số vòng đấu lẻ cho phép cân bằng màu quân trắng-đen tốt nhất. Khoảng thời gian giữa các vòng đấu Ghép cặp bị cấm - Tên người dùng của những người chơi không được với nhau (Ví dụ: Anh chị em ruột). Hai tên người dùng trên mỗi dòng, được phân tách bằng dấu cách. + Tên người dùng của những kỳ thủ không được đấu với nhau (Ví dụ: Anh chị em ruột). Hai tên người dùng trên mỗi dòng, được phân tách bằng dấu cách. Giải đấu hệ Thụy Sĩ mới Khi nào nên chơi giải hệ Thụy Sĩ thay vì giải Đấu trường? Trong một giải đấu hệ Thụy Sĩ, tất cả người chơi chơi cùng số lượng ván đấu, và chỉ chơi mỗi người một lần. @@ -61,8 +61,8 @@ Ngoài ra, 0.5 điểm \"bye\" cộng một lần duy nhất sẽ được cộn Sau đó, hệ thống sẽ tự động rút họ khỏi giải đấu để tránh thua thêm những ván khác. Họ có thể tham gia lại bất cứ lúc nào. Những gì được thực hiện liên quan đến vắng mặt? - Người chơi đăng ký các sự kiện của giải hệ Thụy Sĩ nhưng không chơi trò chơi của họ có thể gặp vấn đề. -Để giải quyết vấn đề này, Lichess ngăn những người chơi không chơi được trò chơi tham gia một sự kiện hệ Thụy Sĩ mới trong một khoảng thời gian nhất định. + Người chơi đăng ký các sự kiện của giải hệ Thụy Sĩ nhưng không chơi ván của họ có thể gặp vấn đề. +Để giải quyết vấn đề này, Lichess ngăn những người chơi không chơi được ván đấu tham gia một sự kiện hệ Thụy Sĩ mới trong một khoảng thời gian nhất định. Người tạo ở sự kiện hệ Thụy Sĩ vẫn có thể quyết định cho phép họ tham gia sự kiện đó. Người chơi có thể tham gia muộn không? Có, trước khi quá nửa số vòng của giải đã bắt đầu; ví dụ: trong 1 giải hệ Thụy Sĩ 11 vòng, người chơi có thể tham gia trước khi vòng 6 bắt đầu, còn trong giải hệ Thụy Sĩ 12 ván sẽ là trước khi vòng 7 bắt đầu. @@ -77,14 +77,14 @@ Thứ gần nhất bạn có thể có với một giải đấu vòng tròn là Hiện tại chúng tôi chưa có ý định thêm các hệ thống giải đấu khác vào Lichess. Trong một giải đấu hệ Thụy Sĩ %1$s, mỗi kì thủ không nhất thiết phải đấu với tất cả những người tham gia khác. Các kì thủ một đối một trong mỗi vòng và được ghép cặp với nhau bằng cách sử dụng một bộ quy tắc được thiết kế để đảm bảo rằng mỗi đấu thủ sẽ đấu với các đối thủ có điểm số tương tự nhưng không gặp lại cùng một đối thủ ở các vòng trước. Người chiến thắng là người có tổng điểm cao nhất sau tất cả các vòng. Mỗi vòng đấu tất cả các đấu thủ sẽ đều được chơi trừ khi số người chơi là số lẻ. Giải đấu hệ Thụy Sĩ chỉ có thể được tạo bởi đội trưởng và chỉ có thể được tham gia bởi các thành viên trong đội. %1$s để bắt đầu chơi trong các giải đấu hệ Thụy Sĩ. - Tham gia hoặc tạo một nhóm + Tham gia hoặc tạo một đội Đang chơi Sắp bắt đầu So sánh Khoảng thời gian của giải đấu Chọn trước khoảng thời gian theo phút Chọn trước khoảng thời gian theo vòng, nhưng không xác định về thời gian - Số lượng trò chơi + Số lượng ván đấu Số lượng tối đa ván đấu có thể chơi cho tới khi hết giờ Giới hạn số ván, đồng đều cho tất cả người chơi Hệ thống bắt cặp diff --git a/translation/dest/team/gsw-CH.xml b/translation/dest/team/gsw-CH.xml index 8ce0578d6d46e..651bf8441dc4f 100644 --- a/translation/dest/team/gsw-CH.xml +++ b/translation/dest/team/gsw-CH.xml @@ -9,7 +9,7 @@ Alli Teams Vu dir gleiteti Teams Neus Team - Mini Teams + Eigeni Teams Keis Team g\'funde Tritt em Team bi Gib dem Team en neue Leiter, bevor du gahsch - oder lös das Team uf. @@ -39,7 +39,7 @@ Benachrichtig alli Mitglider Schick e privati Nachricht a alli Mitglider vom Team Schick e privati Nachricht a ALLI Mitglider vu dem Team. Du chasch die Funktion benutze, um Schpiller uf z\'fordere sich ame Turnier oder ame Team-Kampf z\'beteilige. Schpiller wo die Nachrichte nöd wänd empfange, chönd s\'Team verlah. - Teams wonich leitä + Teams wo ich leite Wottsch eis vu de kommende Turnier verlinke? Teamleiter-Chat Team uflöösä diff --git a/translation/dest/team/lb-LU.xml b/translation/dest/team/lb-LU.xml index f8f4f95a0b18c..49a74aba7b495 100644 --- a/translation/dest/team/lb-LU.xml +++ b/translation/dest/team/lb-LU.xml @@ -25,7 +25,10 @@ All d\'Memberen kontaktéieren Ekipp opléisen Léist d\'Ekipp fir ëmmer op. + Falsche Bäitrëttscode. Dës Ekipp gëtt et schonn. + Nächst Turnéieren + Vergaangen Turnéieren Ofgeleenten Ufroen Ekippensäit diff --git a/translation/dest/team/ta-IN.xml b/translation/dest/team/ta-IN.xml index e01841a0a938d..1032927672d07 100644 --- a/translation/dest/team/ta-IN.xml +++ b/translation/dest/team/ta-IN.xml @@ -1,28 +1,28 @@ - அணி - அணிகள் + குழு + குழுக்கள் %s உறுப்பினர்கள் %s உறுப்பினர்கள் - எல்லா அணிகளும் - தலைவர் அணிகள் - புது அணி - என் அணிகள் - எந்த அணியும் கிட்டவில்லை - அணியோடு சேர் - வெளியேறும் முன் புதிய குழுத் தலைவரைச் சேர்க்கவும் அல்லது அணியை மூடவும். - அணியை விட்டுவிலகு + அனைத்து குழுக்களும் + தலைவர் குழுக்கள் + புது குழு + எனது குழுக்கள் + எந்தக் குழுவும் கிட்டவில்லை + குழுவில் சேர் + வெளியேறும் முன் புதிய குழுத் தலைவரைச் சேர்க்கவும் அல்லது குழுவை மூடவும். + குழுவை விட்டுவிலகு சேர்க்கை கோரிக்கைகளைக் கைமுறையாக மதிப்பாய்வு செய்யவும் - சரிபார்க்கப்பட்டால், வீரர்கள் அணியில் சேர ஒரு கோரிக்கையை எழுத வேண்டும், அதை நீங்கள் மறுக்கலாம் அல்லது ஏற்கலாம். + சரிபார்க்கப்பட்டால், வீரர்கள் குழுவில் சேர ஒரு கோரிக்கையை எழுத வேண்டும், அதை நீங்கள் மறுக்கலாம் அல்லது ஏற்கலாம். - அணி தலைவர் + குழுத் தலைவர் அணி தலைவர்கள் - அணியின் அண்மை உறுப்பினர்கள் - ஒருவரை அணியிலிருந்து வெளியேற்றவும் - அணியிலிருந்து யாரை வெளியேற்ற விரும்புகிறீர்கள்? + குழுவின் அண்மை உறுப்பினர்கள் + ஒருவரை குழுவிலிருந்து வெளியேற்றவும் + குழுவிலிருந்து யாரை வெளியேற்ற விரும்புகிறீர்கள்? %s சேர கோரிக்கை %s சேர கோரிக்கைகள் @@ -30,4 +30,29 @@ உங்கள் சேர்வதற்கான கோரிக்கை குழுத் தலைவரால் மதிப்பாய்வு செய்யப்படும். உங்கள் சேர்வதற்கான கோரிக்கை குழுத் தலைவரால் மதிப்பாய்வு செய்யப்படுகிறது. குழுத் தலைவரால் நீங்கள் சேருவதற்கான கோரிக்கை நிராகரிக்கப்பட்டது. + குழு செய்திகளுக்குக் குழுசேரவும் + குழுப் போர் + பல குழுக்களின் போர், ஒவ்வொரு வீரரும் தங்கள் குழுவிற்கு புள்ளிகளைப் பெறுகிறார்கள் + குழு போட்டி + உங்கள் குழு உறுப்பினர்கள் மட்டுமே சேரக்கூடிய ஒரு கோதா போட்டி + உங்கள் குழு உறுப்பினர்கள் மட்டுமே சேரக்கூடிய சுவிஸ் போட்டி + அனைத்து உறுப்பினர்களுக்கும் செய்தி அனுப்பவும் + குழுவின் ஒவ்வொரு உறுப்பினருக்கும் தனிப்பட்ட செய்தியை அனுப்பவும் + குழுவில் உள்ள அனைத்து உறுப்பினர்களுக்கும் தனிப்பட்ட செய்தியை அனுப்பவும். +ஒரு போட்டி அல்லது குழு போரில் சேர வீரர்களை அழைக்க இதைப் பயன்படுத்தலாம். +உங்கள் செய்திகளைப் பெற விரும்பாத வீரர்கள் குழுவை விட்டு வெளியேறலாம். + நான் வழிநடத்தும் குழுக்கள் + வரவிருக்கும் போட்டிகளில் ஒன்றை இணைக்க வேண்டுமா? + தலைவர்கள் அரட்டை + குழுவை மூடு + குழுவை நிரந்தரமாக மூடுகிறது. + குழு நுழைவு குறியீடு + (விரும்பினால்) இந்தக் குழுவில் சேர புதிய உறுப்பினர்கள் தெரிந்து கொள்ள வேண்டிய நுழைவுக் குறியீடு. + தவறான நுழைவு குறியீடு. + இந்தக் குழு ஏற்கனவே உள்ளது. + வரவிருக்கும் போட்டிகள் + போட்டிகள் நிறைவடைந்தன + நிராகரிக்கப்பட்ட கோரிக்கைகள் + செய்திகள் மற்றும் நிகழ்வுகளுக்கு அதிகாரப்பூர்வ %s குழுவில் சேரவும் + குழுப் பக்கம் diff --git a/translation/dest/team/vi-VN.xml b/translation/dest/team/vi-VN.xml index 15edba7524ad7..653f8ae3ba5d3 100644 --- a/translation/dest/team/vi-VN.xml +++ b/translation/dest/team/vi-VN.xml @@ -24,11 +24,11 @@ %s yêu cầu tham gia - Yêu cầu tham gia của bạn sẽ được xem xét bởi trưởng nhóm. - Yêu cầu tham gia của bạn đang được xem xét bởi trưởng nhóm. - Yêu cầu tham gia của bạn bị từ chối bởi trưởng nhóm. + Yêu cầu tham gia của bạn sẽ được xem xét bởi đội trưởng. + Yêu cầu tham gia của bạn đang được đội trưởng xem xét. + Yêu cầu tham gia của bạn đã bị từ chối bởi một đội trưởng. Đăng kí nhận tin nhắn của đội - Giải đấu đa đội + Giải đa đội Một trận đấu giữa nhiều đội, mỗi người chơi ghi điểm cho đội của họ Giải đấu trong đội Một giải đấu Đấu trường mà chỉ thành viên trong đội của bạn có thể tham gia @@ -36,9 +36,9 @@ Nhắn tin tới tất cả thành viên Gửi một tin nhắn riêng cho mỗi thành viên của đội Gửi tin nhắn riêng tới TẤT CẢ các thành viên của đội. -Bạn có thể dùng chức năng này để gọi các người chơi đến tham gia một giải đấu hoặc giải đấu đa đội. +Bạn có thể dùng chức năng này để gọi các người chơi đến tham gia một giải đấu hoặc giải đa đội. Những người chơi không muốn nhận tin nhắn của bạn có thể sẽ rời khỏi đội. - Những đội mà tôi làm đội trưởng + Tôi làm đội trưởng của những đội Có thể bạn muốn đường dẫn tới một trong những giải đấu sắp tới? Trò chuyện cho các đội trưởng Giải tán đội diff --git a/translation/dest/tfa/ar-SA.xml b/translation/dest/tfa/ar-SA.xml index ae894f2acb728..dfa96d742afff 100644 --- a/translation/dest/tfa/ar-SA.xml +++ b/translation/dest/tfa/ar-SA.xml @@ -1,15 +1,18 @@ - المصادقة الثنائية - المصادقة الثنائية تضيف طبقة أمان أخرى الى حسابك. + التوثيق ذو العاملين + التوثيق ذو العاملين يضيف طبقة أمان أخرى إلى حسابك. + احصل على تطبيق يحوي مزيّة التوثيق ذو العاملين. نوصي بالتطبيقات التالية: أمسح رمز ال QR باستخدام التطبيق. - أدخل كلمة المرور الخاصة بك ورمز المصادقة الذي تم إنشاؤه بواسطة التطبيق لإكمال الإعداد. سوف تحتاج إلى رمز المصادقة في كل مرة تقوم بتسجيل الدخول. - إذا لم تتمكن من مسح الرمز ، قم بإدخال السر %s في التطبيق الخاص بك. + أدخل كلمة المرور الخاصة بك ورمز المصادقة من التطبيق لإكمال الإعداد. سوف تحتاج إلى رمز المصادقة في كل مرة تسجل فيها الدخول. + إذا لم تتمكن من مسح الرمز، أدخل الرمز %s في التطبيق الخاص بك. رمز التحقق - تفعيل المصادقة الثنائية - تعطيل المصادقة الثنائية - المصادقة الثنائية مفعلة - افتح تطبيق المصادقة الثنائية على جهازك لعرض رمز المصادقة الخاص بك والتحقق من هويتك. - الرجاء تمكين المصادقة الثنائية لتأمين حسابك على https://lichess.org/account/twofactor. -لقد تلقيت هذه الرسالة لأن حسابك لديه مسؤوليات خاصة مثل قائد الفريق أو المدرب أو المعلم أو صاحب بث + ملاحظة: إذا فقدت الوصول إلى رموز التوثيق ذو العاملين، فيمكنك إعادة تعيين كلمة المرور عبر%s البريد الإلكتروني. + تفعيل التوثيق ذو العاملين + تعطيل التوثيق ذو العاملين + التوثيق ذو العاملين مفعل + تحتاج إلى كلمة المرور الخاصة بحسابك ورمز التفعيل من التطبيق الذي تستخدمه لتعطيل التوثيق ذو العاملين. + افتح تطبيق التوثيق ذو العاملين على جهازك لعرض رمز التوثيق الخاص بك والتحقق من هويتك. + يرجى تمكين التوثيق ذو العاملين لتأمين حسابك على https://lichess.org/account/twofactor. +لقد تلقيت هذه الرسالة لأن حسابك لديه صلاحيات خاصة مثل قائد الفريق أو المدرب أو المعلم أو صاحب بث. diff --git a/translation/dest/tfa/en-US.xml b/translation/dest/tfa/en-US.xml index 08262cffb91ae..4acdf5aabe945 100644 --- a/translation/dest/tfa/en-US.xml +++ b/translation/dest/tfa/en-US.xml @@ -7,9 +7,11 @@ Enter your password and the authentication code generated by the app to complete the setup. You will need an authentication code every time you log in. If you cannot scan the code, enter the secret %s into your app. Authentication code + Note: If you lose access to your two-factor authentication codes, you can do a %s via email. Enable two-factor authentication Disable two-factor authentication Two-factor authentication enabled + You need your password and an authentication code from your authenticator app to disable two-factor authentication. Open the two-factor authentication app on your device to view your authentication code and verify your identity. Please enable two-factor authentication to secure your account at https://lichess.org/account/twofactor. You received this message because your account has special responsibilities such as team leader, coach, teacher or streamer diff --git a/translation/dest/tfa/gl-ES.xml b/translation/dest/tfa/gl-ES.xml index 086281f19fc9c..e97dad7ea939b 100644 --- a/translation/dest/tfa/gl-ES.xml +++ b/translation/dest/tfa/gl-ES.xml @@ -7,6 +7,7 @@ Introduce o teu contrasinal e o código de autenticación xerado pola aplicación para completar a configuración. Necesitarás un código de autenticación cada vez que comeces a sesión. Se non podes escanear o código, introduce a chave secreta %s na túa aplicación. Código de autenticación + Nota: se perdes o acceso ós teus códigos de autenticación en dous pasos, podes %s usando o correo electrónico. Activar a autenticación en dous pasos Desactivar a autenticación en dous pasos A autenticación en dous pasos está activada diff --git a/translation/dest/tfa/gsw-CH.xml b/translation/dest/tfa/gsw-CH.xml index 6290c2295e58c..6e48da2421fb7 100644 --- a/translation/dest/tfa/gsw-CH.xml +++ b/translation/dest/tfa/gsw-CH.xml @@ -1,19 +1,19 @@ - Zwei-Faktorä Autentifizierig - Mit de \"Zwei-Faktor Authentifizierig\" machsch dis Konto es Level sicherer. + Zwei-Faktor-Autentifizierig + Mit de \"Zwei-Faktor-Authentifizierig\" machsch dis Konto um es Level sicherer. B\'sorg dir doch e App, für die Zwei-Faktor-Authentifizierig. Mir empfehled folgendi Apps: - Scann dä QR Code mit dä App. + Scann de QR Code mit de App. Tipp dis Passwort ine und de Authentifizierigscode, wo vu de App generiert worde isch, zum Abschlüsse vu dere Irichtig. Künftig bruchsch, zum Amälde, jedesmal so en Code. - Falls dä Code nicht einscannä chasch, tipp dä gheim Code %s i dini App. + Falls du de Code nöd chasch iscänne, tipp de G\'heimcode %s i dini App. Authentifizierigscode Hiwis: Wänn du de Zuegriff uf dini Cods für d\'Zwei-Faktor-Authentifizierig verlürsch, chasch per E-Mail en %s schicke. Aktivier d\'Zwei-Faktor-Authentifizierig Deaktivier d\'Zwei-Faktor-Authentifizierig - Zwei-Faktor Authentifizierig isch aktiviert - Du bruchsch dis Passwort und en Code vu dinere Authenticator-App, zum Deaktiviere vu de Zwei-Faktor-Authentifizierig. - Mach uf dim Grät d\'App für die zweischtufig Authentifizierig uf, det findsch din Authentifizierigscode, wo dini Identität beschtätigt. - Bitte tue d\'Zwei-Faktore-Authentifizierig aktiviere und demit dis Konto uf - -https://lichess.org/account/twofactor - sichere! -Du häsch de Hiwis übercho, will du - mit dim Konto - bsunderi Funkzione usüebsch, z.B. als Teamleiter, als Trainer, als Lehrer oder Streamer. + Zwei-Faktor-Authentifizierig isch aktiviert + Zur Deaktivierig vu de Zwei-Faktor-Authentifizierig bruchsch dis Passwort und en Code vu de Authenticator-App. + Mach d\'App für d\'Zwei-Faktor-Authentifizierig uf, det findsch de Code, wo dini Identität beschtätigt. + Bitte d\'Zwei-Faktore-Authentifizierig aktiviere und dis Konto uf - +https://lichess.org/account/twofactor - sichere! +Du chunsch die Mäldig über, will du - mit dim Konto - b\'sunderi Funkzione usüebsch, z.B. als Teamleiter, als Trainer, als Lehrer oder Streamer. diff --git a/translation/dest/tfa/he-IL.xml b/translation/dest/tfa/he-IL.xml index df6db743d6bdb..c7f03260caac6 100644 --- a/translation/dest/tfa/he-IL.xml +++ b/translation/dest/tfa/he-IL.xml @@ -7,9 +7,11 @@ הכניסו את הסיסמה ואת קוד האימות שנוצר על ידי האפליקציה כדי להשלים את הרישום. תזדקק/י לקוד אימות בכל התחברות מחדש. אם אין ביכולתכם לסרוק את הקוד, הקישו את הרצף הסודי %s לאפליקציה שלכם. קוד אימות + הערה: אם איבדתם את הגישה לקודים המשמשים אתכם לאימות דו־שלבי, השתמשו ב%s באמצעות אימייל. הפעלת אימות דו־שלבי השבתת אימות דו־שלבי אימות דו-שלבי הופעל + יש צורך בסיסמה ובקוד אימות מהאפליקציה שאתם משתמשים בה כדי לבטל אימות דו־שלבי. פתחו את אפליקציית האימות הדו־שלבי שברשותכם להצגת קוד האימות ואמתו את זהותכם. אנא הפעל/י אימות דו־שלבי כדי לאבטח את חשבונך ב-https://lichess.org/account/twofactor. diff --git a/translation/dest/tfa/it-IT.xml b/translation/dest/tfa/it-IT.xml index 53ea761dde250..51a0e80548824 100644 --- a/translation/dest/tfa/it-IT.xml +++ b/translation/dest/tfa/it-IT.xml @@ -7,9 +7,11 @@ Inserisci la tua password e il codice di autenticazione generato dall\'app per completare l\'installazione. Avrai bisogno di un codice di autenticazione ogni volta che effettui l\'accesso. Se non riesci a scansionare il codice, inserisci il codice segreto %s nell\'app. Codice di autenticazione + Nota: Se perdi l\'accesso ai tuoi codici d\'autenticazione a due fattori, puoi eseguire un %s tramite email. Abilita l\'autenticazione a due fattori Disabilita l\'autenticazione a due fattori Autenticazione a due fattori attivata + Necessiti della tua password e di un codice dalla tua app d\'autenticazione, per disabilitare l\'autenticazione a due fattori. Apri l\'app di autenticazione a due fattori sul tuo dispositivo per visualizzare il tuo codice di autenticazione e verificare la tua identità. Sei pregato di abilitare l\'autenticazione a due fattori per proteggere il tuo profilo, su https://lichess.org/account/twofactor. Hai ricevuto questo messaggio perché il tuo profilo ha responsabilità speciali, come capo squadra, istruttore, insegnante o streamer diff --git a/translation/dest/tfa/nb-NO.xml b/translation/dest/tfa/nb-NO.xml index a36e643d0874e..a831b2d8154af 100644 --- a/translation/dest/tfa/nb-NO.xml +++ b/translation/dest/tfa/nb-NO.xml @@ -7,9 +7,11 @@ Skriv inn passordet ditt og autentiseringskoden generert av appen for å fullføre oppsettet. Du må bruke en ny autentiseringskode hver gang du logger inn. Hvis du ikke får til å skanne koden, kan du taste inn hemmeligheten %s i appen din. Autentiseringskode + Merk: Hvis du mister tilgang til tofaktorautentiseringskodene dine, så kan du %s via e-post. Skru på tofaktorautentisering Skru av tofaktorautentisering Tofaktorautentisering skrudd på + Du trenger passordet ditt og en autentiseringskode fra autentiseringsappen din for å deaktivere tofaktorautentisering. Åpne tofaktorautentiseringsappen på enheten din for å vise autentiseringskoden og bekrefte identiteten din. Sikre kontoen din ved å skru på tofaktorautentisering på https://lichess.org/account/twofactor. Du mottar denne meldingen fordi kontoen din har særskilte ansvarsområder, slik som lagleder, trener, lærer eller strømmer. diff --git a/translation/dest/tfa/sv-SE.xml b/translation/dest/tfa/sv-SE.xml index 5190f1c682e44..742693a77b720 100644 --- a/translation/dest/tfa/sv-SE.xml +++ b/translation/dest/tfa/sv-SE.xml @@ -7,9 +7,11 @@ Ange ditt lösenord och autentiseringskoden som genereras av appen för att slutföra installationen. Du behöver en autentiseringskod varje gång du loggar in. Om du inte kan skanna koden, ange hemligheten %s i din app. Autentiseringskod + Obs: Om du förlorar åtkomst till dina tvåfaktorsautentiseringskoder, kan du göra en %s via e-post. Aktivera tvåfaktorsautentisering Inaktivera tvåfaktorsautentisering Tvåfaktorsautentisering aktiverad + Du behöver ditt lösenord och en autentiseringskod från din autentiseringapp för att inaktivera tvåfaktorsautentisering. Öppna tvåfaktorsautentiseringsappen på din enhet för att visa din autentiseringskod och verifiera din identitet. Aktivera tvåfaktorsautentisering för att skydda ditt konto på https://lichess.org/account/twofactor. Du fick detta meddelande eftersom ditt konto har särskilda ansvarsområden som teamledare, coach, lärare eller streamer diff --git a/translation/dest/tfa/ta-IN.xml b/translation/dest/tfa/ta-IN.xml index c5b27a6070ded..d43a91803d207 100644 --- a/translation/dest/tfa/ta-IN.xml +++ b/translation/dest/tfa/ta-IN.xml @@ -2,13 +2,16 @@ இரு-காரணி அங்கீகாரம் இரு-காரணி அங்கீகாரம் உங்கள் கணக்கில் மற்றொரு பாதுகாப்பு அடுக்கு சேர்க்கிறது. + இரண்டு காரணி அங்கீகாரத்திற்கான பயன்பாட்டைப் பெறவும். பின்வரும் பயன்பாடுகளை நாங்கள் பரிந்துரைக்கிறோம்: பயன்பாட்டின் மூலம் QR குறியீட்டை ஊடறி செய்யவும். அமைப்பை முடிக்க, உங்கள் கடவுச்சொல்லையும் செயலி உருவாக்கிய அங்கீகாரக் குறியீட்டையும் உள்ளிடவும். ஒவ்வொரு முறை உள்நுழையும்போதும் அங்கீகாரக் குறியீடு தேவைப்படும். உங்களால் குறியீட்டை ஊடறி செய்ய முடியாவிட்டால், உங்கள் பயன்பாட்டில் %s என்ற ரகசிய விசையை உள்ளிடவும். அங்கீகார குறியீடு + குறிப்பு: உங்கள் இரு காரணி அங்கீகாரக் குறியீடுகளுக்கான அணுகலை இழந்தால், மின்னஞ்சல் வழியாக %sஐச் செய்யலாம். இரண்டு காரணி அங்கீகாரத்தை இயக்கு இரண்டு காரணி அங்கீகாரத்தை முடக்கு இரண்டு காரணி அங்கீகாரம் இயக்கப்பட்டது + இரண்டு காரணி அங்கீகாரத்தை முடக்க, உங்கள் அங்கீகரிப்பு பயன்பாட்டிலிருந்து உங்கள் கடவுச்சொல் மற்றும் அங்கீகாரக் குறியீடு தேவை. உங்கள் அங்கீகரிப்புக் குறியீட்டைப் பார்க்கவும் உங்கள் அடையாளத்தைச் சரிபார்க்கவும் உங்கள் சாதனத்தில் இரு காரணி அங்கீகார பயன்பாட்டைத் திறக்கவும். Https://lichess.org/account/twofactor இல் உங்கள் கணக்கைப் பாதுகாக்க இரு காரணி அங்கீகாரத்தை இயக்கவும். உங்கள் கணக்கில் தலைப்பு கொண்ட வீரர், குழுத் தலைவர், பயிற்சியாளர், ஆசிரியர் அல்லது ஸ்ட்ரீமர் போன்ற சிறப்புப் பொறுப்புகள் இருப்பதால், இந்தச் செய்தியைப் பெற்றுள்ளீர்கள். diff --git a/translation/dest/tfa/th-TH.xml b/translation/dest/tfa/th-TH.xml index 67f2346644222..1acf72ce1b163 100644 --- a/translation/dest/tfa/th-TH.xml +++ b/translation/dest/tfa/th-TH.xml @@ -6,7 +6,9 @@ สแกน QR code ด้วยแอป ใส่รหัสผ่านของคุณและรหัสรับรองความถูกต้องที่สร้างโดยแอปเพื่อให้การติดตั้งเสร็จสมบูรณ์ คุณจำเป็นต้องมีรหัสรับรองฯทุกครั้งที่เข้าสู่ระบบ รหัสรับรองความถูกต้อง + โปรดทราบ: ถ้าคุณไม่สามารถเข้าถึงรหัสรับรองความถูกต้องแบบสองชั้นได้ คุณสามารถทำ %s ได้ผ่านทางอีเมล ใช้การรับรองความถูกต้องแบบสองชั้น ไม่ใช้การรับรองความถูกต้องแบบสองชั้น การรับรองความถูกต้องแบบสองชั้นถูกใช้งาน + คุณต้องใช้รหัสผ่านและรหัสการตรวจสอบสิทธิ์จากแอปตรวจสอบความถูกต้องของคุณเพื่อปิดใช้งานการตรวจสอบสิทธิ์แบบสองปัจจัย diff --git a/translation/dest/tfa/tr-TR.xml b/translation/dest/tfa/tr-TR.xml index b43f6bf173ca1..346bd6ecd1d4d 100644 --- a/translation/dest/tfa/tr-TR.xml +++ b/translation/dest/tfa/tr-TR.xml @@ -10,6 +10,7 @@ İki faktörlü kimlik doğrulamayı etkinleştir İki faktörlü kimlik doğrulamayı devre dışı bırak İki faktörlü kimlik doğrulama etkinleştirildi + İki faktörlü kimlik doğrulamayı devre dışı bırakmak için şifrenize ve bir doğrulama koduna ihtiyacınız var. Doğrulama kodunu görüntüleyerek kimliğinizi doğrulamak için cihazınızdan iki faktörlü kimlik doğrulama uygulamasını açın. Lütfen hesabınızın güvenliği için https://lichess.org/account/twofactor üzerinden iki faktörlü kimlik doğrulamayı etkinleştirin. Bu mesajı aldınız çünkü hesabınız takım liderliği, koç, öğretmen ya da yayıncı gibi özel bir sorumluluğa sahip diff --git a/translation/dest/tfa/vi-VN.xml b/translation/dest/tfa/vi-VN.xml index fc304b8e03654..bc8f8c5c0c462 100644 --- a/translation/dest/tfa/vi-VN.xml +++ b/translation/dest/tfa/vi-VN.xml @@ -2,7 +2,7 @@ Xác thực 2 bước Xác thực 2 bước sẽ thêm một lớp bảo mật cho tài khoản của bạn. - Tải ứng dụng để xác minh 2 bước. Chúng tôi đề xuất những ứng dụng sau: + Tải ứng dụng để xác minh 2 bước. Chúng tôi đề xuất các ứng dụng sau: Quét mã QR bằng ứng dụng. Nhập mật khẩu của bạn và mã xác thực do ứng dụng tạo để hoàn tất thiết lập. Bạn sẽ cần mã xác thực mỗi khi đăng nhập. Nếu bạn không thể quét mã, hãy nhập khóa bí mật %s vào ứng dụng của bạn. diff --git a/translation/dest/timeago/af-ZA.xml b/translation/dest/timeago/af-ZA.xml index 56c8f9bfa997b..b1a49cf491fa5 100644 --- a/translation/dest/timeago/af-ZA.xml +++ b/translation/dest/timeago/af-ZA.xml @@ -54,4 +54,13 @@ %s jaar gelede %s jare gelede + + nog %s minuut oor + nog %s minute oor + + + nog %s uur oor + nog %s ure oor + + voltooi diff --git a/translation/dest/timeago/ar-SA.xml b/translation/dest/timeago/ar-SA.xml index c81ca011c5082..2be15692ceb09 100644 --- a/translation/dest/timeago/ar-SA.xml +++ b/translation/dest/timeago/ar-SA.xml @@ -106,4 +106,21 @@ منذ %s سنة منذ %s سنة + + %sدقيقة متبقية + %sدقيقة متبقية + %sدقيقتان متبقيتان + %sدقائق متبقية + %sدقيقة متبقية + %sدقائق متبقية + + + %sساعة متبقية + %sساعة واحدة متبقية + %sساعتان متبقيتان + %s ساعات متبقية + %sساعة متبقية + %sساعة متبقية + + مكتمل diff --git a/translation/dest/timeago/ckb-IR.xml b/translation/dest/timeago/ckb-IR.xml index e574ebcde86d4..5c0e2aa00eead 100644 --- a/translation/dest/timeago/ckb-IR.xml +++ b/translation/dest/timeago/ckb-IR.xml @@ -54,4 +54,13 @@ %s ساڵ لەمەوبەر %s ساڵ لەمەوبەر + + %s خولەک ماوە + %s خولەک ماوە + + + %s کاژێر ماوە + %s کاژێر ماوە + + تەواو بوو diff --git a/translation/dest/timeago/da-DK.xml b/translation/dest/timeago/da-DK.xml index a354420e80ec0..5ebd304561369 100644 --- a/translation/dest/timeago/da-DK.xml +++ b/translation/dest/timeago/da-DK.xml @@ -54,4 +54,13 @@ %s år siden %s år siden + + %s minut tilbage + %s minutter tilbage + + + %s time tilbage + %s timer tilbage + + afsluttet diff --git a/translation/dest/timeago/de-DE.xml b/translation/dest/timeago/de-DE.xml index 9842c8a38b7c9..1ef3f6d9df39e 100644 --- a/translation/dest/timeago/de-DE.xml +++ b/translation/dest/timeago/de-DE.xml @@ -54,4 +54,13 @@ vor %s Jahr vor %s Jahren + + %s Minute verbleibend + %s Minuten verbleibend + + + %s Stunde übrig + %s Stunden übrig + + vervollständigt diff --git a/translation/dest/timeago/el-GR.xml b/translation/dest/timeago/el-GR.xml index 747b391d956c0..e0889bda6c603 100644 --- a/translation/dest/timeago/el-GR.xml +++ b/translation/dest/timeago/el-GR.xml @@ -29,7 +29,7 @@ σε %s έτος %s έτη - αυτή την στιγμή + αυτή τη στιγμή %s λεπτό πριν %s λεπτά πριν diff --git a/translation/dest/timeago/en-US.xml b/translation/dest/timeago/en-US.xml index 5a403c12e17e6..72492b7f85b06 100644 --- a/translation/dest/timeago/en-US.xml +++ b/translation/dest/timeago/en-US.xml @@ -54,4 +54,13 @@ %s year ago %s years ago + + %s minute remaining + %s minutes remaining + + + %s hour remaining + %s hours remaining + + completed diff --git a/translation/dest/timeago/es-ES.xml b/translation/dest/timeago/es-ES.xml index ff97bc3df6fe1..cab0d4cefd804 100644 --- a/translation/dest/timeago/es-ES.xml +++ b/translation/dest/timeago/es-ES.xml @@ -54,4 +54,13 @@ hace %s año hace %s años + + %s minutos restantes + %s minutos restantes + + + %s horas restantes + %s horas restantes + + completado diff --git a/translation/dest/timeago/fa-IR.xml b/translation/dest/timeago/fa-IR.xml index 593c42930d888..e9834cf9e3b2e 100644 --- a/translation/dest/timeago/fa-IR.xml +++ b/translation/dest/timeago/fa-IR.xml @@ -54,4 +54,13 @@ %s سال پیش %s سال پیش + + %s دقیقه باقی مانده است + %s دقیقه باقی مانده است + + + %s ساعت باقی مانده است + %s ساعت باقی مانده است + + کامل شده diff --git a/translation/dest/timeago/fr-FR.xml b/translation/dest/timeago/fr-FR.xml index af1a45d7ca3e0..7ce36f9690575 100644 --- a/translation/dest/timeago/fr-FR.xml +++ b/translation/dest/timeago/fr-FR.xml @@ -54,4 +54,13 @@ il y a %s an il y a %s ans + + %s minute restante + %s minutes restantes + + + %s heure restante + %s heures restantes + + terminé diff --git a/translation/dest/timeago/gl-ES.xml b/translation/dest/timeago/gl-ES.xml index 2bc48b5ce9ac1..17ea2696aa460 100644 --- a/translation/dest/timeago/gl-ES.xml +++ b/translation/dest/timeago/gl-ES.xml @@ -54,4 +54,13 @@ Hai %s ano Hai %s anos + + %s minuto restante + %s minutos restantes + + + %s hora restante + %s horas restantes + + completado diff --git a/translation/dest/timeago/gsw-CH.xml b/translation/dest/timeago/gsw-CH.xml index e89ae11f0687f..8d0d87d4f8e23 100644 --- a/translation/dest/timeago/gsw-CH.xml +++ b/translation/dest/timeago/gsw-CH.xml @@ -54,4 +54,13 @@ vor %s Jahr vor %s Jahr + + %s Minute blibt + %s Minute blibed + + + %s Schtund blibt + %s Schtunde blibed + + beändet diff --git a/translation/dest/timeago/he-IL.xml b/translation/dest/timeago/he-IL.xml index eb67e91a1f0a2..014cab7c6e55c 100644 --- a/translation/dest/timeago/he-IL.xml +++ b/translation/dest/timeago/he-IL.xml @@ -80,4 +80,17 @@ לפני %s שנים לפני %s שנים + + דקה %s נותרה + %s דקות נותרו + %s דקות נותרו + %s דקות נותרו + + + שעה %s נותרה + %s שעות נותרו + %s שעות נותרו + %s שעות נותרו + + הושלם diff --git a/translation/dest/timeago/it-IT.xml b/translation/dest/timeago/it-IT.xml index cf39688c8d4b4..e44adecb7faba 100644 --- a/translation/dest/timeago/it-IT.xml +++ b/translation/dest/timeago/it-IT.xml @@ -54,4 +54,5 @@ %s anno fa %s anni fa + completato diff --git a/translation/dest/timeago/ja-JP.xml b/translation/dest/timeago/ja-JP.xml index f9cc0e3e65821..c1c945b31d6fc 100644 --- a/translation/dest/timeago/ja-JP.xml +++ b/translation/dest/timeago/ja-JP.xml @@ -41,4 +41,11 @@ %s 年前 + + 残り %s 分 + + + 残り %s 時間 + + 完了 diff --git a/translation/dest/timeago/lb-LU.xml b/translation/dest/timeago/lb-LU.xml index 21fdf38292b89..dc0ce06e7b638 100644 --- a/translation/dest/timeago/lb-LU.xml +++ b/translation/dest/timeago/lb-LU.xml @@ -54,4 +54,12 @@ virun %s Joer virun %s Joer + + %s Minutt iwwereg + %s Minutten iwwereg + + + %s Stonn iwwereg + %s Stonnen iwwereg + diff --git a/translation/dest/timeago/nb-NO.xml b/translation/dest/timeago/nb-NO.xml index 9bb693b1a4048..72c9457fbe223 100644 --- a/translation/dest/timeago/nb-NO.xml +++ b/translation/dest/timeago/nb-NO.xml @@ -54,4 +54,13 @@ for %s år siden for %s år siden + + %s minutt igjen + %s minutter igjen + + + %s time igjen + %s timer igjen + + fullført diff --git a/translation/dest/timeago/nl-NL.xml b/translation/dest/timeago/nl-NL.xml index 0858b4af9a9d6..a1e73aaa90689 100644 --- a/translation/dest/timeago/nl-NL.xml +++ b/translation/dest/timeago/nl-NL.xml @@ -54,4 +54,13 @@ %s jaar geleden %s jaar geleden + + %s minuut resterend + %s minuten resterend + + + %s uur resterend + %s uur resterend + + voltooid diff --git a/translation/dest/timeago/nn-NO.xml b/translation/dest/timeago/nn-NO.xml index 7c11e96953301..d5ae6452d2902 100644 --- a/translation/dest/timeago/nn-NO.xml +++ b/translation/dest/timeago/nn-NO.xml @@ -54,4 +54,13 @@ %s år sidan %s år sidan + + %s minutt igjen + %s minutt igjen + + + %s time igjen + %s timar igjen + + fullført diff --git a/translation/dest/timeago/pl-PL.xml b/translation/dest/timeago/pl-PL.xml index 928954ec993d6..e897b657f6c22 100644 --- a/translation/dest/timeago/pl-PL.xml +++ b/translation/dest/timeago/pl-PL.xml @@ -80,4 +80,17 @@ %s lat temu %s lat temu + + Pozostała %s minuta + Pozostały %s minuty + Pozostało %s minut + Pozostało %s minut + + + Pozostała %s godzina + Pozostały %s godziny + Pozostało %s godzin + Pozostało %s godzin + + ukończone diff --git a/translation/dest/timeago/pt-BR.xml b/translation/dest/timeago/pt-BR.xml index 50b07c187023f..a4a5c2d8b853c 100644 --- a/translation/dest/timeago/pt-BR.xml +++ b/translation/dest/timeago/pt-BR.xml @@ -54,4 +54,13 @@ %s ano atrás %s anos atrás + + %s minuto restante + %s minutos restantes + + + %s hora restante + %s horas restantes + + concluído diff --git a/translation/dest/timeago/pt-PT.xml b/translation/dest/timeago/pt-PT.xml index 39e48d666367e..ad067adc5f483 100644 --- a/translation/dest/timeago/pt-PT.xml +++ b/translation/dest/timeago/pt-PT.xml @@ -10,7 +10,7 @@ em %s minutos - em %s horas + em %s hora em %s horas @@ -54,4 +54,13 @@ há %s ano há %s anos + + %s minuto restante + %s minutos restantes + + + %s hora restante + %s horas restantes + + concluído diff --git a/translation/dest/timeago/ru-RU.xml b/translation/dest/timeago/ru-RU.xml index f822d3313f811..f99d144827db5 100644 --- a/translation/dest/timeago/ru-RU.xml +++ b/translation/dest/timeago/ru-RU.xml @@ -80,4 +80,17 @@ %s лет назад %s лет назад + + осталась %s минута + осталось %s минуты + осталось %s минут + осталось %s минут + + + остался %s час + осталось %s часа + осталось %s часов + осталось %s часов + + завершено diff --git a/translation/dest/timeago/sl-SI.xml b/translation/dest/timeago/sl-SI.xml index 7ee0e28b1fbca..8e6a140cb6976 100644 --- a/translation/dest/timeago/sl-SI.xml +++ b/translation/dest/timeago/sl-SI.xml @@ -80,4 +80,17 @@ Pred %s leti Pred %s leti + + še %s minuta + še %s minuti + še %s minute + še %s minut + + + še %s ura + še %s uri + še %s ure + še %s ur + + končano diff --git a/translation/dest/timeago/so-SO.xml b/translation/dest/timeago/so-SO.xml index 3ea04e700dfa8..e437aa4842f33 100644 --- a/translation/dest/timeago/so-SO.xml +++ b/translation/dest/timeago/so-SO.xml @@ -1,2 +1,66 @@ - + + hadda + + %s sikin gudihii + %s sikin gudohood + + + %s mirir gudihii + %s mirir gudohood + + + %s saacad gudeheed + %s saacadood gudohood + + + %s maalin gudeheed + %s maalmood gudohood + + + %s usbuuc gudihii + %s usbuuc gudohood + + + %s bil gudeheed + %s bilood gudohood + + + %s sano gudihii + %s sano gudohood + + immika + + %s mirir ka hor + %s mirir ka hor + + + %s saacad ka hor + %s saacadood ka hor + + + %s maalin ka hor + %s maalmood ka hor + + + %s usbuuc ka hor + %s usbuuc ka hor + + + %s bil ka hor + %s bilood ka hor + + + %s sano ka hor + %s sano ka hor + + + %s mirir baa hadhay + %s mirir baa hadhay + + + %s saacad baa hadhay + %s saacadood baa hadhay + + dhamaad + diff --git a/translation/dest/timeago/sv-SE.xml b/translation/dest/timeago/sv-SE.xml index ef085fe0ebdc0..1bb48639ee881 100644 --- a/translation/dest/timeago/sv-SE.xml +++ b/translation/dest/timeago/sv-SE.xml @@ -54,4 +54,13 @@ %s år sedan %s år sedan + + %s minut återstår + %s minuter återstår + + + %s timme återstår + %s timmar återstår + + slutfört diff --git a/translation/dest/timeago/th-TH.xml b/translation/dest/timeago/th-TH.xml index 643d183045691..9569a596161e5 100644 --- a/translation/dest/timeago/th-TH.xml +++ b/translation/dest/timeago/th-TH.xml @@ -41,4 +41,11 @@ %s ปีที่แล้ว + + เหลือ %s นาที + + + เหลือ %s ชั่วโมง + + เสร็จสมบูรณ์ diff --git a/translation/dest/timeago/tp-TP.xml b/translation/dest/timeago/tp-TP.xml index 985d34dc001c0..3de0db3150bd6 100644 --- a/translation/dest/timeago/tp-TP.xml +++ b/translation/dest/timeago/tp-TP.xml @@ -54,4 +54,13 @@ lon tenpo sike pini %s lon tenpo sike pini %s + + tenpo lili %s li lon + tenpo lili %s li lon + + + tenpo suli %s li lon + tenpo suli %s li lon + + pini diff --git a/translation/dest/timeago/uk-UA.xml b/translation/dest/timeago/uk-UA.xml index 926a1f8af5310..fed9026917c98 100644 --- a/translation/dest/timeago/uk-UA.xml +++ b/translation/dest/timeago/uk-UA.xml @@ -80,4 +80,5 @@ %s років тому %s років тому + завершено diff --git a/translation/dest/timeago/vi-VN.xml b/translation/dest/timeago/vi-VN.xml index 38da7f6515cbb..4ad0c2b295c94 100644 --- a/translation/dest/timeago/vi-VN.xml +++ b/translation/dest/timeago/vi-VN.xml @@ -41,4 +41,11 @@ %s năm trước + + Còn lại %s phút + + + Còn lại %s giờ + + đã hoàn thành diff --git a/translation/dest/timeago/zh-CN.xml b/translation/dest/timeago/zh-CN.xml index 9325c138aec68..653d8781cb3b7 100644 --- a/translation/dest/timeago/zh-CN.xml +++ b/translation/dest/timeago/zh-CN.xml @@ -41,4 +41,11 @@ %s年前 + + 还剩 %s 分钟 + + + 还剩 %s 小时 + + 已完成 diff --git a/translation/dest/tourname/ckb-IR.xml b/translation/dest/tourname/ckb-IR.xml index d72ee7179f9d2..429eea4bed34b 100644 --- a/translation/dest/tourname/ckb-IR.xml +++ b/translation/dest/tourname/ckb-IR.xml @@ -3,20 +3,20 @@ یاریگای ڕاپیتی کاتژمێری ڕاپیتی کاتژمێری کاتژمێری%s یاریگا - کاتژمێرانە%s + کاتژمێرانە%s میدان مسابقه سریع روزانه یاریکردنی رۆژانەی ڕاپیت - یاریگای کلاسیکی رۆژانە - رۆژانەی کلاسیک - رۆژانە %s یاریگا + یاریگای کلاسیکی رۆژانە + رۆژانەی کلاسیک + رۆژانە %s یاریگا ڕۆژانە%s - یاریگای ڕاپیتی رۆژھەڵاتی - ڕاپیتی ڕۆژھەڵاتی + یاریگای ڕاپیتی رۆژھەڵاتی + ڕاپیتی ڕۆژھەڵاتی یاریگای کلاسیک بۆ ڕۆژھەڵات کلاسیکی ڕۆژھەڵاتی رۆژھەڵات%s یاریگا ڕۆژھەڵات%s - یاریگای ھەفتەیی ڕاپیت + یاریگای ھەفتانەی ڕاپیت ڕاپیتی ھەفتانە یاریگای ھەفتانەی کلاسیک یاریگای ھەفتانەی کلاسیک diff --git a/translation/dest/tourname/ta-IN.xml b/translation/dest/tourname/ta-IN.xml index c3d54b7525289..f9aa040da69d2 100644 --- a/translation/dest/tourname/ta-IN.xml +++ b/translation/dest/tourname/ta-IN.xml @@ -1,47 +1,47 @@ - மணிநேர விரைவு அரங்கம் - மணிநேர விரைவு - மணிநேரம் %s அரங்கம் + மணிநேர துரித கோதா + மணிநேர துரிதம் + மணிநேரம் %s கோதா மணிநேரம் %s - தினசரி விரைவு அரங்கம் - தினசரி விரைவு - ஆண்டுதோறும் பாரம்பரிய அரங்கம் - தினசரி பாரம்பரியம் - தினசரி %s அரங்கம் + தினசரி துரித கோதா + தினசரி துரிதம் + ஆண்டுதோறும் மரபு கோதா + தினசரி மரபு + தினசரி %s கோதா தினசரி %s - கிழக்கத்திய விரைவு அரங்கம் - கிழக்கத்திய விரைவு - கிழக்கத்திய பாரம்பரிய அரங்கம் - கிழக்கத்திய பாரம்பரியம் - கிழக்கத்திய %s அரங்கம் + கிழக்கத்திய துரித கோதா + கிழக்கத்திய துரிதம் + கிழக்கத்திய மரபு கோதா + கிழக்கத்திய மரபு + கிழக்கத்திய %s கோதா கிழக்கத்திய %s - வாராந்திர விரைவு அரங்கம் - வாராந்திர விரைவு - வாராந்திர பாரம்பரிய அரங்கம் - வாராந்திர பாரம்பரியம் - வாராந்திர %s அரங்கம் + வாராந்திர துரித கோதா + வாராந்திர துரிதம் + வாராந்திர மரபு கோதா + வாராந்திர மரபு + வாராந்திர %s கோதா வாராந்திர %s - மாதாந்திர விரைவு அரங்கம் - மாதாந்திர விரைவு - மாதாந்திர பாரம்பரிய அரங்கம் - மாதாந்திர பாரம்பரியம் - மாதாந்திர %s அரங்கம் + மாதாந்திர துரித கோதா + மாதாந்திர துரிதம் + மாதாந்திர மரபு கோதா + மாதாந்திர மரபு + மாதாந்திர %s கோதா மாதாந்திர %s - வருடாந்திர விரைவு அரங்கம் - வருடாந்திர விரைவு - ஆண்டுதோறும் பாரம்பரிய அரங்கம் - ஆண்டுதோறும் பாரம்பரியம் - ஆண்டுதோறும் %s அரங்கம் + வருடாந்திர துரித கோதா + வருடாந்திர துரிதம் + ஆண்டுதோறும் மரபு கோதா + ஆண்டுதோறும் கோதா + ஆண்டுதோறும் %s கோதா ஆண்டுதோறும் %s - விரைவான கேடய அரங்கம் - விரைவான கேடயம் - பாரம்பரிய கேடய அரங்கம் - பாரம்பரிய கேடயம் - %s கேடய அரங்கம் + துரித கேடய கோதா + துரித கேடயம் + மரபு கேடய கோதா + மரபு கேடயம் + %s கேடய கோதா %s கேடயம் - %s அணி யுத்தம் - சிறந்தவர்கள் %s அரங்கம் + %s குழு யுத்தம் + சிறந்தவர்கள் %s கோதா சிறந்தவர்கள் %s - %s அரங்கம் + %s கோதா diff --git a/translation/dest/tourname/vi-VN.xml b/translation/dest/tourname/vi-VN.xml index 68ea289f90a9c..1876ca57a8728 100644 --- a/translation/dest/tourname/vi-VN.xml +++ b/translation/dest/tourname/vi-VN.xml @@ -40,8 +40,8 @@ Khiên Cờ chậm Giải đấu Khiên %s Khiên %s - Giải đấu đa Đội %s - Giải đấu %s Chuyên Nghiệp + Giải đa Đội %s + Đấu trường %s Chuyên Nghiệp %s Chuyên nghiệp Đấu trường %s diff --git a/translation/dest/ublog/af-ZA.xml b/translation/dest/ublog/af-ZA.xml index 59c478f3ad0fa..0c5f54da18acc 100644 --- a/translation/dest/ublog/af-ZA.xml +++ b/translation/dest/ublog/af-ZA.xml @@ -1,5 +1,10 @@ + Gemeenskapsblogs + Vriendblogs + Blog-onderwerpe + Amptelike Lichess-Blog + Vorige blog-plasings %s se webjoernaal Nuwe inskrywing Bewerk jou webjoernaal se inskrywing diff --git a/translation/dest/ublog/ar-SA.xml b/translation/dest/ublog/ar-SA.xml index 135c79dea1181..8e35d62707705 100644 --- a/translation/dest/ublog/ar-SA.xml +++ b/translation/dest/ublog/ar-SA.xml @@ -2,15 +2,15 @@ مدونة %s منشور جديد - تعديل منشور مدونتك - حفظ المسودة + عدل منشور مدونتك + حفظ المُسَوَّدَة عنوان المنشور مقدمة المنشور نص المنشور المسودات المنشورة تمكين التعليقات - سيتم إنشاء موضوع المنتدى للأشخاص للتعليق على مشاركتك + سيوضع منتدى للأشخاص الذين يريدون التعليق على مشاركتك المنشور على مدونتك إذا تم تحديد الخيار سيتم نشر المقال على مدونتك، اذا لم يتم تحديد الخيار سيتم حفظها بشكل خاص في مسوداتك @@ -37,7 +37,7 @@ لا يوجد مسودات لعرضها. أخر منشورات المدونة - عرض مقالة + عرض كل %s المنشورات عرض مقالة عرض مقالتين عرض %s مقالات @@ -55,4 +55,5 @@ يرجى نشر المحتوى الحصري و المفيد فقط. لا تنسخ محتوى احد اخر. اي شيء غير لائق من الممكن أن يتسبب باغلاق حسابك. نصائحنا البسيطة لكتابة مقالة رائعة + ناقش منشور المدونة هذا في المنتدى diff --git a/translation/dest/ublog/ca-ES.xml b/translation/dest/ublog/ca-ES.xml index 5f9d6bbcbfc23..a12499ff9c168 100644 --- a/translation/dest/ublog/ca-ES.xml +++ b/translation/dest/ublog/ca-ES.xml @@ -1,5 +1,13 @@ + Blogs de la comunitat + Blogs dels amics + Entrades del blog que t\'han agradat + Temes dels blogs + Blog oficial de Lichess + Continua llegint aquesta entrada + Entrades del blog de lichess de %s + Entrades del blog anteriors Blog de %s Nova publicació Edita la teva publicació diff --git a/translation/dest/ublog/ckb-IR.xml b/translation/dest/ublog/ckb-IR.xml index 961425babd60a..d02d667fe9ead 100644 --- a/translation/dest/ublog/ckb-IR.xml +++ b/translation/dest/ublog/ckb-IR.xml @@ -43,5 +43,5 @@ تکایە تەنیا بابەتی پارێزراو وە وە لەسنوری ڕێزدا بکە بە پەیام، بابتی کەسی تر لەبەر مەگرەوە. هەرشتێکی نەگوونجاو دەشێت ببێتە هۆی داخستنی هەژمارەکەت. پێشنیارە سادەکانی ئێمە بۆ نوسینی پەیامێکی کەسی بەهێز - لە مەکۆدا باسی ئەم پۆستەی بلۆگەکە بکەن + لە مەکۆدا باسی ئەم پۆستەی بلۆگەکە بکەن diff --git a/translation/dest/ublog/da-DK.xml b/translation/dest/ublog/da-DK.xml index a52f9fec70251..1946b97950a45 100644 --- a/translation/dest/ublog/da-DK.xml +++ b/translation/dest/ublog/da-DK.xml @@ -1,5 +1,7 @@ + Blogs fra fællesskabet + Venners blogs Blog - %s Nyt indlæg Rediger dit blogindlæg diff --git a/translation/dest/ublog/de-DE.xml b/translation/dest/ublog/de-DE.xml index 137607511631d..d68dc1dd470da 100644 --- a/translation/dest/ublog/de-DE.xml +++ b/translation/dest/ublog/de-DE.xml @@ -1,5 +1,8 @@ + Blogthemen + offizieller Lichess-Blog + Post weiterlesen Blog von %s Neuer Beitrag Bearbeite deinen Blog-Beitrag diff --git a/translation/dest/ublog/el-GR.xml b/translation/dest/ublog/el-GR.xml index 412ff23aa25ce..45ebe3fe5bc70 100644 --- a/translation/dest/ublog/el-GR.xml +++ b/translation/dest/ublog/el-GR.xml @@ -43,5 +43,5 @@ Το περιεχόμενο που δημοσιεύετε πρέπει να είναι ασφαλές, με σεβασμό προς όλους και να μην αντιγράφει δημοσιεύσεις άλλων χρηστών. Ο λογαριασμός σας ενδεχομένως να κλείσει αν εντοπίσουμε οτιδήποτε ακατάλληλο. Απλές συμβουλές μας για να γράψει φανταστικές δημοσιεύσεις - Σχολιάστε αυτήν τη δημοσίευση στο φόρουμ + Σχολιάστε αυτήν τη δημοσίευση στο φόρουμ diff --git a/translation/dest/ublog/en-US.xml b/translation/dest/ublog/en-US.xml index 711175f8a9f63..e6aa2b891e712 100644 --- a/translation/dest/ublog/en-US.xml +++ b/translation/dest/ublog/en-US.xml @@ -1,5 +1,13 @@ + Community blogs + Friends\' blogs + Liked blog posts + Blog topics + Lichess Official Blog + Continue reading this post + Lichess blog posts in %s + Previous blog posts %s\'s Blog New post Edit your blog post diff --git a/translation/dest/ublog/eo-UY.xml b/translation/dest/ublog/eo-UY.xml index da2e3ab9be184..e6aca7109a31d 100644 --- a/translation/dest/ublog/eo-UY.xml +++ b/translation/dest/ublog/eo-UY.xml @@ -1,5 +1,6 @@ + Ŝatitaj blogafiŝoj Blogo de %s Nova afiŝo Redakti vian blogafiŝon diff --git a/translation/dest/ublog/es-ES.xml b/translation/dest/ublog/es-ES.xml index 01829bf05de60..a9126db17c701 100644 --- a/translation/dest/ublog/es-ES.xml +++ b/translation/dest/ublog/es-ES.xml @@ -1,5 +1,13 @@ + Blogs de la comunidad + Blogs de amigos + Entradas de blog preferidas + Temas del blog + Blog oficial de Lichess + Continuar la lectura de esta entrada + Entradas de blog en %s + Entradas de blog anteriores Blog de %s Nueva entrada Edita tu entrada diff --git a/translation/dest/ublog/eu-ES.xml b/translation/dest/ublog/eu-ES.xml index 7700cf34639d2..cd77e1318e594 100644 --- a/translation/dest/ublog/eu-ES.xml +++ b/translation/dest/ublog/eu-ES.xml @@ -43,5 +43,5 @@ Eduki seguru eta errespetuzkoa bakarrik argitaratu. Ez kopiatu beste norbaiten edukirik. Errespetua galduz gero, zure kontua itxiko dugu. Blogeko sarrerak idazteko gure aholkuak - Eztabaidatu artikulu honi buruz foroan + Eztabaidatu artikulu honi buruz foroan diff --git a/translation/dest/ublog/fa-IR.xml b/translation/dest/ublog/fa-IR.xml index e416089943b6f..9e01bea19912f 100644 --- a/translation/dest/ublog/fa-IR.xml +++ b/translation/dest/ublog/fa-IR.xml @@ -43,5 +43,5 @@ لطفا فقط مطالب ایمن و محترمانه منتشر کنید. مطالب دیگران را کپی نکنید. هر چیز نامناسبی می تواند حساب شما را ببندد. نکات پیشنهادی ما برای نوشتن پست های خوب وبلاگ - راجع‌به این نوشته در انجمن بحث کنید + راجع‌به این نوشته در انجمن بحث کنید diff --git a/translation/dest/ublog/fi-FI.xml b/translation/dest/ublog/fi-FI.xml index 64f4265f58ead..d8b05746fec42 100644 --- a/translation/dest/ublog/fi-FI.xml +++ b/translation/dest/ublog/fi-FI.xml @@ -1,5 +1,10 @@ + Blogin aiheet + Lichessin virallinen blogi + Jatka tämän kirjoituksen lukemista + Lichess-blogikirjoitukset vuodelta %s + Edelliset blogikirjoitukset Käyttäjän %s blogi Uusi kirjoitus Muokkaa blogikirjoitustasi @@ -43,5 +48,5 @@ Julkaise vain turvallista ja muita kunnioittavaa sisältöä. Älä kopioi toisten luomaa sisältöä. Epäasiallisen sisällön julkaisu voi johtaa käyttäjätunnuksesi sulkemiseen. Yksinkertaiset vinkkimme erinomaisten blogikirjoitusten laatimiseen - Keskustele tästä blogikirjoituksesta foorumissa + Keskustele tästä blogikirjoituksesta foorumissa diff --git a/translation/dest/ublog/fr-FR.xml b/translation/dest/ublog/fr-FR.xml index d5a5cd354c234..3411ae36271c1 100644 --- a/translation/dest/ublog/fr-FR.xml +++ b/translation/dest/ublog/fr-FR.xml @@ -1,5 +1,13 @@ + Blogues de la communauté + Blogues des amis + Articles de blogues aimés + Sujets des blogues + Blogue officiel de Lichess + Continuer à lire cet article + Articles de blogue de Lichess publiés dans %s + Articles de blogue précédents Blogue de %s Nouvel article Modifier votre article de blogue @@ -43,5 +51,5 @@ Veuillez ne publier que du contenu sûr et respectueux. Ne copiez pas le contenu de quelqu\'un d\'autre. Toute conduite inappropriée peut aboutir à la fermeture de votre compte. Conseils pratiques pour rédiger de bons messages - Discuter de cet article de blogue dans le forum + Discuter de cet article de blogue dans le forum diff --git a/translation/dest/ublog/gl-ES.xml b/translation/dest/ublog/gl-ES.xml index 93c1ab9d2d69f..1c69291b0549d 100644 --- a/translation/dest/ublog/gl-ES.xml +++ b/translation/dest/ublog/gl-ES.xml @@ -1,5 +1,13 @@ + Blogs da comunidade + Blogs das amizades + Artigos que me gustan + Temas do blog + Blog Oficial de Lichess + Segue a ler este artigo + Publicacións de Lichess no %s + Anteriores publicacións O Blog de %s Novo artigo Edita o teu artigo diff --git a/translation/dest/ublog/gsw-CH.xml b/translation/dest/ublog/gsw-CH.xml index 04a2c696c873e..a1762c7b74263 100644 --- a/translation/dest/ublog/gsw-CH.xml +++ b/translation/dest/ublog/gsw-CH.xml @@ -1,5 +1,13 @@ + Gemeinschaftlichi Tagebüecher + Tagebüecher vu Fründe + Bevorzugti Tagebuech Biträg + Tagebuech Theme + Offiziells Lichess Tagebuech + De Bitrag witer läse + Lichess Tagebuech Biträg in %s + Vorherigi Tagebuech Biträg %s\'s Blog Neue Bitrag Bearbeit din Blog Bitrag @@ -43,5 +51,5 @@ Bitte nur sicheri und reschpäktvolli Inhält poschte. Kopier nöd Inhält vu öpper anderem. Irgendwelchi unagmässeni Inhält, chönd zur Schlüssig vu dim Konto fühere. Euseri simple Ratschläg, zum tolli Biträg poschte - Diskutier de Blogbitrag im Forum + Diskutier de Blogbitrag im Forum diff --git a/translation/dest/ublog/he-IL.xml b/translation/dest/ublog/he-IL.xml index fee4363c27929..95d59f85136b2 100644 --- a/translation/dest/ublog/he-IL.xml +++ b/translation/dest/ublog/he-IL.xml @@ -1,5 +1,13 @@ + בלוגים מהקהילה + בלוגים של חברים + בלוגים שאהבת + נושאי הבלוגים + הבלוג הרשמי של Lichess + המשך קריאה של הבלוג + בלוגים של Lichess ב־%s + בלוגים קודמים הבלוג של %s פוסט חדש עריכת הפוסט diff --git a/translation/dest/ublog/it-IT.xml b/translation/dest/ublog/it-IT.xml index dddab620d8b68..777e91a304284 100644 --- a/translation/dest/ublog/it-IT.xml +++ b/translation/dest/ublog/it-IT.xml @@ -1,5 +1,13 @@ + Blog della community + Blog degli amici + Post del blog piaciuti + Argomenti del blog + Blog ufficiale di Lichess + Continua a leggere questo post + Post del blog di Lichess nel %s + Post del blog precedenti Blog di %s Nuovo post Modifica il post del tuo blog @@ -43,4 +51,5 @@ Per favore scrivi solo messaggi educati e rispettosi. Non copiare i contenuti di qualcun altro. Qualsiasi contenuto inappropriato potrebbe portare alla chiusura del tuo account. I nostri suggerimenti per scrivere ottimi messaggi sul blog + Discuti di questo post del blog nel forum diff --git a/translation/dest/ublog/ja-JP.xml b/translation/dest/ublog/ja-JP.xml index 6265ed5d3146c..70c8fc7bd976e 100644 --- a/translation/dest/ublog/ja-JP.xml +++ b/translation/dest/ublog/ja-JP.xml @@ -1,5 +1,13 @@ + コミュニティブログ + 友達のブログ + いいねを押したブログ投稿 + ブログのトピック + Lichess 公式ブログ + 続きを読む + %s 年の Lichess ブログ投稿 + 前のブログ投稿 %s のブログ 新しい投稿 ブログへの投稿を編集 @@ -40,5 +48,5 @@ 投稿できるのは安全で他者の権利を尊重したコンテンツだけです。他の人のコンテンツをコピーしてはいけません。 不適切な内容の場合、あなたのアカウントを停止することがあります。 すばらしいブログを書くための簡単なヒント - このブログ記事についてフォーラムで話す + このブログ記事についてフォーラムで話す diff --git a/translation/dest/ublog/kk-KZ.xml b/translation/dest/ublog/kk-KZ.xml index 8b33c1e2fb1c9..8864113569749 100644 --- a/translation/dest/ublog/kk-KZ.xml +++ b/translation/dest/ublog/kk-KZ.xml @@ -43,5 +43,5 @@ Мазмұны қауіпсіз әрі басқаларға сыйласты жазбаны салыңыз. Біреудің жазбасын көшірмеңіз. Әдепсіз, орынсыз нәрселер болса, тіркелгіңіз жабылады. Керемет блог-жазбаны қалай жазу туралы кеңестер - Бұл жазбаны форумда талқыға салыңыз + Бұл жазбаны форумда талқыға салыңыз diff --git a/translation/dest/ublog/lb-LU.xml b/translation/dest/ublog/lb-LU.xml index fb25ec9ef8668..2b619d24f74c1 100644 --- a/translation/dest/ublog/lb-LU.xml +++ b/translation/dest/ublog/lb-LU.xml @@ -1,5 +1,12 @@ + Blogge vun der Communautéit + Blogge vun de Kolleegen + Blog-Theemen + Offizielle Lichess-Blog + Virufuere mam Liese vun dësem Bäitrag + Lichess-Blog-Bäiträg vun %s + Virege Blog-Bäitrag %s säi Blog Neie Bäitrag Beaarbecht däi Blog-Bäitrag @@ -43,5 +50,5 @@ Wgl. post just sécher a respektvoll Inhalter. Kopéier net d\'Inhalter vun enger anerer Persoun. Alles, wat net ubruecht ass, kann dozou féieren, datt däi Kont zougemaach gëtt. Eis einfach Tuyaue fir flott Blog-Bäiträg ze schreiwen - Iwwert dëse Blog-Bäitrag am Forum diskutéieren + Iwwert dëse Blog-Bäitrag am Forum diskutéieren diff --git a/translation/dest/ublog/nb-NO.xml b/translation/dest/ublog/nb-NO.xml index 1f7fcbd68ebbe..d284cdb3615ac 100644 --- a/translation/dest/ublog/nb-NO.xml +++ b/translation/dest/ublog/nb-NO.xml @@ -1,5 +1,13 @@ + Blogger fra fellesskapet + Venners blogger + Likte blogginnlegg + Bloggemner + Offisiell blogg for Lichess + Fortsett å lese dette innlegget + Blogginnlegg fra Lichess i %s + Tidligere blogginnlegg Bloggen til %s Nytt innlegg Rediger blogginnlegget ditt @@ -43,5 +51,5 @@ Legg bare ut innlegg som er trygge og respektfulle. Ikke kopier andres innhold. Upassende innhold kan føre til at brukerkontoen din blir stengt. Våre enkle tips for å skrive gode blogginnlegg - Diskuter dette blogginnlegget i forumet + Diskuter dette blogginnlegget i forumet diff --git a/translation/dest/ublog/nl-NL.xml b/translation/dest/ublog/nl-NL.xml index 5ae0734138731..a074eeb07d498 100644 --- a/translation/dest/ublog/nl-NL.xml +++ b/translation/dest/ublog/nl-NL.xml @@ -43,5 +43,5 @@ Plaats alleen veilige en respectvolle inhoud. Kopieer geen inhoud van iemand anders. Alles wat ongepast is kan sluiting van je account tot gevolg hebben. Onze simpele tips om geweldige blogberichten te schrijven - Bespreek dit blogbericht in het forum + Bespreek dit blogbericht in het forum diff --git a/translation/dest/ublog/nn-NO.xml b/translation/dest/ublog/nn-NO.xml index 058861bd6a65b..73ecdfdd116ff 100644 --- a/translation/dest/ublog/nn-NO.xml +++ b/translation/dest/ublog/nn-NO.xml @@ -1,5 +1,13 @@ + Bloggar frå sjakkmiljøet + Bloggane til vener + Likte blogginnlegg + Bloggemner + Lichess sin offisielle blogg + Hald fram med å lesa dette innlegget + Lichess-blogginlegg i %s + Tidlegare blogginnlegg Bloggen til %s Nytt innlegg Rediger innlegget ditt diff --git a/translation/dest/ublog/pl-PL.xml b/translation/dest/ublog/pl-PL.xml index 427f4a1bd5620..b17febf99590a 100644 --- a/translation/dest/ublog/pl-PL.xml +++ b/translation/dest/ublog/pl-PL.xml @@ -1,5 +1,13 @@ + Blogi społeczności + Blogi znajomych + Polubione wpisy na blogu + Tematy bloga + Oficjalny blog Lichess + Kontynuuj czytanie tego posta + Posty na blogu Lichess w %s + Poprzednie wpisy na blogu Blog gracza %s Utwórz nowy wpis Edytuj swój wpis na blogu diff --git a/translation/dest/ublog/pt-BR.xml b/translation/dest/ublog/pt-BR.xml index 806f0cad28efb..a16695fc24b45 100644 --- a/translation/dest/ublog/pt-BR.xml +++ b/translation/dest/ublog/pt-BR.xml @@ -1,5 +1,12 @@ + Blogs da comunidade + Blogs de amigos + Postagens curtidas + Tópicos de blog + Blog oficial do Lichess + Continuar lendo a postagem + Postagens anteriores Blog do(a) %s Nova postagem Editar sua postagem no blog diff --git a/translation/dest/ublog/pt-PT.xml b/translation/dest/ublog/pt-PT.xml index 911e94202490d..ad7f0ab30d9a6 100644 --- a/translation/dest/ublog/pt-PT.xml +++ b/translation/dest/ublog/pt-PT.xml @@ -1,5 +1,13 @@ + Blogs da comunidade + Blogs de amigos + Publicações curtidas + Tópicos do Blog + Blog Oficial do Lichess + Continua a ler esta publicação + Publicações do blog do Lichess em %s + Publicações anteriores Blog de %s Nova publicação Modificar a tua publicação diff --git a/translation/dest/ublog/ro-RO.xml b/translation/dest/ublog/ro-RO.xml index 33066e804a5bf..50a07ef1b2b11 100644 --- a/translation/dest/ublog/ro-RO.xml +++ b/translation/dest/ublog/ro-RO.xml @@ -46,5 +46,5 @@ Vă rugăm să postați numai conținut sigur și respectuos. Nu copiați conținutul altcuiva. Orice lucru nepotrivit ar putea duce la închiderea contului tău. Sfaturile noastre simple pentru a scrie cele mai bune postări pe blog - Discută această postare pe forum + Discută această postare pe forum diff --git a/translation/dest/ublog/ru-RU.xml b/translation/dest/ublog/ru-RU.xml index b5a04bc592eb9..950cac30421fa 100644 --- a/translation/dest/ublog/ru-RU.xml +++ b/translation/dest/ublog/ru-RU.xml @@ -1,5 +1,13 @@ + Блоги сообщества + Блоги друзей + Понравившиеся публикации блогов + Темы блогов + Официальный блог Lichess + Продолжить чтение этой публикации + Записи в блоге Lichess за %s + Предыдущие записи блога Блог %s Новое сообщение в блоге Редактировать запись в блоге @@ -49,5 +57,5 @@ Публикуйте только безопасный и уважительный контент, пожалуйста. Не копируйте чужой контент. Неуместное содержание может стать причиной закрытия вашего аккаунта. Наши простые советы по написанию отличных записей в блоге - Обсудить эту запись в блоге на форуме + Обсудить эту запись в блоге на форуме diff --git a/translation/dest/ublog/sk-SK.xml b/translation/dest/ublog/sk-SK.xml index 9dc6b82bfe023..05d5875b230d7 100644 --- a/translation/dest/ublog/sk-SK.xml +++ b/translation/dest/ublog/sk-SK.xml @@ -1,5 +1,13 @@ + Blogy komunít + Blogy priateľov + Blogové príspevky, ktoré sa Vám páčia + Témy blogov + Oficiálny blog Lichessu + Pokračovať čítaním tohto blogu + Blogové príspevky na Lichesse v %s + Predchádzajúce blogové príspevky Blog užívateľa %s Nový príspevok Upraviť príspevok @@ -49,5 +57,5 @@ Prosím, dbajte na to aby vaše príspevky neboli pohoršujúce a rešpektovali ostatných. Nekopírujte príspevky niekoho iného. Akýkoľvek nevhodný obsah môže mať za následok zrušenie Vášho účtu. Naše jednoduché tipy ako písať skvelé blogové príspevky - Diskutovať o tomto príspevku na fóre + Diskutovať o tomto príspevku na fóre diff --git a/translation/dest/ublog/sl-SI.xml b/translation/dest/ublog/sl-SI.xml index fc1541083aa70..ad421fed99b47 100644 --- a/translation/dest/ublog/sl-SI.xml +++ b/translation/dest/ublog/sl-SI.xml @@ -1,5 +1,13 @@ + Blogi skupnosti + Blogi prijateljev + Všečkane objave v spletnem dnevniku + Teme bloga + Uradni blog Lichess + Nadaljujte z branjem te objave + Objave v spletnem dnevniku Lichess v %s + Prejšnje objave v spletnem dnevniku %s blog Nova objava Uredi svojo objavo v blogu diff --git a/translation/dest/ublog/sq-AL.xml b/translation/dest/ublog/sq-AL.xml index 285b41d33960b..e15ce240ecd26 100644 --- a/translation/dest/ublog/sq-AL.xml +++ b/translation/dest/ublog/sq-AL.xml @@ -43,5 +43,5 @@ Ju lutemi, postoni vetëm lëndë të parrezik dhe të respektuar. Mos kopjoni lëndën e dikujt tjetër. Çfarëdo gjëje e papërshtatshme mund të sjellë mbylljen e llogarisë tuaj. Ndihmëzat tona të thjeshta për shkrim postimesh të goditura blogu - Diskutojeni në forum këtë postim blogu + Diskutojeni në forum këtë postim blogu diff --git a/translation/dest/ublog/sv-SE.xml b/translation/dest/ublog/sv-SE.xml index e5a7a6d1bcf3d..8524115954899 100644 --- a/translation/dest/ublog/sv-SE.xml +++ b/translation/dest/ublog/sv-SE.xml @@ -43,5 +43,5 @@ Vänligen lägg bara upp säkert och respektfullt innehåll. Kopiera inte någon annans innehåll. Olämpligt innehåll kan göra att ditt konto stängs. Våra enkla tips för att skriva bra blogginlägg - Diskutera detta blogginlägg i forumet + Diskutera detta blogginlägg i forumet diff --git a/translation/dest/ublog/ta-IN.xml b/translation/dest/ublog/ta-IN.xml index 3ea04e700dfa8..4638afcb73b96 100644 --- a/translation/dest/ublog/ta-IN.xml +++ b/translation/dest/ublog/ta-IN.xml @@ -1,2 +1,47 @@ - + + %s இன் வலைப்பதிவு + புதிய இடுகை + உங்கள் வலைப்பதிவு இடுகையைத் திருத்தவும் + வரைவைச் சேமிக்கவும் + இடுகையின் தலைப்பு + அறிமுகத்தை இடுகையிடவும் + பொருளை இடுகையிடவும் + வரைவுகள் + வெளியிடப்பட்டது + கருத்துகளை இயக்கு + உங்கள் இடுகையில் கருத்து தெரிவிப்பதற்காக ஒரு மன்றத் தலைப்பு உருவாக்கப்படும் + உங்கள் வலைப்பதிவில் வெளியிடவும் + சரிபார்க்கப்பட்டால், இடுகை உங்கள் வலைப்பதிவில் பட்டியலிடப்படும். இல்லையெனில், அது உங்கள் வரைவு இடுகைகளில் தனிப்பட்டதாக இருக்கும் + + வலைப்பதிவு இடுகையை வெளியிட்டது + %s வலைப்பதிவு இடுகைகள் வெளியிடப்பட்டன + + + ஒரு பார்வை + %s பார்வைகள் + + %1$s வெளியிடப்பட்டது %2$s + இந்த இடுகை வெளியிடப்பட்டது + இது ஒரு வரைவு + மேலும் வலைப்பதிவு இடுகைகள் %s + இந்த வலைப்பதிவில் இன்னும் இடுகைகள் இல்லை. + காட்ட வரைவுகள் இல்லை. + சமீபத்திய வலைப்பதிவு இடுகைகள் + + ஒரு இடுகையைப் பார்க்கவும் + அனைத்து %s இடுகைகளையும் காண்க + + உங்கள் இடுகைக்கான படத்தைப் பதிவேற்றவும் + பட மாற்று உரை + படம் ஒப்புதல் + இந்த வலைப்பதிவு இடுகையை உறுதியாக நீக்கவும் + படத்தை நீக்கு + பின்வரும் இணையதளங்களில் உள்ள படங்களைப் பயன்படுத்துவது பாதுகாப்பானது: + உங்கள் இடுகைபற்றிய தலைப்புகளைத் தேர்ந்தெடுக்கவும் + நீங்களே உருவாக்கிய படங்கள், நீங்கள் எடுத்த படங்கள், லிசெஸ்ஸின் திரைப்பிடிப்பு... வேறு யாராலும் பதிப்புரிமை பெறாத எதையும் நீங்கள் பயன்படுத்தலாம். + பாதுகாப்பான மற்றும் மரியாதைக்குரிய உள்ளடக்கத்தை மட்டும் இடுகையிடவும். வேறொருவரின் உள்ளடக்கத்தை நகலெடுக்க வேண்டாம். + முறையற்ற எதுவும் உங்கள் கணக்கை மூடக்கூடும். + சிறந்த வலைப்பதிவு இடுகைகளை எழுத எங்கள் எளிய குறிப்புகள் + இந்த வலைப்பதிவு இடுகையை மன்றத்தில் விவாதிக்கவும் + diff --git a/translation/dest/ublog/th-TH.xml b/translation/dest/ublog/th-TH.xml index 65080ff91bfd6..70126f554519e 100644 --- a/translation/dest/ublog/th-TH.xml +++ b/translation/dest/ublog/th-TH.xml @@ -1,5 +1,13 @@ + บล็อกของชุมชน + บล็อกของเพื่อน + ชอบบล็อกโพสต์ + หัวข้อบล็อก + บล็อกทางการของ Lichess + อ่านโพสต์นี้ต่อ + บล็อกโพสต์ของ Lichessใน %s + บล็อกโพสต์ที่แล้ว บล็อกของ %s โพสต์ใหม่ แก้ไขโพสต์ของคุณ @@ -15,5 +23,5 @@ เครดิตภาพ ลบรูปภาพ - หารือเรื่องบล็อกโพสต์ในฟอรั่ม + หารือเรื่องบล็อกโพสต์ในฟอรั่ม diff --git a/translation/dest/ublog/tp-TP.xml b/translation/dest/ublog/tp-TP.xml index 6e1327bcf794d..38093ba0af3a0 100644 --- a/translation/dest/ublog/tp-TP.xml +++ b/translation/dest/ublog/tp-TP.xml @@ -43,4 +43,5 @@ o sitelen ala e ijo jaki e ijo ike. o lanpan ala e lipu pi jan ante sina sitelen anu pana e ijo jaki la sina ken kama ken ala kepeken ilo Lichess sona pona mi tawa pali lipu + o toki lon lipu ni lon ma toki diff --git a/translation/dest/ublog/uk-UA.xml b/translation/dest/ublog/uk-UA.xml index 564dce3d3113a..df06eb5147ae0 100644 --- a/translation/dest/ublog/uk-UA.xml +++ b/translation/dest/ublog/uk-UA.xml @@ -1,5 +1,11 @@ + Дописи спільноти + Дописи друзів + Вподобані публікації дописів + Теми дописів + Офіційний блог Lichess + Продовжити читання цієї історії Блог %s Новий допис Редагувати ваш запис @@ -49,5 +55,5 @@ Будь ласка, публікуйте тільки безпечний і шанобливий контент. Не копіюйте чужий контент. Невідповідності в дописі можуть спричинити блокування профілю. Наші прості поради для написання хороших дописів - Обговорити цей допис блогу на форумі + Обговорити цей допис блогу на форумі diff --git a/translation/dest/ublog/vi-VN.xml b/translation/dest/ublog/vi-VN.xml index 6073d9406bbbc..76d4327ef8b60 100644 --- a/translation/dest/ublog/vi-VN.xml +++ b/translation/dest/ublog/vi-VN.xml @@ -1,5 +1,13 @@ + Các Blog của cộng đồng + Các Blog của bạn bè + Các Blog đã thích + Các chủ đề Blog + Blog chính thức của Lichess + Tiếp tục đọc bài đăng này + Bài đăng blog của Lichess trong năm %s + Bài blog trước Bài viết của %s Bài đăng mới Chỉnh sửa bài đăng của bạn @@ -12,7 +20,7 @@ Cho phép bình luận Một chủ đề diễn đàn sẽ được tạo để mọi người bình luận vào bài đăng của bạn Công bố trên blog của bạn - Nếu chọn, bài đăng sẽ được đưa vào blog của bạn. Nếu không, nó sẽ ở chế độ riêng tư, trong các bài đăng nháp của bạn + Nếu chọn, bài đăng sẽ được đưa vào blog của bạn. Nếu không, nó sẽ ở chế độ riêng tư trong các bài đăng nháp của bạn Đã đăng %s bài Blog diff --git a/translation/dest/voiceCommands/ar-SA.xml b/translation/dest/voiceCommands/ar-SA.xml index 3ea04e700dfa8..d298c2b9ed975 100644 --- a/translation/dest/voiceCommands/ar-SA.xml +++ b/translation/dest/voiceCommands/ar-SA.xml @@ -1,2 +1,22 @@ - + + الأوامر الصوتية + شاهد الفيديو التوضيحي + استخدم زر %1$s لتبديل الصوت، الزر %2$s لفتح حوار المساعدة، وقائمة %3$s لتغيير إعدادات الكلام. + تشير الأسهم إلى عدة نقلات حين لا نكون واثقين، حدد لون أو رَقْم السهم الذي يشير إلى النقلة لاختيارها. + إذا ظهرت علامة الرادار، فستُلعب نقلتك عند اكتمال الدائرة. خلال ذلك، يمكنك قول %1$s للعب النقلة على الفور، أو قول %2$s لإلغاء الحركة واختيار أخرى بلون/رقم سهم مختلف. يمكن تعديل هذا المؤقت أو إيقاف تشغيله في الإعدادات. + مكن %s إذا كنت في محيط صاخب. اضغط (shift) عندما يكون هذا الإعداد قيد التشغيل لتسجيل الأوامر. + استخدم الأبجدية الصوتية لتسهيل التعرف على أعمدة رقعة الشطرنج. + %s يشرح إعدادات الأوامر الصوتية بالتفصيل. + هذا المنشور + انقل إلى e4 أو أختر القطعة في مربع e4 + أختر أو ألتقط الفيل + خذ الرخ بواسطة الوزير + بيّت (في أي من الجناحين) + الأبجدية الصوتية هي الأفضل + ألغ المؤقت أو أرفض الطلب + لعب النقلة المفضلة أو التأكد من شيئ ما + وضع النوم (إذا تم تمكين كلمة الاستيقاظ) + إيقاف التعرف على الصوت + إظهار حل اللغز + diff --git a/translation/dest/voiceCommands/de-DE.xml b/translation/dest/voiceCommands/de-DE.xml index 56ca6fab26293..6e2ca9954ec5d 100644 --- a/translation/dest/voiceCommands/de-DE.xml +++ b/translation/dest/voiceCommands/de-DE.xml @@ -4,9 +4,9 @@ Video-Tutorial ansehen Benutze den %1$s-Knopf um die Spracherkennung einzuschalten, den %2$s-Knopf, um diese Hilfe aufzurufen und den %3$s-Knopf, um die Spracheinstellungen zu ändern. Wir zeigen Pfeile für mehrere Züge an, falls wir nicht sicher sind. Nenne die Farbe oder die Zahl eines Zug-Pfeils, um ihn auszuwählen. - Wenn ein Pfeil einen sich schließenden Ladekreis anzeigt, wird dieser Zug nach Vollendung des Kreises gespielt. Während dieser Zeit kannst du nur %1$s sagen, um den Zug sofort zu spielen, %2$s, um den Zug abzubrechen, oder die Farbe/Nummer eines anderen Pfeils. Dieser Timer kann in den Einstellungen angepasst oder deaktiviert werden. + Wenn ein Pfeil ein kreisendes Radar anzeigt, wird der Zug nach Vollendung des Kreises gespielt. Während dieser Zeit kannst du nur %1$s sagen, um den Zug sofort zu spielen, %2$s, um den Zug abzubrechen, oder nenne die Farbe/Nummer eines anderen Pfeils. Die Zeitvorgabe kann in den Einstellungen geändert oder ausgeschaltet werden. Aktiviere %s in lauten Umgebungen. Halte Shift während des Sprechens gedrückt, wenn diese Option aktiviert ist. - Verwende das phonetische Alphabet, um die Spracherkennung von Linien auf dem Schachbrett zu verbessern. (a-h-Linie) + Verwende das phonetische Alphabet, um die Spracherkennung von Linien auf dem Schachbrett zu verbessern (a-h-Linie). %s erklärt die Zugeinstellungen per Spracheingabe im Detail. Dieser Blogbeitrag Ziehe nach e4 oder wähle die Figur auf e4 aus @@ -14,9 +14,9 @@ Schlage den Turm mit der Dame Rochiere (egal welche Seite) Das phonetische Alphabet ist am besten - Timer abbrechen oder Anfrage ablehnen + Zeitvorgabe ausschalten oder Anfrage ablehnen Bevorzugten Zug abspielen oder etwas bestätigen Schlafen (falls Wort zum Aufwecken aktiviert) Spracherkennung ausschalten - Aufgaben-Lösung anzeigen + Lösung der Aufgabe anzeigen diff --git a/translation/dest/voiceCommands/el-GR.xml b/translation/dest/voiceCommands/el-GR.xml index 3ea04e700dfa8..0e9342ab9a1f4 100644 --- a/translation/dest/voiceCommands/el-GR.xml +++ b/translation/dest/voiceCommands/el-GR.xml @@ -1,2 +1,5 @@ - + + Φωνητικές εντολές + Παρακολουθήστε το σχετικό βίντεο + diff --git a/translation/dest/voiceCommands/en-US.xml b/translation/dest/voiceCommands/en-US.xml index 094c7ce0cc209..ff4d3388d518d 100644 --- a/translation/dest/voiceCommands/en-US.xml +++ b/translation/dest/voiceCommands/en-US.xml @@ -5,7 +5,7 @@ Use the %1$s button to toggle voice recognition, the %2$s button to open this help dialog, and the %3$s menu to change speech settings. We show arrows for multiple moves when we are not sure. Speak the color or number of a move arrow to select it. If an arrow shows a sweeping radar, that move will be played when the circle is complete. During this time, you may only say %1$s to play the move immediately, %2$s to cancel, or speak the color/number of a different arrow. This timer can be adjusted or turned off in settings. - Enable %s in noisy surroundings. Hold shift while speaking commands when this is on. + Enable %s in noisy surroundings. Hold shift when speaking commands while this option is active. Use the phonetic alphabet to improve recognition of chessboard files. %s explains the voice move settings in detail. This blog post diff --git a/translation/dest/voiceCommands/gsw-CH.xml b/translation/dest/voiceCommands/gsw-CH.xml index bfd6b67296729..5f26714f8b13f 100644 --- a/translation/dest/voiceCommands/gsw-CH.xml +++ b/translation/dest/voiceCommands/gsw-CH.xml @@ -4,8 +4,6 @@ Lueg s\'Video-Tutorial Benutz d\'Schaltflächi %1$s, zum d\'Spracherkännig aktivieren, d\'Schaltflächi %2$s, zum de Hilfedialog ufmache, und s\'Menü %3$s, zum d\'Sprachinschtellige ändere. Mir zeiged Pfil für mehreri Züg a, wänn mir nöd sicher sind. Sprich eifach d\'Farb oder d\'Nummere vu dem Zugpfil, wo du wotsch wähle. - Läsed die änglisch Version, uf Schwizerdütsch git das Chrämpf: -If an arrow shows a sweeping radar, that move will be played when the circle is complete. During this time, you may only say %1$s to play the move immediately, %2$s to cancel, or speak the color/number of a different arrow. This timer can be adjusted or turned off in settings. Aktiviert %s in luter Umgebig. Bim Spräche vu Befehl d\'Umschalttaschte truckt halte, wänn die Funktion aktiviert isch. Benutz s\'Ponetischi-Alphabet, zum d\'Verständlichkeit verbessere. %s erchlärt d\'Ischtellige vu de Sprachschtürig im Detail. diff --git a/translation/dest/voiceCommands/he-IL.xml b/translation/dest/voiceCommands/he-IL.xml index 3ea04e700dfa8..d9c51b6a982b4 100644 --- a/translation/dest/voiceCommands/he-IL.xml +++ b/translation/dest/voiceCommands/he-IL.xml @@ -1,2 +1,22 @@ - + + פקודות קוליות + צפו בסרטון ההסבר + השתמשו בכפתור ״%1$s״ כדי להפעיל את הזיהוי הקולי של המהלכים, בכפתור ״%2$s״ כדי לגשת למסך העזרה הזה ובכפתור ״%3$s כדי לשנות את הגדרות הזיהוי הקולי. + אנו מציגים מספר חצים כשאנו לא בטוחים. אמרו את הצבע או המספר של החץ כדי לבחור אותו. + אם מופיע סימן מסתובב של מכ״ם ליד החץ, המהלך יבוצע כשהעיגול יושלם. במשך הזמן הזה, תוכלו לומר ״%1$s״ כדי לשחק את המהלך באופן מיידי או ״%2$s״ כדי לבטל. תוכלו גם לומר מספר או סימן של חץ אחר. ניתן לבטל את הטיימר או להתאים אותו לצרכיכם בהגדרות. + הפעילו %s בסביבה רועשת. לחצו באופן מתמשך על כפתור ה-Shift כשאתם משתמשים בזיהוי הקולי במצב זה. + השתמשו באלפבית הפונטי כדי לשפר את הזיהוי של טורים בלוח. + ה%s מסבירות את הגדרות הזיהוי הקולי באופן מפורט. + הפוסט הזה בבלוג + זוזו ל-e4 או בחרו כלי שנמצא ב-e4 + בחרו רץ או הכו אותו + הכו את הצריח עם המלכה + בצעו הצרחה (לאחד הצדדים) + האלפבית הפונטי עדיף + בטלו את הטיימר או דחו את הבקשה + בצעו את המהלך שנבחר או אשרו משהו + כיבוי (אם נבחרה מילת הדלקה) + כיבוי הזיהוי הקולי + הצגת הפתרון לחידה + diff --git a/translation/dest/voiceCommands/lb-LU.xml b/translation/dest/voiceCommands/lb-LU.xml index 52261078bebe0..399a5aeec4ce3 100644 --- a/translation/dest/voiceCommands/lb-LU.xml +++ b/translation/dest/voiceCommands/lb-LU.xml @@ -1,5 +1,11 @@ + Sproochbefeeler + Video-Tutorial kucken Dëse Blog-Bäitrag + Wiel oder huel ee Leefer Den Tuerm mat der Damm huelen + Rochéieren (egal wat fir eng Säit) + Sproocherkennung desaktivéieren + Léisung vun der Aufgab weisen diff --git a/translation/dest/voiceCommands/pt-BR.xml b/translation/dest/voiceCommands/pt-BR.xml index 54257d131abf3..0ebfe62db26ad 100644 --- a/translation/dest/voiceCommands/pt-BR.xml +++ b/translation/dest/voiceCommands/pt-BR.xml @@ -9,4 +9,7 @@ Capturar a torre com a rainha Roque (qualquer lado) O alfabeto fonético é melhor + Cancelar relógio ou negar um pedido + Fazer lance preferido ou confirmar algo + Mostrar solução do quebra-cabeça diff --git a/translation/dest/voiceCommands/pt-PT.xml b/translation/dest/voiceCommands/pt-PT.xml index 2be43555b7ce3..4df1c1a33b9fd 100644 --- a/translation/dest/voiceCommands/pt-PT.xml +++ b/translation/dest/voiceCommands/pt-PT.xml @@ -2,8 +2,21 @@ Comandos de voz Ver o vídeo tutorial + Usa o botão %1$s para alternar o reconhecimento de voz, o botão %2$s para abrir esta caixa de diálogo de ajuda e o menu %3$s para alterar as configurações de voz. + Mostramos setas para vários movimentos quando não temos certeza. Fala a cor ou o número de uma seta de movimento para selecioná-la. + Se uma seta exibir um radar abrangente, esse movimento será tocado quando o círculo estiver completo. Durante esse tempo, você só pode dizer %1$s para jogar o movimento imediatamente, %2$s para cancelar, ou falar a cor/número de uma seta diferente. Esse temporizador pode ser ajustado ou desligado nas configurações. + Ative %s em ambientes barulhentos. Segura shift enquanto falas comandos quando estiver ativado. + Usa o alfabeto fonético para melhorar o reconhecimento dos arquivos do tabuleiro. + %s explica detalhadamente as configurações do movimento de voz. Este post no blog + Mover para e4 ou selecionar a peça de e4 Seleciona ou captura um bispo Pega a torre com a dama Roque (qualquer lado) + Alfabeto fonético é melhor + Cancelar temporizador ou negar solicitação + Joga o movimento preferido ou confirma algo + Suspender (se houver palavra ativada) + Desativar reconhecimento de voz + Mostrar solução do problema diff --git a/translation/dest/voiceCommands/sv-SE.xml b/translation/dest/voiceCommands/sv-SE.xml index 8ac0be9939d13..8d5a7a1470ac9 100644 --- a/translation/dest/voiceCommands/sv-SE.xml +++ b/translation/dest/voiceCommands/sv-SE.xml @@ -2,6 +2,9 @@ Röstkommandon Titta på videoinstruktionen + Använd %1$s -knappen för att växla röstigenkänning, %2$s -knappen för att öppna den här hjälpdialogrutan och %3$s -menyn för att ändra talinställningar. + Vi visar pilarna för flera drag när vi inte är säkra. Tala färg eller antal på en dragpil för att välja den. + Om en pil visar en svepande radar, så kommer draget att spelas när cirkeln är klar. Under denna tid kan du bara säga %1$s att spela draget omedelbart, %2$s för att avbryta, eller tala färg/antal av en annan pil. Denna timer kan justeras eller stängas av i inställningarna. Aktivera %s i bullriga omgivningar. Håll shift medan du talar kommandon när detta är på. Använd det fonetiska alfabetet för att förbättra igenkänningen av schackfiler. %s förklarar inställningarna för röstflyttning i detalj. @@ -13,6 +16,7 @@ Fonetiskt alfabet är bäst Avbryt timer eller neka en begäran Spela föredraget drag eller bekräfta något + Viloläge (om uppvakna-ordet aktiverat) Stäng av röstigenkänning Visa pussellösning diff --git a/translation/dest/voiceCommands/ta-IN.xml b/translation/dest/voiceCommands/ta-IN.xml index 3ea04e700dfa8..859f2efd7d403 100644 --- a/translation/dest/voiceCommands/ta-IN.xml +++ b/translation/dest/voiceCommands/ta-IN.xml @@ -1,2 +1,22 @@ - + + குரல் கட்டளைகள் + பயிற்சி காணொளியைப் பாருங்கள் + குரல் அங்கீகாரத்தை நிலைமாற்ற %1$s பொத்தானையும், இந்த உதவி உரையாடலைத் திறக்க %2$s பொத்தானையும், பேச்சு அமைப்புகளை மாற்ற %3$s பட்டியலையும் பயன்படுத்தவும். + உறுதியாகத் தெரியாதபோது பல நகர்வுகளுக்கான அம்புகளைக் காட்டுகிறோம். நகர்த்தும் அம்புக்குறியைத் தேர்ந்தெடுக்க அதன் நிறம் அல்லது எண்ணைப் பேசவும். + ஒரு அம்பு ஒரு விரவு வானலையுணரியைக் காட்டினால், வட்டம் முடிந்ததும் அந்த நகர்வு இயக்கப்படும். இந்த நேரத்தில், நகர்வை உடனடியாக இயக்க %1$s, ரத்துசெய்ய %2$s அல்லது வேறு அம்புக்குறியின் நிறம்/எண்ணைப் பேசலாம். இந்தக் காலக்கணிப்பி அமைப்புகளில் சரிசெய்யலாம் அல்லது முடக்கலாம். + இரைச்சல் நிறைந்த சூழலில் %sஐ இயக்கவும். இது இயக்கத்தில் இருக்கும் போது கட்டளைகளைப் பேசும் போது Shift ஐ அழுத்திப் பிடிக்கவும். + சதுரங்க பலகை கோப்புகளின் அங்கீகாரத்தை மேம்படுத்த ஒலிப்பு எழுத்துக்களைப் பயன்படுத்தவும். + %s குரல் நகர்வு அமைப்புகளை விரிவாக விளக்குகிறது. + இந்த வலைப்பதிவு இடுகை + E4 க்கு நகர்த்தவும் அல்லது e4 பகுதியைத் தேர்ந்தெடுக்கவும் + மந்திரியைத் தேர்ந்தெடுக்கவும் அல்லது கைப்பற்றவும் + ராணியை கொண்டு யானையை வெளியேற்றுங்கள் + கோட்டை கட்டு (இருபுறமும்) + ஒலிப்பு எழுத்துக்கள் சிறந்தது + காலக்கணிப்பியை ரத்துசெய்யவும் அல்லது கோரிக்கையை நிராகரிக்கவும் + விருப்பமான நகர்வை விளையாடவும் அல்லது எதையாவது உறுதிப்படுத்தவும் + உறக்கம் (விழிப்பு வார்த்தை இயக்கப்பட்டிருந்தால்) + குரல் அங்கீகாரத்தை முடக்கு + புதிர் தீர்வைக் காட்டு + diff --git a/translation/dest/voiceCommands/tr-TR.xml b/translation/dest/voiceCommands/tr-TR.xml index 3ea04e700dfa8..b89ec9b1c6fca 100644 --- a/translation/dest/voiceCommands/tr-TR.xml +++ b/translation/dest/voiceCommands/tr-TR.xml @@ -1,2 +1,13 @@ - + + Sesli komutlar + Öğretici videoyu izle + Ses tanımayı açıp kapatmak için %1$s tuşunu, bu yardım penceresini açmak için %2$s tuşunu ve konuşma ayarlarını değiştirmek için %3$s menüsünü kullanın. + Bu blog yazısı + E4\'e hareket et veya e4\'teki taşı seç + Vezir ile kaleyi al + Rok at (kısa veya uzun) + Fonetik alfabe en iyisidir + Ses tanımayı kapat + Bulmaca çözümünü göster + diff --git a/translation/dest/voiceCommands/uk-UA.xml b/translation/dest/voiceCommands/uk-UA.xml index 3ea04e700dfa8..c0f2a7d6132c3 100644 --- a/translation/dest/voiceCommands/uk-UA.xml +++ b/translation/dest/voiceCommands/uk-UA.xml @@ -1,2 +1,6 @@ - + + Голосові команди + Фонетичний алфавіт найкращий + Вимкнути розпізнавання голосу + diff --git a/translation/source/arena.xml b/translation/source/arena.xml index 0973965580f65..de2d492a779bd 100644 --- a/translation/source/arena.xml +++ b/translation/source/arena.xml @@ -8,9 +8,9 @@ Some tournaments are rated and will affect your rating. How are scores calculated? A win has a base score of 2 points, a draw 1 point, and a loss is worth no points. -If you win two games consecutively you will start a double point streak, represented by a flame icon. +If you win two games consecutively you will start a double-point streak, represented by a flame icon. The following games will continue to be worth double points until you fail to win a game. -That is, a win will be worth 4 points, a draw 2 points, and a loss will still not award any points. +That is, a win will be worth 4 points, a draw 2 points and a loss will still not award any points. For example, two wins followed by a draw will be worth 6 points: 2 + 2 + (2 x 1) Arena Berserk @@ -22,15 +22,15 @@ Berserk is not available for games with zero initial time (0+1, 0+2). Berserk only grants an extra point if you play at least 7 moves in the game. How is the winner decided? - The player(s) with the most points at the conclusion of the tournament's set time limit will be announced winner(s). + The player(s) with the most points after the tournament's set time limit will be announced the winner(s). When two or more players have the same number of points, the tournament performance is the tie break. How does the pairing work? At the beginning of the tournament, players are paired based on their rating. -As soon as you finish a game, return to the tournament lobby: you will then be paired with a player close to your ranking. This ensures minimum wait time, however you may not face all other players in the tournament. +As soon as you finish a game, return to the tournament lobby: you will then be paired with a player close to your ranking. This ensures minimum wait time, however, you may not face all other players in the tournament. Play fast and return to the lobby to play more games and win more points. How does it end? - The tournament has a countdown clock. When it reaches zero, the tournament rankings are frozen, and the winner is announced. Games in progress must be finished, however they don't count for the tournament. + The tournament has a countdown clock. When it reaches zero, the tournament rankings are frozen, and the winner is announced. Games in progress must be finished, however, they don't count for the tournament. Other important rules There is a countdown for your first move. Failing to make a move within this time will forfeit the game to your opponent. @@ -39,7 +39,7 @@ Play fast and return to the lobby to play more games and win more points. This is a private tournament Share this URL to let people join: %s - Draw streaks: When a player has consecutive draws in an arena, only the first draw will result in a point, or draws lasting more than %s moves in standard games. The draw streak can only be broken by a win, not a loss or a draw. + Draw streaks: When a player has consecutive draws in an arena, only the first draw will result in a point or draws lasting more than %s moves in standard games. The draw streak can only be broken by a win, not a loss or a draw. The minimum game length for drawn games to award points differs by variant. The table below lists the threshold for each variant. Variant Minimum game length diff --git a/translation/source/broadcast.xml b/translation/source/broadcast.xml index 663f54991edfb..1cec3ae14090a 100644 --- a/translation/source/broadcast.xml +++ b/translation/source/broadcast.xml @@ -8,7 +8,7 @@ Live tournament broadcasts New live broadcast - About broadcast + About broadcasts No rounds yet. How to use Lichess Broadcasts. The new round will have the same members and contributors as the previous one. diff --git a/translation/source/challenge.xml b/translation/source/challenge.xml index c5cb7374edbd2..c30b03ffa784d 100644 --- a/translation/source/challenge.xml +++ b/translation/source/challenge.xml @@ -2,7 +2,7 @@ Challenges: %1$s Challenge to a game - Challenge declined + Challenge declined. Challenge accepted! Challenge cancelled. Please register to send challenges to this user. @@ -22,5 +22,5 @@ I'm not willing to play this variant right now. I'm not accepting challenges from bots. I'm only accepting challenges from bots. - Or invite a Lichess User: + Or invite a Lichess user: diff --git a/translation/source/class.xml b/translation/source/class.xml index 5e15c2098057e..c037cb3407514 100644 --- a/translation/source/class.xml +++ b/translation/source/class.xml @@ -25,12 +25,12 @@ Teachers of the class Add Lichess usernames to invite them as teachers. One per line. Reset password - Make sure to copy or write down the password now. You won’t be able to see it again! + Make sure to copy or write down the password now. You won’t ever be able to see it again! Password: %s Generate a new password for the student Invited to %1$s by %2$s Real name - Private. Will never be shown outside the class. Helps remember who the student is. + Private. Will never be shown outside the class. Helps you remember who the student is. Add student Lichess profile %1$s created for %2$s. Student: %1$s @@ -43,7 +43,7 @@ Password: %3$s Never send unsolicited invites to arbitrary players. Create a new Lichess account If the student doesn't have a Lichess account yet, you can create one for them here. - No email address is required. A password will be generated, and you will have to transmit it to the student, so they can log in. + No email address is required. A password will be generated, and you will have to transmit it to the student so that they can log in. Important: a student must not have multiple accounts. If they already have one, use the invite form instead. Only create accounts for real students. Do not use this to make multiple accounts for yourself. You would get banned. @@ -64,13 +64,13 @@ Here is the link to access the class. Upgrade from managed to autonomous Graduate Graduate the account so the student can manage it autonomously. - A graduated account cannot be made managed again. The student will be able to toggle kid mode and reset password themselves. + A graduated account cannot be managed again. The student will be able to toggle kid mode and reset password themselves. The student will remain in the class after their account is graduated. Real, unique email address of the student. We will send a confirmation email to it, with a link to graduate the account. Close account Close the student account permanently. The student will never be able to use this account again. Closing is final. Make sure the student understands and agrees. - You may want to give the student control over the account instead, so that they can continue using it. + You may want to give the student control over the account instead so that they can continue using it. Teachers Teacher diff --git a/translation/source/coordinates.xml b/translation/source/coordinates.xml index 7a65e1f74d73b..2db28790f3197 100644 --- a/translation/source/coordinates.xml +++ b/translation/source/coordinates.xml @@ -4,7 +4,7 @@ Coordinate training Average score as white: %s Average score as black: %s - Knowing the chessboard coordinates is a very important chess skill: + Knowing the chessboard coordinates is a very important skill for several reasons: Most chess courses and exercises use the algebraic notation extensively. It makes it easier to talk to your chess friends, since you both understand the 'language of chess'. You can analyse a game more effectively if you can quickly recognise coordinates. diff --git a/translation/source/onboarding.xml b/translation/source/onboarding.xml new file mode 100644 index 0000000000000..ba51df79d21f9 --- /dev/null +++ b/translation/source/onboarding.xml @@ -0,0 +1,17 @@ + + + Welcome! + Welcome to lichess.org! + This is your profile page. + Will a child use this account? You might want to enable %s. + What now? Here are a few suggestions: + Learn chess rules + Improve with chess tactics puzzles. + Play the Artificial Intelligence. + Play opponents from around the world. + Follow your friends on Lichess. + Play in tournaments. + Learn from %1$s and %2$s. + Configure Lichess to your liking. + Explore the site and have fun :) + diff --git a/translation/source/preferences.xml b/translation/source/preferences.xml index 93fba69600ff0..2cdbe487e10d6 100644 --- a/translation/source/preferences.xml +++ b/translation/source/preferences.xml @@ -16,7 +16,7 @@ Zen mode Show player ratings Show player flairs - This allows hiding all ratings from the website, to help focus on the chess. Games can still be rated, this is only about what you get to see. + This hides all ratings from Lichess, to help focus on the chess. Rated games still impact your rating, this is only about what you get to see. Show board resize handle Only on initial position In-game only diff --git a/translation/source/site.xml b/translation/source/site.xml index db8f2ba469d14..5e249b9525de2 100644 --- a/translation/source/site.xml +++ b/translation/source/site.xml @@ -5,6 +5,7 @@ To invite someone to play, give this URL Game Over Waiting for opponent + Or let your opponent scan this QR code Waiting Your turn %1$s level %2$s @@ -272,7 +273,6 @@ Playing right now Playing now Finished - finishes %s Abort game Game aborted Standard @@ -403,10 +403,9 @@ Continue from here Study Import game - Paste a game PGN to get a browsable replay, -computer analysis, game chat and public shareable URL. + Paste a game PGN to get a browsable replay, computer analysis, game chat and public shareable URL. Variations will be erased. To keep them, import the PGN via a study. - This PGN can be accessed by the public. To import a game privately, use a study. + This PGN can be accessed by the public. To import a game privately, use a study. %s imported game %s imported games @@ -508,7 +507,8 @@ computer analysis, game chat and public shareable URL. Edit profile First name Surname - Set your flair: + Set your flair + Flair There is a setting to hide all user flairs across the entire site. Biography Country or region @@ -748,6 +748,7 @@ computer analysis, game chat and public shareable URL. With friends With everybody Kid mode + Kid mode is enabled. This is about safety. In kid mode, all site communications are disabled. Enable this for your children and school students, to protect them from other internet users. In kid mode, the Lichess logo gets a %s icon, so you know your kids are safe. Your account is managed. Ask your chess teacher about lifting kid mode. @@ -861,9 +862,10 @@ computer analysis, game chat and public shareable URL. and save %s premove line and save %s premove lines + You have received a private message from Lichess. + Click here to read it Sorry :( We had to time you out for a while. - The timeout expires %s. Why? We aim to provide a pleasant chess experience for everyone. To that effect, we must ensure that all players follow good practice. @@ -883,6 +885,7 @@ computer analysis, game chat and public shareable URL. I agree that I will follow all Lichess policies. Search or start new conversation Edit + Bullet Blitz Rapid Classical @@ -923,9 +926,6 @@ computer analysis, game chat and public shareable URL. Time is almost up! [Click to reveal email address] Download - Welcome! - Lichess is a charity and entirely free/libre open source software. -All operating costs, development, and content are funded solely by user donations. Coach manager Streamer manager Cancel the tournament @@ -986,4 +986,6 @@ Leave empty to start games from the normal initial position. Our tips for organising events Instructions Show me everything + Lichess is a charity and entirely free/libre open source software. +All operating costs, development, and content are funded solely by user donations. diff --git a/translation/source/timeago.xml b/translation/source/timeago.xml index 688c6df7d41ab..09b80e223bb9f 100644 --- a/translation/source/timeago.xml +++ b/translation/source/timeago.xml @@ -54,4 +54,13 @@ %s year ago %s years ago + + %s minute remaining + %s minutes remaining + + + %s hour remaining + %s hours remaining + + completed diff --git a/translation/source/ublog.xml b/translation/source/ublog.xml index 25b3e6460be17..2d44135ff8c6d 100644 --- a/translation/source/ublog.xml +++ b/translation/source/ublog.xml @@ -1,5 +1,13 @@ + Community blogs + Friends blogs + Liked blog posts + Blog topics + Lichess Official Blog + Continue reading this post + Lichess blog posts in %s + Previous blog posts %s's Blog New post Edit your blog post diff --git a/ui/@types/lichess/index.d.ts b/ui/@types/lichess/index.d.ts index ef737503830be..5dfc92b67275a 100644 --- a/ui/@types/lichess/index.d.ts +++ b/ui/@types/lichess/index.d.ts @@ -13,15 +13,18 @@ interface Lichess { powertip: LichessPowertip; clockWidget(el: HTMLElement, opts: { time: number; pause?: boolean }): void; spinnerHtml: string; - assetUrl(url: string, opts?: AssetUrlOpts): string; - flairSrc(flair: Flair): string; - loadCss(path: string): void; - loadCssPath(path: string): Promise; - jsModule(name: string): string; - loadIife(path: string, opts?: AssetUrlOpts): Promise; - loadEsm(name: string, opts?: { init?: ModuleOpts; url?: AssetUrlOpts }): Promise; - hopscotch: any; - userComplete: (opts: UserCompleteOpts) => Promise; + asset: { + baseUrl(): string; + url(url: string, opts?: AssetUrlOpts): string; + flairSrc(flair: Flair): string; + loadCss(path: string): void; + loadCssPath(path: string): Promise; + jsModule(name: string): string; + loadIife(path: string, opts?: AssetUrlOpts): Promise; + loadEsm(name: string, opts?: { init?: ModuleOpts; url?: AssetUrlOpts }): Promise; + hopscotch: any; + userComplete(opts: UserCompleteOpts): Promise; + }; slider(): Promise; makeChat(data: any): any; makeChessground(el: HTMLElement, config: CgConfig): CgApi; @@ -196,7 +199,7 @@ interface Pubsub { } interface LichessStorageHelper { - make(k: string): LichessStorage; + make(k: string, ttl?: number): LichessStorage; boolean(k: string): LichessBooleanStorage; get(k: string): string | null; set(k: string, v: string): void; @@ -302,6 +305,7 @@ declare namespace Editor { orientation?: Color; onChange?: (fen: string) => void; inlineCastling?: boolean; + coordinates?: boolean; } export interface OpeningPosition { @@ -426,16 +430,19 @@ interface Paginator { nbPages: number; } +interface EvalScore { + cp?: number; + mate?: number; +} + declare namespace Tree { export type Path = string; - interface ClientEvalBase { + interface ClientEvalBase extends EvalScore { fen: Fen; depth: number; nodes: number; pvs: PvData[]; - cp?: number; - mate?: number; } export interface CloudEval extends ClientEvalBase { cloud: true; @@ -447,9 +454,7 @@ declare namespace Tree { } export type ClientEval = CloudEval | LocalEval; - export interface ServerEval { - cp?: number; - mate?: number; + export interface ServerEval extends EvalScore { best?: Uci; fen: Fen; knodes: number; @@ -457,16 +462,12 @@ declare namespace Tree { pvs: PvDataServer[]; } - export interface PvDataServer { + export interface PvDataServer extends EvalScore { moves: string; - mate?: number; - cp?: number; } - export interface PvData { + export interface PvData extends EvalScore { moves: string[]; - mate?: number; - cp?: number; } export interface TablebaseHit { diff --git a/ui/analyse/css/study/_modal.scss b/ui/analyse/css/study/_modal.scss index ebd07b76d174b..285b02902eeb4 100644 --- a/ui/analyse/css/study/_modal.scss +++ b/ui/analyse/css/study/_modal.scss @@ -65,7 +65,13 @@ } } - &.chapter-new .tabs-horiz { - margin: -1em 0 1.6em 0; + &.chapter-new { + .tabs-horiz { + margin: -1em 0 1.6em 0; + } + .import-from__chapter { + @extend %break-word-hard, %ellipsis; + max-width: 100%; + } } } diff --git a/ui/analyse/css/study/panel/_multiboard.scss b/ui/analyse/css/study/panel/_multiboard.scss index 9262a07f36866..c7b6d210cd716 100644 --- a/ui/analyse/css/study/panel/_multiboard.scss +++ b/ui/analyse/css/study/panel/_multiboard.scss @@ -79,3 +79,40 @@ } } } +.mini-game { + .cg-gauge { + display: flex; + flex-flow: row nowrap; + } + &__board { + flex: 1 1 auto; + } + &__gauge { + position: relative; + flex: 0 0 5%; + background: if($theme-light, #fff, #a0a0a0); + border: $border; + border-left: 0; + overflow: hidden; + @extend %box-radius-right; + + &::after { + content: ''; + display: block; + position: absolute; + top: 0; + bottom: 0; + #{$start-direction}: 0; + #{$end-direction}: 0; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.7) inset; + } + + &__black { + display: block; + width: 100%; + height: 50%; + background: if($theme-light, #888, #666); + transition: height 1s; + } + } +} diff --git a/ui/analyse/package.json b/ui/analyse/package.json index ed73968474310..ae446c8c63890 100644 --- a/ui/analyse/package.json +++ b/ui/analyse/package.json @@ -23,7 +23,7 @@ "chart": "workspace:*", "chat": "workspace:*", "chess": "workspace:*", - "chessops": "^0.12.8", + "chessops": "^0.13.0", "common": "workspace:*", "debounce-promise": "^3.1.2", "game": "workspace:*", diff --git a/ui/analyse/src/autoShape.ts b/ui/analyse/src/autoShape.ts index 9b779ced39a55..d3b71f1dacaca 100644 --- a/ui/analyse/src/autoShape.ts +++ b/ui/analyse/src/autoShape.ts @@ -129,9 +129,10 @@ export function compute(ctrl: AnalyseCtrl): DrawShape[] { function hiliteVariations(ctrl: AnalyseCtrl, autoShapes: DrawShape[]) { const chap = ctrl.study?.data.chapter; - const isGamebookEditor = chap?.gamebook && !ctrl.study?.gamebookPlay(); + const isGamebookEditor = chap?.gamebook && !ctrl.study?.gamebookPlay; for (const [i, node] of ctrl.node.children.entries()) { + if (node.comp && !ctrl.showComputer()) continue; const userShape = findShape(node.uci, ctrl.node.shapes); if (userShape && i === ctrl.fork.selected()) autoShapes.push({ ...userShape }); // so we can hilite it diff --git a/ui/analyse/src/crazy/crazyView.ts b/ui/analyse/src/crazy/crazyView.ts index 21766872481de..b407a552014e5 100644 --- a/ui/analyse/src/crazy/crazyView.ts +++ b/ui/analyse/src/crazy/crazyView.ts @@ -38,11 +38,7 @@ export default function (ctrl: AnalyseCtrl, color: Color, position: Position) { h( 'div.pocket-c2', h('piece.' + role + '.' + color, { - attrs: { - 'data-role': role, - 'data-color': color, - 'data-nb': nb, - }, + attrs: { 'data-role': role, 'data-color': color, 'data-nb': nb }, }), ), ); diff --git a/ui/analyse/src/ctrl.ts b/ui/analyse/src/ctrl.ts index c5c300000d01a..d58dc84db2640 100644 --- a/ui/analyse/src/ctrl.ts +++ b/ui/analyse/src/ctrl.ts @@ -6,7 +6,7 @@ import * as util from './util'; import { plural } from './view/util'; import debounce from 'common/debounce'; import GamebookPlayCtrl from './study/gamebook/gamebookPlayCtrl'; -import type makeStudyCtrl from './study/studyCtrl'; +import StudyCtrl from './study/studyCtrl'; import { isTouchDevice } from 'common/device'; import throttle from 'common/throttle'; import { @@ -24,11 +24,11 @@ import { build as makeTree, path as treePath, ops as treeOps, TreeWrapper } from import { compute as computeAutoShapes } from './autoShape'; import { Config as ChessgroundConfig } from 'chessground/config'; import { CevalCtrl, isEvalBetter, sanIrreversible, EvalMeta } from 'ceval'; -import { ctrl as treeViewCtrl, TreeView } from './treeView/treeView'; +import { TreeView } from './treeView/treeView'; import { defined, prop, Prop, toggle, Toggle } from 'common'; import { DrawShape } from 'chessground/draw'; import { lichessRules } from 'chessops/compat'; -import { make as makeEvalCache, EvalCache } from './evalCache'; +import EvalCache from './evalCache'; import { make as makeFork, ForkCtrl } from './fork'; import { make as makePractice, PracticeCtrl } from './practice/practiceCtrl'; import { make as makeRetro, RetroCtrl } from './retrospect/retroCtrl'; @@ -41,7 +41,7 @@ import { Position, PositionError } from 'chessops/chess'; import { Result } from '@badrap/result'; import { setupPosition } from 'chessops/variant'; import { storedBooleanProp } from 'common/storage'; -import { AnaMove, StudyCtrl } from './study/interfaces'; +import { AnaMove } from './study/interfaces'; import { StudyPracticeCtrl } from './study/practice/interfaces'; import { valid as crazyValid } from './crazy/crazyCtrl'; import { PromotionCtrl } from 'chess/promotion'; @@ -127,12 +127,12 @@ export default class AnalyseCtrl { constructor( readonly opts: AnalyseOpts, readonly redraw: Redraw, - makeStudy?: typeof makeStudyCtrl, + makeStudy?: typeof StudyCtrl, ) { this.data = opts.data; this.element = opts.element; this.trans = opts.trans; - this.treeView = treeViewCtrl('column'); + this.treeView = new TreeView('column'); this.promotion = new PromotionCtrl( this.withCg, () => this.withCg(g => g.set(this.cgConfig)), @@ -142,7 +142,9 @@ export default class AnalyseCtrl { if (this.data.forecast) this.forecast = new ForecastCtrl(this.data.forecast, this.data, redraw); if (this.opts.wiki) this.wiki = wikiTheory(); if (lichess.blindMode) - lichess.loadEsm('analysisBoard.nvui', { init: this }).then(nvui => (this.nvui = nvui)); + lichess.asset + .loadEsm('analysisBoard.nvui', { init: this }) + .then(nvui => (this.nvui = nvui)); this.instanciateEvalCache(); @@ -161,9 +163,10 @@ export default class AnalyseCtrl { this.onToggleComputer(); this.startCeval(); this.explorer.setNode(); - this.study = opts.study - ? makeStudy?.(opts.study, this, (opts.tagTypes || '').split(','), opts.practice, opts.relay) - : undefined; + this.study = + opts.study && makeStudy + ? new makeStudy(opts.study, this, (opts.tagTypes || '').split(','), opts.practice, opts.relay) + : undefined; this.studyPractice = this.study ? this.study.practice : undefined; if (location.hash === '#practice' || (this.study && this.study.data.chapter.practice)) @@ -320,6 +323,8 @@ export default class AnalyseCtrl { }); }); + serverMainline = () => this.mainline.slice(0, game.playedTurns(this.data) + 1); + makeCgOpts(): ChessgroundConfig { const node = this.node, color = this.turnColor(), @@ -632,7 +637,7 @@ export default class AnalyseCtrl { if (this.retro) this.retro.onCeval(); if (this.practice) this.practice.onCeval(); if (this.studyPractice) this.studyPractice.onCeval(); - this.evalCache.onCeval(); + this.evalCache.onLocalCeval(); } this.redraw(); } @@ -764,10 +769,10 @@ export default class AnalyseCtrl { !isTouchDevice() && !chap?.practice && chap?.conceal === undefined && - !this.study?.gamebookPlay() && - !this.retro && + !this.study?.gamebookPlay && + !this.retro?.isSolving() && this.variationArrowsProp() && - this.node.children.length > 1 + this.node.children.filter(x => !x.comp || this.showComputer()).length > 1 ); } @@ -792,7 +797,6 @@ export default class AnalyseCtrl { private onToggleComputer() { if (!this.showComputer()) { - this.tree.removeComputerVariations(); if (this.ceval.enabled()) this.toggleCeval(); } this.resetAutoShapes(); @@ -810,7 +814,6 @@ export default class AnalyseCtrl { mergeAnalysisData(data: ServerEvalData) { if (this.study && this.study.data.chapter.id !== data.ch) return; this.tree.merge(data.tree); - if (!this.showComputer()) this.tree.removeComputerVariations(); this.data.analysis = data.analysis; if (data.analysis) data.analysis.partial = !!treeOps.findInMainline( @@ -862,7 +865,7 @@ export default class AnalyseCtrl { if (uci) this.playUci(uci); } - canEvalGet(): boolean { + canEvalGet = (): boolean => { if (this.node.ply >= 15 && !this.opts.study) return false; // cloud eval does not support threefold repetition @@ -875,12 +878,12 @@ export default class AnalyseCtrl { fens.add(fen); } return true; - } + }; - instanciateEvalCache() { - this.evalCache = makeEvalCache({ + instanciateEvalCache = () => { + this.evalCache = new EvalCache({ variant: this.data.game.variant.key, - canGet: () => this.canEvalGet(), + canGet: this.canEvalGet, canPut: () => !!( this.ceval?.cacheable() && @@ -892,7 +895,7 @@ export default class AnalyseCtrl { send: this.opts.socketSend, receive: this.onNewCeval, }); - } + }; closeTools = () => { if (this.retro) this.retro = undefined; @@ -937,7 +940,7 @@ export default class AnalyseCtrl { this.togglePractice(); } - gamebookPlay = (): GamebookPlayCtrl | undefined => this.study && this.study.gamebookPlay(); + gamebookPlay = (): GamebookPlayCtrl | undefined => this.study?.gamebookPlay; isGamebook = (): boolean => !!(this.study && this.study.data.chapter.gamebook); diff --git a/ui/analyse/src/evalCache.ts b/ui/analyse/src/evalCache.ts index 33847c7e32b6f..5f0929f47f9a4 100644 --- a/ui/analyse/src/evalCache.ts +++ b/ui/analyse/src/evalCache.ts @@ -1,6 +1,6 @@ import { defined, prop } from 'common'; import throttle from 'common/throttle'; -import { CachedEval, EvalGetData, EvalPutData } from './interfaces'; +import { EvalHit, EvalGetData, EvalPutData } from './interfaces'; import { AnalyseSocketSend } from './socket'; export interface EvalCacheOpts { @@ -12,13 +12,6 @@ export interface EvalCacheOpts { canGet(): boolean; } -export interface EvalCache { - onCeval(): void; - fetch(path: Tree.Path, multiPv: number): void; - onCloudEval(serverEval: CachedEval): void; - clear(): void; -} - const evalPutMinDepth = 20; const evalPutMinNodes = 3e6; const evalPutMaxMoves = 10; @@ -69,49 +62,55 @@ function toCeval(e: Tree.ServerEval): Tree.ClientEval { return res; } -export function make(opts: EvalCacheOpts): EvalCache { - let fetchedByFen: Dictionary = {}; - const upgradable = prop(false); - lichess.pubsub.on('socket.in.crowd', d => upgradable(d.nb > 2 && d.nb < 99999)); - return { - onCeval: throttle(500, () => { - const node = opts.getNode(), - ev = node.ceval; - const fetched = fetchedByFen[node.fen]; - if ( - ev && - !ev.cloud && - node.fen in fetchedByFen && - (!fetched || fetched.depth < ev.depth) && - qualityCheck(ev) && - opts.canPut() - ) { - opts.send('evalPut', toPutData(opts.variant, ev)); - } - }), - fetch(path: Tree.Path, multiPv: number): void { - const node = opts.getNode(); - if ((node.ceval && node.ceval.cloud) || !opts.canGet()) return; - const serverEval = fetchedByFen[node.fen]; - if (serverEval) return opts.receive(toCeval(serverEval), path); - else if (node.fen in fetchedByFen) return; - // waiting for response - else fetchedByFen[node.fen] = undefined; // mark as waiting - const obj: EvalGetData = { - fen: node.fen, - path, - }; - if (opts.variant !== 'standard') obj.variant = opts.variant; - if (multiPv > 1) obj.mpv = multiPv; - if (upgradable()) obj.up = true; - opts.send('evalGet', obj); - }, - onCloudEval(serverEval: CachedEval) { - fetchedByFen[serverEval.fen] = serverEval; - opts.receive(toCeval(serverEval), serverEval.path); - }, - clear() { - fetchedByFen = {}; - }, +type AwaitingEval = null; +const awaitingEval: AwaitingEval = null; + +export default class EvalCache { + private fetchedByFen: Map = new Map(); + private upgradable = prop(false); + + constructor(readonly opts: EvalCacheOpts) { + lichess.pubsub.on('socket.in.crowd', d => this.upgradable(d.nb > 2 && d.nb < 99999)); + } + + onLocalCeval = throttle(500, () => { + const node = this.opts.getNode(), + ev = node.ceval; + const fetched = this.fetchedByFen.get(node.fen); + if ( + ev && + !ev.cloud && + this.fetchedByFen.has(node.fen) && + (!fetched || fetched.depth < ev.depth) && + qualityCheck(ev) && + this.opts.canPut() + ) { + this.opts.send('evalPut', toPutData(this.opts.variant, ev)); + } + }); + + fetch = (path: Tree.Path, multiPv: number): void => { + const node = this.opts.getNode(); + if ((node.ceval && node.ceval.cloud) || !this.opts.canGet()) return; + const fetched = this.fetchedByFen.get(node.fen); + if (fetched) return this.opts.receive(toCeval(fetched), path); + else if (fetched === awaitingEval) return; + // waiting for response + else this.fetchedByFen.set(node.fen, awaitingEval); // mark as waiting + const obj: EvalGetData = { + fen: node.fen, + path, + }; + if (this.opts.variant !== 'standard') obj.variant = this.opts.variant; + if (multiPv > 1) obj.mpv = multiPv; + if (this.upgradable()) obj.up = true; + this.opts.send('evalGet', obj); }; + + onCloudEval = (ev: EvalHit) => { + this.fetchedByFen.set(ev.fen, ev); + this.opts.receive(toCeval(ev), ev.path); + }; + + clear = () => this.fetchedByFen.clear(); } diff --git a/ui/analyse/src/explorer/explorerConfig.ts b/ui/analyse/src/explorer/explorerConfig.ts index 866637e2a9053..c49b88533aa9b 100644 --- a/ui/analyse/src/explorer/explorerConfig.ts +++ b/ui/analyse/src/explorer/explorerConfig.ts @@ -150,10 +150,7 @@ export function view(ctrl: ExplorerConfigCtrl): VNode[] { 'section.save', h( 'button.button.button-green.text', - { - attrs: dataIcon(licon.Checkmark), - hook: bind('click', ctrl.toggleOpen), - }, + { attrs: dataIcon(licon.Checkmark), hook: bind('click', ctrl.toggleOpen) }, ctrl.root.trans.noarg('allSet'), ), ), @@ -352,12 +349,8 @@ const playerModal = (ctrl: ExplorerConfigCtrl) => { spellcheck: 'false', }, hook: onInsert(input => - lichess - .userComplete({ - input, - tag: 'span', - onSelect: v => onSelect(v.name), - }) + lichess.asset + .userComplete({ input, tag: 'span', onSelect: v => onSelect(v.name) }) .then(() => input.focus()), ), }), @@ -371,27 +364,19 @@ const playerModal = (ctrl: ExplorerConfigCtrl) => { ...ctrl.data.playerName.previous(), ]), ].map(name => - h( - 'div', - { - key: name, - }, - [ - h( - `button.button${nameToOptionalColor(name)}`, - { - hook: bind('click', () => onSelect(name)), - }, - name, - ), - name && ctrl.data.playerName.previous().includes(name) - ? h('button.remove', { - attrs: dataIcon(licon.X), - hook: bind('click', () => ctrl.removePlayer(name), ctrl.root.redraw), - }) - : null, - ], - ), + h('div', { key: name }, [ + h( + `button.button${nameToOptionalColor(name)}`, + { hook: bind('click', () => onSelect(name)) }, + name, + ), + name && ctrl.data.playerName.previous().includes(name) + ? h('button.remove', { + attrs: dataIcon(licon.X), + hook: bind('click', () => ctrl.removePlayer(name), ctrl.root.redraw), + }) + : null, + ]), ), ), ], diff --git a/ui/analyse/src/explorer/explorerView.ts b/ui/analyse/src/explorer/explorerView.ts index bce5396778ad6..8ad9d0b251860 100644 --- a/ui/analyse/src/explorer/explorerView.ts +++ b/ui/analyse/src/explorer/explorerView.ts @@ -1,8 +1,8 @@ -import { h, VNode } from 'snabbdom'; +import { VNode } from 'snabbdom'; import * as licon from 'common/licon'; import { numberFormat } from 'common/number'; import { perf } from 'game/perf'; -import { bind, dataIcon, MaybeVNode, MaybeVNodes } from 'common/snabbdom'; +import { bind, dataIcon, MaybeVNode, LooseVNodes, looseH as h } from 'common/snabbdom'; import { view as renderConfig } from './explorerConfig'; import { moveArrowAttributes, ucfirst } from './explorerUtil'; import AnalyseCtrl from '../ctrl'; @@ -24,9 +24,7 @@ function resultBar(move: OpeningMoveStats): VNode { const percent = (move[key] * 100) / sum; return h( 'span.' + key, - { - attrs: { style: 'width: ' + Math.round((move[key] * 1000) / sum) / 10 + '%' }, - }, + { attrs: { style: 'width: ' + Math.round((move[key] * 1000) / sum) / 10 + '%' } }, percent > 12 ? Math.round(percent) + (percent > 20 ? '%' : '') : '', ); }; @@ -64,21 +62,12 @@ function showMoveTable(ctrl: AnalyseCtrl, data: OpeningData): VNode | null { moveArrowAttributes(ctrl, { fen: data.fen, onClick: (_, uci) => uci && ctrl.explorerMove(uci) }), movesWithCurrent.map(move => { const total = move.white + move.draws + move.black; - return h( - `tr${move.uci ? '' : '.sum'}`, - { - key: move.uci, - attrs: { - 'data-uci': move.uci, - }, - }, - [ - h('td', move.san[0] === 'P' ? move.san.slice(1) : move.san), - h('td', ((total / sumTotal) * 100).toFixed(0) + '%'), - h('td', numberFormat(total)), - h('td', { attrs: { title: moveTooltip(ctrl, move) } }, resultBar(move)), - ], - ); + return h(`tr${move.uci ? '' : '.sum'}`, { key: move.uci, attrs: { 'data-uci': move.uci } }, [ + h('td', move.san[0] === 'P' ? move.san.slice(1) : move.san), + h('td', ((total / sumTotal) * 100).toFixed(0) + '%'), + h('td', numberFormat(total)), + h('td', { attrs: { title: moveTooltip(ctrl, move) } }, resultBar(move)), + ]); }), ), ]); @@ -131,39 +120,25 @@ function showGameTable(ctrl: AnalyseCtrl, fen: Fen, title: string, games: Openin games.map(game => { return openedId === game.id ? gameActions(ctrl, game) - : h( - 'tr', - { - key: game.id, - attrs: { 'data-id': game.id, 'data-uci': game.uci || '' }, - }, - [ - ctrl.explorer.opts.showRatings - ? h( - 'td', - [game.white, game.black].map(p => h('span', '' + p.rating)), - ) - : null, + : h('tr', { key: game.id, attrs: { 'data-id': game.id, 'data-uci': game.uci || '' } }, [ + ctrl.explorer.opts.showRatings && h( 'td', - [game.white, game.black].map(p => h('span', p.name)), + [game.white, game.black].map(p => h('span', '' + p.rating)), ), - h('td', showResult(game.winner)), - h('td', game.month || game.year), - isMasters - ? undefined - : h( - 'td', - game.speed && - h('i', { - attrs: { - title: ucfirst(game.speed), - ...dataIcon(perf.icons[game.speed]), - }, - }), - ), - ], - ); + h( + 'td', + [game.white, game.black].map(p => h('span', p.name)), + ), + h('td', showResult(game.winner)), + h('td', game.month || game.year), + !isMasters && + h( + 'td', + game.speed && + h('i', { attrs: { title: ucfirst(game.speed), ...dataIcon(perf.icons[game.speed]) } }), + ), + ]); }), ), ]); @@ -183,73 +158,44 @@ function gameActions(ctrl: AnalyseCtrl, game: OpeningGame): VNode { ctrl.explorer.gameMenu(null); ctrl.redraw(); }; - return h( - 'tr', - { - key: game.id + '-m', - }, - [ + return h('tr', { key: game.id + '-m' }, [ + h('td.game_menu', { attrs: { colspan: ctrl.explorer.db() == 'masters' ? 4 : 5 } }, [ h( - 'td.game_menu', - { - attrs: { colspan: ctrl.explorer.db() == 'masters' ? 4 : 5 }, - }, - [ + 'div.game_title', + `${game.white.name} - ${game.black.name}, ${showResult(game.winner).text}, ${game.year}`, + ), + h('div.menu', [ + h( + 'a.text', + { attrs: dataIcon(licon.Eye), hook: bind('click', _ => openGame(ctrl, game.id)) }, + 'View', + ), + ctrl.study && h( - 'div.game_title', - `${game.white.name} - ${game.black.name}, ${showResult(game.winner).text}, ${game.year}`, + 'a.text', + { attrs: dataIcon(licon.BubbleSpeech), hook: bind('click', _ => send(false), ctrl.redraw) }, + 'Cite', ), - h('div.menu', [ - h( - 'a.text', - { - attrs: dataIcon(licon.Eye), - hook: bind('click', _ => openGame(ctrl, game.id)), - }, - 'View', - ), - ...(ctrl.study - ? [ - h( - 'a.text', - { - attrs: dataIcon(licon.BubbleSpeech), - hook: bind('click', _ => send(false), ctrl.redraw), - }, - 'Cite', - ), - h( - 'a.text', - { - attrs: dataIcon(licon.PlusButton), - hook: bind('click', _ => send(true), ctrl.redraw), - }, - 'Insert', - ), - ] - : []), - h( - 'a.text', - { - attrs: dataIcon(licon.X), - hook: bind('click', _ => ctrl.explorer.gameMenu(null), ctrl.redraw), - }, - 'Close', - ), - ]), - ], - ), - ], - ); + ctrl.study && + h( + 'a.text', + { attrs: dataIcon(licon.PlusButton), hook: bind('click', _ => send(true), ctrl.redraw) }, + 'Insert', + ), + h( + 'a.text', + { attrs: dataIcon(licon.X), hook: bind('click', _ => ctrl.explorer.gameMenu(null), ctrl.redraw) }, + 'Close', + ), + ]), + ]), + ]); } const closeButton = (ctrl: AnalyseCtrl): VNode => h( 'button.button.button-empty.text', - { - attrs: dataIcon(licon.X), - hook: bind('click', ctrl.toggleExplorer, ctrl.redraw), - }, + { attrs: dataIcon(licon.X), hook: bind('click', ctrl.toggleExplorer, ctrl.redraw) }, ctrl.trans.noarg('close'), ); @@ -264,9 +210,8 @@ const showEmpty = (ctrl: AnalyseCtrl, data?: OpeningData): VNode => ), data?.queuePosition ? h('p.explanation', `Indexing ${data.queuePosition} other players first ...`) - : !ctrl.explorer.config.fullHouse() - ? h('p.explanation', ctrl.trans.noarg('maybeIncludeMoreGamesFromThePreferencesMenu')) - : null, + : !ctrl.explorer.config.fullHouse() && + h('p.explanation', ctrl.trans.noarg('maybeIncludeMoreGamesFromThePreferencesMenu')), ]), ]); @@ -285,22 +230,9 @@ const openingTitle = (ctrl: AnalyseCtrl, data?: OpeningData) => { const title = opening ? `${opening.eco} ${opening.name}` : ''; return h( 'div.title', - { - attrs: opening ? { title } : {}, - }, + { attrs: opening ? { title } : {} }, opening - ? [ - h( - 'a', - { - attrs: { - href: `/opening/${opening.name}`, - target: '_blank', - }, - }, - title, - ), - ] + ? [h('a', { attrs: { href: `/opening/${opening.name}`, target: '_blank' } }, title)] : [showTitle(ctrl, ctrl.data.game.variant)], ); }; @@ -385,7 +317,7 @@ const explorerTitle = (explorer: ExplorerCtrl) => { }, explorer.root.trans('player'), ); - const active = (nodes: MaybeVNodes, title: string) => + const active = (nodes: LooseVNodes, title: string) => h( 'span.active.text.' + db, { @@ -402,9 +334,7 @@ const explorerTitle = (explorer: ExplorerCtrl) => { return h('div.explorer-title', [ db == 'masters' ? active([h('strong', 'Masters'), ' database'], masterDbExplanation) - : explorer.config.allDbs.includes('masters') - ? otherLink('Masters', masterDbExplanation) - : undefined, + : explorer.config.allDbs.includes('masters') && otherLink('Masters', masterDbExplanation), db == 'lichess' ? active([h('strong', 'Lichess'), ' database'], lichessDbExplanation) : otherLink('Lichess', lichessDbExplanation), @@ -414,15 +344,15 @@ const explorerTitle = (explorer: ExplorerCtrl) => { [ h(`strong${playerName.length > 14 ? '.long' : ''}`, playerName), ' ' + explorer.root.trans(explorer.config.data.color() == 'white' ? 'asWhite' : 'asBlack'), - explorer.isIndexing() && !explorer.config.data.open() - ? h('i.ddloader', { - attrs: { - title: queuePosition - ? `Indexing ${queuePosition} other players first ...` - : 'Indexing ...', - }, - }) - : undefined, + explorer.isIndexing() && + !explorer.config.data.open() && + h('i.ddloader', { + attrs: { + title: queuePosition + ? `Indexing ${queuePosition} other players first ...` + : 'Indexing ...', + }, + }), ], explorer.root.trans('switchSides'), ) @@ -465,10 +395,7 @@ export default function (ctrl: AnalyseCtrl): VNode | undefined { return h( `section.explorer-box.sub-box${configOpened ? '.explorer__config' : ''}`, { - class: { - loading, - reduced: !configOpened && (!!explorer.failing() || explorer.movesAway() > 2), - }, + class: { loading, reduced: !configOpened && (!!explorer.failing() || explorer.movesAway() > 2) }, hook: { insert: vnode => ((vnode.elm as HTMLElement).scrollTop = 0), postpatch(_, vnode) { diff --git a/ui/analyse/src/explorer/tablebaseView.ts b/ui/analyse/src/explorer/tablebaseView.ts index 43cec0d45870b..d56ee6846f4b4 100644 --- a/ui/analyse/src/explorer/tablebaseView.ts +++ b/ui/analyse/src/explorer/tablebaseView.ts @@ -18,14 +18,10 @@ export function showTablebase( 'tbody', moveArrowAttributes(ctrl, { fen, onClick: (_, uci) => uci && ctrl.explorerMove(uci) }), moves.map(move => - h( - 'tr', - { - key: move.uci, - attrs: { 'data-uci': move.uci }, - }, - [h('td', move.san), h('td', [showDtz(ctrl, fen, move), showDtm(ctrl, fen, move)])], - ), + h('tr', { key: move.uci, attrs: { 'data-uci': move.uci } }, [ + h('td', move.san), + h('td', [showDtz(ctrl, fen, move), showDtm(ctrl, fen, move)]), + ]), ), ), ]), @@ -37,9 +33,7 @@ function showDtm(ctrl: AnalyseCtrl, fen: Fen, move: TablebaseMoveStats) { return h( 'result.' + winnerOf(fen, move), { - attrs: { - title: ctrl.trans.pluralSame('mateInXHalfMoves', Math.abs(move.dtm)) + ' (Depth To Mate)', - }, + attrs: { title: ctrl.trans.pluralSame('mateInXHalfMoves', Math.abs(move.dtm)) + ' (Depth To Mate)' }, }, 'DTM ' + Math.abs(move.dtm), ); diff --git a/ui/analyse/src/forecast/forecastView.ts b/ui/analyse/src/forecast/forecastView.ts index 64651217c7b5d..8057979219b1e 100644 --- a/ui/analyse/src/forecast/forecastView.ts +++ b/ui/analyse/src/forecast/forecastView.ts @@ -50,59 +50,53 @@ function makeCnodes(ctrl: AnalyseCtrl, fctrl: ForecastCtrl): ForecastStep[] { export default function (ctrl: AnalyseCtrl, fctrl: ForecastCtrl): VNode { const cNodes = makeCnodes(ctrl, fctrl); const isCandidate = fctrl.isCandidate(cNodes); - return h( - 'div.forecast', - { - class: { loading: fctrl.loading() }, - }, - [ - fctrl.loading() ? h('div.overlay', spinner()) : null, - h('div.box', [ - h('div.top', ctrl.trans.noarg('conditionalPremoves')), - h( - 'div.list', - fctrl.forecasts().map((nodes, i) => - h( - 'button.entry.text', - { - attrs: dataIcon(licon.PlayTriangle), - hook: bind( - 'click', - _ => { - const path = fctrl.showForecast(findCurrentPath(ctrl) || '', ctrl.tree, nodes); - ctrl.userJump(path); - }, - ctrl.redraw, - ), - }, - [ - h('button.del', { - hook: bind('click', _ => fctrl.removeIndex(i), ctrl.redraw), - attrs: { 'data-icon': licon.X, type: 'button' }, - }), - h('sans', renderNodesHtml(nodes)), - ], - ), + return h('div.forecast', { class: { loading: fctrl.loading() } }, [ + fctrl.loading() ? h('div.overlay', spinner()) : null, + h('div.box', [ + h('div.top', ctrl.trans.noarg('conditionalPremoves')), + h( + 'div.list', + fctrl.forecasts().map((nodes, i) => + h( + 'button.entry.text', + { + attrs: dataIcon(licon.PlayTriangle), + hook: bind( + 'click', + () => { + const path = fctrl.showForecast(findCurrentPath(ctrl) || '', ctrl.tree, nodes); + ctrl.userJump(path); + }, + ctrl.redraw, + ), + }, + [ + h('button.del', { + hook: bind('click', _ => fctrl.removeIndex(i), ctrl.redraw), + attrs: { 'data-icon': licon.X, type: 'button' }, + }), + h('sans', renderNodesHtml(nodes)), + ], ), ), - h( - 'button.add.text', - { - class: { enabled: isCandidate }, - attrs: dataIcon(isCandidate ? licon.PlusButton : licon.InfoCircle), - hook: bind('click', _ => fctrl.addNodes(makeCnodes(ctrl, fctrl)), ctrl.redraw), - }, - [ - isCandidate - ? h('span', [ - h('span', ctrl.trans.noarg('addCurrentVariation')), - h('sans', renderNodesHtml(cNodes)), - ]) - : h('span', ctrl.trans.noarg('playVariationToCreateConditionalPremoves')), - ], - ), - ]), - fctrl.onMyTurn() ? onMyTurn(ctrl, fctrl, cNodes) : null, - ], - ); + ), + h( + 'button.add.text', + { + class: { enabled: isCandidate }, + attrs: dataIcon(isCandidate ? licon.PlusButton : licon.InfoCircle), + hook: bind('click', _ => fctrl.addNodes(makeCnodes(ctrl, fctrl)), ctrl.redraw), + }, + [ + isCandidate + ? h('span', [ + h('span', ctrl.trans.noarg('addCurrentVariation')), + h('sans', renderNodesHtml(cNodes)), + ]) + : h('span', ctrl.trans.noarg('playVariationToCreateConditionalPremoves')), + ], + ), + ]), + fctrl.onMyTurn() ? onMyTurn(ctrl, fctrl, cNodes) : null, + ]); } diff --git a/ui/analyse/src/fork.ts b/ui/analyse/src/fork.ts index 0df8b7d25112d..e3a7f47900a05 100644 --- a/ui/analyse/src/fork.ts +++ b/ui/analyse/src/fork.ts @@ -35,11 +35,7 @@ export function make(ctrl: AnalyseCtrl): ForkCtrl { prev = node; selected = 0; } - return { - node, - selected, - displayed: displayed(), - }; + return { node, selected, displayed: displayed() }; }, next() { if (!displayed()) return false; @@ -94,7 +90,7 @@ const eventToIndex = (e: MouseEvent): number | undefined => { }; export function view(ctrl: AnalyseCtrl, concealOf?: ConcealOf) { - if (ctrl.retro) return; + if (ctrl.retro?.isSolving()) return; const state = ctrl.fork.state(); if (!state.displayed) return; const isMainline = concealOf && ctrl.onMainline; @@ -120,16 +116,9 @@ export function view(ctrl: AnalyseCtrl, concealOf?: ConcealOf) { if (!conceal) return h( 'move', - { - class: classes, - attrs: { 'data-it': it }, - }, + { class: classes, attrs: { 'data-it': it } }, renderIndexAndMove( - { - withDots: true, - showEval: ctrl.showComputer(), - showGlyphs: ctrl.showComputer(), - }, + { withDots: true, showEval: ctrl.showComputer(), showGlyphs: ctrl.showComputer() }, node, )!, ); diff --git a/ui/analyse/src/ground.ts b/ui/analyse/src/ground.ts index d3b78d5f4b0ea..2df268bd849ab 100644 --- a/ui/analyse/src/ground.ts +++ b/ui/analyse/src/ground.ts @@ -23,18 +23,7 @@ export const render = (ctrl: AnalyseCtrl): VNode => export function promote(ground: CgApi, key: Key, role: cg.Role) { const piece = ground.state.pieces.get(key); if (piece && piece.role == 'pawn') { - ground.setPieces( - new Map([ - [ - key, - { - color: piece.color, - role, - promoted: true, - }, - ], - ]), - ); + ground.setPieces(new Map([[key, { color: piece.color, role, promoted: true }]])); } } diff --git a/ui/analyse/src/interfaces.ts b/ui/analyse/src/interfaces.ts index f2b20d015cae7..99645b370734e 100644 --- a/ui/analyse/src/interfaces.ts +++ b/ui/analyse/src/interfaces.ts @@ -74,7 +74,7 @@ export interface ServerEvalData { division?: Division; } -export interface CachedEval { +export interface EvalHit { fen: Fen; knodes: number; depth: number; @@ -82,6 +82,15 @@ export interface CachedEval { path: string; } +export interface EvalHitMulti extends EvalScore { + fen: Fen; + depth: number; +} + +export interface EvalHitMultiArray { + multi: EvalHitMulti[]; +} + // similar, but not identical, to game/Game export interface Game { id: string; diff --git a/ui/analyse/src/plugins/nvui.ts b/ui/analyse/src/plugins/nvui.ts index 5162f52e39621..6f2d75763c582 100644 --- a/ui/analyse/src/plugins/nvui.ts +++ b/ui/analyse/src/plugins/nvui.ts @@ -36,7 +36,7 @@ import throttle from 'common/throttle'; import { Role } from 'chessground/types'; import explorerView from '../explorer/explorerView'; import { ops, path as treePath } from 'tree'; -import { view as cevalView, renderEval, Eval } from 'ceval'; +import { view as cevalView, renderEval } from 'ceval'; import * as control from '../control'; import { lichessRules } from 'chessops/compat'; import { makeSan } from 'chessops/san'; @@ -87,24 +87,13 @@ export function initModule(ctrl: AnalyseController) { h('p', `${d.game.rated ? 'Rated' : 'Casual'} ${d.game.perf}`), d.clock ? h('p', `Clock: ${d.clock.initial / 60} + ${d.clock.increment}`) : null, h('h2', 'Moves'), - h( - 'p.moves', - { - attrs: { - role: 'log', - 'aria-live': 'off', - }, - }, - renderCurrentLine(ctrl, style), - ), + h('p.moves', { attrs: { role: 'log', 'aria-live': 'off' } }, renderCurrentLine(ctrl, style)), ...(!ctrl.studyPractice ? [ h( 'button', { - attrs: { - 'aria-pressed': `${ctrl.explorer.enabled()}`, - }, + attrs: { 'aria-pressed': `${ctrl.explorer.enabled()}` }, hook: bind('click', _ => ctrl.explorer.toggle(), ctrl.redraw), }, ctrl.trans.noarg('openingExplorerAndTablebase'), @@ -118,12 +107,7 @@ export function initModule(ctrl: AnalyseController) { h('h2', 'Current position'), h( 'p.position.lastMove', - { - attrs: { - 'aria-live': 'assertive', - 'aria-atomic': 'true', - }, - }, + { attrs: { 'aria-live': 'assertive', 'aria-atomic': 'true' } }, // make sure consecutive positions are different so that they get re-read renderCurrentNode(ctrl, style) + (ctrl.node.ply % 2 === 0 ? '' : ' '), ), @@ -144,12 +128,7 @@ export function initModule(ctrl: AnalyseController) { h('label', [ 'Command input', h('input.move.mousetrap', { - attrs: { - name: 'move', - type: 'text', - autocomplete: 'off', - autofocus: true, - }, + attrs: { name: 'move', type: 'text', autocomplete: 'off', autofocus: true }, }), ]), ], @@ -196,10 +175,7 @@ export function initModule(ctrl: AnalyseController) { h( 'div.boardstatus', { - attrs: { - 'aria-live': 'polite', - 'aria-atomic': 'true', - }, + attrs: { 'aria-live': 'polite', 'aria-atomic': 'true' }, }, '', ), @@ -312,7 +288,7 @@ function renderEvalAndDepth(ctrl: AnalyseController): string { } } -function evalInfo(bestEv: Eval | undefined): string { +function evalInfo(bestEv: EvalScore | undefined): string { if (bestEv) { if (defined(bestEv.cp)) return renderEval(bestEv.cp).replace('-', '−'); else if (defined(bestEv.mate)) @@ -367,18 +343,10 @@ function renderResult(ctrl: AnalyseController): VNode[] { } return [ h('h2', 'Game status'), - h( - 'div.status', - { - attrs: { - role: 'status', - 'aria-live': 'assertive', - 'aria-atomic': 'true', - }, - }, - - [h('div.result', result), h('div.status', viewStatus(ctrl))], - ), + h('div.status', { attrs: { role: 'status', 'aria-live': 'assertive', 'aria-atomic': 'true' } }, [ + h('div.result', result), + h('div.status', viewStatus(ctrl)), + ]), ]; } return []; @@ -473,12 +441,7 @@ function renderAcpl(ctrl: AnalyseController, style: Style): MaybeVNodes | undefi .map(node => h( 'option', - { - attrs: { - value: node.ply, - selected: node.ply === ctrl.node.ply, - }, - }, + { attrs: { value: node.ply, selected: node.ply === ctrl.node.ply } }, [plyToTurn(node.ply), renderSan(node.san!, node.uci, style), renderComments(node, style)].join( ' ', ), @@ -501,17 +464,13 @@ function requestAnalysisButton( 'button', { hook: bind('click', _ => - xhr - .text(`/${ctrl.data.game.id}/request-analysis`, { - method: 'post', - }) - .then( - () => { - inProgress(true); - notify('Server-side analysis in progress'); - }, - _ => notify('Cannot run server-side analysis'), - ), + xhr.text(`/${ctrl.data.game.id}/request-analysis`, { method: 'post' }).then( + () => { + inProgress(true); + notify('Server-side analysis in progress'); + }, + () => notify('Cannot run server-side analysis'), + ), ), }, 'Request a computer analysis', @@ -560,9 +519,7 @@ function userHtml(ctrl: AnalyseController, player: Player) { ? h('span', [ h( 'a', - { - attrs: { href: '/@/' + user.username }, - }, + { attrs: { href: '/@/' + user.username } }, user.title ? `${user.title} ${user.username}` : user.username, ), rating ? ` ${rating}` : ``, diff --git a/ui/analyse/src/practice/practiceCtrl.ts b/ui/analyse/src/practice/practiceCtrl.ts index 3cd3a19a6fc44..998eb50f9e0ef 100644 --- a/ui/analyse/src/practice/practiceCtrl.ts +++ b/ui/analyse/src/practice/practiceCtrl.ts @@ -1,4 +1,4 @@ -import { winningChances, Eval, CevalCtrl } from 'ceval'; +import { winningChances, CevalCtrl } from 'ceval'; import { path as treePath } from 'tree'; import { detectThreefold } from '../nodeFinder'; import { tablebaseGuaranteed } from '../explorer/explorerCtrl'; @@ -96,10 +96,12 @@ export function make(root: AnalyseCtrl, playableDepth: () => number): PracticeCt if (outcome && outcome.winner) verdict = 'goodMove'; else { const isFiftyMoves = node.fen.split(' ')[4] === '100'; - const nodeEval: Eval = + const nodeEval: EvalScore = tbhitToEval(node.tbhit) || - (node.threefold || (outcome && !outcome.winner) || isFiftyMoves ? { cp: 0 } : (node.ceval as Eval)); - const prevEval: Eval = tbhitToEval(prev.tbhit) || prev.ceval!; + (node.threefold || (outcome && !outcome.winner) || isFiftyMoves + ? { cp: 0 } + : (node.ceval as EvalScore)); + const prevEval: EvalScore = tbhitToEval(prev.tbhit) || prev.ceval!; const shift = -winningChances.povDiff(root.bottomColor(), nodeEval, prevEval); best = nodeBestUci(prev); diff --git a/ui/analyse/src/practice/practiceView.ts b/ui/analyse/src/practice/practiceView.ts index 20e8ce32b3900..2cec508a7b219 100644 --- a/ui/analyse/src/practice/practiceView.ts +++ b/ui/analyse/src/practice/practiceView.ts @@ -88,9 +88,7 @@ function renderRunning(root: AnalyseCtrl, ctrl: PracticeCtrl): VNode { ctrl.isMyTurn() ? h( 'a', - { - hook: bind('click', () => root.practice!.hint(), ctrl.redraw), - }, + { hook: bind('click', () => root.practice!.hint(), ctrl.redraw) }, root.trans.noarg( hint ? (hint.mode === 'piece' ? 'seeBestMove' : 'hideBestMove') : 'getAHint', ), diff --git a/ui/analyse/src/retrospect/retroView.ts b/ui/analyse/src/retrospect/retroView.ts index fb6664f0e5ce9..a77a3e3a78c4e 100644 --- a/ui/analyse/src/retrospect/retroView.ts +++ b/ui/analyse/src/retrospect/retroView.ts @@ -8,31 +8,16 @@ import { h, VNode } from 'snabbdom'; function skipOrViewSolution(ctrl: RetroCtrl) { return h('div.choices', [ - h( - 'a', - { - hook: bind('click', ctrl.viewSolution, ctrl.redraw), - }, - ctrl.noarg('viewTheSolution'), - ), - h( - 'a', - { - hook: bind('click', ctrl.skip), - }, - ctrl.noarg('skipThisMove'), - ), + h('a', { hook: bind('click', ctrl.viewSolution, ctrl.redraw) }, ctrl.noarg('viewTheSolution')), + h('a', { hook: bind('click', ctrl.skip) }, ctrl.noarg('skipThisMove')), ]); } function jumpToNext(ctrl: RetroCtrl) { - return h( - 'a.half.continue', - { - hook: bind('click', ctrl.jumpToNext), - }, - [h('i', { attrs: dataIcon(licon.PlayTriangle) }), ctrl.noarg('next')], - ); + return h('a.half.continue', { hook: bind('click', ctrl.jumpToNext) }, [ + h('i', { attrs: dataIcon(licon.PlayTriangle) }), + ctrl.noarg('next'), + ]); } const minDepth = 8; @@ -64,11 +49,7 @@ const feedback = { h( 'move', renderIndexAndMove( - { - withDots: true, - showGlyphs: true, - showEval: false, - }, + { withDots: true, showGlyphs: true, showEval: false }, ctrl.current()!.fault.node, )!, ), @@ -88,13 +69,7 @@ const feedback = { h('div.instruction', [ h('strong', ctrl.noarg('youBrowsedAway')), h('div.choices.off', [ - h( - 'a', - { - hook: bind('click', ctrl.jumpToNext), - }, - ctrl.noarg('resumeLearning'), - ), + h('a', { hook: bind('click', ctrl.jumpToNext) }, ctrl.noarg('resumeLearning')), ]), ]), ]), @@ -135,13 +110,7 @@ const feedback = { 'bestWasX', h( 'strong', - renderIndexAndMove( - { - withDots: true, - showEval: false, - }, - ctrl.current()!.solution.node, - )!, + renderIndexAndMove({ withDots: true, showEval: false }, ctrl.current()!.solution.node)!, ), ), ), @@ -191,6 +160,7 @@ const feedback = { : h( 'a', { + key: 'reset', hook: bind('click', ctrl.reset), }, ctrl.noarg('doItAgain'), @@ -198,6 +168,7 @@ const feedback = { h( 'a', { + key: 'flip', hook: bind('click', ctrl.flip), }, ctrl.noarg(ctrl.color === 'white' ? 'reviewBlackMistakes' : 'reviewWhiteMistakes'), @@ -228,10 +199,7 @@ export default function (root: AnalyseCtrl): VNode | undefined { h('span', `${Math.min(completion[0] + 1, completion[1])} / ${completion[1]}`), h('button.fbt', { hook: bind('click', root.toggleRetro, root.redraw), - attrs: { - 'data-icon': licon.X, - 'aria-label': 'Close learn window', - }, + attrs: { 'data-icon': licon.X, 'aria-label': 'Close learn window' }, }), ]), h('div.feedback.' + fb, renderFeedback(root, fb)), diff --git a/ui/analyse/src/serverSideUnderboard.ts b/ui/analyse/src/serverSideUnderboard.ts index c733d9fbd204c..42417d1ed84c9 100644 --- a/ui/analyse/src/serverSideUnderboard.ts +++ b/ui/analyse/src/serverSideUnderboard.ts @@ -72,11 +72,13 @@ export default function (element: HTMLElement, ctrl: AnalyseCtrl) { (loading ? chartLoader() : ''), ); else if (loading && !$('#acpl-chart-container-loader').length) $panel.append(chartLoader()); - lichess.loadEsm('chart.game').then(m => - m.acpl($('#acpl-chart')[0] as HTMLCanvasElement, data, ctrl.mainline, ctrl.trans).then(chart => { - advChart = chart; - }), - ); + lichess.asset.loadEsm('chart.game').then(m => { + m.acpl($('#acpl-chart')[0] as HTMLCanvasElement, data, ctrl.serverMainline(), ctrl.trans).then( + chart => { + advChart = chart; + }, + ); + }); } const storage = lichess.storage.make('analysis.panel'); @@ -88,7 +90,7 @@ export default function (element: HTMLElement, ctrl: AnalyseCtrl) { .filter('.' + panel) .addClass('active'); if ((panel == 'move-times' || ctrl.opts.hunter) && !timeChartLoaded) - lichess.loadEsm('chart.game').then(m => { + lichess.asset.loadEsm('chart.game').then(m => { timeChartLoaded = true; m.movetime($('#movetimes-chart')[0] as HTMLCanvasElement, data, ctrl.trans, ctrl.opts.hunter); }); diff --git a/ui/analyse/src/socket.ts b/ui/analyse/src/socket.ts index 9fb06633f62c0..6f0323cc64a6d 100644 --- a/ui/analyse/src/socket.ts +++ b/ui/analyse/src/socket.ts @@ -1,7 +1,7 @@ import { initial as initialBoardFen } from 'chessground/fen'; import { ops as treeOps } from 'tree'; import AnalyseCtrl from './ctrl'; -import { CachedEval, EvalGetData, EvalPutData, ServerEvalData } from './interfaces'; +import { EvalGetData, EvalPutData, ServerEvalData, EvalHitMulti, EvalHitMultiArray } from './interfaces'; import { AnaDests, AnaDrop, AnaMove, ChapterData, EditChapterData } from './study/interfaces'; import { FormData as StudyFormData } from './study/studyForm'; @@ -163,8 +163,9 @@ export function make(send: AnalyseSocketSend, ctrl: AnalyseCtrl): Socket { analysisProgress(data: ServerEvalData) { ctrl.mergeAnalysisData(data); }, - evalHit(e: CachedEval) { - ctrl.evalCache.onCloudEval(e); + evalHit: ctrl.evalCache.onCloudEval, + evalHitMulti(e: EvalHitMulti | EvalHitMultiArray) { + if (ctrl.study) ('multi' in e ? e.multi : [e]).forEach(ctrl.study.multiBoard.onCloudEval); }, }; diff --git a/ui/analyse/src/start.ts b/ui/analyse/src/start.ts index 781bdfa460b31..2bd1088a28dd4 100644 --- a/ui/analyse/src/start.ts +++ b/ui/analyse/src/start.ts @@ -13,7 +13,7 @@ export default function ( opts.element = document.querySelector('main.analyse') as HTMLElement; opts.trans = lichess.trans(opts.i18n); - const ctrl = (lichess.analysis = new makeCtrl(opts, redraw, deps?.makeStudy)); + const ctrl = (lichess.analysis = new makeCtrl(opts, redraw, deps?.StudyCtrl)); const view = makeView(deps); const blueprint = view(ctrl); diff --git a/ui/analyse/src/study/chapterEditForm.ts b/ui/analyse/src/study/chapterEditForm.ts index 4f9620e641c74..d2828b7e1e5c9 100644 --- a/ui/analyse/src/study/chapterEditForm.ts +++ b/ui/analyse/src/study/chapterEditForm.ts @@ -9,82 +9,58 @@ import { StudyChapterConfig, StudyChapterMeta, } from './interfaces'; -import { defined, prop, Prop } from 'common'; +import { defined, prop } from 'common'; import { h, VNode } from 'snabbdom'; import { Redraw } from '../interfaces'; import { snabDialog } from 'common/dialog'; import { StudySocketSend } from '../socket'; -export interface StudyChapterEditFormCtrl { - current: Prop; - open(data: StudyChapterMeta): void; - toggle(data: StudyChapterMeta): void; - submit(data: Omit): void; - delete(id: string): void; - clearAnnotations(id: string): void; - clearVariations(id: string): void; - isEditing(id: string): boolean; - redraw: Redraw; - trans: Trans; -} +export class StudyChapterEditForm { + current = prop(null); -export function ctrl( - send: StudySocketSend, - chapterConfig: (id: string) => Promise, - trans: Trans, - redraw: Redraw, -): StudyChapterEditFormCtrl { - const current = prop(null); + constructor( + private readonly send: StudySocketSend, + private readonly chapterConfig: (id: string) => Promise, + readonly trans: Trans, + readonly redraw: Redraw, + ) {} - function open(data: StudyChapterMeta) { - current({ - id: data.id, - name: data.name, - }); - chapterConfig(data.id).then(d => { - current(d!); - redraw(); + open = (data: StudyChapterMeta) => { + this.current({ id: data.id, name: data.name }); + this.chapterConfig(data.id).then(d => { + this.current(d!); + this.redraw(); }); - } + }; - function isEditing(id: string) { - const c = current(); - return c ? c.id === id : false; - } + isEditing = (id: string) => this.current()?.id === id; - return { - open, - toggle(data) { - if (isEditing(data.id)) current(null); - else open(data); - }, - current, - submit(data) { - const c = current(); - if (c) { - send('editChapter', { id: c.id, ...data }); - current(null); - } - }, - delete(id) { - send('deleteChapter', id); - current(null); - }, - clearAnnotations(id) { - send('clearAnnotations', id); - current(null); - }, - clearVariations(id) { - send('clearVariations', id); - current(null); - }, - isEditing, - trans, - redraw, + toggle = (data: StudyChapterMeta) => { + if (this.isEditing(data.id)) this.current(null); + else this.open(data); + }; + submit = (data: Omit) => { + const c = this.current(); + if (c) { + this.send('editChapter', { id: c.id, ...data }); + this.current(null); + } + }; + delete = (id: string) => { + this.send('deleteChapter', id); + this.current(null); + }; + clearAnnotations = (id: string) => { + this.send('clearAnnotations', id); + this.current(null); + }; + clearVariations = (id: string) => { + this.send('clearVariations', id); + this.current(null); }; } -export function view(ctrl: StudyChapterEditFormCtrl): VNode | undefined { +export function view(ctrl: StudyChapterEditForm): VNode | undefined { const data = ctrl.current(), noarg = ctrl.trans.noarg; return data @@ -110,18 +86,9 @@ export function view(ctrl: StudyChapterEditFormCtrl): VNode | undefined { }, [ h('div.form-group', [ - h( - 'label.form-label', - { - attrs: { for: 'chapter-name' }, - }, - noarg('name'), - ), + h('label.form-label', { attrs: { for: 'chapter-name' } }, noarg('name')), h('input#chapter-name.form-control', { - attrs: { - minlength: 2, - maxlength: 80, - }, + attrs: { minlength: 2, maxlength: 80 }, hook: onInsert(el => { if (!el.value) { el.value = data.name; @@ -142,7 +109,7 @@ export function view(ctrl: StudyChapterEditFormCtrl): VNode | undefined { const isLoaded = (data: StudyChapterMeta | StudyChapterConfig): data is StudyChapterConfig => 'orientation' in data; -function viewLoaded(ctrl: StudyChapterEditFormCtrl, data: StudyChapterConfig): VNode[] { +function viewLoaded(ctrl: StudyChapterEditForm, data: StudyChapterConfig): VNode[] { const mode = data.practice ? 'practice' : defined(data.conceal) @@ -154,44 +121,22 @@ function viewLoaded(ctrl: StudyChapterEditFormCtrl, data: StudyChapterConfig): V return [ h('div.form-split', [ h('div.form-group.form-half', [ - h( - 'label.form-label', - { - attrs: { for: 'chapter-orientation' }, - }, - noarg('orientation'), - ), + h('label.form-label', { attrs: { for: 'chapter-orientation' } }, noarg('orientation')), h( 'select#chapter-orientation.form-control', - ['white', 'black'].map(function (color) { - return option(color, data.orientation, noarg(color)); - }), + ['white', 'black'].map(color => option(color, data.orientation, noarg(color))), ), ]), h('div.form-group.form-half', [ - h( - 'label.form-label', - { - attrs: { for: 'chapter-mode' }, - }, - noarg('analysisMode'), - ), + h('label.form-label', { attrs: { for: 'chapter-mode' } }, noarg('analysisMode')), h( 'select#chapter-mode.form-control', - chapterForm.modeChoices.map(c => { - return option(c[0], mode, noarg(c[1])); - }), + chapterForm.modeChoices.map(c => option(c[0], mode, noarg(c[1]))), ), ]), ]), h('div.form-group', [ - h( - 'label.form-label', - { - attrs: { for: 'chapter-description' }, - }, - noarg('pinnedChapterComment'), - ), + h('label.form-label', { attrs: { for: 'chapter-description' } }, noarg('pinnedChapterComment')), h( 'select#chapter-description.form-control', [ @@ -206,7 +151,7 @@ function viewLoaded(ctrl: StudyChapterEditFormCtrl, data: StudyChapterConfig): V { hook: bind( 'click', - _ => { + () => { if (confirm(noarg('clearAllCommentsInThisChapter'))) ctrl.clearAnnotations(data.id); }, ctrl.redraw, @@ -220,7 +165,7 @@ function viewLoaded(ctrl: StudyChapterEditFormCtrl, data: StudyChapterConfig): V { hook: bind( 'click', - _ => { + () => { if (confirm(noarg('clearVariations'))) ctrl.clearVariations(data.id); }, ctrl.redraw, @@ -236,7 +181,7 @@ function viewLoaded(ctrl: StudyChapterEditFormCtrl, data: StudyChapterConfig): V { hook: bind( 'click', - _ => { + () => { if (confirm(noarg('deleteThisChapter'))) ctrl.delete(data.id); }, ctrl.redraw, @@ -245,13 +190,7 @@ function viewLoaded(ctrl: StudyChapterEditFormCtrl, data: StudyChapterConfig): V }, noarg('deleteChapter'), ), - h( - 'button.button', - { - attrs: { type: 'submit' }, - }, - noarg('saveChapter'), - ), + h('button.button', { attrs: { type: 'submit' } }, noarg('saveChapter')), ]), ]; } diff --git a/ui/analyse/src/study/chapterNewForm.ts b/ui/analyse/src/study/chapterNewForm.ts index fe4f1cff87a41..5f568eabdc1a1 100644 --- a/ui/analyse/src/study/chapterNewForm.ts +++ b/ui/analyse/src/study/chapterNewForm.ts @@ -1,13 +1,12 @@ import { parseFen } from 'chessops/fen'; -import { defined, prop, Prop } from 'common'; +import { defined, prop, Prop, toggle } from 'common'; import * as licon from 'common/licon'; import { snabDialog } from 'common/dialog'; -import { bind, bindSubmit, onInsert } from 'common/snabbdom'; -import { StoredProp, storedStringProp } from 'common/storage'; +import { bind, bindSubmit, onInsert, looseH as h } from 'common/snabbdom'; +import { storedStringProp } from 'common/storage'; import * as xhr from 'common/xhr'; -import { h, VNode } from 'snabbdom'; +import { VNode } from 'snabbdom'; import AnalyseCtrl from '../ctrl'; -import { Redraw } from '../interfaces'; import { StudySocketSend } from '../socket'; import { spinnerVdom as spinner } from 'common/spinner'; import { option } from '../view/util'; @@ -25,115 +24,75 @@ export const modeChoices = [ export const fieldValue = (e: Event, id: string) => ((e.target as HTMLElement).querySelector('#chapter-' + id) as HTMLInputElement)?.value; -export interface StudyChapterNewFormCtrl { - root: AnalyseCtrl; - vm: { - variants: Variant[]; - open: boolean; - initial: Prop; - tab: StoredProp; - editor: LichessEditor | null; - editorFen: Prop; - isDefaultName: boolean; - }; - open(): void; - openInitial(): void; - close(): void; - toggle(): void; - submit(d: Omit): void; - chapters: Prop; - startTour(): void; - multiPgnMax: number; - redraw: Redraw; -} - -export function ctrl( - send: StudySocketSend, - chapters: Prop, - setTab: () => void, - root: AnalyseCtrl, -): StudyChapterNewFormCtrl { - const multiPgnMax = 32; +export class StudyChapterNewForm { + readonly multiPgnMax = 32; + variants: Variant[] = []; + isOpen = toggle(false); + initial = toggle(false); + tab = storedStringProp('analyse.study.form.tab', 'init'); + editor: LichessEditor | null = null; + editorFen: Prop = prop(null); + isDefaultName = toggle(true); - const vm = { - variants: [], - open: false, - initial: prop(false), - tab: storedStringProp('analyse.study.form.tab', 'init'), - editor: null, - editorFen: prop(null), - isDefaultName: true, - }; - - function loadVariants() { - if (!vm.variants.length) - xhrVariants().then(function (vs) { - vm.variants = vs; - root.redraw(); - }); + constructor( + private readonly send: StudySocketSend, + readonly chapters: Prop, + readonly setTab: () => void, + readonly root: AnalyseCtrl, + ) { + lichess.pubsub.on('analyse.close-all', () => this.isOpen(false)); } - const open = () => { + open = () => { lichess.pubsub.emit('analyse.close-all'); - vm.open = true; - loadVariants(); - vm.initial(false); - }; - const close = () => { - vm.open = false; + this.isOpen(true); + this.loadVariants(); + this.initial(false); }; - lichess.pubsub.on('analyse.close-all', close); + toggle = () => (this.isOpen() ? this.isOpen(false) : this.open()); - return { - vm, - open, - root, - openInitial() { - open(); - vm.initial(true); - }, - close, - toggle() { - if (vm.open) close(); - else open(); - }, - submit(d) { - const study = root.study!; - const dd = { - ...d, - sticky: study.vm.mode.sticky, - initial: vm.initial(), - }; - if (!dd.pgn) send('addChapter', dd); - else importPgn(study.data.id, dd); - close(); - setTab(); - }, - chapters, - startTour: () => - chapterTour(tab => { - vm.tab(tab); - root.redraw(); - }), - multiPgnMax, - redraw: root.redraw, + loadVariants = () => { + if (!this.variants.length) + xhrVariants().then(vs => { + this.variants = vs; + this.redraw(); + }); }; + + openInitial = () => { + this.open(); + this.initial(true); + }; + submit = (d: Omit) => { + const study = this.root.study!; + const dd = { ...d, sticky: study.vm.mode.sticky, initial: this.initial() }; + if (!dd.pgn) this.send('addChapter', dd); + else importPgn(study.data.id, dd); + this.isOpen(false); + this.setTab(); + }; + startTour = () => + chapterTour(tab => { + this.tab(tab); + this.redraw(); + }); + redraw = this.root.redraw; } -export function view(ctrl: StudyChapterNewFormCtrl): VNode { +export function view(ctrl: StudyChapterNewForm): VNode { const trans = ctrl.root.trans, study = ctrl.root.study!; - const activeTab = ctrl.vm.tab(); - const makeTab = function (key: string, name: string, title: string) { - return h( + const activeTab = ctrl.tab(); + const makeTab = (key: string, name: string, title: string) => + h( 'span.' + key, { class: { active: activeTab === key }, attrs: { role: 'tab', title, tabindex: '0' }, hook: onInsert(el => { const select = (e: Event) => { - ctrl.vm.tab(key); + ctrl.tab(key); ctrl.root.redraw(); e.preventDefault(); }; @@ -145,7 +104,6 @@ export function view(ctrl: StudyChapterNewFormCtrl): VNode { }, name, ); - }; const gameOrPgn = activeTab === 'game' || activeTab === 'pgn'; const currentChapter = study.data.chapter; const mode = currentChapter.practice @@ -160,57 +118,44 @@ export function view(ctrl: StudyChapterNewFormCtrl): VNode { return snabDialog({ class: 'chapter-new', onClose() { - ctrl.close(); + ctrl.isOpen(false); ctrl.redraw(); }, noClickAway: true, onInsert: dlg => dlg.show(), vnodes: [ - activeTab === 'edit' - ? null - : h('h2', [ - noarg('newChapter'), - h('i.help', { - attrs: { 'data-icon': licon.InfoCircle }, - hook: bind('click', ctrl.startTour), - }), - ]), + activeTab !== 'edit' && + h('h2', [ + noarg('newChapter'), + h('i.help', { attrs: { 'data-icon': licon.InfoCircle }, hook: bind('click', ctrl.startTour) }), + ]), h( 'form.form3', { - hook: bindSubmit(e => { - ctrl.submit({ - name: fieldValue(e, 'name'), - game: fieldValue(e, 'game'), - variant: fieldValue(e, 'variant') as VariantKey, - pgn: fieldValue(e, 'pgn'), - orientation: fieldValue(e, 'orientation') as Orientation, - mode: fieldValue(e, 'mode') as ChapterMode, - fen: fieldValue(e, 'fen') || (ctrl.vm.tab() === 'edit' ? ctrl.vm.editorFen() : null), - isDefaultName: ctrl.vm.isDefaultName, - }); - }, ctrl.redraw), + hook: bindSubmit( + e => + ctrl.submit({ + name: fieldValue(e, 'name'), + game: fieldValue(e, 'game'), + variant: fieldValue(e, 'variant') as VariantKey, + pgn: fieldValue(e, 'pgn'), + orientation: fieldValue(e, 'orientation') as Orientation, + mode: fieldValue(e, 'mode') as ChapterMode, + fen: fieldValue(e, 'fen') || (ctrl.tab() === 'edit' ? ctrl.editorFen() : null), + isDefaultName: ctrl.isDefaultName(), + }), + ctrl.redraw, + ), }, [ h('div.form-group', [ - h( - 'label.form-label', - { - attrs: { for: 'chapter-name' }, - }, - noarg('name'), - ), + h('label.form-label', { attrs: { for: 'chapter-name' } }, noarg('name')), h('input#chapter-name.form-control', { - attrs: { - minlength: 2, - maxlength: 80, - }, + attrs: { minlength: 2, maxlength: 80 }, hook: onInsert(el => { if (!el.value) { - el.value = trans('chapterX', ctrl.vm.initial() ? 1 : ctrl.chapters().length + 1); - el.onchange = function () { - ctrl.vm.isDefaultName = false; - }; + el.value = trans('chapterX', ctrl.initial() ? 1 : ctrl.chapters().length + 1); + el.onchange = () => ctrl.isDefaultName(false); el.select(); el.focus(); } @@ -224,163 +169,131 @@ export function view(ctrl: StudyChapterNewFormCtrl): VNode { makeTab('fen', 'FEN', noarg('loadAPositionFromFen')), makeTab('pgn', 'PGN', noarg('loadAGameFromPgn')), ]), - activeTab === 'edit' - ? h( - 'div.board-editor-wrap', - { - hook: { - insert(vnode) { - xhr.json('/editor.json').then(async data => { - data.el = vnode.elm; - data.fen = ctrl.root.node.fen; - data.embed = true; - data.options = { - inlineCastling: true, - orientation: currentChapter.setup.orientation, - onChange: ctrl.vm.editorFen, - }; - ctrl.vm.editor = await lichess.loadEsm('editor', { init: data }); - ctrl.vm.editorFen(ctrl.vm.editor.getFen()); - }); - }, - destroy: _ => { - ctrl.vm.editor = null; - }, + activeTab === 'edit' && + h( + 'div.board-editor-wrap', + { + hook: { + insert(vnode) { + xhr.json('/editor.json').then(async data => { + data.el = vnode.elm; + data.fen = ctrl.root.node.fen; + data.embed = true; + data.options = { + inlineCastling: true, + orientation: currentChapter.setup.orientation, + onChange: ctrl.editorFen, + coordinates: true, + }; + ctrl.editor = await lichess.asset.loadEsm('editor', { init: data }); + ctrl.editorFen(ctrl.editor.getFen()); + }); }, + destroy: () => (ctrl.editor = null), }, - [spinner()], - ) - : null, - activeTab === 'game' - ? h('div.form-group', [ - h( - 'label.form-label', - { - attrs: { for: 'chapter-game' }, - }, - trans('loadAGameFromXOrY', 'lichess.org', 'chessgames.com'), - ), - h('textarea#chapter-game.form-control', { - attrs: { - placeholder: noarg('urlOfTheGame'), - }, - hook: onInsert((el: HTMLTextAreaElement) => { - el.addEventListener('change', () => el.reportValidity()); - el.addEventListener('input', _ => { - const ok = el.value - .trim() - .split('\n') - .every(line => - line - .trim() - .match( - new RegExp( - `^((.*${location.host}/\\w{8,12}.*)|\\w{8}|\\w{12}|(.*chessgames\\.com/.*[?&]gid=\\d+.*)|)$`, - ), + }, + [spinner()], + ), + activeTab === 'game' && + h('div.form-group', [ + h( + 'label.form-label', + { attrs: { for: 'chapter-game' } }, + trans('loadAGameFromXOrY', 'lichess.org', 'chessgames.com'), + ), + h('textarea#chapter-game.form-control', { + attrs: { placeholder: noarg('urlOfTheGame') }, + hook: onInsert((el: HTMLTextAreaElement) => { + el.addEventListener('change', () => el.reportValidity()); + el.addEventListener('input', () => { + const ok = el.value + .trim() + .split('\n') + .every(line => + line + .trim() + .match( + new RegExp( + `^((.*${location.host}/\\w{8,12}.*)|\\w{8}|\\w{12}|(.*chessgames\\.com/.*[?&]gid=\\d+.*)|)$`, ), - ); - el.setCustomValidity(ok ? '' : 'Invalid game ID(s) or URL(s)'); - }); - }), + ), + ); + el.setCustomValidity(ok ? '' : 'Invalid game ID(s) or URL(s)'); + }); }), - ]) - : null, - activeTab === 'fen' - ? h('div.form-group', [ - h('input#chapter-fen.form-control', { - attrs: { - value: ctrl.root.node.fen, - placeholder: noarg('loadAPositionFromFen'), - }, - hook: onInsert((el: HTMLInputElement) => { - el.addEventListener('change', () => el.reportValidity()); - el.addEventListener('input', _ => - el.setCustomValidity(parseFen(el.value.trim()).isOk ? '' : 'Invalid FEN'), - ); - }), - }), - ]) - : null, - activeTab === 'pgn' - ? h('div.form-group', [ - h('textarea#chapter-pgn.form-control', { - attrs: { - placeholder: trans.pluralSame('pasteYourPgnTextHereUpToNbGames', ctrl.multiPgnMax), - }, + }), + ]), + activeTab === 'fen' && + h('div.form-group', [ + h('input#chapter-fen.form-control', { + attrs: { value: ctrl.root.node.fen, placeholder: noarg('loadAPositionFromFen') }, + hook: onInsert((el: HTMLInputElement) => { + el.addEventListener('change', () => el.reportValidity()); + el.addEventListener('input', _ => + el.setCustomValidity(parseFen(el.value.trim()).isOk ? '' : 'Invalid FEN'), + ); }), - h( - 'a.button.button-empty', - { - hook: bind('click', () => { + }), + ]), + activeTab === 'pgn' && + h('div.form-group', [ + h('textarea#chapter-pgn.form-control', { + attrs: { + placeholder: trans.pluralSame('pasteYourPgnTextHereUpToNbGames', ctrl.multiPgnMax), + }, + }), + h( + 'button.button.button-empty.import-from__chapter', + { + hook: bind( + 'click', + () => { xhr .text(`/study/${study.data.id}/${currentChapter.id}.pgn`) .then(pgnData => $('#chapter-pgn').val(pgnData)); - }), - }, - trans('importFromChapterX', study.currentChapter().name), - ), - window.FileReader - ? h('input#chapter-pgn-file.form-control', { - attrs: { - type: 'file', - accept: '.pgn', - }, - hook: bind('change', e => { - const file = (e.target as HTMLInputElement).files![0]; - if (!file) return; - const reader = new FileReader(); - reader.onload = function () { - (document.getElementById('chapter-pgn') as HTMLTextAreaElement).value = - reader.result as string; - }; - reader.readAsText(file); - }), - }) - : null, - ]) - : null, - h('div.form-split', [ - h('div.form-group.form-half', [ - h( - 'label.form-label', - { - attrs: { for: 'chapter-variant' }, + return false; + }, + undefined, + false, + ), }, - noarg('Variant'), + trans('importFromChapterX', study.currentChapter().name), ), + window.FileReader && + h('input#chapter-pgn-file.form-control', { + attrs: { type: 'file', accept: '.pgn' }, + hook: bind('change', e => { + const file = (e.target as HTMLInputElement).files![0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = function () { + (document.getElementById('chapter-pgn') as HTMLTextAreaElement).value = + reader.result as string; + }; + reader.readAsText(file); + }), + }), + ]), + h('div.form-split', [ + h('div.form-group.form-half', [ + h('label.form-label', { attrs: { for: 'chapter-variant' } }, noarg('Variant')), h( 'select#chapter-variant.form-control', - { - attrs: { disabled: gameOrPgn }, - }, + { attrs: { disabled: gameOrPgn } }, gameOrPgn - ? [ - h( - 'option', - { - attrs: { value: 'standard' }, - }, - noarg('automatic'), - ), - ] - : ctrl.vm.variants.map(v => option(v.key, currentChapter.setup.variant.key, v.name)), + ? [h('option', { attrs: { value: 'standard' } }, noarg('automatic'))] + : ctrl.variants.map(v => option(v.key, currentChapter.setup.variant.key, v.name)), ), ]), h('div.form-group.form-half', [ - h( - 'label.form-label', - { - attrs: { for: 'chapter-orientation' }, - }, - noarg('orientation'), - ), + h('label.form-label', { attrs: { for: 'chapter-orientation' } }, noarg('orientation')), h( 'select#chapter-orientation.form-control', { - hook: bind('change', e => { - ctrl.vm.editor && - ctrl.vm.editor.setOrientation((e.target as HTMLInputElement).value as Color); - }), + hook: bind( + 'change', + e => ctrl.editor?.setOrientation((e.target as HTMLInputElement).value as Color), + ), }, [...(activeTab === 'pgn' ? ['automatic'] : []), 'white', 'black'].map(c => option(c, currentChapter.setup.orientation, noarg(c)), @@ -389,13 +302,7 @@ export function view(ctrl: StudyChapterNewFormCtrl): VNode { ]), ]), h('div.form-group', [ - h( - 'label.form-label', - { - attrs: { for: 'chapter-mode' }, - }, - noarg('analysisMode'), - ), + h('label.form-label', { attrs: { for: 'chapter-mode' } }, noarg('analysisMode')), h( 'select#chapter-mode.form-control', modeChoices.map(c => option(c[0], mode, noarg(c[1]))), @@ -403,13 +310,7 @@ export function view(ctrl: StudyChapterNewFormCtrl): VNode { ]), h( 'div.form-actions.single', - h( - 'button.button', - { - attrs: { type: 'submit' }, - }, - noarg('createChapter'), - ), + h('button.button', { attrs: { type: 'submit' } }, noarg('createChapter')), ), ], ), diff --git a/ui/analyse/src/study/commentForm.ts b/ui/analyse/src/study/commentForm.ts index b990bd7dc5ebf..5929baeb93fa4 100644 --- a/ui/analyse/src/study/commentForm.ts +++ b/ui/analyse/src/study/commentForm.ts @@ -1,4 +1,4 @@ -import { prop, Prop } from 'common'; +import { prop } from 'common'; import { onInsert } from 'common/snabbdom'; import throttle from 'common/throttle'; import { h, VNode } from 'snabbdom'; @@ -11,74 +11,39 @@ interface Current { node: Tree.Node; } -export interface CommentForm { - root: AnalyseCtrl; - current: Prop; - opening: Prop; - submit(text: string): void; - start(chapterId: string, path: Tree.Path, node: Tree.Node): void; - onSetPath(chapterId: string, path: Tree.Path, node: Tree.Node): void; - redraw(): void; - delete(chapterId: string, path: Tree.Path, id: string): void; -} - -export function ctrl(root: AnalyseCtrl): CommentForm { - const current = prop(null), - opening = prop(false); +export class CommentForm { + current = prop(null); + opening = prop(false); + constructor(readonly root: AnalyseCtrl) {} - function submit(text: string): void { - if (!current()) return; - doSubmit(text); - } + submit = (text: string) => this.current() && this.doSubmit(text); - const doSubmit = throttle(500, (text: string) => { - const cur = current(); - if (cur) - root.study!.makeChange('setComment', { - ch: cur.chapterId, - path: cur.path, - text, - }); + doSubmit = throttle(500, (text: string) => { + const cur = this.current(); + if (cur) this.root.study!.makeChange('setComment', { ch: cur.chapterId, path: cur.path, text }); }); - function start(chapterId: string, path: Tree.Path, node: Tree.Node): void { - opening(true); - current({ - chapterId, - path, - node, - }); - root.userJump(path); - } + start = (chapterId: string, path: Tree.Path, node: Tree.Node): void => { + this.opening(true); + this.current({ chapterId, path, node }); + this.root.userJump(path); + }; - return { - root, - current, - opening, - submit, - start, - onSetPath(chapterId: string, path: Tree.Path, node: Tree.Node): void { - const cur = current(); - if (cur && (path !== cur.path || chapterId !== cur.chapterId || cur.node !== node)) { - cur.chapterId = chapterId; - cur.path = path; - cur.node = node; - } - }, - redraw: root.redraw, - delete(chapterId: string, path: Tree.Path, id: string) { - root.study!.makeChange('deleteComment', { - ch: chapterId, - path, - id, - }); - }, + onSetPath = (chapterId: string, path: Tree.Path, node: Tree.Node): void => { + const cur = this.current(); + if (cur && (path !== cur.path || chapterId !== cur.chapterId || cur.node !== node)) { + cur.chapterId = chapterId; + cur.path = path; + cur.node = node; + } + }; + delete = (chapterId: string, path: Tree.Path, id: string) => { + this.root.study!.makeChange('deleteComment', { ch: chapterId, path, id }); }; } -export function viewDisabled(root: AnalyseCtrl, why: string): VNode { - return h('div.study__comments', [currentComments(root, true), h('div.study__message', why)]); -} +export const viewDisabled = (root: AnalyseCtrl, why: string): VNode => + h('div.study__comments', [currentComments(root, true), h('div.study__message', why)]); export function view(root: AnalyseCtrl): VNode { const study = root.study!, @@ -105,9 +70,7 @@ export function view(root: AnalyseCtrl): VNode { return h( 'div.study__comments', - { - hook: onInsert(() => root.enableWiki(root.data.game.variant.key === 'standard')), - }, + { hook: onInsert(() => root.enableWiki(root.data.game.variant.key === 'standard')) }, [ currentComments(root, !study.members.canContribute()), h('form.form3', [ diff --git a/ui/analyse/src/study/description.ts b/ui/analyse/src/study/description.ts index 9d26f8383796b..34e4021ae9720 100644 --- a/ui/analyse/src/study/description.ts +++ b/ui/analyse/src/study/description.ts @@ -1,8 +1,8 @@ -import { h, VNode } from 'snabbdom'; +import { VNode } from 'snabbdom'; import * as licon from 'common/licon'; -import { bind, onInsert } from 'common/snabbdom'; +import { bind, onInsert, looseH as h } from 'common/snabbdom'; import { richHTML } from 'common/richText'; -import { StudyCtrl } from './interfaces'; +import StudyCtrl from './studyCtrl'; export type Save = (t: string) => void; @@ -30,54 +30,29 @@ export const descTitle = (chapter: boolean) => `Pinned ${chapter ? 'chapter' : ' export function view(study: StudyCtrl, chapter: boolean): VNode | undefined { const desc = chapter ? study.chapterDesc : study.studyDesc, - contrib = study.members.canContribute() && !study.gamebookPlay(); + contrib = study.members.canContribute() && !study.gamebookPlay; if (desc.edit) return edit(desc, chapter ? study.data.chapter.id : study.data.id, chapter); const isEmpty = desc.text === '-'; if (!desc.text || (isEmpty && !contrib)) return; return h(`div.study-desc${chapter ? '.chapter-desc' : ''}${isEmpty ? '.empty' : ''}`, [ - contrib && !isEmpty - ? h('div.contrib', [ - h('span', descTitle(chapter)), - isEmpty - ? null - : h('a', { - attrs: { - 'data-icon': licon.Pencil, - title: 'Edit', - }, - hook: bind( - 'click', - _ => { - desc.edit = true; - }, - desc.redraw, - ), - }), + contrib && + !isEmpty && + h('div.contrib', [ + h('span', descTitle(chapter)), + !isEmpty && h('a', { - attrs: { - 'data-icon': licon.Trash, - title: 'Delete', - }, - hook: bind('click', () => { - if (confirm('Delete permanent description?')) desc.save(''); - }), + attrs: { 'data-icon': licon.Pencil, title: 'Edit' }, + hook: bind('click', () => (desc.edit = true), desc.redraw), }), - ]) - : null, + h('a', { + attrs: { 'data-icon': licon.Trash, title: 'Delete' }, + hook: bind('click', () => { + if (confirm('Delete permanent description?')) desc.save(''); + }), + }), + ]), isEmpty - ? h( - 'a.text.button', - { - hook: bind( - 'click', - _ => { - desc.edit = true; - }, - desc.redraw, - ), - }, - descTitle(chapter), - ) + ? h('a.text.button', { hook: bind('click', () => (desc.edit = true), desc.redraw) }, descTitle(chapter)) : h('div.text', { hook: richHTML(desc.text) }), ]); } @@ -87,17 +62,8 @@ const edit = (ctrl: DescriptionCtrl, id: string, chapter: boolean): VNode => h('div.title', [ descTitle(chapter), h('button.button.button-empty.button-green', { - attrs: { - 'data-icon': licon.Checkmark, - title: 'Save and close', - }, - hook: bind( - 'click', - () => { - ctrl.edit = false; - }, - ctrl.redraw, - ), + attrs: { 'data-icon': licon.Checkmark, title: 'Save and close' }, + hook: bind('click', () => (ctrl.edit = false), ctrl.redraw), }), ]), h('form.form3', [ @@ -105,9 +71,7 @@ const edit = (ctrl: DescriptionCtrl, id: string, chapter: boolean): VNode => h('textarea#form-control.desc-text.' + id, { hook: onInsert(el => { el.value = ctrl.text === '-' ? '' : ctrl.text || ''; - el.oninput = () => { - ctrl.save(el.value.trim()); - }; + el.oninput = () => ctrl.save(el.value.trim()); el.focus(); }), }), diff --git a/ui/analyse/src/study/gamebook/gamebookButtons.ts b/ui/analyse/src/study/gamebook/gamebookButtons.ts index 1ea9a67f421e5..648abbd9a854e 100644 --- a/ui/analyse/src/study/gamebook/gamebookButtons.ts +++ b/ui/analyse/src/study/gamebook/gamebookButtons.ts @@ -1,43 +1,35 @@ -import { h, VNode } from 'snabbdom'; +import { VNode } from 'snabbdom'; import * as licon from 'common/licon'; -import { bind, dataIcon } from 'common/snabbdom'; +import { bind, dataIcon, looseH as h } from 'common/snabbdom'; import AnalyseCtrl from '../../ctrl'; -import { StudyCtrl } from '../interfaces'; +import StudyCtrl from '../studyCtrl'; export function playButtons(root: AnalyseCtrl): VNode | undefined { const study = root.study!, - ctrl = study.gamebookPlay(); + ctrl = study.gamebookPlay; if (!ctrl) return; const state = ctrl.state, fb = state.feedback, myTurn = fb === 'play'; return h('div.gamebook-buttons', [ - root.path - ? h( - 'button.fbt.text.back', - { - attrs: { - 'data-icon': licon.LessThan, - type: 'button', - }, - hook: bind('click', () => root.userJump(''), ctrl.redraw), - }, - root.trans.noarg('back'), - ) - : null, - myTurn - ? h( - 'button.fbt.text.solution', - { - attrs: { - 'data-icon': licon.PlayTriangle, - type: 'button', - }, - hook: bind('click', ctrl.solution, ctrl.redraw), - }, - root.trans.noarg('viewTheSolution'), - ) - : undefined, + root.path && + h( + 'button.fbt.text.back', + { + attrs: { 'data-icon': licon.LessThan, type: 'button' }, + hook: bind('click', () => root.userJump(''), ctrl.redraw), + }, + root.trans.noarg('back'), + ), + myTurn && + h( + 'button.fbt.text.solution', + { + attrs: { 'data-icon': licon.PlayTriangle, type: 'button' }, + hook: bind('click', ctrl.solution, ctrl.redraw), + }, + root.trans.noarg('viewTheSolution'), + ), overrideButton(study), ]); } @@ -50,15 +42,10 @@ export function overrideButton(study: StudyCtrl): VNode | undefined { 'button.fbt.text.preview', { class: { active: o === 'play' }, - attrs: { - 'data-icon': licon.Eye, - type: 'button', - }, + attrs: { 'data-icon': licon.Eye, type: 'button' }, hook: bind( 'click', - () => { - study.setGamebookOverride(o === 'play' ? undefined : 'play'); - }, + () => study.setGamebookOverride(o === 'play' ? undefined : 'play'), study.redraw, ), }, @@ -66,7 +53,7 @@ export function overrideButton(study: StudyCtrl): VNode | undefined { ); else { const isAnalyse = o === 'analyse', - ctrl = study.gamebookPlay(); + ctrl = study.gamebookPlay; if (isAnalyse || (ctrl && ctrl.state.feedback === 'end')) return h( 'a.fbt.text.preview', @@ -75,9 +62,7 @@ export function overrideButton(study: StudyCtrl): VNode | undefined { attrs: dataIcon(licon.Microscope), hook: bind( 'click', - () => { - study.setGamebookOverride(isAnalyse ? undefined : 'analyse'); - }, + () => study.setGamebookOverride(isAnalyse ? undefined : 'analyse'), study.redraw, ), }, diff --git a/ui/analyse/src/study/gamebook/gamebookEdit.ts b/ui/analyse/src/study/gamebook/gamebookEdit.ts index f0b857ed1e3c8..8b27906f1dd84 100644 --- a/ui/analyse/src/study/gamebook/gamebookEdit.ts +++ b/ui/analyse/src/study/gamebook/gamebookEdit.ts @@ -41,25 +41,18 @@ export function render(ctrl: AnalyseCtrl): VNode { if (!ctrl.path) { if (isMyMove) content = [ - h( - 'div.legend.todo.clickable', - { - hook: commentHook, - class: { done: isCommented }, - }, - [iconTag(licon.BubbleSpeech), h('p', 'Help the player find the initial move, with a comment.')], - ), + h('div.legend.todo.clickable', { hook: commentHook, class: { done: isCommented } }, [ + iconTag(licon.BubbleSpeech), + h('p', 'Help the player find the initial move, with a comment.'), + ]), renderHint(ctrl), ]; else content = [ - h( - 'div.legend.clickable', - { - hook: commentHook, - }, - [iconTag(licon.BubbleSpeech), h('p', 'Introduce the gamebook with a comment')], - ), + h('div.legend.clickable', { hook: commentHook }, [ + iconTag(licon.BubbleSpeech), + h('p', 'Introduce the gamebook with a comment'), + ]), h('div.legend.todo', { class: { done: !!ctrl.node.children[0] } }, [ iconTag(licon.PlayTriangle), h('p', "Put the opponent's first move on the board."), @@ -68,66 +61,41 @@ export function render(ctrl: AnalyseCtrl): VNode { } else if (ctrl.onMainline) { if (isMyMove) content = [ - h( - 'div.legend.todo.clickable', - { - hook: commentHook, - class: { done: isCommented }, - }, - [ - iconTag(licon.BubbleSpeech), - h('p', 'Explain the opponent move, and help the player find the next move, with a comment.'), - ], - ), + h('div.legend.todo.clickable', { hook: commentHook, class: { done: isCommented } }, [ + iconTag(licon.BubbleSpeech), + h('p', 'Explain the opponent move, and help the player find the next move, with a comment.'), + ]), renderHint(ctrl), ]; else content = [ - h( - 'div.legend.clickable', - { - hook: commentHook, - }, - [ - iconTag(licon.BubbleSpeech), - h( - 'p', - "You may reflect on the player's correct move, with a comment; or leave empty to jump immediately to the next move.", - ), - ], - ), + h('div.legend.clickable', { hook: commentHook }, [ + iconTag(licon.BubbleSpeech), + h( + 'p', + "You may reflect on the player's correct move, with a comment; or leave empty to jump immediately to the next move.", + ), + ]), hasVariation ? null - : h( - 'div.legend.clickable', - { - hook: bind('click', () => control.prev(ctrl), ctrl.redraw), - }, - [ - iconTag(licon.PlayTriangle), - h('p', 'Add variation moves to explain why specific other moves are wrong.'), - ], - ), + : h('div.legend.clickable', { hook: bind('click', () => control.prev(ctrl), ctrl.redraw) }, [ + iconTag(licon.PlayTriangle), + h('p', 'Add variation moves to explain why specific other moves are wrong.'), + ]), renderDeviation(ctrl), ]; } else content = [ - h( - 'div.legend.todo.clickable', - { - hook: commentHook, - class: { done: isCommented }, - }, - [iconTag(licon.BubbleSpeech), h('p', 'Explain why this move is wrong in a comment')], - ), + h('div.legend.todo.clickable', { hook: commentHook, class: { done: isCommented } }, [ + iconTag(licon.BubbleSpeech), + h('p', 'Explain why this move is wrong in a comment'), + ]), h('div.legend', [h('p', 'Or promote it as the mainline if it is the right move.')]), ]; return h( 'div.gamebook-edit', - { - hook: { insert: _ => lichess.loadCssPath('analyse.gamebook.edit') }, - }, + { hook: { insert: () => lichess.asset.loadCssPath('analyse.gamebook.edit') } }, content, ); } diff --git a/ui/analyse/src/study/gamebook/gamebookPlayView.ts b/ui/analyse/src/study/gamebook/gamebookPlayView.ts index aebb1d6afbab0..5ceccc73ff60f 100644 --- a/ui/analyse/src/study/gamebook/gamebookPlayView.ts +++ b/ui/analyse/src/study/gamebook/gamebookPlayView.ts @@ -1,60 +1,38 @@ -import { h, VNode } from 'snabbdom'; +import { VNode } from 'snabbdom'; import GamebookPlayCtrl from './gamebookPlayCtrl'; import * as licon from 'common/licon'; -import { iconTag, bind, dataIcon } from 'common/snabbdom'; +import { iconTag, bind, dataIcon, looseH as h } from 'common/snabbdom'; import { richHTML } from 'common/richText'; // eslint-disable-next-line no-duplicate-imports import { State } from './gamebookPlayCtrl'; export function render(ctrl: GamebookPlayCtrl): VNode { const state = ctrl.state; - return h( - 'div.gamebook', - { - hook: { insert: _ => lichess.loadCssPath('analyse.gamebook.play') }, - }, - [ - state.comment || state.feedback == 'play' || state.feedback == 'end' - ? h( - 'div.comment', - { - class: { hinted: state.showHint }, - }, - [ - state.comment - ? h('div.content', { hook: richHTML(state.comment) }) - : h( - 'div.content', - state.feedback == 'play' - ? ctrl.trans('whatWouldYouPlay') - : state.feedback == 'end' - ? ctrl.trans('youCompletedThisLesson') - : undefined, - ), - hintZone(ctrl), - ], - ) - : undefined, - h('div.floor', [ - renderFeedback(ctrl, state), - h('img.mascot', { - attrs: { - width: 120, - height: 120, - src: lichess.assetUrl('images/mascot/octopus.svg'), - }, - }), + return h('div.gamebook', { hook: { insert: _ => lichess.asset.loadCssPath('analyse.gamebook.play') } }, [ + (state.comment || state.feedback == 'play' || state.feedback == 'end') && + h('div.comment', { class: { hinted: state.showHint } }, [ + state.comment + ? h('div.content', { hook: richHTML(state.comment) }) + : h( + 'div.content', + state.feedback == 'play' + ? ctrl.trans('whatWouldYouPlay') + : state.feedback == 'end' && ctrl.trans('youCompletedThisLesson'), + ), + hintZone(ctrl), ]), - ], - ); + h('div.floor', [ + renderFeedback(ctrl, state), + h('img.mascot', { + attrs: { width: 120, height: 120, src: lichess.asset.url('images/mascot/octopus.svg') }, + }), + ]), + ]); } function hintZone(ctrl: GamebookPlayCtrl) { const state = ctrl.state, - buttonData = () => ({ - attrs: { type: 'button' }, - hook: bind('click', ctrl.hint, ctrl.redraw), - }); + buttonData = () => ({ attrs: { type: 'button' }, hook: bind('click', ctrl.hint, ctrl.redraw) }); if (state.showHint) return h('button', buttonData(), [h('div.hint', { hook: richHTML(state.hint!) })]); if (state.hint) return h('button.hint', buttonData(), ctrl.trans.noarg('getAHint')); return undefined; @@ -66,24 +44,14 @@ function renderFeedback(ctrl: GamebookPlayCtrl, state: State) { if (fb === 'bad') return h( 'button.feedback.act.bad' + (state.comment ? '.com' : ''), - { - attrs: { type: 'button' }, - hook: bind('click', ctrl.retry), - }, + { attrs: { type: 'button' }, hook: bind('click', ctrl.retry) }, [iconTag(licon.Reload), h('span', ctrl.trans.noarg('retry'))], ); if (fb === 'good' && state.comment) - return h( - 'button.feedback.act.good.com', - { - attrs: { type: 'button' }, - hook: bind('click', ctrl.next), - }, - [ - h('span.text', { attrs: dataIcon(licon.PlayTriangle) }, ctrl.trans.noarg('next')), - h('kbd', ''), - ], - ); + return h('button.feedback.act.good.com', { attrs: { type: 'button' }, hook: bind('click', ctrl.next) }, [ + h('span.text', { attrs: dataIcon(licon.PlayTriangle) }, ctrl.trans.noarg('next')), + h('kbd', ''), + ]); if (fb === 'end') return renderEnd(ctrl); return h( 'div.feedback.info.' + fb + (state.init ? '.init' : ''), @@ -108,26 +76,19 @@ function renderFeedback(ctrl: GamebookPlayCtrl, state: State) { function renderEnd(ctrl: GamebookPlayCtrl) { const study = ctrl.root.study!; return h('div.feedback.end', [ - study.nextChapter() - ? h( - 'button.next.text', - { - attrs: { - 'data-icon': licon.PlayTriangle, - type: 'button', - }, - hook: bind('click', study.goToNextChapter), - }, - study.trans.noarg('nextChapter'), - ) - : undefined, + study.nextChapter() && + h( + 'button.next.text', + { + attrs: { 'data-icon': licon.PlayTriangle, type: 'button' }, + hook: bind('click', study.goToNextChapter), + }, + study.trans.noarg('nextChapter'), + ), h( 'button.retry', { - attrs: { - 'data-icon': licon.Reload, - type: 'button', - }, + attrs: { 'data-icon': licon.Reload, type: 'button' }, hook: bind('click', () => ctrl.root.userJump(''), ctrl.redraw), }, study.trans.noarg('playAgain'), @@ -135,10 +96,7 @@ function renderEnd(ctrl: GamebookPlayCtrl) { h( 'button.analyse', { - attrs: { - 'data-icon': licon.Microscope, - type: 'button', - }, + attrs: { 'data-icon': licon.Microscope, type: 'button' }, hook: bind('click', () => study.setGamebookOverride('analyse'), ctrl.redraw), }, study.trans.noarg('analysis'), diff --git a/ui/analyse/src/study/interfaces.ts b/ui/analyse/src/study/interfaces.ts index 3805af067ab93..75fa34ea6ef27 100644 --- a/ui/analyse/src/study/interfaces.ts +++ b/ui/analyse/src/study/interfaces.ts @@ -1,83 +1,8 @@ import * as cg from 'chessground/types'; -import { Config as CgConfig } from 'chessground/config'; import { Prop } from 'common'; -import { NotifCtrl } from './notif'; -import { AnalyseData, Redraw } from '../interfaces'; -import { StudyPracticeCtrl } from './practice/interfaces'; -import StudyChaptersCtrl from './studyChapters'; -import { DescriptionCtrl } from './description'; -import GamebookPlayCtrl from './gamebook/gamebookPlayCtrl'; +import { AnalyseData } from '../interfaces'; import { GamebookOverride } from './gamebook/interfaces'; -import { GlyphCtrl } from './studyGlyph'; -import { CommentForm } from './commentForm'; -import TopicsCtrl from './topics'; -import RelayCtrl from './relay/relayCtrl'; -import ServerEval from './serverEval'; -import { MultiBoardCtrl } from './multiBoard'; -import { StudyShareCtrl } from './studyShare'; -import { TagsCtrl } from './studyTags'; -import { StudyFormCtrl } from './studyForm'; -import { StudyMemberCtrl } from './studyMembers'; import { Opening } from '../explorer/interfaces'; -import { StudySocketSendParams } from '../socket'; -import { SearchCtrl } from './studySearch'; - -export interface StudyCtrl { - data: StudyData; - currentChapter(): StudyChapterMeta; - socketHandler(t: string, d: any): boolean; - vm: StudyVm; - setTab(tab: Tab): void; - relay?: RelayCtrl; - multiBoard: MultiBoardCtrl; - form: StudyFormCtrl; - members: StudyMemberCtrl; - chapters: StudyChaptersCtrl; - notif: NotifCtrl; - commentForm: CommentForm; - glyphForm: GlyphCtrl; - topics: TopicsCtrl; - serverEval: ServerEval; - share: StudyShareCtrl; - tags: TagsCtrl; - studyDesc: DescriptionCtrl; - chapterDesc: DescriptionCtrl; - search: SearchCtrl; - toggleLike(): void; - position(): Position; - isChapterOwner(): boolean; - canJumpTo(path: Tree.Path): boolean; - onJump(): void; - onFlip(): void; - withPosition(obj: T): T & { ch: string; path: string }; - setPath(path: Tree.Path, node: Tree.Node): void; - deleteNode(path: Tree.Path): void; - promote(path: Tree.Path, toMainline: boolean): void; - forceVariation(path: Tree.Path, force: boolean): void; - setChapter(id: string, force?: boolean): void; - toggleSticky(): void; - toggleWrite(): void; - isWriting(): boolean; - makeChange(...args: StudySocketSendParams): boolean; - startTour(): void; - userJump(path: Tree.Path): void; - currentNode(): Tree.Node; - practice?: StudyPracticeCtrl; - gamebookPlay(): GamebookPlayCtrl | undefined; - prevChapter(): StudyChapterMeta | undefined; - nextChapter(): StudyChapterMeta | undefined; - hasNextChapter(): boolean; - goToPrevChapter(): void; - goToNextChapter(): void; - mutateCgConfig(config: Required>): void; - isUpdatedRecently(): boolean; - setGamebookOverride(o: GamebookOverride): void; - explorerGame(gameId: string, insert: boolean): void; - onPremoveSet(): void; - looksNew(): boolean; - redraw: Redraw; - trans: Trans; -} export type Tab = 'intro' | 'members' | 'chapters'; export type ToolTab = 'tags' | 'comments' | 'glyphs' | 'serverEval' | 'share' | 'multiBoard'; @@ -181,6 +106,11 @@ export interface StudyChapter { features: StudyChapterFeatures; description?: string; relay?: StudyChapterRelay; + serverEval?: StudyChapterServerEval; +} + +export interface StudyChapterServerEval { + path: string; } export interface StudyChapterRelay { @@ -192,7 +122,7 @@ export interface StudyChapterRelay { interface StudyChapterSetup { gameId?: string; variant: { - key: string; + key: VariantKey; name: string; }; orientation: Color; diff --git a/ui/analyse/src/study/inviteForm.ts b/ui/analyse/src/study/inviteForm.ts index 50a8015a63ba7..91afe0436a373 100644 --- a/ui/analyse/src/study/inviteForm.ts +++ b/ui/analyse/src/study/inviteForm.ts @@ -75,12 +75,9 @@ export function view(ctrl: ReturnType): VNode { h('div.input-wrapper', [ // because typeahead messes up with snabbdom h('input', { - attrs: { - placeholder: ctrl.trans.noarg('searchByUsername'), - spellcheck: 'false', - }, + attrs: { placeholder: ctrl.trans.noarg('searchByUsername'), spellcheck: 'false' }, hook: onInsert(input => - lichess + lichess.asset .userComplete({ input, tag: 'span', @@ -100,12 +97,7 @@ export function view(ctrl: ReturnType): VNode { candidates.map(function (username: string) { return h( 'span.button.button-metal', - { - key: username, - hook: bind('click', _ => { - ctrl.invite(username); - }), - }, + { key: username, hook: bind('click', () => ctrl.invite(username)) }, username, ); }), diff --git a/ui/analyse/src/study/multiBoard.ts b/ui/analyse/src/study/multiBoard.ts index 14322a2ec0d2a..c7f4f6c78d0c1 100644 --- a/ui/analyse/src/study/multiBoard.ts +++ b/ui/analyse/src/study/multiBoard.ts @@ -1,13 +1,17 @@ import debounce from 'common/debounce'; import * as licon from 'common/licon'; import { renderClock, fenColor } from 'common/mini-board'; -import { bind, MaybeVNodes } from 'common/snabbdom'; import { spinnerVdom as spinner } from 'common/spinner'; -import { h, VNode } from 'snabbdom'; +import { VNode } from 'snabbdom'; +import { bind, MaybeVNodes, looseH as h } from 'common/snabbdom'; import { multiBoard as xhrLoad } from './studyXhr'; import { opposite as CgOpposite } from 'chessground/util'; import { opposite as oppositeColor } from 'chessops/util'; -import { StudyCtrl, ChapterPreview, ChapterPreviewPlayer, Position, StudyChapterMeta } from './interfaces'; +import { ChapterPreview, ChapterPreviewPlayer, Position, StudyChapterMeta } from './interfaces'; +import StudyCtrl from './studyCtrl'; +import { EvalHitMulti } from '../interfaces'; +import { WinningChances } from 'ceval/src/types'; +import { povChances } from 'ceval/src/winningChances'; export class MultiBoardCtrl { loading = false; @@ -15,14 +19,18 @@ export class MultiBoardCtrl { pager?: Paginator; playing = false; + private winningChances: Map = new Map(); + constructor( readonly studyId: string, readonly redraw: () => void, readonly trans: Trans, + private readonly send: SocketSend, + private readonly variant: () => VariantKey, ) {} addNode = (pos: Position, node: Tree.Node) => { - const cp = this.pager && this.pager.currentPageResults.find(cp => cp.id == pos.chapterId); + const cp = this.pager?.currentPageResults.find(cp => cp.id == pos.chapterId); if (cp?.playing) { cp.fen = node.fen; cp.lastMove = node.uci; @@ -31,6 +39,7 @@ export class MultiBoardCtrl { // at this point `(cp: ChapterPreview).lastMoveAt` becomes outdated but should be ok since not in use anymore // to mitigate bad usage, setting it as `undefined` cp.lastMoveAt = undefined; + this.requestCloudEvals(); this.redraw(); } }; @@ -48,20 +57,29 @@ export class MultiBoardCtrl { if (changed) this.redraw(); }; - reload = (onInsert?: boolean) => { + reload = async (onInsert?: boolean) => { if (this.pager && !onInsert) { this.loading = true; this.redraw(); } - xhrLoad(this.studyId, this.page, this.playing).then(p => { - this.pager = p; - if (p.nbPages < this.page) { - if (!p.nbPages) this.page = 1; - else this.setPage(p.nbPages); - } - this.loading = false; - this.redraw(); - }); + this.pager = await xhrLoad(this.studyId, this.page, this.playing); + if (this.pager.nbPages < this.page) { + if (!this.pager.nbPages) this.page = 1; + else this.setPage(this.pager.nbPages); + } + this.loading = false; + this.redraw(); + + this.requestCloudEvals(); + }; + + private requestCloudEvals = () => { + if (this.pager?.currentPageResults.length) { + this.send('evalGetMulti', { + fens: this.pager?.currentPageResults.map(c => c.fen), + ...(this.variant() != 'standard' ? { variant: this.variant() } : {}), + }); + } }; reloadEventually = debounce(this.reload, 1000); @@ -82,6 +100,14 @@ export class MultiBoardCtrl { this.playing = v; this.reload(); }; + + onCloudEval = (d: EvalHitMulti) => { + this.winningChances.set(d.fen, povChances('white', d)); + this.redraw(); + }; + + getWinningChances = (preview: ChapterPreview): WinningChances | undefined => + this.winningChances.get(preview.fen); } export function view(ctrl: MultiBoardCtrl, study: StudyCtrl): VNode | undefined { @@ -112,7 +138,7 @@ function renderPager(pager: Paginator, study: StudyCtrl): MaybeV const ctrl = study.multiBoard; return [ h('div.top', [renderPagerNav(pager, ctrl), renderPlayingToggle(ctrl)]), - h('div.now-playing', pager.currentPageResults.map(makePreview(study))), + h('div.now-playing', pager.currentPageResults.map(makePreview(study, ctrl.getWinningChances))), ]; } @@ -120,9 +146,7 @@ function renderPlayingToggle(ctrl: MultiBoardCtrl): VNode { return h('label.playing', [ h('input', { attrs: { type: 'checkbox', checked: ctrl.playing }, - hook: bind('change', e => { - ctrl.setPlaying((e.target as HTMLInputElement).checked); - }), + hook: bind('change', e => ctrl.setPlaying((e.target as HTMLInputElement).checked)), }), ctrl.trans.noarg('playing'), ]); @@ -139,10 +163,7 @@ function renderPagerNav(pager: Paginator, ctrl: MultiBoardCtrl): pagerButton(ctrl.trans.noarg('next'), licon.JumpNext, ctrl.nextPage, page < pager.nbPages, ctrl), pagerButton(ctrl.trans.noarg('last'), licon.JumpLast, ctrl.lastPage, page < pager.nbPages, ctrl), h('button.fbt', { - attrs: { - 'data-icon': licon.Search, - title: 'Search', - }, + attrs: { 'data-icon': licon.Search, title: 'Search' }, hook: bind('click', () => lichess.pubsub.emit('study.search.open')), }), ]); @@ -156,25 +177,20 @@ function pagerButton( ctrl: MultiBoardCtrl, ): VNode { return h('button.fbt', { - attrs: { - 'data-icon': icon, - disabled: !enable, - title: text, - }, + attrs: { 'data-icon': icon, disabled: !enable, title: text }, hook: bind('mousedown', click, ctrl.redraw), }); } -const makePreview = (study: StudyCtrl) => (preview: ChapterPreview) => +type GetWinningChances = (preview: ChapterPreview) => WinningChances | undefined; + +const makePreview = (study: StudyCtrl, winningChances: GetWinningChances) => (preview: ChapterPreview) => h( `a.mini-game.mini-game-${preview.id}.mini-game--init.is2d`, { - attrs: { - 'data-state': `${preview.fen},${preview.orientation},${preview.lastMove}`, - }, + attrs: { 'data-state': `${preview.fen},${preview.orientation},${preview.lastMove}` }, class: { - active: - !study.multiBoard.loading && study.vm.chapterId == preview.id && !study.relay?.tourShow.active, + active: !study.multiBoard.loading && study.vm.chapterId == preview.id && !study.relay?.tourShow(), }, hook: { insert(vnode) { @@ -205,11 +221,25 @@ const makePreview = (study: StudyCtrl) => (preview: ChapterPreview) => }, [ boardPlayer(preview, CgOpposite(preview.orientation)), - h('span.cg-wrap'), + h('span.cg-gauge', [h('span.mini-game__board', h('span.cg-wrap')), evalGauge(preview, winningChances)]), boardPlayer(preview, preview.orientation), ], ); +const evalGauge = (chap: ChapterPreview, winningChances: GetWinningChances): VNode => + h( + 'span.mini-game__gauge', + h('span.mini-game__gauge__black', { + hook: { + postpatch(old, vnode) { + const chances = winningChances(chap) ?? old.data?.chances; + vnode.data!.chances = chances; + (vnode.elm as HTMLElement).style.height = `${((1 - (chances || 0)) / 2) * 100}%`; + }, + }, + }), + ); + const userName = (u: ChapterPreviewPlayer) => u.title ? [h('span.utitle', u.title), ' ' + u.name] : [u.name]; diff --git a/ui/analyse/src/study/nextChapter.ts b/ui/analyse/src/study/nextChapter.ts index 6b9b4806db0b1..8979f85f042a2 100644 --- a/ui/analyse/src/study/nextChapter.ts +++ b/ui/analyse/src/study/nextChapter.ts @@ -9,14 +9,9 @@ export const renderNextChapter = (ctrl: AnalyseCtrl) => ? h( 'button.next.text', { - attrs: { - 'data-icon': licon.PlayTriangle, - type: 'button', - }, + attrs: { 'data-icon': licon.PlayTriangle, type: 'button' }, hook: bind('click', ctrl.study.goToNextChapter), - class: { - highlighted: !!ctrl.outcome() || ctrl.node == treeOps.last(ctrl.mainline), - }, + class: { highlighted: !!ctrl.outcome() || ctrl.node == treeOps.last(ctrl.mainline) }, }, ctrl.trans.noarg('nextChapter'), ) diff --git a/ui/analyse/src/study/notif.ts b/ui/analyse/src/study/notif.ts index 75ca5ba1ace2d..7a5a7ebe9bcdb 100644 --- a/ui/analyse/src/study/notif.ts +++ b/ui/analyse/src/study/notif.ts @@ -3,31 +3,24 @@ import { h, VNode } from 'snabbdom'; interface Notif { duration: number; text: string; - class?: string; } -export interface NotifCtrl { - set(n: Notif): void; - get(): Notif | undefined; -} - -export function ctrl(redraw: () => void) { - let current: Notif | undefined; - let timeout: number; - return { - set(n: Notif) { - clearTimeout(timeout); - current = n; - timeout = setTimeout(function () { - current = undefined; - redraw(); - }, n.duration); - }, - get: () => current, +export class NotifCtrl { + current: Notif | undefined; + timeout: number; + constructor(readonly redraw: () => void) {} + set = (n: Notif) => { + clearTimeout(this.timeout); + this.current = n; + this.timeout = setTimeout(() => { + this.current = undefined; + this.redraw(); + }, n.duration); }; + get = () => this.current; } export function view(ctrl: NotifCtrl): VNode | undefined { const c = ctrl.get(); - return c ? h('div.notif.' + c.class, c.text) : undefined; + return c ? h('div.notif', c.text) : undefined; } diff --git a/ui/analyse/src/study/playerBars.ts b/ui/analyse/src/study/playerBars.ts index 6f68a29dacd47..19882713e7938 100644 --- a/ui/analyse/src/study/playerBars.ts +++ b/ui/analyse/src/study/playerBars.ts @@ -14,10 +14,7 @@ export default function (ctrl: AnalyseCtrl): VNode[] | undefined { const study = ctrl.study; if (!study) return; const tags = study.data.chapter.tags, - playerNames = { - white: findTag(tags, 'white')!, - black: findTag(tags, 'black')!, - }; + playerNames = { white: findTag(tags, 'white')!, black: findTag(tags, 'black')! }; const clocks = renderClocks(ctrl), ticking = !isFinished(study.data.chapter) && ctrl.turnColor(), @@ -50,22 +47,16 @@ function renderPlayer( const title = findTag(tags, `${color}title`), elo = hideRatings ? undefined : findTag(tags, `${color}elo`), result = resultOf(tags, color === 'white'); - return h( - `div.study__player.study__player-${top ? 'top' : 'bot'}`, - { - class: { ticking }, - }, - [ - h('div.left', [ - result && h('span.result', result), - h('span.info', [ - title && h('span.utitle', title == 'BOT' ? { attrs: { 'data-bot': true } } : {}, title + ' '), - h('span.name', playerNames[color]), - elo && h('span.elo', elo), - ]), + return h(`div.study__player.study__player-${top ? 'top' : 'bot'}`, { class: { ticking } }, [ + h('div.left', [ + result && h('span.result', result), + h('span.info', [ + title && h('span.utitle', title == 'BOT' ? { attrs: { 'data-bot': true } } : {}, title + ' '), + h('span.name', playerNames[color]), + elo && h('span.elo', elo), ]), - materialDiffs[top ? 0 : 1], - clocks?.[color === 'white' ? 0 : 1], - ], - ); + ]), + materialDiffs[top ? 0 : 1], + clocks?.[color === 'white' ? 0 : 1], + ]); } diff --git a/ui/analyse/src/study/practice/studyPracticeCtrl.ts b/ui/analyse/src/study/practice/studyPracticeCtrl.ts index 87c79c2cb30e3..9880867dc5cad 100644 --- a/ui/analyse/src/study/practice/studyPracticeCtrl.ts +++ b/ui/analyse/src/study/practice/studyPracticeCtrl.ts @@ -1,102 +1,99 @@ import * as xhr from '../studyXhr'; -import { prop } from 'common'; +import { Prop, prop } from 'common'; import { storedBooleanProp } from 'common/storage'; import makeSuccess from './studyPracticeSuccess'; import { readOnlyProp } from '../../util'; -import { StudyPracticeData, Goal, StudyPracticeCtrl } from './interfaces'; +import { StudyPracticeData, Goal } from './interfaces'; import { StudyData } from '../interfaces'; import AnalyseCtrl from '../../ctrl'; -export default function ( - root: AnalyseCtrl, - studyData: StudyData, - data: StudyPracticeData, -): StudyPracticeCtrl { - const goal = prop(root.data.practiceGoal!), - nbMoves = prop(0), - // null = ongoing, true = win, false = fail - success = prop(null), - autoNext = storedBooleanProp('analyse.practice-auto-next', true); +export default class StudyPractice { + goal: Prop; + nbMoves = prop(0); + // null = ongoing, true = win, false = fail + success = prop(null); + autoNext = storedBooleanProp('analyse.practice-auto-next', true); - lichess.sound.load('practiceSuccess', `${lichess.sound.baseUrl}/other/energy3`); - lichess.sound.load('practiceFailure', `${lichess.sound.baseUrl}/other/failure2`); - - function onLoad() { - root.showAutoShapes = readOnlyProp(true); - root.variationArrowsProp = readOnlyProp(false); - root.showGauge = readOnlyProp(true); - root.showComputer = readOnlyProp(true); - goal(root.data.practiceGoal!); - nbMoves(0); - success(null); - const chapter = studyData.chapter; - history.replaceState(null, chapter.name, data.url + '/' + chapter.id); + constructor( + readonly root: AnalyseCtrl, + readonly studyData: StudyData, + readonly data: StudyPracticeData, + ) { + this.goal = prop(root.data.practiceGoal!); + lichess.sound.load('practiceSuccess', `${lichess.sound.baseUrl}/other/energy3`); + lichess.sound.load('practiceFailure', `${lichess.sound.baseUrl}/other/failure2`); + this.onLoad(); } - onLoad(); - function computeNbMoves(): number { - let plies = root.node.ply - root.tree.root.ply; - if (root.bottomColor() !== root.data.player.color) plies--; + onLoad = () => { + this.root.showAutoShapes = readOnlyProp(true); + this.root.variationArrowsProp = readOnlyProp(false); + this.root.showGauge = readOnlyProp(true); + this.root.showComputer = readOnlyProp(true); + this.goal(this.root.data.practiceGoal!); + this.nbMoves(0); + this.success(null); + const chapter = this.studyData.chapter; + history.replaceState(null, chapter.name, this.data.url + '/' + chapter.id); + }; + + computeNbMoves = (): number => { + let plies = this.root.node.ply - this.root.tree.root.ply; + if (this.root.bottomColor() !== this.root.data.player.color) plies--; return Math.ceil(plies / 2); - } + }; - function checkSuccess(): void { - const gamebook = root.study!.gamebookPlay(); + checkSuccess = (): void => { + const gamebook = this.root.study?.gamebookPlay; if (gamebook) { - if (gamebook.state.feedback === 'end') onVictory(); + if (gamebook.state.feedback === 'end') this.onVictory(); return; } - if (!root.study!.data.chapter.practice) { - return saveNbMoves(); + if (!this.root.study?.data.chapter.practice) { + return this.saveNbMoves(); } - if (success() !== null) return; - nbMoves(computeNbMoves()); - const res = success(makeSuccess(root, goal(), nbMoves())); - if (res) onVictory(); - else if (res === false) onFailure(); - } + if (this.success() !== null) return; + this.nbMoves(this.computeNbMoves()); + const res = this.success(makeSuccess(this.root, this.goal(), this.nbMoves())); + if (res) this.onVictory(); + else if (res === false) this.onFailure(); + }; - function onVictory(): void { - saveNbMoves(); + onVictory = (): void => { + this.saveNbMoves(); lichess.sound.play('practiceSuccess'); - if (studyData.chapter.practice && autoNext()) setTimeout(root.study!.goToNextChapter, 1000); - } + if (this.studyData.chapter.practice && this.autoNext()) + setTimeout(this.root.study!.goToNextChapter, 1000); + }; - function saveNbMoves(): void { - const chapterId = root.study!.currentChapter().id, - former = data.completion[chapterId]; - if (typeof former === 'undefined' || nbMoves() < former) { - data.completion[chapterId] = nbMoves(); - xhr.practiceComplete(chapterId, nbMoves()); + saveNbMoves = (): void => { + const chapterId = this.root.study!.currentChapter().id, + former = this.data.completion[chapterId]; + if (typeof former === 'undefined' || this.nbMoves() < former) { + this.data.completion[chapterId] = this.nbMoves(); + xhr.practiceComplete(chapterId, this.nbMoves()); } - } + }; - function onFailure(): void { - root.node.fail = true; + onFailure = (): void => { + this.root.node.fail = true; lichess.sound.play('practiceFailure'); - } + }; - return { - onLoad, - onJump() { - // reset failure state if no failed move found in mainline history - if (success() === false && !root.nodeList.find(n => !!n.fail)) success(null); - checkSuccess(); - }, - onCeval: checkSuccess, - data, - goal, - success, - nbMoves, - reset() { - root.tree.root.children = []; - root.userJump(''); - root.practice!.reset(); - onLoad(); - root.practice!.resume(); - }, - isWhite: root.bottomIsWhite, - analysisUrl: () => `/analysis/standard/${root.node.fen.replace(/ /g, '_')}?color=${root.bottomColor()}`, - autoNext, + onJump = () => { + // reset failure state if no failed move found in mainline history + if (this.success() === false && !this.root.nodeList.find(n => !!n.fail)) this.success(null); + this.checkSuccess(); + }; + onCeval = this.checkSuccess; + reset = () => { + this.root.tree.root.children = []; + this.root.userJump(''); + this.root.practice!.reset(); + this.onLoad(); + this.root.practice!.resume(); }; + isWhite = this.root.bottomIsWhite; + analysisUrl = () => + `/analysis/standard/${this.root.node.fen.replace(/ /g, '_')}?color=${this.root.bottomColor()}`; } diff --git a/ui/analyse/src/study/practice/studyPracticeView.ts b/ui/analyse/src/study/practice/studyPracticeView.ts index e9c89421de438..fa6e4e79fcbf2 100644 --- a/ui/analyse/src/study/practice/studyPracticeView.ts +++ b/ui/analyse/src/study/practice/studyPracticeView.ts @@ -6,31 +6,19 @@ import { toggle } from 'common/controls'; import { richHTML } from 'common/richText'; import { option, plural } from '../../view/util'; import { view as descView } from '../description'; -import { StudyCtrl } from '../interfaces'; import { StudyPracticeCtrl, StudyPracticeData } from './interfaces'; +import StudyCtrl from '../studyCtrl'; -function selector(data: StudyPracticeData) { - return h( +const selector = (data: StudyPracticeData) => + h( 'select.selector', - { - hook: bind('change', e => { - location.href = '/practice/' + (e.target as HTMLInputElement).value; - }), - }, + { hook: bind('change', e => (location.href = '/practice/' + (e.target as HTMLInputElement).value)) }, [ - h( - 'option', - { - attrs: { disabled: true, selected: true }, - }, - 'Practice list', - ), + h('option', { attrs: { disabled: true, selected: true } }, 'Practice list'), ...data.structure.map(section => h( 'optgroup', - { - attrs: { label: section.name }, - }, + { attrs: { label: section.name } }, section.studies.map(study => option(section.id + '/' + study.slug + '/' + study.id, '', study.name), ), @@ -38,7 +26,6 @@ function selector(data: StudyPracticeData) { ), ], ); -} function renderGoal(practice: StudyPracticeCtrl, inMoves: number) { const goal = practice.goal(); @@ -64,7 +51,7 @@ function renderGoal(practice: StudyPracticeCtrl, inMoves: number) { export function underboard(ctrl: StudyCtrl): MaybeVNodes { if (ctrl.vm.loading) return [h('div.feedback', spinner())]; const p = ctrl.practice!, - gb = ctrl.gamebookPlay(), + gb = ctrl.gamebookPlay, pinned = ctrl.data.chapter.description; if (gb) return pinned ? [h('div.feedback.ongoing', [h('div.comment', { hook: richHTML(pinned) })])] : []; else if (!ctrl.data.chapter.practice) return [descView(ctrl, true)]; @@ -74,24 +61,17 @@ export function underboard(ctrl: StudyCtrl): MaybeVNodes { h( 'a.feedback.win', ctrl.nextChapter() - ? { - hook: bind('click', ctrl.goToNextChapter), - } - : { - attrs: { href: '/practice' }, - }, + ? { hook: bind('click', ctrl.goToNextChapter) } + : { attrs: { href: '/practice' } }, [h('span', 'Success!'), ctrl.nextChapter() ? 'Go to next exercise' : 'Back to practice menu'], ), ]; case false: return [ - h( - 'a.feedback.fail', - { - hook: bind('click', p.reset, ctrl.redraw), - }, - [h('span', [renderGoal(p, p.goal().moves!)]), h('strong', 'Click to retry')], - ), + h('a.feedback.fail', { hook: bind('click', p.reset, ctrl.redraw) }, [ + h('span', [renderGoal(p, p.goal().moves!)]), + h('strong', 'Click to retry'), + ]), ]; default: return [ @@ -144,10 +124,7 @@ export function side(ctrl: StudyCtrl): VNode { 'a.ps__chapter', { key: chapter.id, - attrs: { - href: data.url + '/' + chapter.id, - 'data-id': chapter.id, - }, + attrs: { href: data.url + '/' + chapter.id, 'data-id': chapter.id }, class: { active, loading }, }, [ @@ -165,13 +142,7 @@ export function side(ctrl: StudyCtrl): VNode { .reduce((a, b) => a.concat(b), []), ), h('div.finally', [ - h('a.back', { - attrs: { - 'data-icon': licon.LessThan, - href: '/practice', - title: 'More practice', - }, - }), + h('a.back', { attrs: { 'data-icon': licon.LessThan, href: '/practice', title: 'More practice' } }), thunk('select.selector', selector, [data]), ]), ]); diff --git a/ui/analyse/src/study/relay/interfaces.ts b/ui/analyse/src/study/relay/interfaces.ts index 1c64a09953aca..f350abce8f38c 100644 --- a/ui/analyse/src/study/relay/interfaces.ts +++ b/ui/analyse/src/study/relay/interfaces.ts @@ -38,11 +38,6 @@ export interface RelaySync { delay?: number; } -export interface RelayTourShow { - active: boolean; - disable(): void; -} - export interface LogEvent { id: string; moves: number; diff --git a/ui/analyse/src/study/relay/relayCtrl.ts b/ui/analyse/src/study/relay/relayCtrl.ts index 29b175fa31927..8a647864fce0c 100644 --- a/ui/analyse/src/study/relay/relayCtrl.ts +++ b/ui/analyse/src/study/relay/relayCtrl.ts @@ -1,15 +1,15 @@ -import { RelayData, LogEvent, RelayTourShow, RelaySync, RelayRound } from './interfaces'; +import { RelayData, LogEvent, RelaySync, RelayRound } from './interfaces'; import { RelayTab, StudyChapter, StudyChapterRelay } from '../interfaces'; import { isFinished } from '../studyChapters'; import { StudyMemberCtrl } from '../studyMembers'; import { AnalyseSocketSend } from '../../socket'; -import { prop } from 'common'; +import { Toggle, prop, toggle } from 'common'; export default class RelayCtrl { log: LogEvent[] = []; cooldown = false; clockInterval?: number; - tourShow: RelayTourShow; + tourShow: Toggle; tab = prop('overview'); constructor( @@ -21,12 +21,7 @@ export default class RelayCtrl { chapter: StudyChapter, ) { this.applyChapterRelay(chapter, chapter.relay); - this.tourShow = { - active: (location.pathname.match(/\//g) || []).length < 5, - disable: () => { - this.tourShow.active = false; - }, - }; + this.tourShow = toggle((location.pathname.match(/\//g) || []).length < 5); } setSync = (v: boolean) => { @@ -86,7 +81,7 @@ export default class RelayCtrl { }, 4500); this.redraw(); if (event.error) { - lichess.sound.play('error'); + if (this.data.sync.log.slice(-2).every(e => e.error)) lichess.sound.play('error'); console.warn(`relay synchronisation error: ${event.error}`); } }, diff --git a/ui/analyse/src/study/relay/relayManagerView.ts b/ui/analyse/src/study/relay/relayManagerView.ts index a929ed84455e4..9a3901b0e99d9 100644 --- a/ui/analyse/src/study/relay/relayManagerView.ts +++ b/ui/analyse/src/study/relay/relayManagerView.ts @@ -7,27 +7,16 @@ import { memoize } from 'common'; export default function (ctrl: RelayCtrl): VNode | undefined { return ctrl.members.canContribute() - ? h( - 'div.relay-admin', - { - hook: onInsert(_ => lichess.loadCssPath('analyse.relay-admin')), - }, - [ - h('h2', [ - h('span.text', { attrs: dataIcon(licon.RadioTower) }, 'Broadcast manager'), - h('a', { - attrs: { - href: `/broadcast/round/${ctrl.id}/edit`, - 'data-icon': licon.Gear, - }, - }), - ]), - ctrl.data.sync?.url || ctrl.data.sync?.ids - ? (ctrl.data.sync.ongoing ? stateOn : stateOff)(ctrl) - : null, - renderLog(ctrl), - ], - ) + ? h('div.relay-admin', { hook: onInsert(_ => lichess.asset.loadCssPath('analyse.relay-admin')) }, [ + h('h2', [ + h('span.text', { attrs: dataIcon(licon.RadioTower) }, 'Broadcast manager'), + h('a', { attrs: { href: `/broadcast/round/${ctrl.id}/edit`, 'data-icon': licon.Gear } }), + ]), + ctrl.data.sync?.url || ctrl.data.sync?.ids + ? (ctrl.data.sync.ongoing ? stateOn : stateOff)(ctrl) + : null, + renderLog(ctrl), + ]) : undefined; } @@ -44,25 +33,10 @@ function renderLog(ctrl: RelayCtrl) { .map(e => { const err = e.error && - h( - 'a', - url - ? { - attrs: { - href: url, - target: '_blank', - rel: 'noopener nofollow', - }, - } - : {}, - e.error, - ); + h('a', url ? { attrs: { href: url, target: '_blank', rel: 'noopener nofollow' } } : {}, e.error); return h( 'div' + (err ? '.err' : ''), - { - key: e.at, - attrs: dataIcon(err ? licon.CautionCircle : licon.Checkmark), - }, + { key: e.at, attrs: dataIcon(err ? licon.CautionCircle : licon.Checkmark) }, [h('div', [...(err ? [err] : logSuccess(e)), h('time', dateFormatter()(new Date(e.at)))])], ); }); @@ -76,10 +50,7 @@ function stateOn(ctrl: RelayCtrl) { ids = sync?.ids; return h( 'div.state.on.clickable', - { - hook: bind('click', _ => ctrl.setSync(false)), - attrs: dataIcon(licon.ChasingArrows), - }, + { hook: bind('click', _ => ctrl.setSync(false)), attrs: dataIcon(licon.ChasingArrows) }, [ h( 'div', @@ -90,8 +61,8 @@ function stateOn(ctrl: RelayCtrl) { url.replace(/https?:\/\//, ''), ] : ids - ? ['Connected to', h('br'), ids.length, ' game(s)'] - : [], + ? ['Connected to', h('br'), ids.length, ' game(s)'] + : [], ), ], ); @@ -100,10 +71,7 @@ function stateOn(ctrl: RelayCtrl) { const stateOff = (ctrl: RelayCtrl) => h( 'div.state.off.clickable', - { - hook: bind('click', _ => ctrl.setSync(true)), - attrs: dataIcon(licon.PlayTriangle), - }, + { hook: bind('click', _ => ctrl.setSync(true)), attrs: dataIcon(licon.PlayTriangle) }, [h('div.fat', 'Click to connect')], ); diff --git a/ui/analyse/src/study/relay/relayTourView.ts b/ui/analyse/src/study/relay/relayTourView.ts index 78e27560a5e80..1d538ff1f32a6 100644 --- a/ui/analyse/src/study/relay/relayTourView.ts +++ b/ui/analyse/src/study/relay/relayTourView.ts @@ -1,18 +1,19 @@ import AnalyseCtrl from '../../ctrl'; import RelayCtrl from './relayCtrl'; import * as licon from 'common/licon'; -import { bind, dataIcon, onInsert } from 'common/snabbdom'; -import { h, VNode } from 'snabbdom'; +import { bind, dataIcon, onInsert, looseH as h } from 'common/snabbdom'; +import { VNode } from 'snabbdom'; import { innerHTML } from 'common/richText'; import { RelayRound } from './interfaces'; -import { RelayTab, StudyCtrl } from '../interfaces'; +import { RelayTab } from '../interfaces'; import { view as multiBoardView } from '../multiBoard'; import { scrollToInnerSelector } from 'common'; +import StudyCtrl from '../studyCtrl'; export default function (ctrl: AnalyseCtrl): VNode | undefined { const study = ctrl.study; const relay = study?.relay; - if (!study || !relay?.tourShow.active) return undefined; + if (!study || !relay?.tourShow()) return undefined; const makeTab = (key: RelayTab, name: string) => h( @@ -62,9 +63,9 @@ const leaderboard = (relay: RelayCtrl): VNode[] => { players.map(player => h('tr', [ h('th', player.name), - withRating ? h('td', player.rating) : undefined, - h('td', player.score), - h('td', player.played), + withRating ? h('td', `${player.rating}`) : undefined, + h('td', `${player.score}`), + h('td', `${player.played}`), ]), ), ), @@ -91,24 +92,19 @@ const overview = (relay: RelayCtrl, study: StudyCtrl) => { ' ', round.ongoing ? study.trans.noarg('playingRightNow') - : round.startsAt - ? h( + : !!round.startsAt && + h( 'time.timeago', - { - hook: onInsert(el => el.setAttribute('datetime', '' + round.startsAt)), - }, + { hook: onInsert(el => el.setAttribute('datetime', '' + round.startsAt)) }, lichess.timeago(round.startsAt), - ) - : null, + ), ], ), relay.data.tour.markup - ? h('div', { - hook: innerHTML(relay.data.tour.markup, () => relay.data.tour.markup!), - }) + ? h('div', { hook: innerHTML(relay.data.tour.markup, () => relay.data.tour.markup!) }) : h('div', relay.data.tour.description), ]), - study.looksNew() ? null : multiBoardView(study.multiBoard, study), + !study.looksNew() && multiBoardView(study.multiBoard, study), ]; }; @@ -123,16 +119,7 @@ const schedule = (relay: RelayCtrl): VNode[] => [ 'tbody', relay.data.rounds.map(round => h('tr', [ - h( - 'th', - h( - 'a.link', - { - attrs: { href: relay.roundPath(round) }, - }, - round.name, - ), - ), + h('th', h('a.link', { attrs: { href: relay.roundPath(round) } }, round.name)), h('td', round.startsAt ? lichess.dateFormat()(new Date(round.startsAt)) : undefined), h( 'td', @@ -149,45 +136,22 @@ const schedule = (relay: RelayCtrl): VNode[] => [ const roundStateIcon = (round: RelayRound) => round.ongoing ? h('ongoing', { attrs: { ...dataIcon(licon.DiscBig), title: 'Ongoing' } }) - : round.finished - ? h('finished', { attrs: { ...dataIcon(licon.Checkmark), title: 'Finished' } }) - : null; + : round.finished && h('finished', { attrs: { ...dataIcon(licon.Checkmark), title: 'Finished' } }); export function rounds(ctrl: StudyCtrl): VNode { const canContribute = ctrl.members.canContribute(); const relay = ctrl.relay!; return h( 'div.study__relay__rounds', - { - hook: onInsert(el => scrollToInnerSelector(el, '.active')), - }, + { hook: onInsert(el => scrollToInnerSelector(el, '.active')) }, relay.data.rounds .map(round => - h( - 'div', - { - key: round.id, - class: { active: ctrl.data.id == round.id }, - }, - [ - h( - 'a.link', - { - attrs: { href: relay.roundPath(round) }, - }, - round.name, - ), - roundStateIcon(round), - canContribute - ? h('a.act', { - attrs: { - ...dataIcon(licon.Gear), - href: `/broadcast/round/${round.id}/edit`, - }, - }) - : null, - ], - ), + h('div', { key: round.id, class: { active: ctrl.data.id == round.id } }, [ + h('a.link', { attrs: { href: relay.roundPath(round) } }, round.name), + roundStateIcon(round), + canContribute && + h('a.act', { attrs: { ...dataIcon(licon.Gear), href: `/broadcast/round/${round.id}/edit` } }), + ]), ) .concat( canContribute @@ -197,10 +161,7 @@ export function rounds(ctrl: StudyCtrl): VNode { h( 'a.text', { - attrs: { - href: `/broadcast/${relay.data.tour.id}/new`, - 'data-icon': licon.PlusButton, - }, + attrs: { href: `/broadcast/${relay.data.tour.id}/new`, 'data-icon': licon.PlusButton }, }, ctrl.trans.noarg('addRound'), ), diff --git a/ui/analyse/src/study/serverEval.ts b/ui/analyse/src/study/serverEval.ts index 994d78b408b4d..79ee60ad6f9f3 100644 --- a/ui/analyse/src/study/serverEval.ts +++ b/ui/analyse/src/study/serverEval.ts @@ -34,10 +34,12 @@ export function view(ctrl: ServerEval): VNode { { hook: onInsert(el => { lichess.requestIdleCallback(async () => { - (await lichess.loadEsm('chart.game')).acpl( + const serverEvalPath = ctrl.root.study?.data.chapter?.serverEval?.path; + const analysedMainline = ctrl.root.mainline.slice(0, (serverEvalPath?.length || 999) / 2 + 1); + (await lichess.asset.loadEsm('chart.game')).acpl( el as HTMLCanvasElement, ctrl.root.data, - ctrl.root.mainline, + analysedMainline, ctrl.root.trans, ); }, 800); @@ -67,10 +69,7 @@ function requestButton(ctrl: ServerEval) { h( 'a.button.text', { - attrs: { - 'data-icon': licon.BarChart, - disabled: root.mainline.length < 5, - }, + attrs: { 'data-icon': licon.BarChart, disabled: root.mainline.length < 5 }, hook: bind('click', ctrl.request, root.redraw), }, noarg('requestAComputerAnalysis'), diff --git a/ui/analyse/src/study/studyChapters.ts b/ui/analyse/src/study/studyChapters.ts index 367bb9b8b211a..e3750760c49c3 100644 --- a/ui/analyse/src/study/studyChapters.ts +++ b/ui/analyse/src/study/studyChapters.ts @@ -1,23 +1,17 @@ import { prop, Prop, scrollToInnerSelector } from 'common'; import * as licon from 'common/licon'; -import { bind, dataIcon, iconTag } from 'common/snabbdom'; -import { h, VNode } from 'snabbdom'; +import { bind, dataIcon, iconTag, looseH as h } from 'common/snabbdom'; +import { VNode } from 'snabbdom'; import AnalyseCtrl from '../ctrl'; import { StudySocketSend } from '../socket'; -import { ctrl as chapterEditForm, StudyChapterEditFormCtrl } from './chapterEditForm'; -import { ctrl as chapterNewForm, StudyChapterNewFormCtrl } from './chapterNewForm'; -import { - LocalPaths, - StudyChapter, - StudyChapterConfig, - StudyChapterMeta, - StudyCtrl, - TagArray, -} from './interfaces'; +import { StudyChapterEditForm } from './chapterEditForm'; +import { StudyChapterNewForm } from './chapterNewForm'; +import { LocalPaths, StudyChapter, StudyChapterConfig, StudyChapterMeta, TagArray } from './interfaces'; +import StudyCtrl from './studyCtrl'; export default class StudyChaptersCtrl { - newForm: StudyChapterNewFormCtrl; - editForm: StudyChapterEditFormCtrl; + newForm: StudyChapterNewForm; + editForm: StudyChapterEditForm; list: Prop; localPaths: LocalPaths = {}; @@ -29,15 +23,15 @@ export default class StudyChaptersCtrl { root: AnalyseCtrl, ) { this.list = prop(initChapters); - this.newForm = chapterNewForm(send, this.list, setTab, root); - this.editForm = chapterEditForm(send, chapterConfig, root.trans, root.redraw); + this.newForm = new StudyChapterNewForm(send, this.list, setTab, root); + this.editForm = new StudyChapterEditForm(send, chapterConfig, root.trans, root.redraw); } get = (id: string) => this.list().find(c => c.id === id); sort = (ids: string[]) => this.send('sortChapters', ids); firstChapterId = () => this.list()[0].id; toggleNewForm = () => { - if (this.newForm.vm.open || this.list().length < 64) this.newForm.toggle(); + if (this.newForm.isOpen() || this.list().length < 64) this.newForm.toggle(); else alert('You have reached the limit of 64 chapters per study. Please create a new study.'); }; } @@ -96,7 +90,7 @@ export function view(ctrl: StudyCtrl): VNode { }); }; if (window.Sortable) makeSortable(); - else lichess.loadIife('javascripts/vendor/Sortable.min.js').then(makeSortable); + else lichess.asset.loadIife('javascripts/vendor/Sortable.min.js').then(makeSortable); } } @@ -134,7 +128,7 @@ export function view(ctrl: StudyCtrl): VNode { .map((chapter, i) => { const editing = ctrl.chapters.editForm.isEditing(chapter.id), loading = ctrl.vm.loading && chapter.id === ctrl.vm.nextChapterId, - active = !ctrl.vm.loading && current && !ctrl.relay?.tourShow.active && current.id === chapter.id; + active = !ctrl.vm.loading && current && !ctrl.relay?.tourShow() && current.id === chapter.id; return h( 'div', { @@ -145,24 +139,19 @@ export function view(ctrl: StudyCtrl): VNode { [ h('span', loading ? h('span.ddloader') : ['' + (i + 1)]), h('h3', chapter.name), - chapter.ongoing - ? h('ongoing', { attrs: { ...dataIcon(licon.DiscBig), title: 'Ongoing' } }) - : null, - !chapter.ongoing && chapter.res ? h('res', chapter.res) : null, - canContribute ? h('i.act', { attrs: { ...dataIcon(licon.Gear), title: 'Edit chapter' } }) : null, + chapter.ongoing && h('ongoing', { attrs: { ...dataIcon(licon.DiscBig), title: 'Ongoing' } }), + !chapter.ongoing && chapter.res && h('res', chapter.res), + canContribute && h('i.act', { attrs: { ...dataIcon(licon.Gear), title: 'Edit chapter' } }), ], ); }) .concat( ctrl.members.canContribute() ? [ - h( - 'div.add', - { - hook: bind('click', ctrl.chapters.toggleNewForm, ctrl.redraw), - }, - [h('span', iconTag(licon.PlusButton)), h('h3', ctrl.trans.noarg('addNewChapter'))], - ), + h('div.add', { hook: bind('click', ctrl.chapters.toggleNewForm, ctrl.redraw) }, [ + h('span', iconTag(licon.PlusButton)), + h('h3', ctrl.trans.noarg('addNewChapter')), + ]), ] : [], ), diff --git a/ui/analyse/src/study/studyComments.ts b/ui/analyse/src/study/studyComments.ts index 5b722799caa08..d0c37481d1866 100644 --- a/ui/analyse/src/study/studyComments.ts +++ b/ui/analyse/src/study/studyComments.ts @@ -4,7 +4,7 @@ import { bind } from 'common/snabbdom'; import { richHTML } from 'common/richText'; import AnalyseCtrl from '../ctrl'; import { nodeFullName } from '../view/util'; -import { StudyCtrl } from './interfaces'; +import StudyCtrl from './studyCtrl'; export type AuthorObj = { id: string; @@ -15,13 +15,7 @@ export type Author = AuthorObj | string; function authorDom(author: Author): string | VNode { if (!author) return 'Unknown'; if (typeof author === 'string') return author; - return h( - 'span.user-link.ulpt', - { - attrs: { 'data-href': '/@/' + author.id }, - }, - author.name, - ); + return h('span.user-link.ulpt', { attrs: { 'data-href': '/@/' + author.id } }, author.name); } export const isAuthorObj = (author: Author): author is AuthorObj => typeof author === 'object'; @@ -45,13 +39,10 @@ export function currentComments(ctrl: AnalyseCtrl, includingMine: boolean): VNod return h('div.study__comment.' + comment.id, [ study.members.canContribute() && study.vm.mode.write ? h('a.edit', { - attrs: { - 'data-icon': licon.Trash, - title: 'Delete', - }, + attrs: { 'data-icon': licon.Trash, title: 'Delete' }, hook: bind( 'click', - _ => { + () => { if (confirm('Delete ' + authorText(by) + "'s comment?")) study.commentForm.delete(chapter.id, ctrl.path, comment.id); }, diff --git a/ui/analyse/src/study/studyCtrl.ts b/ui/analyse/src/study/studyCtrl.ts index 6b9afbbe04585..45d0c66f445ce 100644 --- a/ui/analyse/src/study/studyCtrl.ts +++ b/ui/analyse/src/study/studyCtrl.ts @@ -4,22 +4,21 @@ import { prop, defined } from 'common'; import throttle, { throttlePromiseDelay } from 'common/throttle'; import debounce from 'common/debounce'; import AnalyseCtrl from '../ctrl'; -import { ctrl as memberCtrl } from './studyMembers'; -import practiceCtrl from './practice/studyPracticeCtrl'; +import { StudyMemberCtrl } from './studyMembers'; +import StudyPractice from './practice/studyPracticeCtrl'; import { StudyPracticeData, StudyPracticeCtrl } from './practice/interfaces'; -import { ctrl as commentFormCtrl, CommentForm } from './commentForm'; -import { ctrl as glyphFormCtrl, GlyphCtrl } from './studyGlyph'; -import { ctrl as studyFormCtrl } from './studyForm'; +import { CommentForm } from './commentForm'; +import { GlyphForm } from './studyGlyph'; +import { StudyForm } from './studyForm'; import TopicsCtrl from './topics'; -import { ctrl as notifCtrl } from './notif'; -import { ctrl as shareCtrl } from './studyShare'; -import { ctrl as tagsCtrl } from './studyTags'; +import { NotifCtrl } from './notif'; +import { StudyShare } from './studyShare'; +import { TagsForm } from './studyTags'; import ServerEval from './serverEval'; import * as tours from './studyTour'; import * as xhr from './studyXhr'; import { path as treePath } from 'tree'; import { - StudyCtrl, StudyVm, Tab, ToolTab, @@ -46,6 +45,7 @@ import { storedMap, storedBooleanProp } from 'common/storage'; import { opposite } from 'chessops/util'; import StudyChaptersCtrl from './studyChapters'; import { SearchCtrl } from './studySearch'; +import { GamebookOverride } from './gamebook/interfaces'; interface Handlers { path(d: WithWhoAndPos): void; @@ -78,24 +78,40 @@ interface Handlers { // data.position.path represents the server state // ctrl.path is the client state -export default function ( - data: StudyData, - ctrl: AnalyseCtrl, - tagTypes: TagTypes, - practiceData?: StudyPracticeData, - relayData?: RelayData, -): StudyCtrl { - const send = ctrl.socket.send; - const redraw = ctrl.redraw; - - const relayRecProp = storedBooleanProp('analyse.relay.rec', true); - const nonRelayRecMapProp = storedMap('study.rec', 100, () => true); - const chapterFlipMapProp = storedMap('chapter.flip', 400, () => false); - - const vm: StudyVm = (() => { +export default class StudyCtrl { + relayRecProp = storedBooleanProp('analyse.relay.rec', true); + nonRelayRecMapProp = storedMap('study.rec', 100, () => true); + chapterFlipMapProp = storedMap('chapter.flip', 400, () => false); + vm: StudyVm; + notif: NotifCtrl; + members: StudyMemberCtrl; + chapters: StudyChaptersCtrl; + relay?: RelayCtrl; + multiBoard: MultiBoardCtrl; + form: StudyForm; + commentForm: CommentForm; + glyphForm: GlyphForm; + topics: TopicsCtrl; + serverEval: ServerEval; + share: StudyShare; + tags: TagsForm; + studyDesc: DescriptionCtrl; + chapterDesc: DescriptionCtrl; + search: SearchCtrl; + practice?: StudyPracticeCtrl; + gamebookPlay?: GamebookPlayCtrl; + + constructor( + readonly data: StudyData, + readonly ctrl: AnalyseCtrl, + tagTypes: TagTypes, + practiceData?: StudyPracticeData, + private readonly relayData?: RelayData, + ) { + this.notif = new NotifCtrl(ctrl.redraw); const isManualChapter = data.chapter.id !== data.position.chapterId; const sticked = data.features.sticky && !ctrl.initialPath && !isManualChapter && !practiceData; - return { + this.vm = { loading: false, tab: prop(relayData || data.chapters.length > 1 ? 'chapters' : 'members'), toolTab: prop('tags'), @@ -103,7 +119,7 @@ export default function ( // path is at ctrl.path mode: { sticky: sticked, - write: relayData ? relayRecProp() : nonRelayRecMapProp(data.id), + write: relayData ? this.relayRecProp() : this.nonRelayRecMapProp(data.id), }, // how many events missed because sync=off behind: 0, @@ -111,661 +127,621 @@ export default function ( updatedAt: Date.now() - data.secondsSinceUpdate * 1000, gamebookOverride: undefined, }; - })(); - const notif = notifCtrl(redraw); + this.members = new StudyMemberCtrl({ + initDict: data.members, + myId: practiceData ? undefined : ctrl.opts.userId, + ownerId: data.ownerId, + send: this.send, + tab: this.vm.tab, + startTour: this.startTour, + notif: this.notif, + onBecomingContributor: () => (this.vm.mode.write = !relayData || this.relayRecProp()), + admin: data.admin, + redraw: ctrl.redraw, + trans: ctrl.trans, + }); + this.chapters = new StudyChaptersCtrl( + data.chapters, + this.send, + () => this.setTab('chapters'), + chapterId => xhr.chapterConfig(data.id, chapterId), + this.ctrl, + ); + this.relay = + relayData && + new RelayCtrl(this.data.id, relayData, this.send, this.redraw, this.members, this.data.chapter); + this.multiBoard = new MultiBoardCtrl( + this.data.id, + this.redraw, + this.ctrl.trans, + this.ctrl.socket.send, + () => this.data.chapter.setup.variant.key, + ); + this.form = new StudyForm( + (d, isNew) => { + this.send('editStudy', d); + if ( + isNew && + data.chapter.setup.variant.key === 'standard' && + ctrl.mainline.length === 1 && + !data.chapter.setup.fromFen && + !this.relay + ) + this.chapters.newForm.openInitial(); + }, + () => data, + ctrl.trans, + this.redraw, + this.relay, + ); + this.commentForm = new CommentForm(ctrl); + this.glyphForm = new GlyphForm(ctrl); + this.tags = new TagsForm(this, tagTypes); + this.studyDesc = new DescriptionCtrl( + data.description, + debounce(t => { + data.description = t; + this.send('descStudy', t); + }, 500), + this.redraw, + ); + this.chapterDesc = new DescriptionCtrl( + data.chapter.description, + debounce(t => { + data.chapter.description = t; + this.send('descChapter', { id: this.vm.chapterId, desc: t }); + }, 500), + this.redraw, + ); + + this.serverEval = new ServerEval(ctrl, () => this.vm.chapterId); + + this.search = new SearchCtrl( + this.relay?.fullRoundName() || data.name, + this.chapters.list, + this.setChapter, + this.redraw, + ); + + this.topics = new TopicsCtrl( + topics => this.send('setTopics', topics), + () => data.topics || [], + ctrl.trans, + this.redraw, + ); + + this.share = new StudyShare( + data, + this.currentChapter, + this.currentNode, + this.onMainline, + this.bottomColor, + this.relay, + this.redraw, + ctrl.trans, + ); + + this.practice = practiceData && new StudyPractice(ctrl, data, practiceData); + + if (this.vm.mode.sticky && !this.isGamebookPlay()) this.ctrl.userJump(this.data.position.path); + else if (this.data.chapter.relay && !defined(this.ctrl.requestInitialPly)) + this.ctrl.userJump(this.data.chapter.relay.path); + + this.configureAnalysis(); + + this.ctrl.flipped = this.chapterFlipMapProp(this.data.chapter.id); + if (this.members.canContribute()) this.form.openIfNew(); + + this.instanciateGamebookPlay(); + } - const startTour = () => tours.study(ctrl); + send = this.ctrl.socket.send; + redraw = this.ctrl.redraw; - const setTab = (tab: Tab) => { - relay?.tourShow.disable(); - vm.tab(tab); - redraw(); - }; + startTour = () => tours.study(this.ctrl); - const members = memberCtrl({ - initDict: data.members, - myId: practiceData ? undefined : ctrl.opts.userId, - ownerId: data.ownerId, - send, - tab: vm.tab, - startTour, - notif, - onBecomingContributor() { - vm.mode.write = !relayData || relayRecProp(); - }, - admin: data.admin, - redraw, - trans: ctrl.trans, - }); - - const chapters = new StudyChaptersCtrl( - data.chapters, - send, - () => setTab('chapters'), - chapterId => xhr.chapterConfig(data.id, chapterId), - ctrl, - ); - - const currentChapter = (): StudyChapterMeta => chapters.get(vm.chapterId)!; + setTab = (tab: Tab) => { + this.relay?.tourShow(false); + this.vm.tab(tab); + this.redraw(); + }; - const isChapterOwner = (): boolean => ctrl.opts.userId === data.chapter.ownerId; + currentChapter = (): StudyChapterMeta => this.chapters.get(this.vm.chapterId)!; - const multiBoard = new MultiBoardCtrl(data.id, redraw, ctrl.trans); + isChapterOwner = (): boolean => this.ctrl.opts.userId === this.data.chapter.ownerId; - const relay = relayData - ? new RelayCtrl(data.id, relayData, send, redraw, members, data.chapter) - : undefined; + isWriting = (): boolean => this.vm.mode.write && !this.isGamebookPlay(); - const form = studyFormCtrl( - (d, isNew) => { - send('editStudy', d); - if ( - isNew && - data.chapter.setup.variant.key === 'standard' && - ctrl.mainline.length === 1 && - !data.chapter.setup.fromFen && - !relay - ) - chapters.newForm.openInitial(); - }, - () => data, - ctrl.trans, - redraw, - relay, - ); - - const isWriting = (): boolean => vm.mode.write && !isGamebookPlay(); - - function makeChange(...args: StudySocketSendParams): boolean { - if (isWriting()) { - send(...args); + makeChange = (...args: StudySocketSendParams): boolean => { + if (this.isWriting()) { + this.send(...args); return true; } - return (vm.mode.sticky = false); - } - - const commentForm: CommentForm = commentFormCtrl(ctrl); - const glyphForm: GlyphCtrl = glyphFormCtrl(ctrl); - const tags = tagsCtrl(ctrl, () => data.chapter, tagTypes); - const studyDesc = new DescriptionCtrl( - data.description, - debounce(t => { - data.description = t; - send('descStudy', t); - }, 500), - redraw, - ); - const chapterDesc = new DescriptionCtrl( - data.chapter.description, - debounce(t => { - data.chapter.description = t; - send('descChapter', { id: vm.chapterId, desc: t }); - }, 500), - redraw, - ); - - const serverEval = new ServerEval(ctrl, () => vm.chapterId); - - const search = new SearchCtrl(relay?.fullRoundName() || data.name, chapters.list, setChapter, redraw); - - const topics: TopicsCtrl = new TopicsCtrl( - topics => send('setTopics', topics), - () => data.topics || [], - ctrl.trans, - redraw, - ); - - function addChapterId(req: T): T & { ch: string } { - return { - ...req, - ch: vm.chapterId, - }; - } + return (this.vm.mode.sticky = false); + }; - const isGamebookPlay = () => - data.chapter.gamebook && - vm.gamebookOverride !== 'analyse' && - (vm.gamebookOverride === 'play' || !members.canContribute()); + addChapterId = (req: T): T & { ch: string } => ({ + ...req, + ch: this.vm.chapterId, + }); - if (vm.mode.sticky && !isGamebookPlay()) ctrl.userJump(data.position.path); - else if (data.chapter.relay && !defined(ctrl.requestInitialPly)) ctrl.userJump(data.chapter.relay.path); + isGamebookPlay = () => + this.data.chapter.gamebook && + this.vm.gamebookOverride !== 'analyse' && + (this.vm.gamebookOverride === 'play' || !this.members.canContribute()); - function configureAnalysis() { - const canContribute = members.canContribute(); + configureAnalysis = () => { + const canContribute = this.members.canContribute(); // unwrite if member lost privileges - vm.mode.write = vm.mode.write && canContribute; - lichess.pubsub.emit('chat.writeable', data.features.chat); + this.vm.mode.write = this.vm.mode.write && canContribute; + lichess.pubsub.emit('chat.writeable', this.data.features.chat); // official broadcasts cannot have local mods - lichess.pubsub.emit('chat.permissions', { local: canContribute && !relayData?.tour.official }); - lichess.pubsub.emit('palantir.toggle', data.features.chat && !!members.myMember()); + lichess.pubsub.emit('chat.permissions', { local: canContribute && !this.relayData?.tour.official }); + lichess.pubsub.emit('palantir.toggle', this.data.features.chat && !!this.members.myMember()); const computer: boolean = - !isGamebookPlay() && !!(data.chapter.features.computer || data.chapter.practice); - if (!computer) ctrl.getCeval().enabled(false); - ctrl.getCeval().allowed(computer); - if (!data.chapter.features.explorer) ctrl.explorer.disable(); - ctrl.explorer.allowed(data.chapter.features.explorer); - } - configureAnalysis(); + !this.isGamebookPlay() && !!(this.data.chapter.features.computer || this.data.chapter.practice); + if (!computer) this.ctrl.getCeval().enabled(false); + this.ctrl.getCeval().allowed(computer); + if (!this.data.chapter.features.explorer) this.ctrl.explorer.disable(); + this.ctrl.explorer.allowed(this.data.chapter.features.explorer); + }; - function configurePractice() { - if (!data.chapter.practice && ctrl.practice) ctrl.togglePractice(); - if (data.chapter.practice) ctrl.restartPractice(); - if (practice) practice.onLoad(); - } + configurePractice = () => { + if (!this.data.chapter.practice && this.ctrl.practice) this.ctrl.togglePractice(); + if (this.data.chapter.practice) this.ctrl.restartPractice(); + this.practice?.onLoad(); + }; - function onReload(d: ReloadData) { + onReload = (d: ReloadData) => { const s = d.study!; - const prevPath = ctrl.path; - const sameChapter = data.chapter.id === s.chapter.id; - vm.mode.sticky = (vm.mode.sticky && s.features.sticky) || (!data.features.sticky && s.features.sticky); - if (vm.mode.sticky) vm.behind = 0; - data.position = s.position; - data.name = s.name; - data.visibility = s.visibility; - data.features = s.features; - data.settings = s.settings; - data.chapter = s.chapter; - data.likes = s.likes; - data.liked = s.liked; - data.description = s.description; - chapterDesc.set(data.chapter.description); - studyDesc.set(data.description); - document.title = data.name; - members.dict(s.members); - chapters.list(s.chapters); - ctrl.flipped = chapterFlipMapProp(data.chapter.id); - - const merge = !vm.mode.write && sameChapter; - ctrl.reloadData(d.analysis, merge); - vm.gamebookOverride = undefined; - configureAnalysis(); - vm.loading = false; - - instanciateGamebookPlay(); - if (relay) relay.applyChapterRelay(data.chapter, s.chapter.relay); + const prevPath = this.ctrl.path; + const sameChapter = this.data.chapter.id === s.chapter.id; + this.vm.mode.sticky = + (this.vm.mode.sticky && s.features.sticky) || (!this.data.features.sticky && s.features.sticky); + if (this.vm.mode.sticky) this.vm.behind = 0; + this.data.position = s.position; + this.data.name = s.name; + this.data.visibility = s.visibility; + this.data.features = s.features; + this.data.settings = s.settings; + this.data.chapter = s.chapter; + this.data.likes = s.likes; + this.data.liked = s.liked; + this.data.description = s.description; + this.chapterDesc.set(this.data.chapter.description); + this.studyDesc.set(this.data.description); + document.title = this.data.name; + this.members.dict(s.members); + this.chapters.list(s.chapters); + this.ctrl.flipped = this.chapterFlipMapProp(this.data.chapter.id); + + const merge = !this.vm.mode.write && sameChapter; + this.ctrl.reloadData(d.analysis, merge); + this.vm.gamebookOverride = undefined; + this.configureAnalysis(); + this.vm.loading = false; + + this.instanciateGamebookPlay(); + this.relay?.applyChapterRelay(this.data.chapter, s.chapter.relay); let nextPath: Tree.Path; - if (vm.mode.sticky) { - vm.chapterId = data.position.chapterId; + if (this.vm.mode.sticky) { + this.vm.chapterId = this.data.position.chapterId; nextPath = - (vm.justSetChapterId === vm.chapterId && chapters.localPaths[vm.chapterId]) || data.position.path; + (this.vm.justSetChapterId === this.vm.chapterId && this.chapters.localPaths[this.vm.chapterId]) || + this.data.position.path; } else { nextPath = sameChapter ? prevPath - : data.chapter.relay - ? data.chapter.relay!.path - : chapters.localPaths[vm.chapterId] || treePath.root; + : this.data.chapter.relay + ? this.data.chapter.relay!.path + : this.chapters.localPaths[this.vm.chapterId] || treePath.root; } // path could be gone (because of subtree deletion), go as far as possible - ctrl.userJump(ctrl.tree.longestValidPath(nextPath)); + this.ctrl.userJump(this.ctrl.tree.longestValidPath(nextPath)); - vm.justSetChapterId = undefined; + this.vm.justSetChapterId = undefined; - configurePractice(); - serverEval.reset(); - commentForm.onSetPath(data.chapter.id, ctrl.path, ctrl.node); - redraw(); - ctrl.startCeval(); - } + this.configurePractice(); + this.serverEval.reset(); + this.commentForm.onSetPath(this.data.chapter.id, this.ctrl.path, this.ctrl.node); + this.redraw(); + this.ctrl.startCeval(); + }; - const xhrReload = throttlePromiseDelay( + xhrReload = throttlePromiseDelay( () => 700, () => { - vm.loading = true; + this.vm.loading = true; return xhr - .reload(practice ? 'practice/load' : 'study', data.id, vm.mode.sticky ? undefined : vm.chapterId) - .then(onReload, lichess.reload); + .reload( + this.practice ? 'practice/load' : 'study', + this.data.id, + this.vm.mode.sticky ? undefined : this.vm.chapterId, + ) + .then(this.onReload, lichess.reload); }, ); - const onSetPath = throttle(300, (path: Tree.Path) => { - if (vm.mode.sticky && path !== data.position.path) - makeChange( - 'setPath', - addChapterId({ - path, - }), - ); + onSetPath = throttle(300, (path: Tree.Path) => { + if (this.vm.mode.sticky && path !== this.data.position.path) + this.makeChange('setPath', this.addChapterId({ path })); }); - ctrl.flipped = chapterFlipMapProp(data.chapter.id); - if (members.canContribute()) form.openIfNew(); - - const currentNode = () => ctrl.node; - const onMainline = () => ctrl.tree.pathIsMainline(ctrl.path); - const bottomColor = () => - ctrl.flipped ? opposite(data.chapter.setup.orientation) : data.chapter.setup.orientation; - - const share = shareCtrl( - data, - currentChapter, - currentNode, - onMainline, - bottomColor, - relay, - redraw, - ctrl.trans, - ); - - const practice: StudyPracticeCtrl | undefined = practiceData && practiceCtrl(ctrl, data, practiceData); - - let gamebookPlay: GamebookPlayCtrl | undefined; + currentNode = () => this.ctrl.node; + onMainline = () => this.ctrl.tree.pathIsMainline(this.ctrl.path); + bottomColor = () => + this.ctrl.flipped ? opposite(this.data.chapter.setup.orientation) : this.data.chapter.setup.orientation; - function instanciateGamebookPlay() { - if (!isGamebookPlay()) return (gamebookPlay = undefined); - if (gamebookPlay && gamebookPlay.chapterId === vm.chapterId) return; - gamebookPlay = new GamebookPlayCtrl(ctrl, vm.chapterId, ctrl.trans, redraw); - vm.mode.sticky = false; + instanciateGamebookPlay = () => { + if (!this.isGamebookPlay()) return (this.gamebookPlay = undefined); + if (this.gamebookPlay?.chapterId === this.vm.chapterId) return; + this.gamebookPlay = new GamebookPlayCtrl(this.ctrl, this.vm.chapterId, this.ctrl.trans, this.redraw); + this.vm.mode.sticky = false; return undefined; - } - instanciateGamebookPlay(); + }; - function mutateCgConfig(config: Required>) { + mutateCgConfig = (config: Required>) => { config.drawable.onChange = (shapes: Tree.Shape[]) => { - if (vm.mode.write) { - ctrl.tree.setShapes(shapes, ctrl.path); - makeChange( + if (this.vm.mode.write) { + this.ctrl.tree.setShapes(shapes, this.ctrl.path); + this.makeChange( 'shapes', - addChapterId({ - path: ctrl.path, + this.addChapterId({ + path: this.ctrl.path, shapes, }), ); } - gamebookPlay && gamebookPlay.onShapeChange(shapes); + this.gamebookPlay?.onShapeChange(shapes); }; - } + }; - function wrongChapter(serverData: WithPosition & { s?: boolean }): boolean { - if (serverData.p.chapterId !== vm.chapterId) { + wrongChapter = (serverData: WithPosition & { s?: boolean }): boolean => { + if (serverData.p.chapterId !== this.vm.chapterId) { // sticky should really be on the same chapter - if (vm.mode.sticky && serverData.s) xhrReload(); + if (this.vm.mode.sticky && serverData.s) this.xhrReload(); return true; } return false; - } + }; - function setMemberActive(who?: { u: string }) { - who && members.setActive(who.u); - vm.updatedAt = Date.now(); - } + setMemberActive = (who?: { u: string }) => { + who && this.members.setActive(who.u); + this.vm.updatedAt = Date.now(); + }; - function withPosition(obj: T): T & { ch: string; path: string } { - return { ...obj, ch: vm.chapterId, path: ctrl.path }; - } + withPosition = (obj: T): T & { ch: string; path: string } => ({ + ...obj, + ch: this.vm.chapterId, + path: this.ctrl.path, + }); - const likeToggler = debounce(() => send('like', { liked: data.liked }), 1000); + likeToggler = debounce(() => this.send('like', { liked: this.data.liked }), 1000); - function setChapter(id: string, force?: boolean) { - const alreadySet = id === vm.chapterId && !force; - if (relay?.tourShow.active) { - relay.tourShow.disable(); - if (alreadySet) redraw(); + setChapter = (id: string, force?: boolean) => { + const alreadySet = id === this.vm.chapterId && !force; + if (this.relay?.tourShow()) { + this.relay.tourShow(false); + if (alreadySet) this.redraw(); } if (alreadySet) return; - if (!vm.mode.sticky || !makeChange('setChapter', id)) { - vm.mode.sticky = false; - if (!vm.behind) vm.behind = 1; - vm.chapterId = id; - xhrReload(); + if (!this.vm.mode.sticky || !this.makeChange('setChapter', id)) { + this.vm.mode.sticky = false; + if (!this.vm.behind) this.vm.behind = 1; + this.vm.chapterId = id; + this.xhrReload(); } - vm.loading = true; - vm.nextChapterId = id; - vm.justSetChapterId = id; - redraw(); - } + this.vm.loading = true; + this.vm.nextChapterId = id; + this.vm.justSetChapterId = id; + this.redraw(); + }; - const [prevChapter, nextChapter] = [-1, +1].map(delta => (): StudyChapterMeta | undefined => { - const chs = chapters.list(); - const i = chs.findIndex(ch => ch.id === vm.chapterId); + private deltaChapter = (delta: number): StudyChapterMeta | undefined => { + const chs = this.chapters.list(); + const i = chs.findIndex(ch => ch.id === this.vm.chapterId); return i < 0 ? undefined : chs[i + delta]; - }); - const hasNextChapter = () => { - const chs = chapters.list(); - return chs[chs.length - 1].id != vm.chapterId; + }; + prevChapter = () => this.deltaChapter(-1); + nextChapter = () => this.deltaChapter(+1); + hasNextChapter = () => { + const chs = this.chapters.list(); + return chs[chs.length - 1].id != this.vm.chapterId; + }; + + isUpdatedRecently = () => Date.now() - this.vm.updatedAt < 300 * 1000; + toggleLike = () => { + this.data.liked = !this.data.liked; + this.redraw(); + this.likeToggler(); + }; + position = () => this.data.position; + canJumpTo = (path: Tree.Path) => + this.gamebookPlay + ? this.gamebookPlay.canJumpTo(path) + : this.data.chapter.conceal === undefined || + this.isChapterOwner() || + treePath.contains(this.ctrl.path, path) || // can always go back + this.ctrl.tree.lastMainlineNode(path).ply <= this.data.chapter.conceal!; + onJump = () => { + if (this.gamebookPlay) this.gamebookPlay.onJump(); + else this.chapters.localPaths[this.vm.chapterId] = this.ctrl.path; // don't remember position on gamebook + this.practice?.onJump(); + }; + onFlip = () => this.chapterFlipMapProp(this.data.chapter.id, this.ctrl.flipped); + + setPath = (path: Tree.Path, node: Tree.Node) => { + this.onSetPath(path); + this.commentForm.onSetPath(this.vm.chapterId, path, node); + }; + deleteNode = (path: Tree.Path) => + this.makeChange( + 'deleteNode', + this.addChapterId({ + path, + jumpTo: this.ctrl.path, + }), + ); + promote = (path: Tree.Path, toMainline: boolean) => + this.makeChange( + 'promote', + this.addChapterId({ + toMainline, + path, + }), + ); + forceVariation = (path: Tree.Path, force: boolean) => + this.makeChange( + 'forceVariation', + this.addChapterId({ + force, + path, + }), + ); + toggleSticky = () => { + this.vm.mode.sticky = !this.vm.mode.sticky && this.data.features.sticky; + this.xhrReload(); + }; + toggleWrite = () => { + this.vm.mode.write = !this.vm.mode.write && this.members.canContribute(); + if (this.relayData) this.relayRecProp(this.vm.mode.write); + else this.nonRelayRecMapProp(this.data.id, this.vm.mode.write); + this.xhrReload(); + }; + goToPrevChapter = () => { + const chapter = this.prevChapter(); + if (chapter) this.setChapter(chapter.id); + }; + goToNextChapter = () => { + const chapter = this.nextChapter(); + if (chapter) this.setChapter(chapter.id); + }; + setGamebookOverride = (o: GamebookOverride) => { + this.vm.gamebookOverride = o; + this.instanciateGamebookPlay(); + this.configureAnalysis(); + this.ctrl.userJump(this.ctrl.path); + if (!o) this.xhrReload(); + }; + explorerGame = (gameId: string, insert: boolean) => + this.makeChange('explorerGame', this.withPosition({ gameId, insert })); + onPremoveSet = () => this.gamebookPlay?.onPremoveSet(); + looksNew = () => { + const cs = this.chapters.list(); + return cs.length == 1 && cs[0].name == 'Chapter 1' && !this.currentChapter().ongoing; + }; + trans = this.ctrl.trans; + socketHandler = (t: string, d: any) => { + const handler = (this.socketHandlers as any as SocketHandlers)[t]; + if (handler) { + handler(d); + return true; + } + return !!this.relay && this.relay.socketHandler(t, d); }; - const socketHandlers: Handlers = { - path(d) { + socketHandlers: Handlers = { + path: d => { const position = d.p, who = d.w; - setMemberActive(who); - if (!vm.mode.sticky) { - vm.behind++; - return redraw(); + this.setMemberActive(who); + if (!this.vm.mode.sticky) { + this.vm.behind++; + return this.redraw(); } - if (position.chapterId !== data.position.chapterId || !ctrl.tree.pathExists(position.path)) { - return xhrReload(); + if (position.chapterId !== this.data.position.chapterId || !this.ctrl.tree.pathExists(position.path)) { + return this.xhrReload(); } - data.position.path = position.path; + this.data.position.path = position.path; if (who && who.s === lichess.sri) return; - ctrl.userJump(position.path); - redraw(); + this.ctrl.userJump(position.path); + this.redraw(); }, - addNode(d) { + addNode: d => { const position = d.p, node = d.n, who = d.w, sticky = d.s; - setMemberActive(who); - if (vm.toolTab() == 'multiBoard' || (relay && relay.tourShow.active)) multiBoard.addNode(d.p, d.n); - if (sticky && !vm.mode.sticky) vm.behind++; - if (wrongChapter(d)) { - if (sticky && !vm.mode.sticky) redraw(); + this.setMemberActive(who); + if (this.vm.toolTab() == 'multiBoard' || this.relay?.tourShow()) this.multiBoard.addNode(d.p, d.n); + if (sticky && !this.vm.mode.sticky) this.vm.behind++; + if (this.wrongChapter(d)) { + if (sticky && !this.vm.mode.sticky) this.redraw(); return; } if (sticky && who && who.s === lichess.sri) { - data.position.path = position.path + node.id; + this.data.position.path = position.path + node.id; return; } - if (relay) relay.applyChapterRelay(data.chapter, d.relay); - const newPath = ctrl.tree.addNode(node, position.path); - if (!newPath) return xhrReload(); - ctrl.tree.addDests(d.d, newPath); - if (sticky) data.position.path = newPath; + this.relay?.applyChapterRelay(this.data.chapter, d.relay); + const newPath = this.ctrl.tree.addNode(node, position.path); + if (!newPath) return this.xhrReload(); + this.ctrl.tree.addDests(d.d, newPath); + if (sticky) this.data.position.path = newPath; if ( - (sticky && vm.mode.sticky) || - (position.path === ctrl.path && position.path === treePath.fromNodeList(ctrl.mainline)) + (sticky && this.vm.mode.sticky) || + (position.path === this.ctrl.path && position.path === treePath.fromNodeList(this.ctrl.mainline)) ) - ctrl.jump(newPath); - redraw(); + this.ctrl.jump(newPath); + this.redraw(); }, - deleteNode(d) { + deleteNode: d => { const position = d.p, who = d.w; - setMemberActive(who); - if (wrongChapter(d)) return; + this.setMemberActive(who); + if (this.wrongChapter(d)) return; // deleter already has it done if (who && who.s === lichess.sri) return; - if (!ctrl.tree.pathExists(d.p.path)) return xhrReload(); - ctrl.tree.deleteNodeAt(position.path); - if (vm.mode.sticky) ctrl.jump(ctrl.path); - redraw(); + if (!this.ctrl.tree.pathExists(d.p.path)) return this.xhrReload(); + this.ctrl.tree.deleteNodeAt(position.path); + if (this.vm.mode.sticky) this.ctrl.jump(this.ctrl.path); + this.redraw(); }, - promote(d) { + promote: d => { const position = d.p, who = d.w; - setMemberActive(who); - if (wrongChapter(d)) return; + this.setMemberActive(who); + if (this.wrongChapter(d)) return; if (who && who.s === lichess.sri) return; - if (!ctrl.tree.pathExists(d.p.path)) return xhrReload(); - ctrl.tree.promoteAt(position.path, d.toMainline); - if (vm.mode.sticky) ctrl.jump(ctrl.path); - ctrl.treeVersion++; - redraw(); - }, - reload: xhrReload, - changeChapter(d) { - setMemberActive(d.w); - if (!vm.mode.sticky) vm.behind++; - data.position = d.p; - if (vm.mode.sticky) xhrReload(); - else redraw(); - }, - updateChapter(d) { - setMemberActive(d.w); - xhrReload(); - }, - descChapter(d) { - setMemberActive(d.w); + if (!this.ctrl.tree.pathExists(d.p.path)) return this.xhrReload(); + this.ctrl.tree.promoteAt(position.path, d.toMainline); + if (this.vm.mode.sticky) this.ctrl.jump(this.ctrl.path); + this.ctrl.treeVersion++; + this.redraw(); + }, + reload: this.xhrReload, + changeChapter: d => { + this.setMemberActive(d.w); + if (!this.vm.mode.sticky) this.vm.behind++; + this.data.position = d.p; + if (this.vm.mode.sticky) this.xhrReload(); + else this.redraw(); + }, + updateChapter: d => { + this.setMemberActive(d.w); + this.xhrReload(); + }, + descChapter: d => { + this.setMemberActive(d.w); if (d.w && d.w.s === lichess.sri) return; - if (data.chapter.id === d.chapterId) { - data.chapter.description = d.desc; - chapterDesc.set(d.desc); + if (this.data.chapter.id === d.chapterId) { + this.data.chapter.description = d.desc; + this.chapterDesc.set(d.desc); } - redraw(); + this.redraw(); }, - descStudy(d) { - setMemberActive(d.w); + descStudy: d => { + this.setMemberActive(d.w); if (d.w && d.w.s === lichess.sri) return; - data.description = d.desc; - studyDesc.set(d.desc); - redraw(); - }, - setTopics(d) { - setMemberActive(d.w); - data.topics = d.topics; - redraw(); - }, - addChapter(d) { - setMemberActive(d.w); - if (d.s && !vm.mode.sticky) vm.behind++; - if (d.s) data.position = d.p; + this.data.description = d.desc; + this.studyDesc.set(d.desc); + this.redraw(); + }, + setTopics: d => { + this.setMemberActive(d.w); + this.data.topics = d.topics; + this.redraw(); + }, + addChapter: d => { + this.setMemberActive(d.w); + if (d.s && !this.vm.mode.sticky) this.vm.behind++; + if (d.s) this.data.position = d.p; else if (d.w && d.w.s === lichess.sri) { - vm.mode.write = relayData ? relayRecProp() : nonRelayRecMapProp(data.id); - vm.chapterId = d.p.chapterId; + this.vm.mode.write = this.relayData ? this.relayRecProp() : this.nonRelayRecMapProp(this.data.id); + this.vm.chapterId = d.p.chapterId; } - xhrReload(); - }, - members(d) { - members.update(d); - configureAnalysis(); - redraw(); - }, - chapters(d) { - chapters.list(d); - if (vm.toolTab() == 'multiBoard' || (relay && relay.tourShow.active)) multiBoard.addResult(d); - if (!currentChapter()) { - vm.chapterId = d[0].id; - if (!vm.mode.sticky) xhrReload(); + this.xhrReload(); + }, + members: d => { + this.members.update(d); + this.configureAnalysis(); + this.redraw(); + }, + chapters: d => { + this.chapters.list(d); + if (this.vm.toolTab() == 'multiBoard' || this.relay?.tourShow()) this.multiBoard.addResult(d); + if (!this.currentChapter()) { + this.vm.chapterId = d[0].id; + if (!this.vm.mode.sticky) this.xhrReload(); } - redraw(); + this.redraw(); }, - shapes(d) { + shapes: d => { const position = d.p, who = d.w; - setMemberActive(who); - if (d.p.chapterId !== vm.chapterId) return; - if (who && who.s === lichess.sri) return redraw(); // update shape indicator in column move view - ctrl.tree.setShapes(d.s, ctrl.path); - if (ctrl.path === position.path) ctrl.withCg(cg => cg.setShapes(d.s)); - redraw(); - }, - validationError(d) { + this.setMemberActive(who); + if (d.p.chapterId !== this.vm.chapterId) return; + if (who && who.s === lichess.sri) return this.redraw(); // update shape indicator in column move view + this.ctrl.tree.setShapes(d.s, this.ctrl.path); + if (this.ctrl.path === position.path) this.ctrl.withCg(cg => cg.setShapes(d.s)); + this.redraw(); + }, + validationError: d => { alert(d.error); }, - setComment(d) { + setComment: d => { const position = d.p, who = d.w; - setMemberActive(who); - if (wrongChapter(d)) return; - ctrl.tree.setCommentAt(d.c, position.path); - redraw(); - }, - setTags(d) { - setMemberActive(d.w); - if (d.chapterId !== vm.chapterId) return; - data.chapter.tags = d.tags; - redraw(); - }, - deleteComment(d) { + this.setMemberActive(who); + if (this.wrongChapter(d)) return; + this.ctrl.tree.setCommentAt(d.c, position.path); + this.redraw(); + }, + setTags: d => { + this.setMemberActive(d.w); + if (d.chapterId !== this.vm.chapterId) return; + this.data.chapter.tags = d.tags; + this.redraw(); + }, + deleteComment: d => { const position = d.p, who = d.w; - setMemberActive(who); - if (wrongChapter(d)) return; - ctrl.tree.deleteCommentAt(d.id, position.path); - redraw(); + this.setMemberActive(who); + if (this.wrongChapter(d)) return; + this.ctrl.tree.deleteCommentAt(d.id, position.path); + this.redraw(); }, - glyphs(d) { + glyphs: d => { const position = d.p, who = d.w; - setMemberActive(who); - if (wrongChapter(d)) return; - ctrl.tree.setGlyphsAt(d.g, position.path); - if (ctrl.path === position.path) ctrl.setAutoShapes(); - redraw(); + this.setMemberActive(who); + if (this.wrongChapter(d)) return; + this.ctrl.tree.setGlyphsAt(d.g, position.path); + if (this.ctrl.path === position.path) this.ctrl.setAutoShapes(); + this.redraw(); }, - clock(d) { + clock: d => { const position = d.p, who = d.w; - setMemberActive(who); - if (wrongChapter(d)) return; - ctrl.tree.setClockAt(d.c, position.path); - redraw(); + this.setMemberActive(who); + if (this.wrongChapter(d)) return; + this.ctrl.tree.setClockAt(d.c, position.path); + this.redraw(); }, - forceVariation(d) { + forceVariation: d => { const position = d.p, who = d.w; - setMemberActive(who); - if (wrongChapter(d)) return; - ctrl.tree.forceVariationAt(position.path, d.force); - redraw(); + this.setMemberActive(who); + if (this.wrongChapter(d)) return; + this.ctrl.tree.forceVariationAt(position.path, d.force); + this.redraw(); }, - conceal(d) { - if (wrongChapter(d)) return; - data.chapter.conceal = d.ply; - redraw(); + conceal: d => { + if (this.wrongChapter(d)) return; + this.data.chapter.conceal = d.ply; + this.redraw(); }, - liking(d) { - data.likes = d.l.likes; - if (d.w && d.w.s === lichess.sri) data.liked = d.l.me; - redraw(); + liking: d => { + this.data.likes = d.l.likes; + if (d.w && d.w.s === lichess.sri) this.data.liked = d.l.me; + this.redraw(); }, error(msg: string) { alert(msg); }, }; - - return { - data, - form, - setTab, - members, - chapters, - notif, - commentForm, - glyphForm, - serverEval, - share, - tags, - studyDesc, - chapterDesc, - topics, - search, - vm, - relay, - multiBoard, - isUpdatedRecently() { - return Date.now() - vm.updatedAt < 300 * 1000; - }, - toggleLike() { - data.liked = !data.liked; - redraw(); - likeToggler(); - }, - position() { - return data.position; - }, - currentChapter, - isChapterOwner, - canJumpTo(path: Tree.Path) { - if (gamebookPlay) return gamebookPlay.canJumpTo(path); - return ( - data.chapter.conceal === undefined || - isChapterOwner() || - treePath.contains(ctrl.path, path) || // can always go back - ctrl.tree.lastMainlineNode(path).ply <= data.chapter.conceal! - ); - }, - onJump() { - if (gamebookPlay) gamebookPlay.onJump(); - else chapters.localPaths[vm.chapterId] = ctrl.path; // don't remember position on gamebook - if (practice) practice.onJump(); - }, - onFlip() { - chapterFlipMapProp(data.chapter.id, ctrl.flipped); - }, - withPosition, - setPath(path, node) { - onSetPath(path); - commentForm.onSetPath(vm.chapterId, path, node); - }, - deleteNode(path) { - makeChange( - 'deleteNode', - addChapterId({ - path, - jumpTo: ctrl.path, - }), - ); - }, - promote(path, toMainline) { - makeChange( - 'promote', - addChapterId({ - toMainline, - path, - }), - ); - }, - forceVariation(path, force) { - makeChange( - 'forceVariation', - addChapterId({ - force, - path, - }), - ); - }, - setChapter, - toggleSticky() { - vm.mode.sticky = !vm.mode.sticky && data.features.sticky; - xhrReload(); - }, - toggleWrite() { - vm.mode.write = !vm.mode.write && members.canContribute(); - if (relayData) relayRecProp(vm.mode.write); - else nonRelayRecMapProp(data.id, vm.mode.write); - xhrReload(); - }, - isWriting, - makeChange, - startTour, - userJump: ctrl.userJump, - currentNode, - practice, - gamebookPlay: () => gamebookPlay, - prevChapter, - nextChapter, - hasNextChapter, - goToPrevChapter() { - const chapter = prevChapter(); - if (chapter) setChapter(chapter.id); - }, - goToNextChapter() { - const chapter = nextChapter(); - if (chapter) setChapter(chapter.id); - }, - setGamebookOverride(o) { - vm.gamebookOverride = o; - instanciateGamebookPlay(); - configureAnalysis(); - ctrl.userJump(ctrl.path); - if (!o) xhrReload(); - }, - mutateCgConfig, - explorerGame(gameId: string, insert: boolean) { - makeChange('explorerGame', withPosition({ gameId, insert })); - }, - onPremoveSet() { - if (gamebookPlay) gamebookPlay.onPremoveSet(); - }, - looksNew() { - const cs = chapters.list(); - return cs.length == 1 && cs[0].name == 'Chapter 1' && !currentChapter().ongoing; - }, - redraw, - trans: ctrl.trans, - socketHandler: (t: string, d: any) => { - const handler = (socketHandlers as any as SocketHandlers)[t]; - if (handler) { - handler(d); - return true; - } - return !!relay && relay.socketHandler(t, d); - }, - }; } diff --git a/ui/analyse/src/study/studyDeps.ts b/ui/analyse/src/study/studyDeps.ts index 1dead1b3e5e8f..457c7ad9c2a1e 100644 --- a/ui/analyse/src/study/studyDeps.ts +++ b/ui/analyse/src/study/studyDeps.ts @@ -1,9 +1,9 @@ import relayManager from './relay/relayManagerView'; import relayTour from './relay/relayTourView'; import renderPlayerBars from './playerBars'; -import makeStudy from './studyCtrl'; +import StudyCtrl from './studyCtrl'; -export { relayManager, relayTour, renderPlayerBars, makeStudy }; +export { relayManager, relayTour, renderPlayerBars, StudyCtrl }; export * as gbEdit from './gamebook/gamebookEdit'; export * as gbPlay from './gamebook/gamebookPlayView'; diff --git a/ui/analyse/src/study/studyForm.ts b/ui/analyse/src/study/studyForm.ts index 624c46c6fdcf7..a8159b00f0c14 100644 --- a/ui/analyse/src/study/studyForm.ts +++ b/ui/analyse/src/study/studyForm.ts @@ -1,24 +1,13 @@ -import { h, VNode } from 'snabbdom'; +import { VNode } from 'snabbdom'; import * as licon from 'common/licon'; import { snabDialog } from 'common/dialog'; -import { prop, Prop } from 'common'; -import { bindSubmit, bindNonPassive } from 'common/snabbdom'; +import { prop } from 'common'; +import { bindSubmit, bindNonPassive, looseH as h } from 'common/snabbdom'; import { emptyRedButton } from '../view/util'; import { StudyData } from './interfaces'; import { Redraw } from '../interfaces'; import RelayCtrl from './relay/relayCtrl'; -export interface StudyFormCtrl { - open: Prop; - openIfNew(): void; - save(data: FormData, isNew: boolean): void; - getData(): StudyData; - isNew(): boolean; - trans: Trans; - redraw: Redraw; - relay?: RelayCtrl; -} - export interface FormData { name: string; visibility: string; @@ -39,66 +28,42 @@ interface Select { } type Choice = [string, string]; +export class StudyForm { + initAt = Date.now(); + open = prop(false); + + constructor( + private readonly doSave: (data: FormData, isNew: boolean) => void, + readonly getData: () => StudyData, + readonly trans: Trans, + readonly redraw: Redraw, + readonly relay?: RelayCtrl, + ) {} + + isNew = (): boolean => { + const d = this.getData(); + return d.from === 'scratch' && !!d.isNew && Date.now() - this.initAt < 9000; + }; + + openIfNew = () => { + if (this.isNew()) this.open(true); + }; + save = (data: FormData, isNew: boolean) => { + this.doSave(data, isNew); + this.open(false); + }; +} + const select = (s: Select): VNode => h('div.form-group.form-half', [ - h( - 'label.form-label', - { - attrs: { for: 'study-' + s.key }, - }, - s.name, - ), + h('label.form-label', { attrs: { for: 'study-' + s.key } }, s.name), h( `select#study-${s.key}.form-control`, - s.choices.map(function (o) { - return h( - 'option', - { - attrs: { - value: o[0], - selected: s.selected === o[0], - }, - }, - o[1], - ); - }), + s.choices.map(o => h('option', { attrs: { value: o[0], selected: s.selected === o[0] } }, o[1])), ), ]); -export function ctrl( - save: (data: FormData, isNew: boolean) => void, - getData: () => StudyData, - trans: Trans, - redraw: Redraw, - relay?: RelayCtrl, -): StudyFormCtrl { - const initAt = Date.now(); - - function isNew(): boolean { - const d = getData(); - return d.from === 'scratch' && !!d.isNew && Date.now() - initAt < 9000; - } - - const open = prop(false); - - return { - open, - openIfNew() { - if (isNew()) open(true); - }, - save(data: FormData, isNew: boolean) { - save(data, isNew); - open(false); - }, - getData, - isNew, - trans, - redraw, - relay, - }; -} - -export function view(ctrl: StudyFormCtrl): VNode { +export function view(ctrl: StudyForm): VNode { const data = ctrl.getData(); const isNew = ctrl.isNew(); const updateName = (vnode: VNode, isUpdate: boolean) => { @@ -153,10 +118,7 @@ export function view(ctrl: StudyFormCtrl): VNode { h('div.form-group' + (ctrl.relay ? '.none' : ''), [ h('label.form-label', { attrs: { for: 'study-name' } }, ctrl.trans.noarg('name')), h('input#study-name.form-control', { - attrs: { - minlength: 3, - maxlength: 100, - }, + attrs: { minlength: 3, maxlength: 100 }, hook: { insert: vnode => updateName(vnode, false), postpatch: (_, vnode) => updateName(vnode, true), @@ -229,36 +191,30 @@ export function view(ctrl: StudyFormCtrl): VNode { selected: '' + data.settings.description, }), ]), - ctrl.relay - ? h('div.form-actions-secondary', [ - h( - 'a.text', - { - attrs: { - 'data-icon': licon.RadioTower, - href: `/broadcast/${ctrl.relay.data.tour.id}/edit`, - }, - }, - 'Tournament settings', - ), - h( - 'a.text', - { - attrs: { 'data-icon': licon.RadioTower, href: `/broadcast/round/${data.id}/edit` }, + ctrl.relay && + h('div.form-actions-secondary', [ + h( + 'a.text', + { + attrs: { + 'data-icon': licon.RadioTower, + href: `/broadcast/${ctrl.relay.data.tour.id}/edit`, }, - 'Round settings', - ), - ]) - : null, + }, + 'Tournament settings', + ), + h( + 'a.text', + { attrs: { 'data-icon': licon.RadioTower, href: `/broadcast/round/${data.id}/edit` } }, + 'Round settings', + ), + ]), h('div.form-actions', [ h('div', { attrs: { style: 'display: flex' } }, [ h( 'form', { - attrs: { - action: '/study/' + data.id + '/delete', - method: 'post', - }, + attrs: { action: '/study/' + data.id + '/delete', method: 'post' }, hook: bindNonPassive( 'submit', _ => @@ -268,29 +224,19 @@ export function view(ctrl: StudyFormCtrl): VNode { }, [h(emptyRedButton, ctrl.trans.noarg(isNew ? 'cancel' : 'deleteStudy'))], ), - isNew - ? null - : h( - 'form', - { - attrs: { - action: '/study/' + data.id + '/clear-chat', - method: 'post', - }, - hook: bindNonPassive('submit', _ => - confirm(ctrl.trans.noarg('deleteTheStudyChatHistory')), - ), - }, - [h(emptyRedButton, ctrl.trans.noarg('clearChat'))], - ), + !isNew && + h( + 'form', + { + attrs: { action: '/study/' + data.id + '/clear-chat', method: 'post' }, + hook: bindNonPassive('submit', _ => + confirm(ctrl.trans.noarg('deleteTheStudyChatHistory')), + ), + }, + [h(emptyRedButton, ctrl.trans.noarg('clearChat'))], + ), ]), - h( - 'button.button', - { - attrs: { type: 'submit' }, - }, - ctrl.trans.noarg(isNew ? 'start' : 'save'), - ), + h('button.button', { attrs: { type: 'submit' } }, ctrl.trans.noarg(isNew ? 'start' : 'save')), ]), ], ), diff --git a/ui/analyse/src/study/studyGlyph.ts b/ui/analyse/src/study/studyGlyph.ts index 422e3b1dce3b9..9e43df056a704 100644 --- a/ui/analyse/src/study/studyGlyph.ts +++ b/ui/analyse/src/study/studyGlyph.ts @@ -1,4 +1,4 @@ -import { prop, Prop } from 'common'; +import { prop } from 'common'; import { bind } from 'common/snabbdom'; import throttle from 'common/throttle'; import { spinnerVdom as spinner } from 'common/spinner'; @@ -12,65 +12,45 @@ interface AllGlyphs { position: Tree.Glyph[]; } -export interface GlyphCtrl { - root: AnalyseCtrl; - all: Prop; - loadGlyphs(): void; - toggleGlyph(id: Tree.GlyphId): void; -} +const renderGlyph = (ctrl: GlyphForm, node: Tree.Node) => (glyph: Tree.Glyph) => + h( + 'button', + { + hook: bind('click', () => ctrl.toggleGlyph(glyph.id)), + attrs: { 'data-symbol': glyph.symbol, type: 'button' }, + class: { active: !!node.glyphs && !!node.glyphs.find(g => g.id === glyph.id) }, + }, + [glyph.name], + ); -function renderGlyph(ctrl: GlyphCtrl, node: Tree.Node) { - return (glyph: Tree.Glyph) => - h( - 'button', - { - hook: bind('click', () => ctrl.toggleGlyph(glyph.id)), - attrs: { 'data-symbol': glyph.symbol, type: 'button' }, - class: { - active: !!node.glyphs && !!node.glyphs.find(g => g.id === glyph.id), - }, - }, - [glyph.name], - ); -} +export class GlyphForm { + all = prop(null); -export function ctrl(root: AnalyseCtrl): GlyphCtrl { - const all = prop(null); + constructor(readonly root: AnalyseCtrl) {} - function loadGlyphs() { - if (!all()) + loadGlyphs = () => { + if (!this.all()) xhr.glyphs().then(gs => { - all(gs); - root.redraw(); + this.all(gs); + this.root.redraw(); }); - } + }; - const toggleGlyph = throttle(500, (id: Tree.GlyphId) => { - root.study!.makeChange( - 'toggleGlyph', - root.study!.withPosition({ - id, - }), - ); - root.redraw(); + toggleGlyph = throttle(500, (id: Tree.GlyphId) => { + this.root.study!.makeChange('toggleGlyph', this.root.study!.withPosition({ id })); + this.root.redraw(); }); - - return { root, all, loadGlyphs, toggleGlyph }; } -export function viewDisabled(why: string): VNode { - return h('div.study__glyphs', [h('div.study__message', why)]); -} +export const viewDisabled = (why: string): VNode => h('div.study__glyphs', [h('div.study__message', why)]); -export function view(ctrl: GlyphCtrl): VNode { +export function view(ctrl: GlyphForm): VNode { const all = ctrl.all(), node = ctrl.root.node; return h( 'div.study__glyphs' + (all ? '' : '.empty'), - { - hook: { insert: ctrl.loadGlyphs }, - }, + { hook: { insert: ctrl.loadGlyphs } }, all ? [ h('div.move', all.move.map(renderGlyph(ctrl, node))), diff --git a/ui/analyse/src/study/studyMembers.ts b/ui/analyse/src/study/studyMembers.ts index f0d8e10865b5c..40ac597bff36e 100644 --- a/ui/analyse/src/study/studyMembers.ts +++ b/ui/analyse/src/study/studyMembers.ts @@ -1,36 +1,15 @@ import { AnalyseSocketSend } from '../socket'; -import { h, VNode } from 'snabbdom'; +import { VNode } from 'snabbdom'; import * as licon from 'common/licon'; -import { iconTag, bind, onInsert, dataIcon, bindNonPassive } from 'common/snabbdom'; +import { iconTag, bind, onInsert, dataIcon, bindNonPassive, looseH as h } from 'common/snabbdom'; import { makeCtrl as inviteFormCtrl, StudyInviteFormCtrl } from './inviteForm'; import { NotifCtrl } from './notif'; import { prop, Prop, scrollTo } from 'common'; import { titleNameToId } from '../view/util'; -import { StudyCtrl, StudyMember, StudyMemberMap, Tab } from './interfaces'; +import { StudyMember, StudyMemberMap, Tab } from './interfaces'; import { textRaw as xhrTextRaw } from 'common/xhr'; import { userLink } from 'common/userLink'; - -export interface StudyMemberCtrl { - dict: Prop; - confing: Prop; - myId?: string; - inviteForm: StudyInviteFormCtrl; - update(members: StudyMemberMap): void; - setActive(id: string): void; - isActive(id: string): boolean; - owner(): StudyMember; - myMember(): StudyMember | undefined; - isOwner(): boolean; - canContribute(): boolean; - max: number; - setRole(id: string, role: string): void; - kick(id: string): void; - leave(): void; - ordered(): StudyMember[]; - size(): number; - isOnline(userId: string): boolean; - hasOnlineContributor(): boolean; -} +import StudyCtrl from './studyCtrl'; interface Opts { initDict: StudyMemberMap; @@ -56,116 +35,105 @@ function memberActivity(onIdle: () => void) { return schedule; } -export function ctrl(opts: Opts): StudyMemberCtrl { - const dict = prop(opts.initDict); - const confing = prop(null); - const active: { [id: string]: () => void } = {}; - let online: { [id: string]: boolean } = {}; - let spectatorIds: string[] = []; - const max = 30; - - const owner = () => dict()[opts.ownerId]; +export class StudyMemberCtrl { + dict: Prop; + confing = prop(null); + inviteForm: StudyInviteFormCtrl; + readonly active: Map void> = new Map(); + online: { [id: string]: boolean } = {}; + spectatorIds: string[] = []; + max = 30; + + constructor(readonly opts: Opts) { + this.dict = prop(opts.initDict); + this.inviteForm = inviteFormCtrl( + opts.send, + this.dict, + () => opts.tab('members'), + opts.redraw, + opts.trans, + ); + lichess.pubsub.on('socket.in.crowd', d => { + const names: string[] = d.users || []; + this.inviteForm.spectators(names); + this.spectatorIds = names.map(titleNameToId); + this.updateOnline(); + }); + } - const isOwner = () => opts.myId === opts.ownerId || (opts.admin && canContribute()); + owner = () => this.dict()[this.opts.ownerId]; - const myMember = () => (opts.myId ? dict()[opts.myId] : undefined); + isOwner = () => this.opts.myId === this.opts.ownerId || (this.opts.admin && this.canContribute()); - const canContribute = (): boolean => myMember()?.role === 'w'; + myMember = () => (this.opts.myId ? this.dict()[this.opts.myId] : undefined); - const inviteForm = inviteFormCtrl(opts.send, dict, () => opts.tab('members'), opts.redraw, opts.trans); + canContribute = (): boolean => this.myMember()?.role === 'w'; - function setActive(id: string) { - if (opts.tab() !== 'members') return; - if (active[id]) active[id](); + setActive = (id: string) => { + if (this.opts.tab() !== 'members') return; + const active = this.active.get(id); + if (active) active(); else - active[id] = memberActivity(() => { - delete active[id]; - opts.redraw(); - }); - opts.redraw(); - } + this.active.set( + id, + memberActivity(() => { + this.active.delete(id); + this.opts.redraw(); + }), + ); + this.opts.redraw(); + }; - function updateOnline() { - online = {}; - const members: StudyMemberMap = dict(); - spectatorIds.forEach(function (id) { - if (members[id]) online[id] = true; + updateOnline = () => { + this.online = {}; + const members: StudyMemberMap = this.dict(); + this.spectatorIds.forEach(id => { + if (members[id]) this.online[id] = true; }); - if (opts.tab() === 'members') opts.redraw(); - } - - lichess.pubsub.on('socket.in.crowd', d => { - const names: string[] = d.users || []; - inviteForm.spectators(names); - spectatorIds = names.map(titleNameToId); - updateOnline(); - }); + if (this.opts.tab() === 'members') this.opts.redraw(); + }; - return { - dict, - confing, - myId: opts.myId, - inviteForm, - update(members: StudyMemberMap) { - if (isOwner()) confing(Object.keys(members).find(sri => !dict()[sri]) || null); - const wasViewer = myMember() && !canContribute(); - const wasContrib = myMember() && canContribute(); - dict(members); - if (wasViewer && canContribute()) { - if (lichess.once('study-tour')) opts.startTour(); - opts.onBecomingContributor(); - opts.notif.set({ - text: opts.trans.noarg('youAreNowAContributor'), - duration: 3000, - }); - } else if (wasContrib && !canContribute()) - opts.notif.set({ - text: opts.trans.noarg('youAreNowASpectator'), - duration: 3000, - }); - updateOnline(); - }, - setActive, - isActive(id: string) { - return !!active[id]; - }, - owner, - myMember, - isOwner, - canContribute, - max, - setRole(id: string, role: string) { - setActive(id); - opts.send('setRole', { - userId: id, - role, + update = (members: StudyMemberMap) => { + if (this.isOwner()) this.confing(Object.keys(members).find(sri => !this.dict()[sri]) || null); + const wasViewer = this.myMember() && !this.canContribute(); + const wasContrib = this.myMember() && this.canContribute(); + this.dict(members); + if (wasViewer && this.canContribute()) { + if (lichess.once('study-tour')) this.opts.startTour(); + this.opts.onBecomingContributor(); + this.opts.notif.set({ + text: this.opts.trans.noarg('youAreNowAContributor'), + duration: 3000, }); - confing(null); - }, - kick(id: string) { - opts.send('kick', id); - confing(null); - }, - leave() { - opts.send('leave'); - }, - ordered() { - const d = dict(); - return Object.keys(d) - .map(id => d[id]) - .sort((a, b) => (a.role === 'r' && b.role === 'w' ? 1 : a.role === 'w' && b.role === 'r' ? -1 : 0)); - }, - size() { - return Object.keys(dict()).length; - }, - isOnline(userId: string) { - return online[userId]; - }, - hasOnlineContributor() { - const members = dict(); - for (const i in members) if (online[i] && members[i].role === 'w') return true; - return false; - }, + } else if (wasContrib && !this.canContribute()) + this.opts.notif.set({ + text: this.opts.trans.noarg('youAreNowASpectator'), + duration: 3000, + }); + this.updateOnline(); + }; + setRole = (userId: string, role: string) => { + this.setActive(userId); + this.opts.send('setRole', { userId, role }); + this.confing(null); + }; + kick = (id: string) => { + this.opts.send('kick', id); + this.confing(null); + }; + leave = () => this.opts.send('leave'); + ordered = () => { + const d = this.dict(); + return Object.keys(d) + .map(id => d[id]) + .sort((a, b) => (a.role === 'r' && b.role === 'w' ? 1 : a.role === 'w' && b.role === 'r' ? -1 : 0)); + }; + size = () => Object.keys(this.dict()).length; + isOnline = (userId: string) => this.online[userId]; + hasOnlineContributor = () => { + const members = this.dict(); + for (const i in members) if (this.online[i] && members[i].role === 'w') return true; + return false; }; } @@ -180,7 +148,7 @@ export function view(ctrl: StudyCtrl): VNode { { class: { contrib, - active: members.isActive(member.user.id), + active: members.active.has(member.user.id), online: members.isOnline(member.user.id), }, attrs: { title: ctrl.trans.noarg(contrib ? 'contributor' : 'spectator') }, @@ -190,21 +158,18 @@ export function view(ctrl: StudyCtrl): VNode { } function configButton(ctrl: StudyCtrl, member: StudyMember) { - if (isOwner && (member.user.id !== members.myId || ctrl.data.admin)) + if (isOwner && (member.user.id !== members.opts.myId || ctrl.data.admin)) return h('i.act', { attrs: dataIcon(licon.Gear), hook: bind( 'click', - _ => members.confing(members.confing() == member.user.id ? null : member.user.id), + () => members.confing(members.confing() == member.user.id ? null : member.user.id), ctrl.redraw, ), }); - if (!isOwner && member.user.id === members.myId) + if (!isOwner && member.user.id === members.opts.myId) return h('i.act.leave', { - attrs: { - 'data-icon': licon.InternalArrow, - title: ctrl.trans.noarg('leaveTheStudy'), - }, + attrs: { 'data-icon': licon.InternalArrow, title: ctrl.trans.noarg('leaveTheStudy') }, hook: bind('click', members.leave, ctrl.redraw), }); return undefined; @@ -222,16 +187,10 @@ export function view(ctrl: StudyCtrl): VNode { h('div.role', [ h('div.switch', [ h('input.cmn-toggle', { - attrs: { - id: roleId, - type: 'checkbox', - checked: member.role === 'w', - }, + attrs: { id: roleId, type: 'checkbox', checked: member.role === 'w' }, hook: bind( 'change', - e => { - members.setRole(member.user.id, (e.target as HTMLInputElement).checked ? 'w' : 'r'); - }, + e => members.setRole(member.user.id, (e.target as HTMLInputElement).checked ? 'w' : 'r'), ctrl.redraw, ), }), @@ -243,10 +202,7 @@ export function view(ctrl: StudyCtrl): VNode { 'div.kick', h( 'a.button.button-red.button-empty.text', - { - attrs: dataIcon(licon.X), - hook: bind('click', _ => members.kick(member.user.id), ctrl.redraw), - }, + { attrs: dataIcon(licon.X), hook: bind('click', _ => members.kick(member.user.id), ctrl.redraw) }, ctrl.trans.noarg('kick'), ), ), @@ -256,59 +212,39 @@ export function view(ctrl: StudyCtrl): VNode { const ordered: StudyMember[] = members.ordered(); - return h( - 'div.study__members', - { - hook: onInsert(() => lichess.pubsub.emit('chat.resize')), - }, - [ - ...ordered - .map(member => { - const confing = members.confing() === member.user.id; - return [ - h( - 'div', - { - key: member.user.id, - class: { editing: !!confing }, - }, - [ - h('div.left', [statusIcon(member), userLink({ ...member.user, line: false })]), - configButton(ctrl, member), - ], - ), - confing ? memberConfig(member) : null, - ]; - }) - .reduce((a, b) => a.concat(b), []), - isOwner && ordered.length < members.max - ? h( - 'div.add', - { - key: 'add', - hook: bind('click', members.inviteForm.toggle), - }, - [ - h('div.left', [ - h('span.status', iconTag(licon.PlusButton)), - h('div.user-link', ctrl.trans.noarg('addMembers')), - ]), - ], - ) - : null, - !members.canContribute() && ctrl.data.admin - ? h( - 'form.admin', - { - key: ':admin', - hook: bindNonPassive('submit', () => { - xhrTextRaw(`/study/${ctrl.data.id}/admin`, { method: 'post' }).then(() => location.reload()); - return false; - }), - }, - [h('button.button.button-red.button-thin', 'Enter as admin')], - ) - : null, - ], - ); + return h('div.study__members', { hook: onInsert(() => lichess.pubsub.emit('chat.resize')) }, [ + ...ordered + .map(member => { + const confing = members.confing() === member.user.id; + return [ + h('div', { key: member.user.id, class: { editing: !!confing } }, [ + h('div.left', [statusIcon(member), userLink({ ...member.user, line: false })]), + configButton(ctrl, member), + ]), + confing && memberConfig(member), + ]; + }) + .reduce((a, b) => a.concat(b), []), + isOwner && + ordered.length < members.max && + h('div.add', { key: 'add', hook: bind('click', members.inviteForm.toggle) }, [ + h('div.left', [ + h('span.status', iconTag(licon.PlusButton)), + h('div.user-link', ctrl.trans.noarg('addMembers')), + ]), + ]), + !members.canContribute() && + ctrl.data.admin && + h( + 'form.admin', + { + key: ':admin', + hook: bindNonPassive('submit', () => { + xhrTextRaw(`/study/${ctrl.data.id}/admin`, { method: 'post' }).then(() => location.reload()); + return false; + }), + }, + [h('button.button.button-red.button-thin', 'Enter as admin')], + ), + ]); } diff --git a/ui/analyse/src/study/studySearch.ts b/ui/analyse/src/study/studySearch.ts index 95cfe032065bc..d70ef96ec7e21 100644 --- a/ui/analyse/src/study/studySearch.ts +++ b/ui/analyse/src/study/studySearch.ts @@ -68,30 +68,24 @@ export function view(ctrl: SearchCtrl) { // dynamic extra class necessary to fully redraw the results and produce innerHTML `div.study-search__results.search-query-${cleanQuery}`, ctrl.results().map(c => - h( - 'div', - { - hook: bind('click', () => ctrl.setChapter(c.id)), - }, - [ - h( - 'h3', - { - hook: highlightRegex - ? { - insert(vnode: VNode) { - const el = vnode.elm as HTMLElement; - el.innerHTML = c.name.replace(highlightRegex, '$&'); - }, - } - : {}, - }, - c.name, - ), - c.ongoing ? h('ongoing', { attrs: { ...dataIcon(licon.DiscBig), title: 'Ongoing' } }) : null, - !c.ongoing && c.res ? h('res', c.res) : null, - ], - ), + h('div', { hook: bind('click', () => ctrl.setChapter(c.id)) }, [ + h( + 'h3', + { + hook: highlightRegex + ? { + insert(vnode: VNode) { + const el = vnode.elm as HTMLElement; + el.innerHTML = c.name.replace(highlightRegex, '$&'); + }, + } + : {}, + }, + c.name, + ), + c.ongoing ? h('ongoing', { attrs: { ...dataIcon(licon.DiscBig), title: 'Ongoing' } }) : null, + !c.ongoing && c.res ? h('res', c.res) : null, + ]), ), ), ], diff --git a/ui/analyse/src/study/studyShare.ts b/ui/analyse/src/study/studyShare.ts index 9c2ea36b614d2..31b9ff75ab0a8 100644 --- a/ui/analyse/src/study/studyShare.ts +++ b/ui/analyse/src/study/studyShare.ts @@ -1,83 +1,53 @@ -import { prop, Prop } from 'common'; +import { prop } from 'common'; import * as licon from 'common/licon'; -import { bind, dataIcon } from 'common/snabbdom'; +import { bind, dataIcon, looseH as h } from 'common/snabbdom'; import { text as xhrText, url as xhrUrl } from 'common/xhr'; -import { h, VNode } from 'snabbdom'; +import { VNode } from 'snabbdom'; import { renderIndexAndMove } from '../view/moveView'; import { baseUrl } from '../view/util'; import { StudyChapterMeta, StudyData } from './interfaces'; import RelayCtrl from './relay/relayCtrl'; -export interface StudyShareCtrl { - studyId: string; - variantKey: VariantKey; - chapter: () => StudyChapterMeta; - bottomColor: () => Color; - isPrivate(): boolean; - currentNode: () => Tree.Node; - onMainline: () => boolean; - withPly: Prop; - relay: RelayCtrl | undefined; - cloneable(): boolean; - shareable(): boolean; - redraw: () => void; - trans: Trans; - gamebook: boolean; -} - -function fromPly(ctrl: StudyShareCtrl): VNode { - const renderedMove = renderIndexAndMove( - { - withDots: true, - showEval: false, - }, - ctrl.currentNode(), - ); +function fromPly(ctrl: StudyShare): VNode { + const renderedMove = renderIndexAndMove({ withDots: true, showEval: false }, ctrl.currentNode()); return h( 'div.ply-wrap', - ctrl.onMainline() - ? h('label.ply', [ - h('input', { - attrs: { type: 'checkbox', checked: ctrl.withPly() }, - hook: bind('change', e => ctrl.withPly((e.target as HTMLInputElement).checked), ctrl.redraw), - }), - ...(renderedMove - ? ctrl.trans.vdom('startAtX', h('strong', renderedMove)) - : [ctrl.trans.noarg('startAtInitialPosition')]), - ]) - : null, + ctrl.onMainline() && + h('label.ply', [ + h('input', { + attrs: { type: 'checkbox', checked: ctrl.withPly() }, + hook: bind('change', e => ctrl.withPly((e.target as HTMLInputElement).checked), ctrl.redraw), + }), + ...(renderedMove + ? ctrl.trans.vdom('startAtX', h('strong', renderedMove)) + : [ctrl.trans.noarg('startAtInitialPosition')]), + ]), ); } -export function ctrl( - data: StudyData, - currentChapter: () => StudyChapterMeta, - currentNode: () => Tree.Node, - onMainline: () => boolean, - bottomColor: () => Color, - relay: RelayCtrl | undefined, - redraw: () => void, - trans: Trans, -): StudyShareCtrl { - const withPly = prop(false); - return { - studyId: data.id, - variantKey: data.chapter.setup.variant.key as VariantKey, - chapter: currentChapter, - bottomColor, - isPrivate() { - return data.visibility === 'private'; - }, - currentNode, - onMainline, - withPly, - relay, - cloneable: () => data.features.cloneable, - shareable: () => data.features.shareable, - redraw, - trans, - gamebook: data.chapter.gamebook, - }; +export class StudyShare { + withPly = prop(false); + + constructor( + readonly data: StudyData, + readonly currentChapter: () => StudyChapterMeta, + readonly currentNode: () => Tree.Node, + readonly onMainline: () => boolean, + readonly bottomColor: () => Color, + readonly relay: RelayCtrl | undefined, + readonly redraw: () => void, + readonly trans: Trans, + ) {} + + studyId = this.data.id; + + variantKey = this.data.chapter.setup.variant.key as VariantKey; + + chapter = this.currentChapter; + isPrivate = () => this.data.visibility === 'private'; + cloneable = () => this.data.features.cloneable; + shareable = () => this.data.features.shareable; + gamebook = this.data.chapter.gamebook; } async function writePgnClipboard(url: string): Promise { @@ -94,14 +64,9 @@ async function writePgnClipboard(url: string): Promise { } const copyButton = (rel: string) => - h('button.button.copy', { - attrs: { - 'data-rel': rel, - ...dataIcon(licon.Clipboard), - }, - }); + h('button.button.copy', { attrs: { 'data-rel': rel, ...dataIcon(licon.Clipboard) } }); -export function view(ctrl: StudyShareCtrl): VNode { +export function view(ctrl: StudyShare): VNode { const studyId = ctrl.studyId, chapter = ctrl.chapter(); const isPrivate = ctrl.isPrivate(); @@ -118,18 +83,12 @@ export function view(ctrl: StudyShareCtrl): VNode { ctrl.shareable() ? [ h('div.downloads', [ - ctrl.cloneable() - ? h( - 'a.button.text', - { - attrs: { - ...dataIcon(licon.StudyBoard), - href: `/study/${studyId}/clone`, - }, - }, - ctrl.trans.noarg('cloneStudy'), - ) - : null, + ctrl.cloneable() && + h( + 'a.button.text', + { attrs: { ...dataIcon(licon.StudyBoard), href: `/study/${studyId}/clone` } }, + ctrl.trans.noarg('cloneStudy'), + ), ctrl.relay && h( 'a.button.text', @@ -199,7 +158,7 @@ export function view(ctrl: StudyShareCtrl): VNode { { attrs: { ...dataIcon(licon.Download), - href: xhrUrl(document.body.getAttribute('data-asset-url') + '/export/fen.gif', { + href: xhrUrl(lichess.asset.baseUrl() + '/export/fen.gif', { fen: ctrl.currentNode().fen, color: ctrl.bottomColor(), lastMove: ctrl.currentNode().uci, @@ -243,14 +202,12 @@ export function view(ctrl: StudyShareCtrl): VNode { h('label.form-label', ctrl.trans.noarg(i18n)), h('div.form-control-with-clipboard', [ h(`input#study-share-${i18n}.form-control.copyable.autoselect`, { - attrs: { - readonly: true, - value: `${baseUrl()}${path}`, - }, + attrs: { readonly: true, value: `${baseUrl()}${path}` }, }), copyButton(`study-share-${i18n}`), ]), - ...(pastable ? [fromPly(ctrl), !isPrivate ? youCanPasteThis() : null] : []), + pastable && fromPly(ctrl), + pastable && isPrivate && youCanPasteThis(), ]), ), h( @@ -297,10 +254,7 @@ export function view(ctrl: StudyShareCtrl): VNode { h('label.form-label', 'FEN'), h('div.form-control-with-clipboard', [ h('input#study-share-fen.form-control.copyable.autoselect', { - attrs: { - readonly: true, - value: ctrl.currentNode().fen, - }, + attrs: { readonly: true, value: ctrl.currentNode().fen }, }), copyButton(`study-share-fen`), ]), diff --git a/ui/analyse/src/study/studyTags.ts b/ui/analyse/src/study/studyTags.ts index b154be61c4e09..8184bac6e099e 100644 --- a/ui/analyse/src/study/studyTags.ts +++ b/ui/analyse/src/study/studyTags.ts @@ -1,49 +1,63 @@ import { onInsert } from 'common/snabbdom'; import throttle from 'common/throttle'; import { h, thunk, VNode } from 'snabbdom'; -import AnalyseCtrl from '../ctrl'; import { option } from '../view/util'; -import { StudyChapter, StudyCtrl } from './interfaces'; +import { StudyChapter } from './interfaces'; import { looksLikeLichessGame } from './studyChapters'; +import { prop } from 'common'; +import StudyCtrl from './studyCtrl'; -export interface TagsCtrl { - submit(type: string): (tag: string) => void; - getChapter(): StudyChapter; - types: string[]; +export class TagsForm { + selectedType = prop(undefined); + constructor( + private readonly root: StudyCtrl, // TODO should be root: StudyCtrl + readonly types: string[], + ) {} + + getChapter = () => this.root.data.chapter; + + private makeChange = throttle(500, (name: string, value: string) => { + this.root.makeChange('setTag', { + chapterId: this.getChapter().id, + name, + value: value.slice(0, 140), + }); + }); + + editable = () => this.root.vm.mode.write; + + submit = (name: string) => (value: string) => this.editable() && this.makeChange(name, value); +} + +export function view(root: StudyCtrl): VNode { + const chapter = root.tags.getChapter() as StudyChapter, + tagKey = chapter.tags.map(t => t[1]).join(','), + key = chapter.id + root.data.name + chapter.name + root.data.likes + tagKey + root.vm.mode.write; + return thunk('div.' + chapter.id, doRender, [root, key]); } -function editable(value: string, submit: (v: string, el: HTMLInputElement) => void): VNode { - return h('input', { +const doRender = (root: StudyCtrl): VNode => + h('div', renderPgnTags(root.tags, root.trans, root.data.hideRatings)); + +const editable = (value: string, submit: (v: string, el: HTMLInputElement) => void): VNode => + h('input', { key: value, // force to redraw on change, to visibly update the input value - attrs: { - spellcheck: 'false', - value, - }, + attrs: { spellcheck: 'false', value }, hook: onInsert(el => { - el.onblur = function () { - submit(el.value, el); - }; - el.onkeydown = function (e) { + el.onblur = () => submit(el.value, el); + el.onkeydown = e => { if (e.key === 'Enter') el.blur(); }; }), }); -} const fixed = (text: string) => h('span', text); -let selectedType: string; - type TagRow = (string | VNode)[]; -function renderPgnTags( - chapter: StudyChapter, - submit: ((type: string) => (tag: string) => void) | false, - types: string[], - trans: Trans, - hideRatings?: boolean, -): VNode { +function renderPgnTags(tags: TagsForm, trans: Trans, hideRatings?: boolean): VNode { let rows: TagRow[] = []; + const chapter = tags.getChapter(); if (chapter.setup.variant.key !== 'standard') rows.push(['Variant', fixed(chapter.setup.variant.name)]); rows = rows.concat( chapter.tags @@ -51,9 +65,9 @@ function renderPgnTags( tag => !hideRatings || !['WhiteElo', 'BlackElo'].includes(tag[0]) || !looksLikeLichessGame(chapter.tags), ) - .map(tag => [tag[0], submit ? editable(tag[1], submit(tag[0])) : fixed(tag[1])]), + .map(tag => [tag[0], tags.editable() ? editable(tag[1], tags.submit(tag[0])) : fixed(tag[1])]), ); - if (submit) { + if (tags.editable()) { const existingTypes = chapter.tags.map(t => t[0]); rows.push([ h( @@ -62,9 +76,9 @@ function renderPgnTags( hook: { insert: vnode => { const el = vnode.elm as HTMLInputElement; - selectedType = el.value; + tags.selectedType(el.value); el.addEventListener('change', _ => { - selectedType = el.value; + tags.selectedType(el.value); $(el) .parents('tr') .find('input') @@ -73,22 +87,18 @@ function renderPgnTags( }); }); }, - postpatch: (_, vnode) => { - selectedType = (vnode.elm as HTMLInputElement).value; - }, + postpatch: (_, vnode) => tags.selectedType((vnode.elm as HTMLInputElement).value), }, }, [ h('option', trans.noarg('newTag')), - ...types.map(t => { - if (!existingTypes.includes(t)) return option(t, '', t); - return undefined; - }), + ...tags.types.map(t => (!existingTypes.includes(t) ? option(t, '', t) : undefined)), ], ), editable('', (value, el) => { - if (selectedType) { - submit(selectedType)(value); + const tpe = tags.selectedType(); + if (tpe) { + tags.submit(tpe)(value); el.value = ''; } }), @@ -99,52 +109,7 @@ function renderPgnTags( 'table.study__tags.slist', h( 'tbody', - rows.map(function (r) { - return h( - 'tr', - { - key: '' + r[0], - }, - [h('th', [r[0]]), h('td', [r[1]])], - ); - }), - ), - ); -} - -export function ctrl(root: AnalyseCtrl, getChapter: () => StudyChapter, types: string[]): TagsCtrl { - const submit = throttle(500, function (name: string, value: string) { - root.study!.makeChange('setTag', { - chapterId: getChapter().id, - name, - value: value.slice(0, 140), - }); - }); - - return { - submit(name: string) { - return (value: string) => submit(name, value); - }, - getChapter, - types, - }; -} -function doRender(root: StudyCtrl): VNode { - return h( - 'div', - renderPgnTags( - root.tags.getChapter(), - root.vm.mode.write && root.tags.submit, - root.tags.types, - root.trans, - root.data.hideRatings, + rows.map(r => h('tr', { key: '' + r[0] }, [h('th', [r[0]]), h('td', [r[1]])])), ), ); } - -export function view(root: StudyCtrl): VNode { - const chapter = root.tags.getChapter() as StudyChapter, - tagKey = chapter.tags.map(t => t[1]).join(','), - key = chapter.id + root.data.name + chapter.name + root.data.likes + tagKey + root.vm.mode.write; - return thunk('div.' + chapter.id, doRender, [root, key]); -} diff --git a/ui/analyse/src/study/studyTour.ts b/ui/analyse/src/study/studyTour.ts index 0dbd8ae270593..1233d8757bdb3 100644 --- a/ui/analyse/src/study/studyTour.ts +++ b/ui/analyse/src/study/studyTour.ts @@ -3,7 +3,7 @@ import { Tab } from './interfaces'; export function study(ctrl: AnalyseCtrl) { if (!ctrl.study?.data.chapter.gamebook) - lichess.loadIife('javascripts/study/tour.js').then(() => { + lichess.asset.loadIife('javascripts/study/tour.js').then(() => { window.lichess.studyTour({ userId: ctrl.opts.userId, isContrib: ctrl.study!.members.canContribute(), @@ -21,7 +21,7 @@ export function study(ctrl: AnalyseCtrl) { } export const chapter = (setTab: (tab: string) => void) => - lichess.loadIife('javascripts/study/tour-chapter.js').then(() => { + lichess.asset.loadIife('javascripts/study/tour-chapter.js').then(() => { window.lichess.studyTourChapter({ setTab, }); diff --git a/ui/analyse/src/study/studyView.ts b/ui/analyse/src/study/studyView.ts index 901af931eed42..6f6f61dad55aa 100644 --- a/ui/analyse/src/study/studyView.ts +++ b/ui/analyse/src/study/studyView.ts @@ -2,12 +2,12 @@ import * as commentForm from './commentForm'; import * as glyphForm from './studyGlyph'; import * as practiceView from './practice/studyPracticeView'; import AnalyseCtrl from '../ctrl'; -import { h, VNode } from 'snabbdom'; +import { VNode } from 'snabbdom'; import * as licon from 'common/licon'; -import { iconTag, bind, dataIcon, MaybeVNodes } from 'common/snabbdom'; +import { iconTag, bind, dataIcon, MaybeVNodes, looseH as h } from 'common/snabbdom'; import { playButtons as gbPlayButtons, overrideButton as gbOverrideButton } from './gamebook/gamebookButtons'; import { rounds as relayTourRounds } from './relay/relayTourView'; -import { StudyCtrl, Tab, ToolTab } from './interfaces'; +import { Tab, ToolTab } from './interfaces'; import { view as chapterEditFormView } from './chapterEditForm'; import { view as chapterNewFormView } from './chapterNewForm'; import { view as chapterView } from './studyChapters'; @@ -22,6 +22,7 @@ import { view as studyShareView } from './studyShare'; import { view as tagsView } from './studyTags'; import { view as topicsView, formView as topicsFormView } from './topics'; import { view as searchView } from './studySearch'; +import StudyCtrl from './studyCtrl'; interface ToolButtonOpts { ctrl: StudyCtrl; @@ -47,7 +48,7 @@ function toolButton(opts: ToolButtonOpts): VNode { opts.ctrl.redraw, ), }, - [opts.count ? h('count.data-count', { attrs: { 'data-count': opts.count } }) : null, opts.icon], + [!!opts.count && h('count.data-count', { attrs: { 'data-count': opts.count } }), opts.icon], ); } @@ -59,34 +60,27 @@ function buttons(root: AnalyseCtrl): VNode { return h('div.study__buttons', [ h('div.left-buttons.tabs-horiz', { attrs: { role: 'tablist' } }, [ // distinct classes (sync, write) allow snabbdom to differentiate buttons - showSticky - ? h( - 'a.mode.sync', - { - attrs: { title: noarg('allSyncMembersRemainOnTheSamePosition') }, - class: { on: ctrl.vm.mode.sticky }, - hook: bind('click', ctrl.toggleSticky), - }, - [ctrl.vm.behind ? h('span.behind', '' + ctrl.vm.behind) : h('i.is'), 'SYNC'], - ) - : null, - ctrl.members.canContribute() - ? h( - 'a.mode.write', - { - attrs: { title: noarg('shareChanges') }, - class: { on: ctrl.vm.mode.write }, - hook: bind('click', ctrl.toggleWrite), - }, - [h('i.is'), 'REC'], - ) - : null, - toolButton({ - ctrl, - tab: 'tags', - hint: noarg('pgnTags'), - icon: iconTag(licon.Tag), - }), + !!showSticky && + h( + 'a.mode.sync', + { + attrs: { title: noarg('allSyncMembersRemainOnTheSamePosition') }, + class: { on: ctrl.vm.mode.sticky }, + hook: bind('click', ctrl.toggleSticky), + }, + [ctrl.vm.behind ? h('span.behind', '' + ctrl.vm.behind) : h('i.is'), 'SYNC'], + ), + ctrl.members.canContribute() && + h( + 'a.mode.write', + { + attrs: { title: noarg('shareChanges') }, + class: { on: ctrl.vm.mode.write }, + hook: bind('click', ctrl.toggleWrite), + }, + [h('i.is'), 'REC'], + ), + toolButton({ ctrl, tab: 'tags', hint: noarg('pgnTags'), icon: iconTag(licon.Tag) }), toolButton({ ctrl, tab: 'comments', @@ -97,15 +91,14 @@ function buttons(root: AnalyseCtrl): VNode { }, count: (root.node.comments || []).length, }), - canContribute - ? toolButton({ - ctrl, - tab: 'glyphs', - hint: noarg('annotateWithGlyphs'), - icon: h('i.glyph-icon'), - count: (root.node.glyphs || []).length, - }) - : null, + canContribute && + toolButton({ + ctrl, + tab: 'glyphs', + hint: noarg('annotateWithGlyphs'), + icon: h('i.glyph-icon'), + count: (root.node.glyphs || []).length, + }), toolButton({ ctrl, tab: 'serverEval', @@ -113,24 +106,14 @@ function buttons(root: AnalyseCtrl): VNode { icon: iconTag(licon.BarChart), count: root.data.analysis && '✓', }), - toolButton({ - ctrl, - tab: 'multiBoard', - hint: 'Multiboard', - icon: iconTag(licon.Multiboard), - }), - toolButton({ - ctrl, - tab: 'share', - hint: noarg('shareAndExport'), - icon: iconTag(licon.NodeBranching), - }), - !ctrl.relay && !ctrl.data.chapter.gamebook - ? h('span.help', { - attrs: { title: 'Need help? Get the tour!', ...dataIcon(licon.InfoCircle) }, - hook: bind('click', ctrl.startTour), - }) - : null, + toolButton({ ctrl, tab: 'multiBoard', hint: 'Multiboard', icon: iconTag(licon.Multiboard) }), + toolButton({ ctrl, tab: 'share', hint: noarg('shareAndExport'), icon: iconTag(licon.NodeBranching) }), + !ctrl.relay && + !ctrl.data.chapter.gamebook && + h('span.help', { + attrs: { title: 'Need help? Get the tour!', ...dataIcon(licon.InfoCircle) }, + hook: bind('click', ctrl.startTour), + }), ]), h('div.right', [gbOverrideButton(ctrl)]), ]); @@ -162,13 +145,14 @@ function metadata(ctrl: StudyCtrl): VNode { export function side(ctrl: StudyCtrl): VNode { const activeTab = ctrl.vm.tab(), - tourShow = ctrl.relay?.tourShow; + tourShow = ctrl.relay?.tourShow, + tourShown = !!tourShow && tourShow(); const makeTab = (key: Tab, name: string) => h( `span.${key}`, { - class: { active: !tourShow?.active && activeTab === key }, + class: { active: !tourShown && activeTab === key }, attrs: { role: 'tab' }, hook: bind('mousedown', () => ctrl.setTab(key)), }, @@ -180,52 +164,37 @@ export function side(ctrl: StudyCtrl): VNode { h( 'span.relay-tour.text', { - class: { active: tourShow.active }, - hook: bind( - 'mousedown', - () => { - tourShow.active = true; - }, - ctrl.redraw, - ), - attrs: { - ...dataIcon(licon.RadioTower), - role: 'tab', - }, + class: { active: tourShown }, + hook: bind('mousedown', () => tourShow(true), ctrl.redraw), + attrs: { ...dataIcon(licon.RadioTower), role: 'tab' }, }, 'Broadcast', ); const chaptersTab = - tourShow && ctrl.looksNew() && !ctrl.members.canContribute() - ? null - : makeTab( - 'chapters', - ctrl.trans.pluralSame(ctrl.relay ? 'nbGames' : 'nbChapters', ctrl.chapters.list().length), - ); + (tourShow && ctrl.looksNew() && !ctrl.members.canContribute()) || + makeTab( + 'chapters', + ctrl.trans.pluralSame(ctrl.relay ? 'nbGames' : 'nbChapters', ctrl.chapters.list().length), + ); const tabs = h('div.tabs-horiz', { attrs: { role: 'tablist' } }, [ tourTab, chaptersTab, - !tourTab || ctrl.members.canContribute() || ctrl.data.admin - ? makeTab('members', ctrl.trans.pluralSame('nbMembers', ctrl.members.size())) - : null, + (!tourTab || ctrl.members.canContribute() || ctrl.data.admin) && + makeTab('members', ctrl.trans.pluralSame('nbMembers', ctrl.members.size())), h('span.search.narrow', { - attrs: { - ...dataIcon(licon.Search), - title: 'Search', - }, + attrs: { ...dataIcon(licon.Search), title: 'Search' }, hook: bind('click', () => ctrl.search.open(true)), }), - ctrl.members.isOwner() - ? h('span.more.narrow', { - attrs: { ...dataIcon(licon.Hamburger), title: 'Edit study' }, - hook: bind('click', () => ctrl.form.open(!ctrl.form.open()), ctrl.redraw), - }) - : null, + ctrl.members.isOwner() && + h('span.more.narrow', { + attrs: { ...dataIcon(licon.Hamburger), title: 'Edit study' }, + hook: bind('click', () => ctrl.form.open(!ctrl.form.open()), ctrl.redraw), + }), ]); - const content = tourShow?.active + const content = tourShown ? relayTourRounds(ctrl) : (activeTab === 'members' ? memberView : chapterView)(ctrl); @@ -251,7 +220,7 @@ export function contextMenu(ctrl: StudyCtrl, path: Tree.Path, node: Tree.Node): { hook: bind('click', () => { ctrl.vm.toolTab('glyphs'); - ctrl.userJump(path); + ctrl.ctrl.userJump(path); }), }, ctrl.trans.noarg('annotateWithGlyphs'), @@ -261,14 +230,14 @@ export function contextMenu(ctrl: StudyCtrl, path: Tree.Path, node: Tree.Node): } export const overboard = (ctrl: StudyCtrl) => - ctrl.chapters.newForm.vm.open + ctrl.chapters.newForm.isOpen() ? chapterNewFormView(ctrl.chapters.newForm) : ctrl.chapters.editForm.current() ? chapterEditFormView(ctrl.chapters.editForm) : ctrl.members.inviteForm.open() ? inviteFormView(ctrl.members.inviteForm) : ctrl.topics.open() - ? topicsFormView(ctrl.topics, ctrl.members.myId) + ? topicsFormView(ctrl.topics, ctrl.members.opts.myId) : ctrl.form.open() ? studyFormView(ctrl.form) : ctrl.search.open() @@ -279,7 +248,7 @@ export function underboard(ctrl: AnalyseCtrl): MaybeVNodes { if (ctrl.studyPractice) return practiceView.underboard(ctrl.study!); const study = ctrl.study!, toolTab = study.vm.toolTab(); - if (study.gamebookPlay()) + if (study.gamebookPlay) return [gbPlayButtons(ctrl), descView(study, true), descView(study, false), metadata(study)]; let panel; switch (toolTab) { diff --git a/ui/analyse/src/study/studyXhr.ts b/ui/analyse/src/study/studyXhr.ts index 67578b767492d..45fa4255036df 100644 --- a/ui/analyse/src/study/studyXhr.ts +++ b/ui/analyse/src/study/studyXhr.ts @@ -1,4 +1,4 @@ -import { StudyChapterConfig, ReloadData } from './interfaces'; +import { StudyChapterConfig, ReloadData, ChapterPreview } from './interfaces'; import * as xhr from 'common/xhr'; export const reload = (baseUrl: string, id: string, chapterId?: string): Promise => { @@ -26,5 +26,9 @@ export const importPgn = (studyId: string, data: any) => body: xhr.form(data), }); -export const multiBoard = (studyId: string, page: number, playing: boolean) => +export const multiBoard = ( + studyId: string, + page: number, + playing: boolean, +): Promise> => xhr.json(`/study/${studyId}/multi-board?page=${page}&playing=${playing}`); diff --git a/ui/analyse/src/study/topics.ts b/ui/analyse/src/study/topics.ts index 1c30125092752..d0010d93c7350 100644 --- a/ui/analyse/src/study/topics.ts +++ b/ui/analyse/src/study/topics.ts @@ -5,7 +5,8 @@ import { bind, bindSubmit, onInsert } from 'common/snabbdom'; import * as xhr from 'common/xhr'; import { h, VNode } from 'snabbdom'; import { Redraw } from '../interfaces'; -import { StudyCtrl, Topic } from './interfaces'; +import { Topic } from './interfaces'; +import StudyCtrl from './studyCtrl'; export default class TopicsCtrl { open = prop(false); @@ -20,21 +21,15 @@ export default class TopicsCtrl { export const view = (ctrl: StudyCtrl): VNode => h('div.study__topics', [ - ...ctrl.topics.getTopics().map(topic => - h( - 'a.topic', - { - attrs: { href: `/study/topic/${encodeURIComponent(topic)}/hot` }, - }, - topic, + ...ctrl.topics + .getTopics() + .map(topic => + h('a.topic', { attrs: { href: `/study/topic/${encodeURIComponent(topic)}/hot` } }, topic), ), - ), ctrl.members.canContribute() ? h( 'a.manage', - { - hook: bind('click', () => ctrl.topics.open(true), ctrl.redraw), - }, + { hook: bind('click', () => ctrl.topics.open(true), ctrl.redraw) }, ctrl.trans.noarg('manageTopics'), ) : null, @@ -65,18 +60,10 @@ export const formView = (ctrl: TopicsCtrl, userId?: string): VNode => [ h( 'textarea', - { - hook: onInsert(elm => setupTagify(elm as HTMLTextAreaElement, userId)), - }, + { hook: onInsert(elm => setupTagify(elm as HTMLTextAreaElement, userId)) }, ctrl.getTopics().join(', ').replace(/[<>]/g, ''), ), - h( - 'button.button', - { - type: 'submit', - }, - ctrl.trans.noarg('save'), - ), + h('button.button', { type: 'submit' }, ctrl.trans.noarg('save')), ], ), ], @@ -87,12 +74,9 @@ export const formView = (ctrl: TopicsCtrl, userId?: string): VNode => }); function setupTagify(elm: HTMLInputElement | HTMLTextAreaElement, userId?: string) { - lichess.loadCssPath('tagify'); - lichess.loadIife('npm/tagify/tagify.min.js').then(() => { - const tagi = (tagify = new (window.Tagify as typeof Tagify)(elm, { - pattern: /.{2,}/, - maxTags: 30, - })); + lichess.asset.loadCssPath('tagify'); + lichess.asset.loadIife('npm/tagify/tagify.min.js').then(() => { + const tagi = (tagify = new (window.Tagify as typeof Tagify)(elm, { pattern: /.{2,}/, maxTags: 30 })); let abortCtrl: AbortController | undefined; // for aborting the call tagi.on('input', e => { const term = (e.detail as Tagify.TagData).value.trim(); diff --git a/ui/analyse/src/treeView/columnView.ts b/ui/analyse/src/treeView/columnView.ts index 53b0b9f5f9aa3..d34934db0eb23 100644 --- a/ui/analyse/src/treeView/columnView.ts +++ b/ui/analyse/src/treeView/columnView.ts @@ -1,6 +1,6 @@ -import { h, VNode } from 'snabbdom'; +import { VNode } from 'snabbdom'; import { isEmpty } from 'common'; -import { MaybeVNodes } from 'common/snabbdom'; +import { LooseVNodes, looseH as h } from 'common/snabbdom'; import { fixCrazySan } from 'chess'; import { path as treePath, ops as treeOps } from 'tree'; import * as moveView from '../view/moveView'; @@ -29,17 +29,11 @@ interface Opts extends BaseOpts { function emptyMove(conceal?: Conceal): VNode { const c: { conceal?: true; hide?: true } = {}; if (conceal) c[conceal] = true; - return h( - 'move.empty', - { - class: c, - }, - '...', - ); + return h('move.empty', { class: c }, '...'); } -function renderChildrenOf(ctx: Ctx, node: Tree.Node, opts: Opts): MaybeVNodes | undefined { - const cs = node.children, +function renderChildrenOf(ctx: Ctx, node: Tree.Node, opts: Opts): LooseVNodes | undefined { + const cs = node.children.filter(x => ctx.showComputer || !x.comp), main = cs[0]; if (!main) return; const conceal = opts.noConceal @@ -52,54 +46,41 @@ function renderChildrenOf(ctx: Ctx, node: Tree.Node, opts: Opts): MaybeVNodes | nonEmpty, ); if (!cs[1] && isEmpty(commentTags) && !main.forceVariation) - return ((isWhite ? [moveView.renderIndex(main.ply, false)] : []) as MaybeVNodes).concat( - renderMoveAndChildrenOf(ctx, main, { - parentPath: opts.parentPath, - isMainline: true, - conceal, - }) || [], - ); - const mainChildren = main.forceVariation - ? undefined - : renderChildrenOf(ctx, main, { - parentPath: opts.parentPath + main.id, - isMainline: true, - conceal, - }); - const passOpts = { - parentPath: opts.parentPath, - isMainline: !main.forceVariation, - conceal, - }; - return (isWhite ? [moveView.renderIndex(main.ply, false)] : ([] as MaybeVNodes)) - .concat( - main.forceVariation - ? [] - : [renderMoveOf(ctx, main, passOpts), isWhite ? emptyMove(passOpts.conceal) : null], - ) - .concat([ - h( - 'interrupt', - commentTags.concat( - renderLines(ctx, main.forceVariation ? cs : cs.slice(1), { - parentPath: opts.parentPath, - isMainline: passOpts.isMainline, - conceal, - noConceal: !conceal, - }), - ), + return [ + isWhite && moveView.renderIndex(main.ply, false), + ...renderMoveAndChildrenOf(ctx, main, { parentPath: opts.parentPath, isMainline: true, conceal }), + ]; + const mainChildren = + !main.forceVariation && + renderChildrenOf(ctx, main, { parentPath: opts.parentPath + main.id, isMainline: true, conceal }); + + const passOpts = { parentPath: opts.parentPath, isMainline: !main.forceVariation, conceal }; + + return [ + isWhite && moveView.renderIndex(main.ply, false), + !main.forceVariation && renderMoveOf(ctx, main, passOpts), + isWhite && !main.forceVariation && emptyMove(conceal), + h( + 'interrupt', + commentTags.concat( + renderLines(ctx, main.forceVariation ? cs : cs.slice(1), { + parentPath: opts.parentPath, + isMainline: passOpts.isMainline, + conceal, + noConceal: !conceal, + }), ), - ] as MaybeVNodes) - .concat( - isWhite && mainChildren ? [moveView.renderIndex(main.ply, false), emptyMove(passOpts.conceal)] : [], - ) - .concat(mainChildren || []); + ), + isWhite && mainChildren && moveView.renderIndex(main.ply, false), + isWhite && mainChildren && emptyMove(conceal), + ...(mainChildren || []), + ]; } if (!cs[1]) return renderMoveAndChildrenOf(ctx, main, opts); return renderInlined(ctx, cs, opts) || [renderLines(ctx, cs, opts)]; } -function renderInlined(ctx: Ctx, nodes: Tree.Node[], opts: Opts): MaybeVNodes | undefined { +function renderInlined(ctx: Ctx, nodes: Tree.Node[], opts: Opts): LooseVNodes | undefined { // only 2 branches if (!nodes[1] || nodes[2]) return; // only if second branch has no sub-branches @@ -115,9 +96,7 @@ function renderInlined(ctx: Ctx, nodes: Tree.Node[], opts: Opts): MaybeVNodes | function renderLines(ctx: Ctx, nodes: Tree.Node[], opts: Opts): VNode { return h( 'lines', - { - class: { single: !nodes[1] }, - }, + { class: { single: !nodes[1] } }, nodes.map(n => { return ( retroLine(ctx, n) || @@ -144,49 +123,26 @@ function renderMainlineMoveOf(ctx: Ctx, node: Tree.Node, opts: Opts): VNode { const path = opts.parentPath + node.id, classes = nodeClasses(ctx, node, path); if (opts.conceal) classes[opts.conceal as string] = true; - return h( - 'move', - { - attrs: { p: path }, - class: classes, - }, - moveView.renderMove(ctx, node), - ); + return h('move', { attrs: { p: path }, class: classes }, moveView.renderMove(ctx, node)); } function renderVariationMoveOf(ctx: Ctx, node: Tree.Node, opts: Opts): VNode { const withIndex = opts.withIndex || node.ply % 2 === 1, path = opts.parentPath + node.id, - content: MaybeVNodes = [withIndex ? moveView.renderIndex(node.ply, true) : null, fixCrazySan(node.san!)], + content: LooseVNodes = [withIndex && moveView.renderIndex(node.ply, true), fixCrazySan(node.san!)], classes = nodeClasses(ctx, node, path); if (opts.conceal) classes[opts.conceal as string] = true; if (node.glyphs) node.glyphs.forEach(g => content.push(moveView.renderGlyph(g))); - return h( - 'move', - { - attrs: { p: path }, - class: classes, - }, - content, - ); + return h('move', { attrs: { p: path }, class: classes }, content); } -function renderMoveAndChildrenOf(ctx: Ctx, node: Tree.Node, opts: Opts): MaybeVNodes { +function renderMoveAndChildrenOf(ctx: Ctx, node: Tree.Node, opts: Opts): LooseVNodes { const path = opts.parentPath + node.id; - if (opts.truncate === 0) - return [ - h( - 'move', - { - attrs: { p: path }, - }, - [h('index', '[...]')], - ), - ]; + if (opts.truncate === 0) return [h('move', { attrs: { p: path } }, [h('index', '[...]')])]; return [ renderMoveOf(ctx, node, opts), ...renderInlineCommentsOf(ctx, node, path), - opts.inline ? renderInline(ctx, opts.inline, opts) : null, + opts.inline && renderInline(ctx, opts.inline, opts), ...(renderChildrenOf(ctx, node, { parentPath: path, isMainline: opts.isMainline, @@ -215,7 +171,7 @@ function renderMainlineCommentsOf( conceal: Conceal, withColor: boolean, path: string, -): MaybeVNodes { +): LooseVNodes { if (!ctx.ctrl.showComments || isEmpty(node.comments)) return []; const colorClass = withColor ? (node.ply % 2 === 0 ? '.black ' : '.white ') : ''; @@ -242,29 +198,18 @@ export default function (ctrl: AnalyseCtrl, concealOf?: ConcealOf): VNode { ctrl, truncateComments: false, concealOf: concealOf || emptyConcealOf, - showComputer: ctrl.showComputer() && !ctrl.retro, + showComputer: ctrl.showComputer() && !ctrl.retro?.isSolving(), showGlyphs: !!ctrl.study || ctrl.showComputer(), showEval: ctrl.showComputer(), currentPath: findCurrentPath(ctrl), }; //I hardcoded the root path, I'm not sure if there's a better way for that to be done const commentTags = renderMainlineCommentsOf(ctx, root, false, false, ''); - return h( - 'div.tview2.tview2-column', - { - hook: mainHook(ctrl), - }, - ( - [ - isEmpty(commentTags) ? null : h('interrupt', commentTags), - root.ply & 1 ? moveView.renderIndex(root.ply, false) : null, - root.ply & 1 ? emptyMove() : null, - ] as MaybeVNodes - ).concat( - renderChildrenOf(ctx, root, { - parentPath: '', - isMainline: true, - }) || [], - ), - ); + const blackStarts = (root.ply & 1) === 1; + return h('div.tview2.tview2-column', { hook: mainHook(ctrl) }, [ + !isEmpty(commentTags) && h('interrupt', commentTags), + blackStarts && moveView.renderIndex(root.ply, false), + blackStarts && emptyMove(), + ...(renderChildrenOf(ctx, root, { parentPath: '', isMainline: true }) || []), + ]); } diff --git a/ui/analyse/src/treeView/common.ts b/ui/analyse/src/treeView/common.ts index a9cd232528517..27703b2eeec95 100644 --- a/ui/analyse/src/treeView/common.ts +++ b/ui/analyse/src/treeView/common.ts @@ -20,10 +20,7 @@ export function mainHook(ctrl: AnalyseCtrl): Hooks { const ctxMenuCallback = (e: MouseEvent) => { const path = eventPath(e); if (path !== null) { - contextMenu(e, { - path, - root: ctrl, - }); + contextMenu(e, { path, root: ctrl }); } ctrl.redraw(); return false; diff --git a/ui/analyse/src/treeView/contextMenu.ts b/ui/analyse/src/treeView/contextMenu.ts index f0395a9f62902..32c0e857c00f2 100644 --- a/ui/analyse/src/treeView/contextMenu.ts +++ b/ui/analyse/src/treeView/contextMenu.ts @@ -1,6 +1,6 @@ import * as licon from 'common/licon'; -import { bind, onInsert } from 'common/snabbdom'; -import { h, VNode } from 'snabbdom'; +import { bind, onInsert, looseH as h } from 'common/snabbdom'; +import { VNode } from 'snabbdom'; import AnalyseCtrl from '../ctrl'; import * as studyView from '../study/studyView'; import { patch, nodeFullName } from '../view/util'; @@ -28,11 +28,7 @@ const elementId = 'analyse-cm'; function getPosition(e: MouseEvent | TouchEvent): Coords | null { let pos = e as PageOrClientPos; if ('touches' in e && e.touches.length > 0) pos = e.touches[0]; - if (pos.pageX || pos.pageY) - return { - x: pos.pageX!, - y: pos.pageY!, - }; + if (pos.pageX || pos.pageY) return { x: pos.pageX!, y: pos.pageY! }; else if (pos.clientX || pos.clientY) return { x: pos.clientX! + document.body.scrollLeft + document.documentElement!.scrollLeft, @@ -57,14 +53,7 @@ function positionMenu(menu: HTMLElement, coords: Coords): void { } function action(icon: string, text: string, handler: () => void): VNode { - return h( - 'a', - { - attrs: { 'data-icon': icon }, - hook: bind('click', handler), - }, - text, - ); + return h('a', { attrs: { 'data-icon': icon }, hook: bind('click', handler) }, text); } function view(opts: Opts, coords: Coords): VNode { @@ -85,23 +74,23 @@ function view(opts: Opts, coords: Coords): VNode { }, [ h('p.title', nodeFullName(node)), - onMainline - ? null - : action(licon.UpTriangle, trans('promoteVariation'), () => ctrl.promote(opts.path, false)), - onMainline ? null : action(licon.Checkmark, trans('makeMainLine'), () => ctrl.promote(opts.path, true)), + + !onMainline && + action(licon.UpTriangle, trans('promoteVariation'), () => ctrl.promote(opts.path, false)), + + !onMainline && action(licon.Checkmark, trans('makeMainLine'), () => ctrl.promote(opts.path, true)), + action(licon.Trash, trans('deleteFromHere'), () => ctrl.deleteNode(opts.path)), - ] - .concat(ctrl.study ? studyView.contextMenu(ctrl.study, opts.path, node) : []) - .concat([ - onMainline - ? action(licon.InternalArrow, trans('forceVariation'), () => ctrl.forceVariation(opts.path, true)) - : null, - ]) - .concat([ - action(licon.Clipboard, trans('copyVariationPgn'), () => - navigator.clipboard.writeText(renderVariationPgn(opts.root.tree.getNodeList(opts.path))), - ), - ]), + + ...(ctrl.study ? studyView.contextMenu(ctrl.study, opts.path, node) : []), + + onMainline && + action(licon.InternalArrow, trans('forceVariation'), () => ctrl.forceVariation(opts.path, true)), + + action(licon.Clipboard, trans('copyVariationPgn'), () => + navigator.clipboard.writeText(renderVariationPgn(opts.root.tree.getNodeList(opts.path))), + ), + ], ); } diff --git a/ui/analyse/src/treeView/inlineView.ts b/ui/analyse/src/treeView/inlineView.ts index 475597446559f..1834e60c6a856 100644 --- a/ui/analyse/src/treeView/inlineView.ts +++ b/ui/analyse/src/treeView/inlineView.ts @@ -15,7 +15,7 @@ import { } from './common'; function renderChildrenOf(ctx: Ctx, node: Tree.Node, opts: Opts): MaybeVNodes | undefined { - const cs = node.children, + const cs = node.children.filter(x => ctx.showComputer || !x.comp), main = cs[0]; if (!main) return; if (opts.isMainline) { @@ -112,11 +112,7 @@ function renderInline(ctx: Ctx, node: Tree.Node, opts: Opts): VNode { if (retro) return h('interrupt', h('lines', retro)); return h( 'inline', - renderMoveAndChildrenOf(ctx, node, { - withIndex: true, - parentPath: opts.parentPath, - isMainline: false, - }), + renderMoveAndChildrenOf(ctx, node, { withIndex: true, parentPath: opts.parentPath, isMainline: false }), ); } @@ -127,36 +123,23 @@ function renderMoveOf(ctx: Ctx, node: Tree.Node, opts: Opts): VNode { fixCrazySan(node.san!), ]; if (node.glyphs && ctx.showGlyphs) node.glyphs.forEach(g => content.push(moveView.renderGlyph(g))); - return h( - 'move', - { - attrs: { p: path }, - class: nodeClasses(ctx, node, path), - }, - content, - ); + return h('move', { attrs: { p: path }, class: nodeClasses(ctx, node, path) }, content); } export default function (ctrl: AnalyseCtrl): VNode { const ctx: Ctx = { ctrl, truncateComments: false, - showComputer: ctrl.showComputer() && !ctrl.retro, + showComputer: ctrl.showComputer() && !ctrl.retro?.isSolving(), showGlyphs: !!ctrl.study || ctrl.showComputer(), showEval: !!ctrl.study || ctrl.showComputer(), currentPath: findCurrentPath(ctrl), }; - return h( - 'div.tview2.tview2-inline', - { - hook: mainHook(ctrl), - }, - [ - ...renderInlineCommentsOf(ctx, ctrl.tree.root, ''), - ...(renderChildrenOf(ctx, ctrl.tree.root, { - parentPath: '', - isMainline: true, - }) || []), - ], - ); + return h('div.tview2.tview2-inline', { hook: mainHook(ctrl) }, [ + ...renderInlineCommentsOf(ctx, ctrl.tree.root, ''), + ...(renderChildrenOf(ctx, ctrl.tree.root, { + parentPath: '', + isMainline: true, + }) || []), + ]); } diff --git a/ui/analyse/src/treeView/treeView.ts b/ui/analyse/src/treeView/treeView.ts index 7c72f844bd166..9ed878776a8f2 100644 --- a/ui/analyse/src/treeView/treeView.ts +++ b/ui/analyse/src/treeView/treeView.ts @@ -8,37 +8,22 @@ import { storedProp, StoredProp } from 'common/storage'; export type TreeViewKey = 'column' | 'inline'; -export interface TreeView { - get: StoredProp; - set(inline: boolean): void; - toggle(): void; - inline(): boolean; -} +export class TreeView { + value: StoredProp; -export function ctrl(initialValue: TreeViewKey = 'column'): TreeView { - const value = storedProp( - 'treeView', - initialValue, - str => str as TreeViewKey, - v => v, - ); - function inline() { - return value() === 'inline'; - } - function set(i: boolean) { - value(i ? 'inline' : 'column'); + constructor(initialValue: TreeViewKey = 'column') { + this.value = storedProp( + 'treeView', + initialValue, + str => str as TreeViewKey, + v => v, + ); } - return { - get: value, - set, - toggle() { - set(!inline()); - }, - inline, - }; + inline = () => this.value() === 'inline'; + set = (inline: boolean) => this.value(inline ? 'inline' : 'column'); + toggle = () => this.set(!this.inline()); } // entry point, dispatching to selected view -export function render(ctrl: AnalyseCtrl, concealOf?: ConcealOf): VNode { - return (ctrl.treeView.inline() || isCol1()) && !concealOf ? inline(ctrl) : column(ctrl, concealOf); -} +export const render = (ctrl: AnalyseCtrl, concealOf?: ConcealOf): VNode => + (ctrl.treeView.inline() || isCol1()) && !concealOf ? inline(ctrl) : column(ctrl, concealOf); diff --git a/ui/analyse/src/view/actionMenu.ts b/ui/analyse/src/view/actionMenu.ts index a9099c75f2d44..5f58884e982dc 100644 --- a/ui/analyse/src/view/actionMenu.ts +++ b/ui/analyse/src/view/actionMenu.ts @@ -2,8 +2,8 @@ import { isEmpty } from 'common'; import * as licon from 'common/licon'; import { domDialog } from 'common/dialog'; import { isTouchDevice } from 'common/device'; -import { bind, dataIcon, MaybeVNodes } from 'common/snabbdom'; -import { h, VNode } from 'snabbdom'; +import { bind, dataIcon, MaybeVNodes, looseH as h } from 'common/snabbdom'; +import { VNode } from 'snabbdom'; import { AutoplayDelay } from '../autoplay'; import { toggle, ToggleSettings } from 'common/controls'; import AnalyseCtrl from '../ctrl'; @@ -16,14 +16,8 @@ interface AutoplaySpeed { } const baseSpeeds: AutoplaySpeed[] = [ - { - name: 'fast', - delay: 1000, - }, - { - name: 'slow', - delay: 5000, - }, + { name: 'fast', delay: 1000 }, + { name: 'slow', delay: 5000 }, ]; const realtimeSpeed: AutoplaySpeed = { @@ -52,10 +46,7 @@ function autoplayButtons(ctrl: AnalyseCtrl): VNode { return h( 'a.button', { - class: { - active, - 'button-empty': !active, - }, + class: { active, 'button-empty': !active }, hook: bind('click', () => ctrl.togglePlay(speed.delay), ctrl.redraw), }, ctrl.trans.noarg(speed.name), @@ -84,10 +75,7 @@ function studyButton(ctrl: AnalyseCtrl) { return h( 'form', { - attrs: { - method: 'post', - action: '/study/as', - }, + attrs: { method: 'post', action: '/study/as' }, hook: bind('submit', e => { const pgnInput = (e.target as HTMLElement).querySelector('input[name=pgn]') as HTMLInputElement; if (pgnInput && (ctrl.synthetic || ctrl.persistence?.isDirty)) { @@ -96,21 +84,12 @@ function studyButton(ctrl: AnalyseCtrl) { }), }, [ - !ctrl.synthetic ? hiddenInput('gameId', ctrl.data.game.id) : null, + !ctrl.synthetic && hiddenInput('gameId', ctrl.data.game.id), hiddenInput('pgn', ''), hiddenInput('orientation', ctrl.bottomColor()), hiddenInput('variant', ctrl.data.game.variant.key), hiddenInput('fen', ctrl.tree.root.fen), - h( - 'button', - { - attrs: { - type: 'submit', - 'data-icon': licon.StudyBoard, - }, - }, - ctrl.trans.noarg('toStudy'), - ), + h('button', { attrs: { type: 'submit', 'data-icon': licon.StudyBoard } }, ctrl.trans.noarg('toStudy')), ], ); } @@ -126,61 +105,50 @@ export function view(ctrl: AnalyseCtrl): VNode { h('div.action-menu__tools', [ h( 'a', - { - hook: bind('click', ctrl.flip), - attrs: { - 'data-icon': licon.ChasingArrows, - title: 'Hotkey: f', - }, - }, + { hook: bind('click', ctrl.flip), attrs: { 'data-icon': licon.ChasingArrows, title: 'Hotkey: f' } }, noarg('flipBoard'), ), - ctrl.ongoing - ? null - : h( - 'a', - { - attrs: { - href: d.userAnalysis - ? '/editor?' + - new URLSearchParams({ - fen: ctrl.node.fen, - variant: d.game.variant.key, - color: ctrl.chessground.state.orientation, - }) - : `/${d.game.id}/edit?fen=${ctrl.node.fen}`, - 'data-icon': licon.Pencil, - rel: 'nofollow', - }, - }, - noarg('boardEditor'), - ), - canContinue - ? h( - 'a', - { - hook: bind('click', () => - domDialog({ cash: $('.continue-with.g_' + d.game.id), show: 'modal' }), - ), - attrs: dataIcon(licon.Swords), + !ctrl.ongoing && + h( + 'a', + { + attrs: { + href: d.userAnalysis + ? '/editor?' + + new URLSearchParams({ + fen: ctrl.node.fen, + variant: d.game.variant.key, + color: ctrl.chessground.state.orientation, + }) + : `/${d.game.id}/edit?fen=${ctrl.node.fen}`, + 'data-icon': licon.Pencil, + rel: 'nofollow', }, - noarg('continueFromHere'), - ) - : null, + }, + noarg('boardEditor'), + ), + canContinue && + h( + 'a', + { + hook: bind('click', () => domDialog({ cash: $('.continue-with.g_' + d.game.id), show: 'modal' })), + attrs: dataIcon(licon.Swords), + }, + noarg('continueFromHere'), + ), studyButton(ctrl), - ctrl.persistence?.isDirty - ? h( - 'a', - { - attrs: { - title: noarg('clearSavedMoves'), - 'data-icon': licon.Trash, - }, - hook: bind('click', ctrl.persistence.clear), + ctrl.persistence?.isDirty && + h( + 'a', + { + attrs: { + title: noarg('clearSavedMoves'), + 'data-icon': licon.Trash, }, - noarg('clearSavedMoves'), - ) - : null, + hook: bind('click', ctrl.persistence.clear), + }, + noarg('clearSavedMoves'), + ), ]), ]; @@ -240,30 +208,28 @@ export function view(ctrl: AnalyseCtrl): VNode { }, ctrl, ), - isTouchDevice() - ? null - : ctrlToggle( - { - name: 'showVariationArrows', - title: 'Variation navigation arrows', - id: 'variationArrows', - checked: ctrl.variationArrowsProp(), - change: ctrl.toggleVariationArrows, - }, - ctrl, - ), - ctrl.ongoing - ? null - : ctrlToggle( - { - name: 'Annotations on board', - title: 'Display analysis symbols on the board', - id: 'move-annotation', - checked: ctrl.showMoveAnnotation(), - change: ctrl.toggleMoveAnnotation, - }, - ctrl, - ), + !isTouchDevice() && + ctrlToggle( + { + name: 'showVariationArrows', + title: 'Variation navigation arrows', + id: 'variationArrows', + checked: ctrl.variationArrowsProp(), + change: ctrl.toggleVariationArrows, + }, + ctrl, + ), + !ctrl.ongoing && + ctrlToggle( + { + name: 'Annotations on board', + title: 'Display analysis symbols on the board', + id: 'move-annotation', + checked: ctrl.showMoveAnnotation(), + change: ctrl.toggleMoveAnnotation, + }, + ctrl, + ), ]; return h('div.action-menu', [ @@ -271,33 +237,32 @@ export function view(ctrl: AnalyseCtrl): VNode { ...displayConfig, ...cevalConfig, ...(ctrl.mainline.length > 4 ? [h('h2', noarg('replayMode')), autoplayButtons(ctrl)] : []), - canContinue - ? h('div.continue-with.none.g_' + d.game.id, [ - h( - 'a.button', - { - attrs: { - href: d.userAnalysis - ? '/?fen=' + ctrl.encodeNodeFen() + '#ai' - : contRoute(d, 'ai') + '?fen=' + ctrl.node.fen, - rel: 'nofollow', - }, + canContinue && + h('div.continue-with.none.g_' + d.game.id, [ + h( + 'a.button', + { + attrs: { + href: d.userAnalysis + ? '/?fen=' + ctrl.encodeNodeFen() + '#ai' + : contRoute(d, 'ai') + '?fen=' + ctrl.node.fen, + rel: 'nofollow', }, - noarg('playWithTheMachine'), - ), - h( - 'a.button', - { - attrs: { - href: d.userAnalysis - ? '/?fen=' + ctrl.encodeNodeFen() + '#friend' - : contRoute(d, 'friend') + '?fen=' + ctrl.node.fen, - rel: 'nofollow', - }, + }, + noarg('playWithTheMachine'), + ), + h( + 'a.button', + { + attrs: { + href: d.userAnalysis + ? '/?fen=' + ctrl.encodeNodeFen() + '#friend' + : contRoute(d, 'friend') + '?fen=' + ctrl.node.fen, + rel: 'nofollow', }, - noarg('playWithAFriend'), - ), - ]) - : null, + }, + noarg('playWithAFriend'), + ), + ]), ]); } diff --git a/ui/analyse/src/view/clocks.ts b/ui/analyse/src/view/clocks.ts index 174bc3583d9c9..74a2bb4afcc1b 100644 --- a/ui/analyse/src/view/clocks.ts +++ b/ui/analyse/src/view/clocks.ts @@ -34,13 +34,7 @@ export default function renderClocks(ctrl: AnalyseCtrl): [VNode, VNode] | undefi } const renderClock = (centis: number | undefined, active: boolean, cls: string, showTenths: boolean): VNode => - h( - 'div.analyse__clock.' + cls, - { - class: { active }, - }, - clockContent(centis, showTenths), - ); + h('div.analyse__clock.' + cls, { class: { active } }, clockContent(centis, showTenths)); function clockContent(centis: number | undefined, showTenths: boolean): Array { if (!centis && centis !== 0) return ['-']; diff --git a/ui/analyse/src/view/moveView.ts b/ui/analyse/src/view/moveView.ts index e3c307ead0864..49dab721ead25 100644 --- a/ui/analyse/src/view/moveView.ts +++ b/ui/analyse/src/view/moveView.ts @@ -11,13 +11,7 @@ export interface Ctx { } export const renderGlyph = (glyph: Tree.Glyph): VNode => - h( - 'glyph', - { - attrs: { title: glyph.name }, - }, - glyph.symbol, - ); + h('glyph', { attrs: { title: glyph.name } }, glyph.symbol); const renderEval = (e: string): VNode => h('eval', e.replace('-', '−')); diff --git a/ui/analyse/src/view/roundTraining.ts b/ui/analyse/src/view/roundTraining.ts index 32989434f1ba6..ca89738fbea85 100644 --- a/ui/analyse/src/view/roundTraining.ts +++ b/ui/analyse/src/view/roundTraining.ts @@ -17,13 +17,11 @@ interface Advice { const renderPlayer = (ctrl: AnalyseCtrl, color: Color): VNode => { const p = game.getPlayer(ctrl.data, color); if (p.user) - return h( - 'a.user-link.ulpt', - { - attrs: { href: '/@/' + p.user.username }, - }, - [p.user.username, ' ', ratingDiff(p)], - ); + return h('a.user-link.ulpt', { attrs: { href: '/@/' + p.user.username } }, [ + p.user.username, + ' ', + ratingDiff(p), + ]); return h( 'span', p.name || @@ -56,11 +54,7 @@ function playerTable(ctrl: AnalyseCtrl, color: Color): VNode { ctrl.trans.noarg('accuracy'), ' ', h('a', { - attrs: { - 'data-icon': licon.InfoCircle, - href: '/page/accuracy', - target: '_blank', - }, + attrs: { 'data-icon': licon.InfoCircle, href: '/page/accuracy', target: '_blank' }, }), ]), ]), diff --git a/ui/analyse/src/view/util.ts b/ui/analyse/src/view/util.ts index 3637affc6944a..030f3b104ae57 100644 --- a/ui/analyse/src/view/util.ts +++ b/ui/analyse/src/view/util.ts @@ -21,13 +21,4 @@ export function titleNameToId(titleName: string): string { } export const option = (value: string, current: string | undefined, name: string) => - h( - 'option', - { - attrs: { - value: value, - selected: value === current, - }, - }, - name, - ); + h('option', { attrs: { value: value, selected: value === current } }, name); diff --git a/ui/analyse/src/view/view.ts b/ui/analyse/src/view/view.ts index 55fe702c2f422..6534e1e6c9a1b 100644 --- a/ui/analyse/src/view/view.ts +++ b/ui/analyse/src/view/view.ts @@ -2,13 +2,13 @@ import { view as cevalView } from 'ceval'; import { parseFen } from 'chessops/fen'; import { defined } from 'common'; import * as licon from 'common/licon'; -import { bind, bindNonPassive, onInsert, dataIcon } from 'common/snabbdom'; +import { bind, bindNonPassive, onInsert, dataIcon, looseH as h, VNodeKids } from 'common/snabbdom'; import { bindMobileMousedown, isMobile } from 'common/device'; import { playable } from 'game'; import * as router from 'game/router'; import * as materialView from 'game/view/material'; import statusView from 'game/view/status'; -import { h, VNode, VNodeChildren } from 'snabbdom'; +import { VNode } from 'snabbdom'; import { path as treePath } from 'tree'; import { render as trainingView } from './roundTraining'; import { view as actionMenu } from './actionMenu'; @@ -27,13 +27,13 @@ import * as pgnExport from '../pgnExport'; import retroView from '../retrospect/retroView'; import practiceView from '../practice/practiceView'; import serverSideUnderboard from '../serverSideUnderboard'; -import { StudyCtrl } from '../study/interfaces'; import { render as renderTreeView } from '../treeView/treeView'; import { spinnerVdom as spinner } from 'common/spinner'; import { stepwiseScroll } from 'common/scroll'; import type * as studyDeps from '../study/studyDeps'; import { renderNextChapter } from '../study/nextChapter'; import * as Prefs from 'common/prefs'; +import StudyCtrl from '../study/studyCtrl'; function makeConcealOf(ctrl: AnalyseCtrl): ConcealOf | undefined { const conceal = @@ -53,10 +53,7 @@ function makeConcealOf(ctrl: AnalyseCtrl): ConcealOf | undefined { } const jumpButton = (icon: string, effect: string, enabled: boolean): VNode => - h('button.fbt', { - class: { disabled: !enabled }, - attrs: { 'data-act': effect, 'data-icon': icon }, - }); + h('button.fbt', { class: { disabled: !enabled }, attrs: { 'data-act': effect, 'data-icon': icon } }); const dataAct = (e: Event): string | null => { const target = e.target as HTMLElement; @@ -84,18 +81,15 @@ function inputs(ctrl: AnalyseCtrl): VNode | undefined { h('div.pair', [ h('label.name', 'FEN'), h('input.copyable.autoselect.analyse__underboard__fen', { - attrs: { - spellcheck: 'false', - enterkeyhint: 'done', - }, + attrs: { spellcheck: 'false', enterkeyhint: 'done' }, hook: { insert: vnode => { const el = vnode.elm as HTMLInputElement; el.value = defined(ctrl.fenInput) ? ctrl.fenInput : ctrl.node.fen; - el.addEventListener('change', _ => { + el.addEventListener('change', () => { if (el.value !== ctrl.node.fen && el.reportValidity()) ctrl.changeFen(el.value.trim()); }); - el.addEventListener('input', _ => { + el.addEventListener('input', () => { ctrl.fenInput = el.value; el.setCustomValidity(parseFen(el.value.trim()).isOk ? '' : 'Invalid FEN'); }); @@ -137,19 +131,18 @@ function inputs(ctrl: AnalyseCtrl): VNode | undefined { }, }, }), - isMobile() - ? null - : h( - 'button.button.button-thin.action.text', - { - attrs: dataIcon(licon.PlayTriangle), - hook: bind('click', _ => { - const pgn = $('.copyables .pgn textarea').val() as string; - if (pgn !== pgnExport.renderFullTxt(ctrl)) ctrl.changePgn(pgn, true); - }), - }, - ctrl.trans.noarg('importPgn'), - ), + !isMobile() && + h( + 'button.button.button-thin.action.text', + { + attrs: dataIcon(licon.PlayTriangle), + hook: bind('click', _ => { + const pgn = $('.copyables .pgn textarea').val() as string; + if (pgn !== pgnExport.renderFullTxt(ctrl)) ctrl.changePgn(pgn, true); + }), + }, + ctrl.trans.noarg('importPgn'), + ), ]), ]), ]); @@ -183,11 +176,7 @@ function controls(ctrl: AnalyseCtrl) { ctrl.studyPractice ? [ h('button.fbt', { - attrs: { - title: noarg('analysis'), - 'data-act': 'analysis', - 'data-icon': licon.Microscope, - }, + attrs: { title: noarg('analysis'), 'data-act': 'analysis', 'data-icon': licon.Microscope }, }), ] : [ @@ -202,19 +191,17 @@ function controls(ctrl: AnalyseCtrl) { active: ctrl.explorer.enabled(), }, }), - ctrl.ceval.possible && ctrl.ceval.allowed() && !ctrl.isGamebook() - ? h('button.fbt', { - attrs: { - title: noarg('practiceWithComputer'), - 'data-act': 'practice', - 'data-icon': licon.Bullseye, - }, - class: { - hidden: menuIsOpen || !!ctrl.retro, - active: !!ctrl.practice, - }, - }) - : null, + ctrl.ceval.possible && + ctrl.ceval.allowed() && + !ctrl.isGamebook() && + h('button.fbt', { + attrs: { + title: noarg('practiceWithComputer'), + 'data-act': 'practice', + 'data-icon': licon.Bullseye, + }, + class: { hidden: menuIsOpen || !!ctrl.retro, active: !!ctrl.practice }, + }), ], ), h('div.jumps', [ @@ -227,11 +214,7 @@ function controls(ctrl: AnalyseCtrl) { ? h('div.noop') : h('button.fbt', { class: { active: menuIsOpen }, - attrs: { - title: noarg('menu'), - 'data-act': 'menu', - 'data-icon': licon.Hamburger, - }, + attrs: { title: noarg('menu'), 'data-act': 'menu', 'data-icon': licon.Hamburger }, }), ], ); @@ -276,10 +259,7 @@ function renderPlayerStrips(ctrl: AnalyseCtrl): [VNode, VNode] | undefined { export default function (deps?: typeof studyDeps) { function renderResult(ctrl: AnalyseCtrl): VNode[] { - const render = (result: string, status: VNodeChildren) => [ - h('div.result', result), - h('div.status', status), - ]; + const render = (result: string, status: VNodeKids) => [h('div.result', result), h('div.status', status)]; if (ctrl.data.game.status.id >= 30) { let result; switch (ctrl.data.game.winner) { @@ -306,14 +286,14 @@ export default function (deps?: typeof studyDeps) { const renderAnalyse = (ctrl: AnalyseCtrl, concealOf?: ConcealOf) => h('div.analyse__moves.areplay', [ h(`div.areplay__v${ctrl.treeVersion}`, [renderTreeView(ctrl, concealOf), ...renderResult(ctrl)]), - !ctrl.practice && !deps?.gbEdit.running(ctrl) ? renderNextChapter(ctrl) : null, + !ctrl.practice && !deps?.gbEdit.running(ctrl) && renderNextChapter(ctrl), ]); return function (ctrl: AnalyseCtrl): VNode { if (ctrl.nvui) return ctrl.nvui.render(); const concealOf = makeConcealOf(ctrl), study = ctrl.study, - showCevalPvs = !(ctrl.retro && ctrl.retro.isSolving()) && !ctrl.practice, + showCevalPvs = !ctrl.retro?.isSolving() && !ctrl.practice, menuIsOpen = ctrl.actionMenu(), gamebookPlay = ctrl.gamebookPlay(), gamebookPlayView = gamebookPlay && deps?.gbPlay.render(gamebookPlay), @@ -367,8 +347,8 @@ export default function (deps?: typeof studyDeps) { }, }, [ - ctrl.keyboardHelp ? keyboardView(ctrl) : null, - study ? deps?.studyView.overboard(study) : null, + ctrl.keyboardHelp && keyboardView(ctrl), + study && deps?.studyView.overboard(study), tour || h( addChapterId(study, 'div.analyse__board.main-board'), @@ -396,44 +376,42 @@ export default function (deps?: typeof studyDeps) { }, [ ...(playerStrips || []), - playerBars ? playerBars[ctrl.bottomIsWhite() ? 1 : 0] : null, + playerBars?.[ctrl.bottomIsWhite() ? 1 : 0], chessground.render(ctrl), - playerBars ? playerBars[ctrl.bottomIsWhite() ? 0 : 1] : null, + playerBars?.[ctrl.bottomIsWhite() ? 0 : 1], ctrl.promotion.view(ctrl.data.game.variant.key === 'antichess'), ], ), - gaugeOn && !tour ? cevalView.renderGauge(ctrl) : null, - menuIsOpen || tour ? null : crazyView(ctrl, ctrl.topColor(), 'top'), + gaugeOn && !tour && cevalView.renderGauge(ctrl), + !menuIsOpen && !tour && crazyView(ctrl, ctrl.topColor(), 'top'), gamebookPlayView || - (tour - ? null - : h(addChapterId(study, 'div.analyse__tools'), [ - ...(menuIsOpen - ? [actionMenu(ctrl)] - : [ - ...cevalView.renderCeval(ctrl), - showCevalPvs ? cevalView.renderPvs(ctrl) : null, - renderAnalyse(ctrl, concealOf), - gamebookEditView, - forkView(ctrl, concealOf), - retroView(ctrl) || practiceView(ctrl) || explorerView(ctrl), - ]), - ])), - menuIsOpen || tour ? null : crazyView(ctrl, ctrl.bottomColor(), 'bottom'), - gamebookPlayView || tour ? null : controls(ctrl), - tour - ? null - : h( - 'div.analyse__underboard', - { - hook: - ctrl.synthetic || playable(ctrl.data) - ? undefined - : onInsert(elm => serverSideUnderboard(elm, ctrl)), - }, - study ? deps?.studyView.underboard(ctrl) : [inputs(ctrl)], - ), - tour ? null : trainingView(ctrl), + (!tour && + h(addChapterId(study, 'div.analyse__tools'), [ + ...(menuIsOpen + ? [actionMenu(ctrl)] + : [ + ...cevalView.renderCeval(ctrl), + showCevalPvs && cevalView.renderPvs(ctrl), + renderAnalyse(ctrl, concealOf), + gamebookEditView, + forkView(ctrl, concealOf), + retroView(ctrl) || practiceView(ctrl) || explorerView(ctrl), + ]), + ])), + !menuIsOpen && !tour && crazyView(ctrl, ctrl.bottomColor(), 'bottom'), + !gamebookPlayView && !tour && controls(ctrl), + !tour && + h( + 'div.analyse__underboard', + { + hook: + ctrl.synthetic || playable(ctrl.data) + ? undefined + : onInsert(elm => serverSideUnderboard(elm, ctrl)), + }, + study ? deps?.studyView.underboard(ctrl) : [inputs(ctrl)], + ), + !tour && trainingView(ctrl), ctrl.studyPractice ? deps?.studyPracticeView.side(study!) : h( @@ -449,28 +427,26 @@ export default function (deps?: typeof studyDeps) { : study ? [deps?.studyView.side(study)] : [ - ctrl.forecast ? forecastView(ctrl, ctrl.forecast) : null, - !ctrl.synthetic && playable(ctrl.data) - ? h( - 'div.back-to-game', - h( - 'a.button.button-empty.text', - { - attrs: { - href: router.game(ctrl.data, ctrl.data.player.color), - 'data-icon': licon.Back, - }, + ctrl.forecast && forecastView(ctrl, ctrl.forecast), + !ctrl.synthetic && + playable(ctrl.data) && + h( + 'div.back-to-game', + h( + 'a.button.button-empty.text', + { + attrs: { + href: router.game(ctrl.data, ctrl.data.player.color), + 'data-icon': licon.Back, }, - ctrl.trans.noarg('backToGame'), - ), - ) - : null, + }, + ctrl.trans.noarg('backToGame'), + ), + ), ], ), study && study.relay && deps?.relayManager(study.relay), - h('div.chat__members.none', { - hook: onInsert(lichess.watchers), - }), + h('div.chat__members.none', { hook: onInsert(lichess.watchers) }), ], ); }; diff --git a/ui/board/src/menu.ts b/ui/board/src/menu.ts index b82b0d749bfc7..3780782fd56b3 100644 --- a/ui/board/src/menu.ts +++ b/ui/board/src/menu.ts @@ -9,10 +9,7 @@ import * as controls from 'common/controls'; export const toggleButton = (toggle: Toggle, title: string) => h('button.fbt.board-menu-toggle', { class: { active: toggle() }, - attrs: { - title, - 'data-icon': licon.Hamburger, - }, + attrs: { title, 'data-icon': licon.Hamburger }, hook: onInsert(bindMobileMousedown(toggle.toggle)), }); @@ -25,9 +22,7 @@ export const menu = ( toggle() ? h( 'div.board-menu', - { - hook: onInsert(onClickAway(() => toggle(false))), - }, + { hook: onInsert(onClickAway(() => toggle(false))) }, content(new BoardMenu(trans, redraw)), ) : undefined; @@ -45,10 +40,7 @@ export class BoardMenu { 'button.button.text', { class: { active }, - attrs: { - title: 'Hotkey: f', - ...dataIcon(licon.ChasingArrows), - }, + attrs: { title: 'Hotkey: f', ...dataIcon(licon.ChasingArrows) }, hook: onInsert(bindMobileMousedown(onChange)), }, name, diff --git a/ui/ceval/css/_settings.scss b/ui/ceval/css/_settings.scss index c65772ac388bd..8a9f6deb7bee2 100644 --- a/ui/ceval/css/_settings.scss +++ b/ui/ceval/css/_settings.scss @@ -35,11 +35,12 @@ margin-#{$start-direction}: 1ch; } - input[type='range'] { + input[type='range'], + span { cursor: pointer; flex: 1 4 auto; padding: 0; - height: 1.6em; + height: 21px; // important width: 100%; margin: 0 1ch; } @@ -81,5 +82,52 @@ outline: 2px solid #bbb; } } + + span { + position: relative; + + input[type='range'] { + height: 100%; + margin: 0; + } + } + + .tick { + position: absolute; + top: -12.5px; // 12.5px above the track. track is 21px high + height: 46px; // extend 12.5px below the track + border-color: $c-primary; + + div { + position: absolute; + left: -4px; // centers the rotated right angle at x = 0 + width: 9px; + height: 9px; + border-left: 3px solid $c-primary; + border-bottom: 3px solid $c-primary; + + &:hover { + border-color: mix(#fff, $c-primary, 20%); + } + + &.arrow-down { + transform: rotate(-45deg); + top: 0; + } + + &.arrow-up { + transform: rotate(135deg); + bottom: 0; + } + } + + &.recommended div { + border-color: $c-secondary; + + &:hover { + border-color: mix(#fff, $c-secondary, 20%); + } + } + } } } diff --git a/ui/ceval/package.json b/ui/ceval/package.json index e02a1c75d86c1..307264068efd4 100644 --- a/ui/ceval/package.json +++ b/ui/ceval/package.json @@ -16,7 +16,7 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@badrap/result": "^0.2.13", - "chessops": "^0.12.7", + "chessops": "^0.13.0", "common": "workspace:*", "idb-keyval": "^6.2.1", "snabbdom": "^3.5.1", diff --git a/ui/ceval/src/ctrl.ts b/ui/ceval/src/ctrl.ts index afa7165bcb2b6..9a354e0492842 100644 --- a/ui/ceval/src/ctrl.ts +++ b/ui/ceval/src/ctrl.ts @@ -1,13 +1,12 @@ import throttle from 'common/throttle'; import { Engines } from './engines/engines'; import { CevalOpts, CevalState, CevalEngine, Work, Step, Hovering, PvBoard, Started } from './types'; -import { sanIrreversible, showEngineError } from './util'; +import { sanIrreversible, showEngineError, fewerCores, constrain } from './util'; import { defaultPosition, setupPosition } from 'chessops/variant'; import { parseFen } from 'chessops/fen'; import { lichessRules } from 'chessops/compat'; import { povChances } from './winningChances'; import { prop, readonlyProp, Prop, Toggle, toggle } from 'common'; -import { hasFeature } from 'common/device'; import { Result } from '@badrap/result'; import { storedIntProp } from 'common/storage'; import { Rules } from 'chessops'; @@ -176,25 +175,33 @@ export default class CevalCtrl { return this.worker?.getState() ?? CevalState.Initial; } + setThreads = (threads: number) => lichess.storage.set('ceval.threads', threads.toString()); + threads = () => { const stored = lichess.storage.get('ceval.threads'); - return Math.min( - this.engines.active?.maxThreads ?? 96, // Can haz threadripper? - stored ? parseInt(stored, 10) : Math.ceil(navigator.hardwareConcurrency / 4), - ); + const desired = stored ? parseInt(stored) : this.recommendedThreads(); + return constrain(desired, { min: this.engines.active?.minThreads ?? 1, max: this.maxThreads() }); }; - hashSize = () => { - const stored = lichess.storage.get('ceval.hash-size'); - return Math.min(this.engines.active?.maxHash ?? 16, stored ? parseInt(stored, 10) : 16); - }; + recommendedThreads = () => + this.engines.external?.maxThreads ?? + constrain(navigator.hardwareConcurrency - (navigator.hardwareConcurrency % 2 ? 0 : 1), { + min: this.engines.active?.minThreads ?? 1, + max: this.maxThreads(), + }); - setThreads = (threads: number) => lichess.storage.set('ceval.threads', threads.toString()); + maxThreads = () => + this.engines.external?.maxThreads ?? + (fewerCores() + ? Math.min(this.engines.active?.maxThreads ?? 32, navigator.hardwareConcurrency) + : this.engines.active?.maxThreads ?? 32); setHashSize = (hash: number) => lichess.storage.set('ceval.hash-size', hash.toString()); - maxThreads = () => - this.engines.external?.maxThreads ?? (hasFeature('sharedMem') ? navigator.hardwareConcurrency : 1); + hashSize = () => { + const stored = lichess.storage.get('ceval.hash-size'); + return Math.min(this.maxHash(), stored ? parseInt(stored, 10) : 16); + }; maxHash = () => this.engines.active?.maxHash ?? 16; diff --git a/ui/ceval/src/engines/engines.ts b/ui/ceval/src/engines/engines.ts index fcd706fe3597f..a2eee35784eb0 100644 --- a/ui/ceval/src/engines/engines.ts +++ b/ui/ceval/src/engines/engines.ts @@ -1,4 +1,4 @@ -import { BrowserEngineInfo, ExternalEngineInfo, EngineInfo, CevalEngine } from '../types'; +import { BrowserEngineInfo, ExternalEngineInfo, EngineInfo, CevalEngine, Requires } from '../types'; import CevalCtrl from '../ctrl'; import { LegacyBot } from './legacyBot'; import { SimpleEngine } from './simpleEngine'; @@ -6,19 +6,25 @@ import { StockfishWebEngine } from './stockfishWebEngine'; import { ThreadedEngine } from './threadedEngine'; import { ExternalEngine } from './externalEngine'; import { storedStringProp, StoredProp } from 'common/storage'; -import { isAndroid, isIOS, isIPad, hasFeature } from 'common/device'; +import { isAndroid, isIOS, isIPad, getFirefoxMajorVersion, features, Feature } from 'common/device'; import { xhrHeader } from 'common/xhr'; -import { pow2floor } from '../util'; import { lichessRules } from 'chessops/compat'; export class Engines { - private localEngines: BrowserEngineInfo[]; - private localEngineMap: Map; - private externalEngines: ExternalEngineInfo[]; - private selectProp: StoredProp; private _active: EngineInfo | undefined = undefined; + localEngines: BrowserEngineInfo[]; + localEngineMap: Map; + externalEngines: ExternalEngineInfo[]; + selectProp: StoredProp; + browserSupport: Requires[] = features().slice(); constructor(private ctrl?: CevalCtrl) { + if ( + ((getFirefoxMajorVersion() ?? 114) > 113 && !('brave' in navigator)) || + lichess.storage.get('ceval.lsfw.forceEnable') === 'true' + ) { + this.browserSupport.push('allowLsfw'); // lsfw is https://github.com/lichess-org/lila-stockfish-web + } this.localEngineMap = this.makeEngineMap(); this.localEngines = [...this.localEngineMap.values()].map(e => e.info); this.externalEngines = this.ctrl?.opts.externalEngines?.map(e => ({ tech: 'EXTERNAL', ...e })) ?? []; @@ -41,7 +47,7 @@ export class Engines { name: 'Fairy Stockfish 14+ NNUE', short: 'FSF 14+', tech: 'NNUE', - requires: ['simd', 'webWorkerDynamicImport'], + requires: ['simd', 'allowLsfw'], variants: [key], assets: { version: 'sfw003', @@ -69,7 +75,7 @@ export class Engines { name: 'Stockfish 16 NNUE · 7MB', short: 'SF 16 · 7MB', tech: 'NNUE', - requires: ['simd', 'webWorkerDynamicImport'], + requires: ['simd', 'allowLsfw'], minMem: 1536, assets: { version: 'sfw003', @@ -85,7 +91,7 @@ export class Engines { name: 'Stockfish 16 NNUE · 40MB', short: 'SF 16 · 40MB', tech: 'NNUE', - requires: ['simd', 'webWorkerDynamicImport'], + requires: ['simd', 'allowLsfw'], minMem: 2048, assets: { version: 'sfw003', @@ -119,7 +125,7 @@ export class Engines { name: 'Fairy Stockfish 14+ HCE', short: 'FSF 14+', tech: 'HCE', - requires: ['simd', 'webWorkerDynamicImport'], + requires: ['simd', 'allowLsfw'], variants: variants.map(v => v[0]), assets: { version: 'sfw003', @@ -137,6 +143,7 @@ export class Engines { short: 'SF 11 MV', tech: 'HCE', requires: ['sharedMem'], + minThreads: 1, variants: variants.map(v => v[0]), assets: { version: 'a022fa', @@ -157,6 +164,7 @@ export class Engines { short: 'SF 11', tech: 'HCE', requires: ['sharedMem'], + minThreads: 1, assets: { version: 'a022fa', root: 'npm/stockfish.wasm', @@ -189,6 +197,7 @@ export class Engines { name: 'Stockfish WASM', short: 'Stockfish', tech: 'HCE', + minThreads: 1, maxThreads: 1, requires: ['wasm'], obsoletedBy: 'sharedMem', @@ -206,6 +215,7 @@ export class Engines { name: 'Stockfish JS', short: 'Stockfish', tech: 'HCE', + minThreads: 1, maxThreads: 1, obsoletedBy: 'wasm', assets: { @@ -219,8 +229,8 @@ export class Engines { ] .filter( e => - (e.info.requires ?? []).map(req => hasFeature(req)).every(x => !!x) && - !(e.info.obsoletedBy && hasFeature(e.info.obsoletedBy)), + e.info.requires?.every((req: Requires) => this.browserSupport.includes(req)) && + !(e.info.obsoletedBy && this.browserSupport.includes(e.info.obsoletedBy as Feature)), ) .map(e => [e.info.id, { info: withDefaults(e.info as BrowserEngineInfo), make: e.make }]), ); @@ -293,11 +303,10 @@ export class Engines { } function maxHashMB() { - if (navigator.deviceMemory) return Math.min(1024, pow2floor(navigator.deviceMemory * 128)); - else if (isAndroid()) return 64; // budget androids are easy to crash @ 128 + if (isAndroid()) return 64; // budget androids are easy to crash @ 128 else if (isIPad()) return 64; // iPadOS safari pretends to be desktop but acts more like iphone else if (isIOS()) return 32; - return 256; // this is safe, mostly desktop firefox / mac safari users here + return 512; // allocating 1024 often fails and offers little benefit over 512, or 16 for that matter } const maxHash = maxHashMB(); @@ -311,9 +320,10 @@ function externalEngineSupports(e: EngineInfo, v: VariantKey) { const withDefaults = (engine: BrowserEngineInfo): BrowserEngineInfo => ({ variants: ['standard', 'chess960', 'fromPosition'], - maxThreads: navigator.hardwareConcurrency ?? 1, minMem: 1024, maxHash, + minThreads: 2, + maxThreads: 32, ...engine, }); diff --git a/ui/ceval/src/engines/externalEngine.ts b/ui/ceval/src/engines/externalEngine.ts index a5ee8471f33a4..a86c6917933e0 100644 --- a/ui/ceval/src/engines/externalEngine.ts +++ b/ui/ceval/src/engines/externalEngine.ts @@ -88,8 +88,8 @@ export class ExternalEngine extends LegacyBot implements CevalEngine { this.state = CevalState.Initial; this.status?.(); - } catch (err: unknown) { - if ((err as Error).name !== 'AbortError') { + } catch (err: any) { + if (err.name !== 'AbortError') { console.error(err); this.state = CevalState.Failed; this.status?.({ error: String(err) }); diff --git a/ui/ceval/src/engines/simpleEngine.ts b/ui/ceval/src/engines/simpleEngine.ts index ce46c5dbe1957..7439facd0747c 100644 --- a/ui/ceval/src/engines/simpleEngine.ts +++ b/ui/ceval/src/engines/simpleEngine.ts @@ -22,19 +22,19 @@ export class SimpleEngine extends LegacyBot implements CevalEngine { return !this.worker ? CevalState.Initial : this.failed - ? CevalState.Failed - : !this.protocol.engineName - ? CevalState.Loading - : this.protocol.isComputing() - ? CevalState.Computing - : CevalState.Idle; + ? CevalState.Failed + : !this.protocol.engineName + ? CevalState.Loading + : this.protocol.isComputing() + ? CevalState.Computing + : CevalState.Idle; } start(work: Work) { this.protocol.compute(work); if (!this.worker) { - this.worker = new Worker(lichess.assetUrl(this.url, { sameDomain: true })); + this.worker = new Worker(lichess.asset.url(this.url, { sameDomain: true })); this.worker.addEventListener('message', e => this.protocol.received(e.data), true); this.worker.addEventListener( 'error', diff --git a/ui/ceval/src/engines/stockfishWebEngine.ts b/ui/ceval/src/engines/stockfishWebEngine.ts index e58a6c3bd4671..e13c6307ccc6c 100644 --- a/ui/ceval/src/engines/stockfishWebEngine.ts +++ b/ui/ceval/src/engines/stockfishWebEngine.ts @@ -45,7 +45,7 @@ export class StockfishWebEngine extends LegacyBot implements CevalEngine { async boot() { const [version, root, js] = [this.info.assets.version, this.info.assets.root, this.info.assets.js]; - const makeModule = await import(lichess.assetUrl(`${root}/${js}`, { version })); + const makeModule = await import(lichess.asset.url(`${root}/${js}`, { version })); const module: StockfishWeb = await new Promise((resolve, reject) => { makeModule @@ -53,7 +53,7 @@ export class StockfishWebEngine extends LegacyBot implements CevalEngine { wasmMemory: sharedWasmMemory(this.info.minMem!), onError: (msg: string) => reject(new Error(msg)), locateFile: (name: string) => - lichess.assetUrl(`${root}/${name}`, { version, sameDomain: name.endsWith('.worker.js') }), + lichess.asset.url(`${root}/${name}`, { version, sameDomain: name.endsWith('.worker.js') }), }) .then(resolve) .catch(reject); @@ -81,7 +81,7 @@ export class StockfishWebEngine extends LegacyBot implements CevalEngine { if (!nnueBuffer || nnueBuffer.byteLength < 128 * 1024) { const req = new XMLHttpRequest(); - req.open('get', lichess.assetUrl(`lifat/nnue/${nnueFilename}`, { noVersion: true }), true); + req.open('get', lichess.asset.url(`lifat/nnue/${nnueFilename}`, { noVersion: true }), true); req.responseType = 'arraybuffer'; req.onprogress = e => this.status?.({ download: { bytes: e.loaded, total: e.total } }); diff --git a/ui/ceval/src/engines/threadedEngine.ts b/ui/ceval/src/engines/threadedEngine.ts index 0db145832ce16..da0a23764e6f9 100644 --- a/ui/ceval/src/engines/threadedEngine.ts +++ b/ui/ceval/src/engines/threadedEngine.ts @@ -98,7 +98,7 @@ export class ThreadedEngine extends LegacyBot implements CevalEngine { if (!wasmBinary) { wasmBinary = await new Promise((resolve, reject) => { const req = new XMLHttpRequest(); - req.open('GET', lichess.assetUrl(wasmPath, { version }), true); + req.open('GET', lichess.asset.url(wasmPath, { version }), true); req.responseType = 'arraybuffer'; req.onerror = event => reject(event); req.onprogress = event => this.status?.({ download: { bytes: event.loaded, total: event.total } }); @@ -117,13 +117,13 @@ export class ThreadedEngine extends LegacyBot implements CevalEngine { } // Load Emscripten module. - await lichess.loadIife(`${root}/${js}`, { version }); + await lichess.asset.loadIife(`${root}/${js}`, { version }); const sf = await window[this.info.id === '__sf11mv' ? 'StockfishMv' : 'Stockfish']!({ wasmBinary, printErr: (msg: string) => this.onError(new Error(msg)), onError: this.onError, locateFile: (path: string) => - lichess.assetUrl(`${root}/${path}`, { version, sameDomain: path.endsWith('.worker.js') }), + lichess.asset.url(`${root}/${path}`, { version, sameDomain: path.endsWith('.worker.js') }), wasmMemory: sharedWasmMemory(this.info.minMem!), }); if (!this.info.isBot) { diff --git a/ui/ceval/src/main.ts b/ui/ceval/src/main.ts index 079632b9bd788..c1db9888fba70 100644 --- a/ui/ceval/src/main.ts +++ b/ui/ceval/src/main.ts @@ -2,7 +2,7 @@ import CevalCtrl from './ctrl'; import * as view from './view/main'; import * as winningChances from './winningChances'; -export type { NodeEvals, Eval, EvalMeta, CevalOpts, ExternalEngineInfo, BrowserEngineInfo } from './types'; +export type { NodeEvals, EvalMeta, CevalOpts, ExternalEngineInfo } from './types'; export { isEvalBetter, renderEval, sanIrreversible } from './util'; export { CevalCtrl, view, winningChances }; export { Engines } from './engines/engines'; diff --git a/ui/ceval/src/types.ts b/ui/ceval/src/types.ts index 9c9b27ca63894..90001ae82e8d4 100644 --- a/ui/ceval/src/types.ts +++ b/ui/ceval/src/types.ts @@ -3,10 +3,7 @@ import { Prop } from 'common'; import { Feature } from 'common/device'; import CevalCtrl from './ctrl'; -export interface Eval { - cp?: number; - mate?: number; -} +export type WinningChances = number; export interface Work { variant: VariantKey; @@ -31,10 +28,11 @@ export interface EngineInfo { tech?: 'HCE' | 'NNUE' | 'EXTERNAL'; short?: string; variants?: VariantKey[]; + minThreads?: number; maxThreads?: number; maxHash?: number; - requires?: Feature[]; - isBot?: boolean; + requires?: Requires[]; + isBot?: boolean; // TODO remove } export interface ExternalEngineInfo extends EngineInfo { @@ -46,9 +44,11 @@ export interface ExternalEngineInfo extends EngineInfo { export interface BrowserEngineInfo extends EngineInfo { minMem?: number; assets: { root?: string; js?: string; wasm?: string; version?: string; nnue?: string }; - obsoletedBy?: 'sharedMem' | 'wasm'; + obsoletedBy?: Feature; } +export type Requires = Feature | 'allowLsfw'; // lsfw = lila-stockfish-web + export type EngineNotifier = (status?: { download?: { bytes: number; total: number }; error?: string; diff --git a/ui/ceval/src/util.ts b/ui/ceval/src/util.ts index 9a175701af95a..6a2b0dc317f00 100644 --- a/ui/ceval/src/util.ts +++ b/ui/ceval/src/util.ts @@ -1,4 +1,6 @@ import { type Dialog, domDialog } from 'common/dialog'; +import { isMobile } from 'common/device'; +import { memoize } from 'common/common'; export function isEvalBetter(a: Tree.ClientEval, b: Tree.ClientEval): boolean { return a.depth > b.depth || (a.depth === b.depth && a.nodes > b.nodes); @@ -17,11 +19,12 @@ export function sanIrreversible(variant: VariantKey, san: string): boolean { return variant === 'threeCheck' && san.includes('+'); } -export const pow2floor = (n: number) => { - let pow2 = 1; - while (pow2 * 2 <= n) pow2 *= 2; - return pow2; -}; +export function constrain(n: number, constraints: { min?: number; max?: number }): number { + const min = constraints.min ?? n; + const max = constraints.max ?? n; + return Math.max(min, Math.min(max, n)); +} +export const fewerCores = memoize(() => isMobile() || navigator.userAgent.includes('CrOS')); export const sharedWasmMemory = (lo: number, hi = 32767): WebAssembly.Memory => { let shrink = 4; // 32767 -> 24576 -> 16384 -> 12288 -> 8192 -> 6144 -> etc @@ -40,10 +43,13 @@ export function showEngineError(engine: string, error: string) { domDialog({ class: 'engine-error', htmlText: - `

${lichess.escapeHtml(engine)} error

` +
-      `${lichess.escapeHtml(error)}

Things to try

    ` + - '
  • Decrease memory slider in engine settings
  • Clear site settings for lichess.org
  • ' + - '
  • Select another engine
  • Update your browser
', + `

${lichess.escapeHtml(engine)} error

` + error.includes('Status 503') + ? `

Your external engine does not appear to be connected.

+

Please check the network and restart your provider if possible.

` + : `${lichess.escapeHtml(error)}

Things to try

    +
  • Decrease memory slider in engine settings
  • +
  • Clear site settings for lichess.org
  • +
  • Select another engine
  • Update your browser
`, }).then((dlg: Dialog) => { const select = () => setTimeout(() => { diff --git a/ui/ceval/src/view/main.ts b/ui/ceval/src/view/main.ts index a56f10814a392..9bf6f03583a70 100644 --- a/ui/ceval/src/view/main.ts +++ b/ui/ceval/src/view/main.ts @@ -1,10 +1,10 @@ import * as winningChances from '../winningChances'; import * as licon from 'common/licon'; import { stepwiseScroll } from 'common/scroll'; -import { bind, MaybeVNodes } from 'common/snabbdom'; +import { bind, LooseVNodes, looseH as h } from 'common/snabbdom'; import { defined, notNull } from 'common'; -import { Eval, ParentCtrl, NodeEvals, CevalState } from '../types'; -import { h, VNode } from 'snabbdom'; +import { ParentCtrl, NodeEvals, CevalState } from '../types'; +import { VNode } from 'snabbdom'; import { Position } from 'chessops/chess'; import { lichessRules } from 'chessops/compat'; import { makeSanAndPlay } from 'chessops/san'; @@ -81,14 +81,8 @@ function localInfo(ctrl: ParentCtrl, ev?: Tree.ClientEval | false): EvalInfo { function threatButton(ctrl: ParentCtrl): VNode | null { if (ctrl.getCeval().download || (ctrl.disableThreatMode && ctrl.disableThreatMode())) return null; return h('button.show-threat', { - class: { - active: ctrl.threatMode(), - hidden: !!ctrl.getNode().check, - }, - attrs: { - 'data-icon': licon.Target, - title: ctrl.trans.noarg('showThreat') + ' (x)', - }, + class: { active: ctrl.threatMode(), hidden: !!ctrl.getNode().check }, + attrs: { 'data-icon': licon.Target, title: ctrl.trans.noarg('showThreat') + ' (x)' }, hook: bind('click', ctrl.toggleThreatMode), }); } @@ -102,21 +96,13 @@ function engineName(ctrl: CevalCtrl): VNode[] { engineTech === 'EXTERNAL' ? h( 'span.technology.good', - { - attrs: { - title: 'Engine running outside of the browser', - }, - }, + { attrs: { title: 'Engine running outside of the browser' } }, engineTech, ) : engine.requires?.includes('simd') ? h( 'span.technology.good', - { - attrs: { - title: 'Multi-threaded WebAssembly with SIMD', - }, - }, + { attrs: { title: 'Multi-threaded WebAssembly with SIMD' } }, engineTech, ) : engine.requires?.includes('sharedMem') @@ -130,7 +116,7 @@ function engineName(ctrl: CevalCtrl): VNode[] { const serverNodes = 4e6; -export function getBestEval(evs: NodeEvals): Eval | undefined { +export function getBestEval(evs: NodeEvals): EvalScore | undefined { const serverEv = evs.server, localEv = evs.client; @@ -156,19 +142,13 @@ export function renderGauge(ctrl: ParentCtrl): VNode | undefined { ev = winningChances.povChances('white', bestEv); gaugeLast = ev; } else ev = gaugeLast; - return h( - 'div.eval-gauge', - { - class: { - empty: ev === null, - reverse: ctrl.getOrientation() === 'black', - }, - }, - [h('div.black', { attrs: { style: `height: ${100 - (ev + 1) * 50}%` } }), ...gaugeTicks], - ); + return h('div.eval-gauge', { class: { empty: ev === null, reverse: ctrl.getOrientation() === 'black' } }, [ + h('div.black', { attrs: { style: `height: ${100 - (ev + 1) * 50}%` } }), + ...gaugeTicks, + ]); } -export function renderCeval(ctrl: ParentCtrl): MaybeVNodes { +export function renderCeval(ctrl: ParentCtrl): LooseVNodes { const ceval = ctrl.getCeval(), trans = ctrl.trans; if (!ceval.allowed() || !ceval.possible) return []; @@ -202,30 +182,29 @@ export function renderCeval(ctrl: ParentCtrl): MaybeVNodes { } if (download) percent = Math.min(100, Math.round((100 * download.bytes) / download.total)); - const progressBar: VNode | null = - enabled || download - ? h( - 'div.bar', - h('span', { - class: { threat: enabled && threatMode }, - attrs: { style: `width: ${percent}%` }, - hook: { - postpatch: (old, vnode) => { - if (old.data!.percent > percent || !!old.data!.threatMode != threatMode) { - const el = vnode.elm as HTMLElement; - const p = el.parentNode as HTMLElement; - p.removeChild(el); - p.appendChild(el); - } - vnode.data!.percent = percent; - vnode.data!.threatMode = threatMode; - }, - }, - }), - ) - : null; - - const body: Array = enabled + const progressBar: VNode | undefined = + (enabled || download) && + h( + 'div.bar', + h('span', { + class: { threat: enabled && threatMode }, + attrs: { style: `width: ${percent}%` }, + hook: { + postpatch: (old, vnode) => { + if (old.data!.percent > percent || !!old.data!.threatMode != threatMode) { + const el = vnode.elm as HTMLElement; + const p = el.parentNode as HTMLElement; + p.removeChild(el); + p.appendChild(el); + } + vnode.data!.percent = percent; + vnode.data!.threatMode = threatMode; + }, + }, + }), + ); + + const body: LooseVNodes = enabled ? [ h('pearl', [pearl]), h('div.engine', [ @@ -243,7 +222,7 @@ export function renderCeval(ctrl: ParentCtrl): MaybeVNodes { ]), ] : [ - pearl ? h('pearl', [pearl]) : null, + pearl && h('pearl', [pearl]), h('help', [ ...engineName(ceval), h('br'), @@ -251,28 +230,17 @@ export function renderCeval(ctrl: ParentCtrl): MaybeVNodes { ]), ]; - const switchButton: VNode | null = - ctrl.mandatoryCeval && ctrl.mandatoryCeval() - ? null - : h( - 'div.switch', - { - attrs: { title: trans.noarg('toggleLocalEvaluation') + ' (L)' }, - }, - [ - h('input#analyse-toggle-ceval.cmn-toggle.cmn-toggle--subtle', { - attrs: { - type: 'checkbox', - checked: enabled, - disabled: !ceval.analysable, - }, - hook: bind('change', ctrl.toggleCeval), - }), - h('label', { attrs: { for: 'analyse-toggle-ceval' } }), - ], - ); + const switchButton: VNode | false = + !ctrl.mandatoryCeval?.() && + h('div.switch', { attrs: { title: trans.noarg('toggleLocalEvaluation') + ' (L)' } }, [ + h('input#analyse-toggle-ceval.cmn-toggle.cmn-toggle--subtle', { + attrs: { type: 'checkbox', checked: enabled, disabled: !ceval.analysable }, + hook: bind('change', ctrl.toggleCeval), + }), + h('label', { attrs: { for: 'analyse-toggle-ceval' } }), + ]); - const settingsGear: VNode | null = h('button.settings-gear', { + const settingsGear = h('button.settings-gear', { attrs: { 'data-icon': licon.Gear, title: 'Engine settings' }, class: { active: ctrl.getCeval().showEnginePrefs() }, // must use ctrl.getCeval() rather than ceval here hook: bind( @@ -284,13 +252,13 @@ export function renderCeval(ctrl: ParentCtrl): MaybeVNodes { }); return [ - h( - 'div.ceval' + (enabled ? '.enabled' : ''), - { - class: { computing: ceval.computing() }, - }, - [switchButton, ...body, threatButton(ctrl), settingsGear, progressBar], - ), + h('div.ceval' + (enabled ? '.enabled' : ''), { class: { computing: ceval.computing() } }, [ + switchButton, + ...body, + threatButton(ctrl), + settingsGear, + progressBar, + ]), renderCevalSettings(ctrl), ]; } @@ -473,17 +441,7 @@ function renderPvMoves(pos: Position, pv: Uci[]): VNode[] { } key += '|' + uci; vnodes.push( - h( - 'span.pv-san', - { - key, - attrs: { - 'data-move-index': i, - 'data-board': `${fen}|${uci}`, - }, - }, - san, - ), + h('span.pv-san', { key, attrs: { 'data-move-index': i, 'data-board': `${fen}|${uci}` } }, san), ); } return vnodes; @@ -523,10 +481,7 @@ const analysisDisabled = (ctrl: ParentCtrl): VNode | undefined => h('span', ctrl.trans.noarg('computerAnalysisDisabled')), h( 'button', - { - hook: bind('click', () => ctrl.toggleComputer?.(), ctrl.redraw), - attrs: { type: 'button' }, - }, + { hook: bind('click', () => ctrl.toggleComputer?.(), ctrl.redraw), attrs: { type: 'button' } }, ctrl.trans.noarg('enable'), ), ]); diff --git a/ui/ceval/src/view/settings.ts b/ui/ceval/src/view/settings.ts index a73ec9e309961..caf639e1cb304 100644 --- a/ui/ceval/src/view/settings.ts +++ b/ui/ceval/src/view/settings.ts @@ -1,8 +1,10 @@ -import { h, VNode } from 'snabbdom'; +import { VNode } from 'snabbdom'; import { ParentCtrl } from '../types'; +import CevalCtrl from '../ctrl'; +import { fewerCores } from '../util'; import { rangeConfig } from 'common/controls'; -import { hasFeature } from 'common/device'; -import { onInsert, bind, dataIcon } from 'common/snabbdom'; +import { hasFeature, isChrome } from 'common/device'; +import { onInsert, bind, dataIcon, looseH as h } from 'common/snabbdom'; import * as Licon from 'common/licon'; import { onClickAway, isReadonlyProp } from 'common'; @@ -19,8 +21,22 @@ const formatHashSize = (v: number): string => (v < 1000 ? v + 'MB' : Math.round( export function renderCevalSettings(ctrl: ParentCtrl): VNode | null { const ceval = ctrl.getCeval(), noarg = ctrl.trans.noarg, + minThreads = ceval.engines.active?.minThreads ?? 1, + maxThreads = ceval.maxThreads(), engCtrl = ctrl.getCeval().engines; + let observer: ResizeObserver; + + function clickThreads(x = ceval.recommendedThreads()) { + ceval.setThreads(x); + ctrl.restartCeval?.(); + ceval.opts.redraw(); + } + + function threadsTick(dir: 'up' | 'down') { + return h(`div.arrow-${dir}`, { hook: bind('click', () => clickThreads()) }); + } + function searchTick() { const millis = ceval.searchMs(); return Math.max( @@ -44,123 +60,128 @@ export function renderCevalSettings(ctrl: ParentCtrl): VNode | null { }, [ ...engineSelection(ctrl), - isReadonlyProp(ceval.searchMs) - ? null - : (id => { - return h( - 'div.setting', - { - attrs: { title: 'Set time to evaluate fresh positions' }, - }, - [ - h('label', 'Search time'), - h('input#' + id, { - attrs: { type: 'range', min: 0, max: searchTicks.length - 1, step: 1 }, - hook: rangeConfig(searchTick, n => { - ceval.searchMs(searchTicks[n][0]); - ctrl.restartCeval?.(); - }), - }), - h('div.range_value', searchTicks[searchTick()][1]), - ], - ); - })('engine-search-ms'), - isReadonlyProp(ceval.multiPv) - ? null - : (id => { - const max = 5; - return h( - 'div.setting', - { - attrs: { - title: - 'Set number of lines atop the move list in addition to move arrows on the board', - }, - }, - [ - h('label', { attrs: { for: id } }, noarg('multipleLines')), - h('input#' + id, { - attrs: { type: 'range', min: 0, max, step: 1 }, - hook: rangeConfig( - () => ceval.multiPv(), - (pvs: number) => { - ceval.multiPv(pvs); - ctrl.clearCeval?.(); - }, - ), - }), - h('div.range_value', `${ceval.multiPv()} / ${max}`), - ], - ); - })('analyse-multipv'), - hasFeature('sharedMem') - ? (id => { - return h( - 'div.setting', - { - attrs: { - title: 'Higher values improve performance, but other apps may run slower', - }, + !isReadonlyProp(ceval.searchMs) && + (id => { + return h('div.setting', { attrs: { title: 'Set time to evaluate fresh positions' } }, [ + h('label', 'Search time'), + h('input#' + id, { + attrs: { type: 'range', min: 0, max: searchTicks.length - 1, step: 1 }, + hook: rangeConfig(searchTick, n => { + ceval.searchMs(searchTicks[n][0]); + ctrl.restartCeval?.(); + }), + }), + h('div.range_value', searchTicks[searchTick()][1]), + ]); + })('engine-search-ms'), + !isReadonlyProp(ceval.multiPv) && + (id => { + const max = 5; + return h( + 'div.setting', + { attrs: { title: 'Set number of evaluation lines and move arrows on the board' } }, + [ + h('label', { attrs: { for: id } }, noarg('multipleLines')), + h('input#' + id, { + attrs: { type: 'range', min: 0, max, step: 1 }, + hook: rangeConfig( + () => ceval.multiPv(), + (pvs: number) => { + ceval.multiPv(pvs); + ctrl.clearCeval?.(); + }, + ), + }), + h('div.range_value', `${ceval.multiPv()} / ${max}`), + ], + ); + })('analyse-multipv'), + (hasFeature('sharedMem') || ceval.engines.external) && + (id => { + return h( + 'div.setting', + { + attrs: { + title: + fewerCores() && !ceval.engines.external + ? 'More threads will use more battery for better analysis' + : "Set this below your CPU's thread count\nThe ticks mark a good safe choice", }, - [ - h('label', { attrs: { for: id } }, 'Threads'), + }, + [ + h('label', { attrs: { for: id } }, 'Threads'), + h('span', [ h('input#' + id, { attrs: { type: 'range', - min: 1, - max: ceval.maxThreads(), + min: minThreads, + max: maxThreads, step: 1, - disabled: ceval.maxThreads() <= 1, + disabled: maxThreads <= minThreads, }, - hook: rangeConfig( - () => ceval.threads(), - x => { - ceval.setThreads(x); - ctrl.restartCeval?.(); - }, - ), + hook: rangeConfig(() => ceval.threads(), clickThreads), }), - h('div.range_value', `${ceval.threads ? ceval.threads() : 1} / ${ceval.maxThreads()}`), - ], - ); - })('analyse-threads') - : null, + h( + 'div.tick', + { + hook: { + update: (_, v) => setupTick(v, ceval), + insert: v => { + setupTick(v, ceval); + observer = new ResizeObserver(() => setupTick(v, ceval)); + observer.observe(v.elm!.parentElement!); + }, + destroy: () => observer?.disconnect(), + }, + }, + !ceval.engines.external && [threadsTick('up'), threadsTick('down')], + ), + ]), + h('div.range_value', `${ceval.threads()} / ${maxThreads}`), + ], + ); + })('analyse-threads'), (id => - h( - 'div.setting', - { + h('div.setting', { attrs: { title: 'Higher values may improve performance' } }, [ + h('label', { attrs: { for: id } }, noarg('memory')), + h('input#' + id, { attrs: { - title: 'Higher values improve performance', + type: 'range', + min: 4, + max: Math.floor(Math.log2(engCtrl.active?.maxHash ?? 4)), + step: 1, + disabled: ceval.maxHash() <= 16, }, - }, - [ - h('label', { attrs: { for: id } }, noarg('memory')), - h('input#' + id, { - attrs: { - type: 'range', - min: 4, - max: Math.floor(Math.log2(engCtrl.active?.maxHash ?? 4)), - step: 1, - disabled: ceval.maxHash() <= 16, + hook: rangeConfig( + () => Math.floor(Math.log2(ceval.hashSize())), + v => { + ceval.setHashSize(Math.pow(2, v)); + ctrl.restartCeval?.(); }, - hook: rangeConfig( - () => Math.floor(Math.log2(ceval.hashSize())), - v => { - ceval.setHashSize(Math.pow(2, v)); - ctrl.restartCeval?.(); - }, - ), - }), + ), + }), - h('div.range_value', formatHashSize(ceval.hashSize())), - ], - ))('analyse-memory'), + h('div.range_value', formatHashSize(ceval.hashSize())), + ]))('analyse-memory'), ], ), ) : null; } +function setupTick(v: VNode, ceval: CevalCtrl) { + const tick = v.elm as HTMLElement; + const parentSpan = tick.parentElement!; + const minThreads = ceval.engines.active?.minThreads ?? 1; + const thumbWidth = isChrome() ? 17 : 19; // it is what it is + const trackWidth = parentSpan.querySelector('input')!.offsetWidth - thumbWidth; + const tickRatio = (ceval.recommendedThreads() - minThreads) / (ceval.maxThreads() - minThreads); + const tickLeft = Math.floor(thumbWidth / 2 + trackWidth * tickRatio); + + tick.style.left = `${tickLeft}px`; + $(tick).toggleClass('recommended', ceval.threads() === ceval.recommendedThreads()); +} + function engineSelection(ctrl: ParentCtrl) { const ceval = ctrl.getCeval(), active = ceval.engines.active, @@ -172,34 +193,22 @@ function engineSelection(ctrl: ParentCtrl) { 'Engine:', h( 'select.select-engine', - { - hook: bind('change', e => ceval.selectEngine((e.target as HTMLSelectElement).value)), - }, + { hook: bind('change', e => ceval.selectEngine((e.target as HTMLSelectElement).value)) }, [ ...engines.map(engine => - h( - 'option', - { - attrs: { - value: engine.id, - selected: active?.id == engine.id, - }, - }, - engine.name, - ), + h('option', { attrs: { value: engine.id, selected: active?.id == engine.id } }, engine.name), ), ], ), - external - ? h('button.delete', { - attrs: { ...dataIcon(Licon.X), title: 'Delete external engine' }, - hook: bind('click', e => { - (e.currentTarget as HTMLElement).blur(); - if (confirm('Remove external engine?')) - ceval.engines.deleteExternal(external.id).then(ok => ok && ctrl.redraw?.()); - }), - }) - : null, + external && + h('button.delete', { + attrs: { ...dataIcon(Licon.X), title: 'Delete external engine' }, + hook: bind('click', e => { + (e.currentTarget as HTMLElement).blur(); + if (confirm('Remove external engine?')) + ceval.engines.deleteExternal(external.id).then(ok => ok && ctrl.redraw?.()); + }), + }), ]), h('br'), ]; diff --git a/ui/ceval/src/winningChances.ts b/ui/ceval/src/winningChances.ts index cac2b5e2e5e22..5b332a6fcd5c2 100644 --- a/ui/ceval/src/winningChances.ts +++ b/ui/ceval/src/winningChances.ts @@ -1,33 +1,35 @@ -import { Eval } from './types'; +import { WinningChances } from './types'; const toPov = (color: Color, diff: number): number => (color === 'white' ? diff : -diff); /** * https://github.com/lichess-org/lila/pull/11148 */ -const rawWinningChances = (cp: number): number => { +const rawWinningChances = (cp: number): WinningChances => { const MULTIPLIER = -0.00368208; // https://github.com/lichess-org/lila/pull/11148 return 2 / (1 + Math.exp(MULTIPLIER * cp)) - 1; }; -const cpWinningChances = (cp: number): number => rawWinningChances(Math.min(Math.max(-1000, cp), 1000)); +const cpWinningChances = (cp: number): WinningChances => + rawWinningChances(Math.min(Math.max(-1000, cp), 1000)); -const mateWinningChances = (mate: number): number => { +const mateWinningChances = (mate: number): WinningChances => { const cp = (21 - Math.min(10, Math.abs(mate))) * 100; const signed = cp * (mate > 0 ? 1 : -1); return rawWinningChances(signed); }; -const evalWinningChances = (ev: Eval): number => +const evalWinningChances = (ev: EvalScore): WinningChances => typeof ev.mate !== 'undefined' ? mateWinningChances(ev.mate) : cpWinningChances(ev.cp!); // winning chances for a color // 1 infinitely winning // -1 infinitely losing -export const povChances = (color: Color, ev: Eval): number => toPov(color, evalWinningChances(ev)); +export const povChances = (color: Color, ev: EvalScore): WinningChances => + toPov(color, evalWinningChances(ev)); // computes the difference, in winning chances, between two evaluations // 1 = e1 is infinitely better than e2 // -1 = e1 is infinitely worse than e2 -export const povDiff = (color: Color, e1: Eval, e2: Eval): number => +export const povDiff = (color: Color, e1: EvalScore, e2: EvalScore): number => (povChances(color, e1) - povChances(color, e2)) / 2; diff --git a/ui/challenge/css/_page.scss b/ui/challenge/css/_page.scss index ebc911eb09214..cf101cb6b905f 100644 --- a/ui/challenge/css/_page.scss +++ b/ui/challenge/css/_page.scss @@ -8,16 +8,40 @@ } .invite { - display: flex; - flex-flow: row wrap; + display: grid; + gap: $block-gap; + grid-template-columns: repeat(auto-fill, minmax(25em, 1fr)); > div { - @extend %box-radius; - @include padding-direction(2em, 2em, 1em, 2em); - + @extend %box-neat; + padding: $block-gap; background: $c-bg-zebra; - margin: 1em; - flex: 1 1 auto; + } + + &__qrcode { + @extend %flex-between-nowrap; + gap: $block-gap; + } + + &__user { + @include breakpoint($mq-x-small) { + grid-area: 1 / 2 / 3 / 2; + } + &__recent { + @extend %flex-center; + margin-top: 1em; + gap: 0.5em; + .button { + background: none; + border: $border; + text-align: $start-direction; + text-transform: none; + } + } + .error { + color: $c-bad; + margin-top: 1em; + } } } @@ -41,30 +65,32 @@ } .details { - @extend %flex-between; + @extend %flex-between, %box-neat; $c-bg: mix($c-good, $c-bg-box, 10%); --c-font: #{$c-good}; --c-bg: #{$c-bg}; - border-radius: 99px; - padding: 0.7em 1.7em; - margin-bottom: 3rem; - font-size: 2em; + padding: $block-gap 4vw; + margin-bottom: 4rem; + font-size: 1.5em; + @include breakpoint($mq-xx-small) { + font-size: 2em; + } background: var(--c-bg); border: 1px solid var(--c-font); - gap: 0.3em 1em; > div { + flex: 0 1 auto; @extend %flex-center, %roboto; - justify-content: center; - flex-grow: 1; - &::before { color: var(--c-font); font-size: 6rem; margin-#{$end-direction}: 0.2em; + @include breakpoint($mq-not-xx-small) { + display: none; + } } div { @@ -77,10 +103,10 @@ } .mode { + text-align: $end-direction; font-weight: bold; + font-size: 0.8em; color: var(--c-font); - text-transform: uppercase; - text-align: center; } } diff --git a/ui/challenge/src/view.ts b/ui/challenge/src/view.ts index 8f0ca1e4e5c51..a8ccf4c875c9b 100644 --- a/ui/challenge/src/view.ts +++ b/ui/challenge/src/view.ts @@ -26,10 +26,7 @@ function allChallenges(ctrl: Ctrl, d: ChallengeData, nb: number): VNode { 'div.challenges', { class: { many: nb > 3 }, - hook: { - insert: userPowertips, - postpatch: userPowertips, - }, + hook: { insert: userPowertips, postpatch: userPowertips }, }, d.in.map(challenge(ctrl, 'in')).concat(d.out.map(challenge(ctrl, 'out'))), ); @@ -44,31 +41,21 @@ function challenge(ctrl: Ctrl, dir: ChallengeDirection) { return h( `div.challenge.${dir}.c-${c.id}`, { - class: { - declined: !!c.declined, - }, + class: { declined: !!c.declined }, }, [ h('div.content', [ - h( - 'div.content__text', - { - attrs: { id: `challenge-text-${c.id}` }, - }, - [ - h('span.head', [renderUser(opponent, ctrl.showRatings), renderLag(opponent)]), - h('span.desc', [ - h('span.is.color-icon.' + myColor), + h('div.content__text', { attrs: { id: `challenge-text-${c.id}` } }, [ + h('span.head', [renderUser(opponent, ctrl.showRatings), renderLag(opponent)]), + h('span.desc', [ + h('span.is.color-icon.' + myColor), + ' • ', + [ctrl.trans()(c.rated ? 'rated' : 'casual'), timeControl(c.timeControl), c.variant.name].join( ' • ', - [ctrl.trans()(c.rated ? 'rated' : 'casual'), timeControl(c.timeControl), c.variant.name].join( - ' • ', - ), - ]), - ], - ), - h('i.perf', { - attrs: { 'data-icon': c.perf.icon }, - }), + ), + ]), + ]), + h('i.perf', { attrs: { 'data-icon': c.perf.icon } }), ]), fromPosition ? h('div.position.mini-board.cg-wrap.is2d', { @@ -89,32 +76,19 @@ function challenge(ctrl: Ctrl, dir: ChallengeDirection) { function inButtons(ctrl: Ctrl, c: Challenge): VNode[] { const trans = ctrl.trans(); return [ - h( - 'form', - { + h('form', { attrs: { method: 'post', action: `/challenge/${c.id}/accept` } }, [ + h('button.button.accept', { attrs: { - method: 'post', - action: `/challenge/${c.id}/accept`, + type: 'submit', + 'aria-describedby': `challenge-text-${c.id}`, + 'data-icon': licon.Checkmark, + title: trans('accept'), }, - }, - [ - h('button.button.accept', { - attrs: { - type: 'submit', - 'aria-describedby': `challenge-text-${c.id}`, - 'data-icon': licon.Checkmark, - title: trans('accept'), - }, - hook: onClick(ctrl.onRedirect), - }), - ], - ), + hook: onClick(ctrl.onRedirect), + }), + ]), h('button.button.decline', { - attrs: { - type: 'submit', - 'data-icon': licon.X, - title: trans('decline'), - }, + attrs: { type: 'submit', 'data-icon': licon.X, title: trans('decline') }, hook: onClick(() => ctrl.decline(c.id, 'generic')), }), h( @@ -140,18 +114,11 @@ function outButtons(ctrl: Ctrl, c: Challenge) { h('div.owner', [ h('span.waiting', ctrl.trans()('waiting')), h('a.view', { - attrs: { - 'data-icon': licon.Eye, - href: '/' + c.id, - title: trans('viewInFullSize'), - }, + attrs: { 'data-icon': licon.Eye, href: '/' + c.id, title: trans('viewInFullSize') }, }), ]), h('button.button.decline', { - attrs: { - 'data-icon': licon.X, - title: trans('cancel'), - }, + attrs: { 'data-icon': licon.X, title: trans('cancel') }, hook: onClick(() => ctrl.cancel(c.id)), }), ]; @@ -174,28 +141,10 @@ const renderUser = (u: ChallengeUser | undefined, showRating: boolean): VNode => : h('span', 'Open challenge'); const renderLag = (u?: ChallengeUser) => - u && - h( - 'signal', - u.lag === undefined - ? [] - : [1, 2, 3, 4].map(i => - h('i', { - class: { off: u.lag! < i }, - }), - ), - ); + u && h('signal', u.lag === undefined ? [] : [1, 2, 3, 4].map(i => h('i', { class: { off: u.lag! < i } }))); const empty = (): VNode => - h( - 'div.empty.text', - { - attrs: { - 'data-icon': licon.InfoCircle, - }, - }, - 'No challenges.', - ); + h('div.empty.text', { attrs: { 'data-icon': licon.InfoCircle } }, 'No challenges.'); const onClick = (f: (e: Event) => void) => ({ insert: (vnode: VNode) => { diff --git a/ui/chart/package.json b/ui/chart/package.json index d366f1e0170ec..1c277cb200966 100644 --- a/ui/chart/package.json +++ b/ui/chart/package.json @@ -9,6 +9,7 @@ "module": "dist/game.js", "types": "dist/game.d.ts", "dependencies": { + "@juggle/resize-observer": "^3.4.0", "@types/highcharts": "=4.2.57", "ceval": "workspace:*", "chart.js": "4.4.0", @@ -25,7 +26,8 @@ "esm": { "src/ratingDistribution.ts": "chart.ratingDistribution", "src/ratingHistory.ts": "chart.ratingHistory", - "src/game.ts": "chart.game" + "src/game.ts": "chart.game", + "src/resizePolyfill.ts": "chart.resizePolyfill" } } } diff --git a/ui/chart/src/acpl.ts b/ui/chart/src/acpl.ts index e2e476af043f9..dfe80e579cb3c 100644 --- a/ui/chart/src/acpl.ts +++ b/ui/chart/src/acpl.ts @@ -23,13 +23,14 @@ import { tooltipBgColor, whiteFill, axisOpts, + resizePolyfill, } from './common'; import division from './division'; import { AcplChart, AnalyseData, Player } from './interface'; import ChartDataLabels from 'chartjs-plugin-datalabels'; +resizePolyfill(); Chart.register(LineController, LinearScale, PointElement, LineElement, Tooltip, Filler, ChartDataLabels); - export default async function ( el: HTMLCanvasElement, data: AnalyseData, @@ -199,14 +200,14 @@ const glyphProperties = (node: Tree.Node): { advice?: Advice; color?: string } = const toBlurArray = (player: Player) => player.blurs?.bits?.split('') ?? []; -function christmasTree(chart: Chart, mainline: Tree.Node[], hoverColors: string[]) { +function christmasTree(chart: AcplChart, mainline: Tree.Node[], hoverColors: string[]) { $('div.advice-summary').on('mouseenter', 'div.symbol', function (this: HTMLElement) { const symbol = this.getAttribute('data-symbol'); const playerColorBit = this.getAttribute('data-color') == 'white' ? 1 : 0; const acplDataset = chart.data.datasets[0]; if (symbol == '??' || symbol == '?!' || symbol == '?') { - acplDataset.hoverBackgroundColor = hoverColors; - acplDataset.borderColor = hoverColors; + acplDataset.pointHoverBackgroundColor = hoverColors; + acplDataset.pointBorderColor = hoverColors; const points = mainline .map((node, i) => node.glyphs?.some(glyph => glyph.symbol == symbol) && (node.ply & 1) == playerColorBit @@ -219,9 +220,9 @@ function christmasTree(chart: Chart, mainline: Tree.Node[], hoverColors: string[ } }); $('div.advice-summary').on('mouseleave', 'div.symbol', function (this: HTMLElement) { - if (chart.getActiveElements().length) chart.setActiveElements([]); - chart.data.datasets[0].hoverBackgroundColor = orangeAccent; - chart.data.datasets[0].borderColor = orangeAccent; + chart.setActiveElements([]); + chart.data.datasets[0].pointHoverBackgroundColor = orangeAccent; + chart.data.datasets[0].pointBorderColor = orangeAccent; chart.update('none'); }); } diff --git a/ui/chart/src/common.ts b/ui/chart/src/common.ts index bb8b79de7dd99..18f1266d445b5 100644 --- a/ui/chart/src/common.ts +++ b/ui/chart/src/common.ts @@ -94,7 +94,7 @@ export function animation(duration: number): ChartOptions<'line'>['animations'] easing: 'easeOutQuad', duration: duration, from: NaN, // the point is initially skipped - delay: ctx => ctx.dataIndex * duration, + delay: ctx => (ctx.mode == 'resize' ? 0 : ctx.dataIndex * duration), }, y: { type: 'number', @@ -104,15 +104,19 @@ export function animation(duration: number): ChartOptions<'line'>['animations'] !ctx.dataIndex ? ctx.chart.scales.y.getPixelForValue(100) : ctx.chart.getDatasetMeta(ctx.datasetIndex).data[ctx.dataIndex - 1].getProps(['y'], true).y, - delay: ctx => ctx.dataIndex * duration, + delay: ctx => (ctx.mode == 'resize' ? 0 : ctx.dataIndex * duration), }, }; } +export function resizePolyfill() { + if ('ResizeObserver' in window === false) lichess.asset.loadEsm('chart.resizePolyfill'); +} + export async function loadHighcharts(tpe: string) { if (highchartsPromise) return highchartsPromise; const file = tpe === 'highstock' ? 'highstock.js' : 'highcharts.js'; - highchartsPromise = lichess.loadIife('npm/highcharts-4.2.5/' + file, { + highchartsPromise = lichess.asset.loadIife('npm/highcharts-4.2.5/' + file, { noVersion: true, }); await highchartsPromise; diff --git a/ui/chart/src/game.ts b/ui/chart/src/game.ts index fc5678e70215f..55bb802e02a43 100644 --- a/ui/chart/src/game.ts +++ b/ui/chart/src/game.ts @@ -1,11 +1,11 @@ import { ChartGame, AcplChart } from './interface'; import movetime from './movetime'; import acpl from './acpl'; -import { gridColor, tooltipBgColor, fontFamily, maybeChart } from './common'; +import { gridColor, tooltipBgColor, fontFamily, maybeChart, resizePolyfill } from './common'; export { type ChartGame, type AcplChart }; -export { gridColor, tooltipBgColor, fontFamily, maybeChart }; +export { gridColor, tooltipBgColor, fontFamily, maybeChart, resizePolyfill }; export function initModule(): ChartGame { return { diff --git a/ui/chart/src/interface.ts b/ui/chart/src/interface.ts index 40fec664afa4a..731ffcaae9c6d 100644 --- a/ui/chart/src/interface.ts +++ b/ui/chart/src/interface.ts @@ -1,6 +1,6 @@ import { Chart } from 'chart.js'; -export interface PlyChart extends Chart { +export interface PlyChart extends Chart<'line'> { selectPly(ply: number, isMainline: boolean): void; } diff --git a/ui/chart/src/movetime.ts b/ui/chart/src/movetime.ts index b6c3569ab080b..c647ed7d68cb1 100644 --- a/ui/chart/src/movetime.ts +++ b/ui/chart/src/movetime.ts @@ -23,10 +23,12 @@ import { tooltipBgColor, whiteFill, axisOpts, + resizePolyfill, } from './common'; import { AnalyseData, Player, PlyChart } from './interface'; import division from './division'; +resizePolyfill(); Chart.register(LineController, LinearScale, PointElement, LineElement, Tooltip, BarElement, BarController); export default async function (el: HTMLCanvasElement, data: AnalyseData, trans: Trans, hunter: boolean) { diff --git a/ui/chart/src/ratingDistribution.ts b/ui/chart/src/ratingDistribution.ts index 0a3889c787dec..64a0c6b3e0670 100644 --- a/ui/chart/src/ratingDistribution.ts +++ b/ui/chart/src/ratingDistribution.ts @@ -1,5 +1,5 @@ import { Point } from 'chart.js/dist/core/core.controller'; -import { animation, fontFamily, gridColor, hoverBorderColor } from './common'; +import { animation, fontFamily, gridColor, hoverBorderColor, resizePolyfill } from './common'; import { DistributionData } from './interface'; import { Chart, @@ -15,6 +15,7 @@ import { } from 'chart.js'; import ChartDataLabels from 'chartjs-plugin-datalabels'; +resizePolyfill(); Chart.register(LineController, LinearScale, PointElement, LineElement, Tooltip, Filler, ChartDataLabels); export async function initModule(data: DistributionData) { diff --git a/ui/chart/src/resizePolyfill.ts b/ui/chart/src/resizePolyfill.ts new file mode 100644 index 0000000000000..e4a3254f9a0e7 --- /dev/null +++ b/ui/chart/src/resizePolyfill.ts @@ -0,0 +1,5 @@ +import { ResizeObserver } from '@juggle/resize-observer'; +// See Chart.js issue #8414 +export default function initModule() { + window.ResizeObserver = ResizeObserver; +} diff --git a/ui/chat/src/ctrl.ts b/ui/chat/src/ctrl.ts index 830757f6edcc2..a6960cd5ec39c 100644 --- a/ui/chat/src/ctrl.ts +++ b/ui/chat/src/ctrl.ts @@ -158,7 +158,7 @@ export default class ChatCtrl { resourceId: this.data.resourceId, redraw: this.redraw, }); - lichess.loadCssPath('chat.mod'); + lichess.asset.loadCssPath('chat.mod'); } }; diff --git a/ui/chat/src/discussion.ts b/ui/chat/src/discussion.ts index 437971bc49373..ab72cbe71b618 100644 --- a/ui/chat/src/discussion.ts +++ b/ui/chat/src/discussion.ts @@ -27,11 +27,7 @@ export default function (ctrl: ChatCtrl): Array { h( `ol.mchat__messages.chat-v-${ctrl.vm.domVersion}${hasMod ? '.as-mod' : ''}`, { - attrs: { - role: 'log', - 'aria-live': 'polite', - 'aria-atomic': 'false', - }, + attrs: { role: 'log', 'aria-live': 'polite', 'aria-atomic': 'false' }, hook: { insert(vnode) { const $el = $(vnode.elm as HTMLElement).on('click', 'a.jump', (e: Event) => { @@ -65,10 +61,7 @@ function renderInput(ctrl: ChatCtrl): VNode | undefined { if (!ctrl.vm.writeable) return; if ((ctrl.data.loginRequired && !ctrl.data.userId) || ctrl.data.restricted) return h('input.mchat__say', { - attrs: { - placeholder: ctrl.trans('loginToChat'), - disabled: true, - }, + attrs: { placeholder: ctrl.trans('loginToChat'), disabled: true }, }); let placeholder: string; if (ctrl.vm.timeout) placeholder = ctrl.trans('youHaveBeenTimedOut'); @@ -191,13 +184,7 @@ function updateText(parseMoves: boolean) { function renderText(t: string, parseMoves: boolean) { if (enhance.isMoreThanText(t)) { const hook = updateText(parseMoves); - return h('t', { - lichessChat: t, - hook: { - create: hook, - update: hook, - }, - }); + return h('t', { lichessChat: t, hook: { create: hook, update: hook } }); } return h('t', t); } @@ -222,12 +209,14 @@ function renderLine(ctrl: ChatCtrl, line: Line): VNode { .match(enhance.userPattern) ?.find(mention => mention.trim().toLowerCase() == `@${ctrl.data.userId}`); + console.log(ctrl.data.hostIds); + return h( 'li', { class: { me: userId === myUserId, - host: userId === ctrl.data.hostId, + host: !!(userId && ctrl.data.hostIds?.includes(userId)), mentioned, }, }, @@ -236,10 +225,7 @@ function renderLine(ctrl: ChatCtrl, line: Line): VNode { : [ myUserId && line.u && myUserId != line.u ? h('action.flag', { - attrs: { - 'data-icon': licon.CautionTriangle, - title: 'Report', - }, + attrs: { 'data-icon': licon.CautionTriangle, title: 'Report' }, }) : null, userNode, diff --git a/ui/chat/src/interfaces.ts b/ui/chat/src/interfaces.ts index b8bdc3f5a474f..f828c4b9cf874 100644 --- a/ui/chat/src/interfaces.ts +++ b/ui/chat/src/interfaces.ts @@ -39,7 +39,7 @@ export interface ChatData { loginRequired: boolean; restricted: boolean; palantir: boolean; - hostId?: string; + hostIds?: string[]; } export interface Line { diff --git a/ui/chat/src/moderation.ts b/ui/chat/src/moderation.ts index 6edab12bc2e42..f0c46d06c62e6 100644 --- a/ui/chat/src/moderation.ts +++ b/ui/chat/src/moderation.ts @@ -23,11 +23,7 @@ export function moderationCtrl(opts: ModerationOpts): ModerationCtrl { opts.redraw(); }); } else { - data = { - id: username.toLowerCase(), - name: username, - text, - }; + data = { id: username.toLowerCase(), name: username, text }; } opts.redraw(); }; @@ -46,11 +42,7 @@ export function moderationCtrl(opts: ModerationOpts): ModerationCtrl { close, async timeout(reason: ModerationReason, text: string) { if (data) { - const body = { - userId: data.id, - reason: reason.key, - text, - }; + const body = { userId: data.id, reason: reason.key, text }; if (new URLSearchParams(window.location.search).get('mod') === 'true') { await timeout(opts.resourceId, body); window.location.reload(); // to load new state since it won't be sent over the socket @@ -172,12 +164,7 @@ export function moderationView(ctrl?: ModerationCtrl): VNode[] | undefined { return h('tr', [ h('td.reason', e.reason), h('td.mod', e.mod), - h( - 'td', - h('time.timeago', { - attrs: { datetime: e.date }, - }), - ), + h('td', h('time.timeago', { attrs: { datetime: e.date } })), ]); }), ), @@ -187,17 +174,8 @@ export function moderationView(ctrl?: ModerationCtrl): VNode[] | undefined { return [ h('div.top', { key: 'mod-' + data.id }, [ - h( - 'span.text', - { - attrs: { 'data-icon': licon.Agent }, - }, - [userLink(data)], - ), - h('a', { - attrs: { 'data-icon': licon.X }, - hook: bind('click', ctrl.close), - }), + h('span.text', { attrs: { 'data-icon': licon.Agent } }, [userLink(data)]), + h('a', { attrs: { 'data-icon': licon.X }, hook: bind('click', ctrl.close) }), ]), h('div.mchat__content.moderation', [ h('i.line-text.block', ['"', data.text, '"']), diff --git a/ui/chat/src/note.ts b/ui/chat/src/note.ts index be3ad908c5d82..94c528253735a 100644 --- a/ui/chat/src/note.ts +++ b/ui/chat/src/note.ts @@ -27,16 +27,9 @@ export function noteCtrl(opts: NoteOpts): NoteCtrl { export function noteView(ctrl: NoteCtrl, autofocus: boolean): VNode { const text = ctrl.text(); - if (text == undefined) - return h('div.loading', { - hook: { - insert: ctrl.fetch, - }, - }); + if (text == undefined) return h('div.loading', { hook: { insert: ctrl.fetch } }); return h('textarea.mchat__note', { - attrs: { - placeholder: ctrl.trans('typePrivateNotesHere'), - }, + attrs: { placeholder: ctrl.trans('typePrivateNotesHere') }, hook: { insert(vnode) { const el = vnode.elm as HTMLTextAreaElement; diff --git a/ui/chat/src/preset.ts b/ui/chat/src/preset.ts index 8acdaa75d246f..f62669c8e36a0 100644 --- a/ui/chat/src/preset.ts +++ b/ui/chat/src/preset.ts @@ -72,16 +72,9 @@ export function presetView(ctrl: PresetCtrl): VNode | undefined { return h( 'span', { - class: { - disabled, - }, - attrs: { - title: p.text, - disabled, - }, - hook: bind('click', () => { - !disabled && ctrl.post(p); - }), + class: { disabled }, + attrs: { title: p.text, disabled }, + hook: bind('click', () => !disabled && ctrl.post(p)), }, p.key, ); diff --git a/ui/chat/src/spam.ts b/ui/chat/src/spam.ts index 65ae3f84e8212..e4ce1daa84dd2 100644 --- a/ui/chat/src/spam.ts +++ b/ui/chat/src/spam.ts @@ -39,6 +39,8 @@ const spamRegex = new RegExp( 'qps.ru', 'tiny.cc/', 'trasderk.blogspot.com', + 't.ly/', + 'shorturl.at/', ] .map(url => url.replace(/\./g, '\\.').replace(/\//g, '\\/')) .join('|'), diff --git a/ui/chat/src/view.ts b/ui/chat/src/view.ts index fbdd227a2d12b..7ff8824148bb8 100644 --- a/ui/chat/src/view.ts +++ b/ui/chat/src/view.ts @@ -12,14 +12,7 @@ import ChatCtrl from './ctrl'; export default function (ctrl: ChatCtrl): VNode { return h( 'section.mchat' + (ctrl.opts.alwaysEnabled ? '' : '.mchat-optional'), - { - class: { - 'mchat-mod': !!ctrl.moderation, - }, - hook: { - destroy: ctrl.destroy, - }, - }, + { class: { 'mchat-mod': !!ctrl.moderation }, hook: { destroy: ctrl.destroy } }, moderationView(ctrl.moderation) || normalView(ctrl), ); } @@ -30,15 +23,12 @@ function renderPalantir(ctrl: ChatCtrl) { return p.instance ? p.instance.render(h) : h('div.mchat__tab.palantir.palantir-slot', { - attrs: { - 'data-icon': licon.Handset, - title: 'Voice chat', - }, + attrs: { 'data-icon': licon.Handset, title: 'Voice chat' }, hook: bind('click', () => { if (!p.loaded) { p.loaded = true; - lichess.loadIife('javascripts/vendor/peerjs.min.js').then(() => { - lichess + lichess.asset.loadIife('javascripts/vendor/peerjs.min.js').then(() => { + lichess.asset .loadEsm('palantir', { init: { uid: ctrl.data.userId!, redraw: ctrl.redraw }, }) @@ -64,8 +54,8 @@ function normalView(ctrl: ChatCtrl) { active === 'note' && ctrl.note ? [noteView(ctrl.note, ctrl.vm.autofocus)] : ctrl.plugin && active === ctrl.plugin.tab.key - ? [ctrl.plugin.view()] - : discussionView(ctrl), + ? [ctrl.plugin.view()] + : discussionView(ctrl), ), ]; } @@ -88,11 +78,7 @@ function tabName(ctrl: ChatCtrl, tab: Tab) { ctrl.opts.alwaysEnabled ? undefined : h('input', { - attrs: { - type: 'checkbox', - title: ctrl.trans.noarg('toggleTheChat'), - checked: ctrl.vm.enabled, - }, + attrs: { type: 'checkbox', title: ctrl.trans.noarg('toggleTheChat'), checked: ctrl.vm.enabled }, hook: bind('change', (e: Event) => { ctrl.setEnabled((e.target as HTMLInputElement).checked); }), diff --git a/ui/chess/src/promotion.ts b/ui/chess/src/promotion.ts index 7b5ff0d9cbb9a..9827c110a5737 100644 --- a/ui/chess/src/promotion.ts +++ b/ui/chess/src/promotion.ts @@ -23,18 +23,7 @@ const PROMOTABLE_ROLES: cg.Role[] = ['queen', 'knight', 'rook', 'bishop']; export function promote(g: CgApi, key: Key, role: cg.Role): void { const piece = g.state.pieces.get(key); if (piece && piece.role == 'pawn') { - g.setPieces( - new Map([ - [ - key, - { - color: piece.color, - role, - promoted: true, - }, - ], - ]), - ); + g.setPieces(new Map([[key, { color: piece.color, role, promoted: true }]])); } } @@ -136,11 +125,7 @@ export class PromotionCtrl { g.setAutoShapes([ { orig: dest, - piece: { - color: cgUtil.opposite(g.state.turnColor), - role, - opacity: 0.8, - }, + piece: { color: cgUtil.opposite(g.state.turnColor), role, opacity: 0.8 }, brush: '', } as DrawShape, ]), @@ -166,9 +151,7 @@ export class PromotionCtrl { return h( 'square', { - attrs: { - style: 'top: ' + top + '%;left: ' + left + '%', - }, + attrs: { style: 'top: ' + top + '%;left: ' + left + '%' }, hook: bind('click', e => { e.stopPropagation(); this.finish(serverRole); diff --git a/ui/cli/src/main.ts b/ui/cli/src/main.ts index dc316bb8d99fe..0c69f4cb0b780 100644 --- a/ui/cli/src/main.ts +++ b/ui/cli/src/main.ts @@ -2,7 +2,7 @@ import { domDialog } from 'common/dialog'; import { load as loadDasher } from 'dasher'; export function initModule({ input }: { input: HTMLInputElement }) { - lichess.userComplete({ + lichess.asset.userComplete({ input, friend: true, focus: true, @@ -36,7 +36,7 @@ function command(q: string) { if (is('tv follow') && parts[1]) location.href = '/@/' + parts[1] + '/tv'; else if (is('tv')) location.href = '/tv'; else if (is('play challenge match') && parts[1]) location.href = '/?user=' + parts[1] + '#friend'; - else if (is('light dark transp system')) loadDasher().then(m => m.subs.background.set(exec)); + else if (is('light dark transp system')) loadDasher().then(m => m.background.set(exec)); else if (is('stream') && parts[1]) location.href = '/streamer/' + parts[1]; else if (is('help')) help(); else alert(`Unknown command: "${q}". Type /help for the list of commands`); diff --git a/ui/common/css/component/_dialog.scss b/ui/common/css/component/_dialog.scss index df83f4ba29c3f..5b344392ef1e8 100644 --- a/ui/common/css/component/_dialog.scss +++ b/ui/common/css/component/_dialog.scss @@ -2,6 +2,7 @@ dialog { @extend %box-radius, %popup-shadow; position: fixed; max-height: 100%; + max-width: 100%; top: 50%; #{$start-direction}: 50%; transform: translate($transform-direction * -50%, -50%); diff --git a/ui/common/css/component/_markdown.scss b/ui/common/css/component/_markdown.scss index c9394cdacf4be..5e0356657041a 100644 --- a/ui/common/css/component/_markdown.scss +++ b/ui/common/css/component/_markdown.scss @@ -1,4 +1,12 @@ -@mixin rendered-markdown($element-margin: 1em, $img-max-height: 50vh) { +@mixin rendered-markdown( + $element-margin: 1em, + $img-max-height: 50vh, + $table: true, + $list: true, + $img: true, + $h1: true, + $h2: true +) { @extend %break-word; strong { @@ -10,51 +18,55 @@ } p, - ol, - ul, - table, pre { margin: $element-margin 0; } - ol, - ul { - padding-#{$start-direction}: 0.5em; - } + @if $list { + ol, + ul { + margin: $element-margin 0; + padding-#{$start-direction}: 0.5em; + } - li ol, - li ul { - margin: 0; - padding-#{$start-direction}: 2em; - } + li ol, + li ul { + margin: 0; + padding-#{$start-direction}: 2em; + } - ol > li { - list-style: decimal inside; - } + ol > li { + list-style: decimal inside; + } - ul > li { - list-style: disc inside; - } + ul > li { + list-style: disc inside; + } - li p { - display: inline-block; - vertical-align: top; - margin-top: 0; + li p { + display: inline-block; + vertical-align: top; + margin-top: 0; + } } - /* should not be used */ - h1 { - font-size: 1em; - margin: 0; + @if $h1 { + /* should not be used */ + h1 { + font-size: 1em; + margin: 0; + } } - h2 { - &:not(:first-child) { - margin-top: calc(0.9 * #{$element-margin}); + @if $h2 { + h2 { + &:not(:first-child) { + margin-top: calc(0.9 * #{$element-margin}); + } + border-bottom: 1px solid $c-brag; + line-height: 1.5em; + padding-bottom: calc(0.25 * #{$element-margin}); } - border-bottom: 1px solid $c-brag; - line-height: 1.5em; - padding-bottom: calc(0.25 * #{$element-margin}); } h3 { @@ -72,23 +84,30 @@ @include fluid-size('font-size', 17px, 23px); } - img { - display: block; - max-width: 100%; - max-height: $img-max-height; - margin: auto; + @if $img { + img { + display: block; + max-width: 100%; + max-height: $img-max-height; + margin: auto; + } } - th, - td { - &[align='left'] { - text-align: left; - } - &[align='center'] { - text-align: center; + @if $table { + table { + margin: $element-margin 0; } - &[align='right'] { - text-align: right; + th, + td { + &[align='left'] { + text-align: left; + } + &[align='center'] { + text-align: center; + } + &[align='right'] { + text-align: right; + } } } } diff --git a/ui/common/css/component/_power-tip.scss b/ui/common/css/component/_power-tip.scss index d4db27f6a67ea..ffecb568ebb77 100644 --- a/ui/common/css/component/_power-tip.scss +++ b/ui/common/css/component/_power-tip.scss @@ -44,10 +44,12 @@ } } &__flag { + @extend %flex-center; + margin-#{$start-direction}: 0.5ch; flex: 1 1 auto; overflow: hidden; - line-height: 1em; font-size: 0.9em; + color: $c-font-dim; } signal { flex: 0 0 auto; diff --git a/ui/common/css/form/_emoji-picker.scss b/ui/common/css/form/_emoji-picker.scss new file mode 100644 index 0000000000000..098850c7e726d --- /dev/null +++ b/ui/common/css/form/_emoji-picker.scss @@ -0,0 +1,6 @@ +.emoji-details { + position: relative; + // ensure the emoji picker is above the text and its licon + z-index: 2; + margin-bottom: 1em; +} diff --git a/ui/common/css/form/_form3.scss b/ui/common/css/form/_form3.scss index 3ba1be72ad2b5..ff883d93d37ef 100644 --- a/ui/common/css/form/_form3.scss +++ b/ui/common/css/form/_form3.scss @@ -82,7 +82,7 @@ textarea.form-control { .form-check { @extend %flex-column; - justify-content: center; + // justify-content: center; div { display: flex; } diff --git a/ui/common/css/vendor/chessground/_themes.scss b/ui/common/css/vendor/chessground/_themes.scss index 2e0bd6264d4a5..36c155de3fbb9 100644 --- a/ui/common/css/vendor/chessground/_themes.scss +++ b/ui/common/css/vendor/chessground/_themes.scss @@ -102,7 +102,7 @@ $board-themes-2d: ( coord-color-black: #6d6655, ), 'newspaper': ( - file-ext: 'png', + file-ext: 'svg', coord-color-white: #fff, coord-color-black: #8d8d8d, ), diff --git a/ui/common/src/controls.ts b/ui/common/src/controls.ts index 51f5f65f255b1..242734af2a736 100644 --- a/ui/common/src/controls.ts +++ b/ui/common/src/controls.ts @@ -1,5 +1,5 @@ -import { h, Hooks } from 'snabbdom'; -import { bind, onInsert } from './snabbdom'; +import { h, Hooks, VNode } from 'snabbdom'; +import { bind } from './snabbdom'; import { toggle as baseToggle } from './common'; import * as xhr from './xhr'; @@ -17,19 +17,11 @@ export function toggle(t: ToggleSettings, trans: Trans, redraw: () => void) { const fullId = 'abset-' + t.id; return h( 'div.setting.' + fullId + (t.cls ? '.' + t.cls : ''), - t.title - ? { - attrs: { title: trans.noarg(t.title) }, - } - : {}, + t.title ? { attrs: { title: trans.noarg(t.title) } } : {}, [ h('div.switch', [ h('input#' + fullId + '.cmn-toggle', { - attrs: { - type: 'checkbox', - checked: t.checked, - disabled: !!t.disabled, - }, + attrs: { type: 'checkbox', checked: t.checked, disabled: !!t.disabled }, hook: bind('change', e => t.change((e.target as HTMLInputElement).checked), redraw), }), h('label', { attrs: { for: fullId } }), @@ -39,12 +31,19 @@ export function toggle(t: ToggleSettings, trans: Trans, redraw: () => void) { ); } -export const rangeConfig = (read: () => number, write: (value: number) => void): Hooks => - onInsert((el: HTMLInputElement) => { - el.value = '' + read(); - el.addEventListener('input', _ => write(parseInt(el.value))); - el.addEventListener('mouseout', _ => el.blur()); - }); +export function rangeConfig(read: () => number, write: (value: number) => void): Hooks { + return { + insert: (v: VNode) => { + const el = v.elm as HTMLInputElement; + el.value = '' + read(); + el.addEventListener('input', _ => write(parseInt(el.value))); + el.addEventListener('mouseout', _ => el.blur()); + }, + update: (_, v: VNode) => { + (v.elm as HTMLInputElement).value = `${read()}`; // force redraw on external value change + }, + }; +} export const boolPrefXhrToggle = (prefKey: string, val: boolean, effect: () => void = lichess.reload) => baseToggle(val, async v => { diff --git a/ui/common/src/device.ts b/ui/common/src/device.ts index 972c2bcac7521..ea94961034c8f 100644 --- a/ui/common/src/device.ts +++ b/ui/common/src/device.ts @@ -56,13 +56,9 @@ export const isChrome = (): boolean => /Chrome\//.test(navigator.userAgent); export const isFirefox = (): boolean => /Firefox/.test(navigator.userAgent); -export const getFirefoxMajorVersion = (): number => { - if (!isFirefox()) { - return 0; - } +export const getFirefoxMajorVersion = (): number | undefined => { const match = /Firefox\/(\d*)/.exec(navigator.userAgent); - if (!match || match.length < 2) return 0; - return parseInt(match[1]); + return match && match.length > 1 ? parseInt(match[1]) : undefined; }; export const isIOSChrome = (): boolean => /CriOS/.test(navigator.userAgent); @@ -72,7 +68,7 @@ export const isTouchDevice = () => !hasMouse(); export const isIPad = (): boolean => navigator?.maxTouchPoints > 2 && /iPad|Macintosh/.test(navigator.userAgent); -export type Feature = 'wasm' | 'sharedMem' | 'simd' | 'webWorkerDynamicImport'; +export type Feature = 'wasm' | 'sharedMem' | 'simd'; export const hasFeature = (feat?: string) => !feat || features().includes(feat as Feature); @@ -91,9 +87,6 @@ export const features = memoize(() => { if (WebAssembly.validate(sourceWithSimd)) features.push('simd'); } } - if (!getFirefoxMajorVersion() || getFirefoxMajorVersion() >= 114) { - features.push('webWorkerDynamicImport'); - } return Object.freeze(features); }); diff --git a/ui/common/src/dialog.ts b/ui/common/src/dialog.ts index bbbd4c83a0b7c..bf195f705f60a 100644 --- a/ui/common/src/dialog.ts +++ b/ui/common/src/dialog.ts @@ -1,5 +1,5 @@ import { VNode, Attrs } from 'snabbdom'; -import { onInsert, lh as h, MaybeVNodes } from './snabbdom'; +import { onInsert, looseH as h, LooseVNodes } from './snabbdom'; import { isTouchDevice } from './device'; import * as xhr from './xhr'; import * as licon from './licon'; @@ -33,8 +33,8 @@ export interface DomDialogOpts extends DialogOpts { } export interface SnabDialogOpts extends DialogOpts { - vnodes?: MaybeVNodes; // snabDialog automatically shows as 'modal' on redraw unless.. - onInsert?: (dialog: Dialog) => void; // if onInsert supplied, call show()/showModal() manually + vnodes?: LooseVNodes; // snabDialog automatically shows as 'modal' on redraw unless.. + onInsert?: (dialog: Dialog) => void; // if supplied, call show() or showModal() manually } // Action can be any "clickable" client button, usually to dismiss the dialog @@ -50,9 +50,9 @@ export interface Action { export const ready = lichess.load.then(async () => { window.addEventListener('resize', onResize); if (window.HTMLDialogElement) return true; - dialogPolyfill = (await import(lichess.assetUrl('npm/dialog-polyfill.esm.js')).catch(() => undefined)) - ?.default; - return dialogPolyfill !== undefined; + registerPolyfill = (await import(lichess.asset.url('npm/dialog-polyfill.esm.js')).catch(() => undefined)) + ?.default.registerDialog; + return registerPolyfill !== undefined; }); // if no 'show' in opts, you must call show or showModal on the resolved promise @@ -138,7 +138,7 @@ class DialogWrapper implements Dialog { readonly view: HTMLElement, readonly o: DialogOpts, ) { - if (dialogPolyfill) dialogPolyfill.registerDialog(dialog); // ios < 15.4 + registerPolyfill?.(dialog); // ios < 15.4 const justThen = Date.now(); const cancelOnInterval = () => Date.now() - justThen > 200 && this.close('cancel'); @@ -213,7 +213,7 @@ function assets(o: DialogOpts) { : Promise.resolve( o.cash ? $as($(o.cash).clone().removeClass('none')).outerHTML : o.htmlText, ), - o.cssPath ? lichess.loadCssPath(o.cssPath) : Promise.resolve(), + o.cssPath ? lichess.asset.loadCssPath(o.cssPath) : Promise.resolve(), ]); } @@ -241,4 +241,4 @@ const focusQuery = ['button', 'input', 'select', 'textarea'] .concat(['[href]', '[tabindex="0"]', '[role="tab"]']) .join(','); -let dialogPolyfill: { registerDialog: (dialog: HTMLDialogElement) => void }; +let registerPolyfill: (dialog: HTMLDialogElement) => void; diff --git a/ui/common/src/linkPopup.ts b/ui/common/src/linkPopup.ts index 80378c179a9c8..bd54a9f72a96b 100644 --- a/ui/common/src/linkPopup.ts +++ b/ui/common/src/linkPopup.ts @@ -10,8 +10,9 @@ export const onClick = (a: HTMLLinkElement, trans: Trans): boolean => { if (isPassList(url)) return true; domDialog({ + class: 'link-popup', cssPath: 'linkPopup', - htmlText: `
`, }).then(dlg => { $('.cancel', dlg.view).on('click', dlg.close); $('a', dlg.view).on('click', () => setTimeout(dlg.close, 1000)); diff --git a/ui/common/src/mini-board.ts b/ui/common/src/mini-board.ts index ef1daf42cd1e0..591a3be335e6f 100644 --- a/ui/common/src/mini-board.ts +++ b/ui/common/src/mini-board.ts @@ -35,8 +35,5 @@ export const fenColor = (fen: string) => (fen.includes(' w') ? 'white' : 'black' export const renderClock = (color: Color, time: number) => h(`span.mini-game__clock.mini-game__clock--${color}`, { - attrs: { - 'data-time': time, - 'data-managed': 1, - }, + attrs: { 'data-time': time, 'data-managed': 1 }, }); diff --git a/ui/common/src/notification.ts b/ui/common/src/notification.ts index 099fa28e92ad8..6f3ab91619a08 100644 --- a/ui/common/src/notification.ts +++ b/ui/common/src/notification.ts @@ -17,7 +17,7 @@ function notify(msg: string | (() => string)) { storage.set('' + Date.now()); if ($.isFunction(msg)) msg = msg(); const notification = new Notification('lichess.org', { - icon: lichess.assetUrl('logo/lichess-favicon-256.png', { noVersion: true }), + icon: lichess.asset.url('logo/lichess-favicon-256.png', { noVersion: true }), body: msg, }); notification.onclick = () => window.focus(); diff --git a/ui/common/src/snabbdom.ts b/ui/common/src/snabbdom.ts index 0b7db3d7c720d..9b23bf8cb247d 100644 --- a/ui/common/src/snabbdom.ts +++ b/ui/common/src/snabbdom.ts @@ -10,7 +10,7 @@ export function onInsert(f: (element: A) => void): Hooks }; } -export function bind(eventName: string, f: (e: Event) => any, redraw?: () => void, passive = true): Hooks { +export function bind(eventName: string, f: (e: Event) => any, redraw?: Redraw, passive = true): Hooks { return onInsert(el => el.addEventListener( eventName, @@ -38,30 +38,27 @@ export const dataIcon = (icon: string): Attrs => ({ export const iconTag = (icon: string) => snabH('i', { attrs: dataIcon(icon) }); -type LooseVNode = VNodeChildElement | boolean; -type VNodeKids = LooseVNode | LooseVNode[]; +type LooseVNode = VNode | string | undefined | null | boolean; +export type LooseVNodes = LooseVNode[]; +export type VNodeKids = LooseVNode | LooseVNodes; -function filterKids(children: VNodeKids): VNodeChildElement[] { - return ( - typeof children === 'boolean' - ? [] - : Array.isArray(children) - ? children.filter(x => typeof x !== 'boolean') - : [children] - ) as VNodeChildElement[]; -} +// '' may be falsy but it's a valid VNode +// 0 may be falsy but it's a valid VNode +const kidFilter = (x: VNodeData | VNodeKids): boolean => (x && x !== true) || x === ''; + +const filterKids = (children: VNodeKids): VNodeChildElement[] => + (Array.isArray(children) ? children : [children]).filter(kidFilter) as VNodeChildElement[]; /* obviate need for some ternary expressions in renders. Allows - lh('div', [ kids && h('div', 'kid') ]) - lh('div', [ noKids || h('div', 'kid') ]) + looseH('div', [ kids && h('div', 'kid') ]) instead of h('div', [ isKid ? h('div', 'kid') : null ]) 'true' values are filtered out of children array same as 'false' (for || case) */ -export function lh(sel: string, dataOrKids?: VNodeData | null | VNodeKids, kids?: VNodeKids): VNode { +export function looseH(sel: string, dataOrKids?: VNodeData | VNodeKids, kids?: VNodeKids): VNode { if (kids) return snabH(sel, dataOrKids as VNodeData, filterKids(kids)); - if (!dataOrKids) return snabH(sel); - if (Array.isArray(dataOrKids) || (typeof dataOrKids === 'object' && 'sel' in dataOrKids)) + if (!kidFilter(dataOrKids)) return snabH(sel); + if (Array.isArray(dataOrKids) || (typeof dataOrKids === 'object' && 'sel' in dataOrKids!)) return snabH(sel, filterKids(dataOrKids as VNodeKids)); else return snabH(sel, dataOrKids as VNodeData); } diff --git a/ui/common/src/spinner.ts b/ui/common/src/spinner.ts index 90f39f6ded85a..f2304e94b62bf 100644 --- a/ui/common/src/spinner.ts +++ b/ui/common/src/spinner.ts @@ -19,23 +19,12 @@ const pathAttrs = [ ]; export const spinnerVdom = (box = '-2 -2 54 54'): VNode => - h( - 'div.spinner', - { - 'aria-label': 'loading', - }, - [ - h('svg', { attrs: { viewBox: box } }, [ - h( - 'g', - { - attrs: { - mask: 'url(#mask)', - fill: 'none', - }, - }, - pathAttrs.map(attrs => h('path', { attrs })), - ), - ]), - ], - ); + h('div.spinner', { 'aria-label': 'loading' }, [ + h('svg', { attrs: { viewBox: box } }, [ + h( + 'g', + { attrs: { mask: 'url(#mask)', fill: 'none' } }, + pathAttrs.map(attrs => h('path', { attrs })), + ), + ]), + ]); diff --git a/ui/common/src/userLink.ts b/ui/common/src/userLink.ts index e61b9ea90fcdb..0b77488ec7351 100644 --- a/ui/common/src/userLink.ts +++ b/ui/common/src/userLink.ts @@ -35,27 +35,14 @@ export const userLink = (u: AnyUser): VNode => 'a', { // can't be inlined because of thunks - class: { - 'user-link': true, - ulpt: u.name != 'ghost', - online: !!u.online, - }, - attrs: { - href: `/@/${u.name}`, - ...u.attrs, - }, + class: { 'user-link': true, ulpt: u.name != 'ghost', online: !!u.online }, + attrs: { href: `/@/${u.name}`, ...u.attrs }, }, - [userLine(u), ...fullName(u), userRating(u)], + [userLine(u), ...fullName(u), u.rating && ` ${userRating(u)} `], ); export const userFlair = (u: HasFlair): VNode | undefined => - u.flair - ? h('img.uflair', { - attrs: { - src: lichess.flairSrc(u.flair), - }, - }) - : undefined; + u.flair ? h('img.uflair', { attrs: { src: lichess.asset.flairSrc(u.flair) } }) : undefined; export const userLine = (u: HasLine): VNode | undefined => u.line !== false @@ -74,7 +61,7 @@ export const fullName = (u: AnyUser) => [userTitle(u), u.name, userFlair(u)]; export const userRating = (u: HasRating): string | undefined => { if (u.rating) { - const rating = `${u.rating}${u.provisional ? '?' : ''} `; + const rating = `${u.rating}${u.provisional ? '?' : ''}`; return u.brackets !== false ? `(${rating})` : rating; } return undefined; diff --git a/ui/coordinateTrainer/src/side.ts b/ui/coordinateTrainer/src/side.ts index 97ca91b79de5c..308d238642554 100644 --- a/ui/coordinateTrainer/src/side.ts +++ b/ui/coordinateTrainer/src/side.ts @@ -41,12 +41,7 @@ const filesAndRanksSelection = (ctrl: CoordinateTrainerCtrl): VNodes => }), h( `label.file_${fileLetter}`, - { - attrs: { - for: `coord_file_${fileLetter}`, - title: fileLetter, - }, - }, + { attrs: { for: `coord_file_${fileLetter}`, title: fileLetter } }, fileLetter, ), ]), @@ -74,16 +69,7 @@ const filesAndRanksSelection = (ctrl: CoordinateTrainerCtrl): VNodes => keyup: ctrl.onRadioInputKeyUp, }, }), - h( - `label.rank_${rank}`, - { - attrs: { - for: `coord_rank_${rank}`, - title: rank, - }, - }, - rank, - ), + h(`label.rank_${rank}`, { attrs: { for: `coord_rank_${rank}`, title: rank } }, rank), ]), ), ), @@ -194,12 +180,7 @@ const configurationButtons = (ctrl: CoordinateTrainerCtrl): VNodes => [ }), h( `label.color_${key}`, - { - attrs: { - for: `coord_color_${key}`, - title: ctrl.trans.noarg(i18n), - }, - }, + { attrs: { for: `coord_color_${key}`, title: ctrl.trans.noarg(i18n) } }, h('i'), ), ]), @@ -222,14 +203,8 @@ const scoreCharts = (ctrl: CoordinateTrainerCtrl): VNode => ? h('div.color-chart', [ h('p', ctrl.trans.vdom(transKey, h('strong', `${average(scoreList).toFixed(2)}`))), h('svg.sparkline', { - attrs: { - height: '80px', - 'stroke-width': '3', - id: `${color}-sparkline`, - }, - hook: { - insert: vnode => ctrl.updateChart(vnode.elm as SVGSVGElement, color), - }, + attrs: { height: '80px', 'stroke-width': '3', id: `${color}-sparkline` }, + hook: { insert: vnode => ctrl.updateChart(vnode.elm as SVGSVGElement, color) }, }), ]) : null, @@ -247,16 +222,7 @@ const timeBox = (ctrl: CoordinateTrainerCtrl): VNode => ]); const backButton = (ctrl: CoordinateTrainerCtrl): VNode => - h( - 'div.back', - h( - 'a.back-button', - { - hook: bind('click', ctrl.stop), - }, - `« ${ctrl.trans('back')}`, - ), - ); + h('div.back', h('a.back-button', { hook: bind('click', ctrl.stop) }, `« ${ctrl.trans('back')}`)); const settings = (ctrl: CoordinateTrainerCtrl): VNode => { const { trans, redraw, showCoordinates, showPieces } = ctrl; diff --git a/ui/coordinateTrainer/src/view.ts b/ui/coordinateTrainer/src/view.ts index 40830e717b0d6..5bc776889378b 100644 --- a/ui/coordinateTrainer/src/view.ts +++ b/ui/coordinateTrainer/src/view.ts @@ -1,36 +1,35 @@ -import { h, VNode, VNodeStyle } from 'snabbdom'; -import { bind, MaybeVNode } from 'common/snabbdom'; +import { VNode, VNodeStyle } from 'snabbdom'; +import { bind, looseH as h } from 'common/snabbdom'; import { renderVoiceBar } from 'voice'; import chessground from './chessground'; import CoordinateTrainerCtrl, { DURATION } from './ctrl'; import { CoordModifier } from './interfaces'; import side from './side'; -const textOverlay = (ctrl: CoordinateTrainerCtrl): VNode | null => { - return ctrl.playing && ctrl.mode() === 'findSquare' - ? h( - 'svg.coords-svg', - { attrs: { viewBox: '0 0 100 100' } }, - ['current', 'next'].map((modifier: CoordModifier) => - h( - `g.${modifier}`, - { - key: `${ctrl.score}-${modifier}`, - style: - modifier === 'current' - ? ({ - remove: { - opacity: 0, - transform: 'translate(-8px, 60px)', - }, - } as unknown as VNodeStyle) - : undefined, - }, - h('text', modifier === 'current' ? ctrl.currentKey : ctrl.nextKey), - ), +const textOverlay = (ctrl: CoordinateTrainerCtrl): VNode | false => { + return ( + ctrl.playing && + ctrl.mode() === 'findSquare' && + h( + 'svg.coords-svg', + { attrs: { viewBox: '0 0 100 100' } }, + ['current', 'next'].map((modifier: CoordModifier) => + h( + `g.${modifier}`, + { + key: `${ctrl.score}-${modifier}`, + style: + modifier === 'current' + ? ({ + remove: { opacity: 0, transform: 'translate(-8px, 60px)' }, + } as unknown as VNodeStyle) + : undefined, + }, + h('text', modifier === 'current' ? ctrl.currentKey : ctrl.nextKey), ), - ) - : null; + ), + ) + ); }; const explanation = (ctrl: CoordinateTrainerCtrl): VNode => { @@ -50,60 +49,47 @@ const explanation = (ctrl: CoordinateTrainerCtrl): VNode => { const table = (ctrl: CoordinateTrainerCtrl): VNode => { return h('div.table', [ - ctrl.hasPlayed ? null : explanation(ctrl), - ctrl.playing - ? null - : h( - 'button.start.button.button-fat', - { - hook: bind('click', ctrl.start), - }, - ctrl.trans('startTraining'), - ), + !ctrl.hasPlayed && explanation(ctrl), + !ctrl.playing && + h('button.start.button.button-fat', { hook: bind('click', ctrl.start) }, ctrl.trans('startTraining')), ]); }; const progress = (ctrl: CoordinateTrainerCtrl): VNode => { return h( 'div.progress', - ctrl.hasPlayed - ? h('div.progress__bar', { style: { width: `${100 * (1 - ctrl.timeLeft / DURATION)}%` } }) - : null, + ctrl.hasPlayed && + h('div.progress__bar', { style: { width: `${100 * (1 - ctrl.timeLeft / DURATION)}%` } }), ); }; -const coordinateInput = (ctrl: CoordinateTrainerCtrl): MaybeVNode => { +const coordinateInput = (ctrl: CoordinateTrainerCtrl): VNode | false => { const coordinateInput = [ - ctrl.coordinateInputMethod() === 'buttons' - ? h( - 'div.files-ranks', - 'abcdefgh12345678'.split('').map((fileOrRank: string) => - h( - 'button.file-rank', - { - on: { - click: () => { - if (ctrl.playing) { - ctrl.keyboardInput.value += fileOrRank; - ctrl.checkKeyboardInput(); - } - }, + ctrl.coordinateInputMethod() === 'buttons' && + h( + 'div.files-ranks', + 'abcdefgh12345678'.split('').map((fileOrRank: string) => + h( + 'button.file-rank', + { + on: { + click: () => { + if (ctrl.playing) { + ctrl.keyboardInput.value += fileOrRank; + ctrl.checkKeyboardInput(); + } }, }, - fileOrRank, - ), + }, + fileOrRank, ), - ) - : null, + ), + ), h('div.voice-container', renderVoiceBar(ctrl.voice, ctrl.redraw, 'coords')), h('div.keyboard-container', [ h('span', [ h('input.keyboard', { - hook: { - insert: vnode => { - ctrl.keyboardInput = vnode.elm as HTMLInputElement; - }, - }, + hook: { insert: vnode => (ctrl.keyboardInput = vnode.elm as HTMLInputElement) }, on: { keyup: ctrl.onKeyboardInputKeyUp }, }), ctrl.playing ? h('span', 'Enter the coordinate') : h('strong', 'Press to start'), @@ -115,25 +101,17 @@ const coordinateInput = (ctrl: CoordinateTrainerCtrl): MaybeVNode => { ), ]), ]; - return ctrl.mode() === 'nameSquare' ? h('div.coordinate-input', [...coordinateInput]) : null; + return ctrl.mode() === 'nameSquare' && h('div.coordinate-input', [...coordinateInput]); }; const view = (ctrl: CoordinateTrainerCtrl): VNode => - h( - 'div.trainer', - { - class: { - wrong: ctrl.wrong, - }, - }, - [ - side(ctrl), - h('div.main-board', chessground(ctrl)), - textOverlay(ctrl), - table(ctrl), - progress(ctrl), - coordinateInput(ctrl), - ], - ); + h('div.trainer', { class: { wrong: ctrl.wrong } }, [ + side(ctrl), + h('div.main-board', chessground(ctrl)), + textOverlay(ctrl), + table(ctrl), + progress(ctrl), + coordinateInput(ctrl), + ]); export default view; diff --git a/ui/dasher/src/background.ts b/ui/dasher/src/background.ts index 5f7cb50faec26..8402c0f9211da 100644 --- a/ui/dasher/src/background.ts +++ b/ui/dasher/src/background.ts @@ -1,24 +1,13 @@ import { h, VNode } from 'snabbdom'; -import { Redraw, Close, bind, header } from './util'; +import { Close, header } from './util'; import debounce from 'common/debounce'; import * as licon from 'common/licon'; -import { onInsert } from 'common/snabbdom'; +import { bind, onInsert, Redraw } from 'common/snabbdom'; import * as xhr from 'common/xhr'; import { elementScrollBarWidth } from 'common/scroll'; import { throttlePromiseDelay } from 'common/throttle'; import { supportsSystemTheme } from 'common/theme'; -export interface BackgroundCtrl { - list: Background[]; - set(k: string): void; - get(): string; - getImage(): string; - setImage(i: string): void; - trans: Trans; - close: Close; - data: BackgroundData; -} - export interface BackgroundData { current: string; image: string; @@ -35,53 +24,52 @@ interface Background { title?: string; } -export function ctrl(data: BackgroundData, trans: Trans, redraw: Redraw, close: Close): BackgroundCtrl { - const list: Background[] = [ - { key: 'system', name: trans.noarg('deviceTheme') }, - { key: 'light', name: trans.noarg('light') }, - { key: 'dark', name: trans.noarg('dark') }, - { key: 'darkBoard', name: 'Dark Board', title: 'Like Dark, but chess boards are also darker' }, - { key: 'transp', name: 'Picture' }, - ]; +export class BackgroundCtrl { + list: Background[]; - const announceFail = () => lichess.announce({ msg: 'Failed to save background preference' }); + constructor( + readonly data: BackgroundData, + readonly trans: Trans, + readonly redraw: Redraw, + readonly close: Close, + ) { + this.list = [ + { key: 'system', name: trans.noarg('deviceTheme') }, + { key: 'light', name: trans.noarg('light') }, + { key: 'dark', name: trans.noarg('dark') }, + { key: 'darkBoard', name: 'Dark Board', title: 'Like Dark, but chess boards are also darker' }, + { key: 'transp', name: 'Picture' }, + ]; + } - const reloadAllTheThings = () => { + private announceFail = (err: string) => + lichess.announce({ msg: `Failed to save background preference: ${err}` }); + + private reloadAllTheThings = () => { if ($('canvas').length || window.Highcharts) lichess.reload(); }; - return { - list, - trans, - get: () => data.current, - set: throttlePromiseDelay( - () => 700, - (c: string) => { - data.current = c; - applyBackground(data, list); - redraw(); - return xhr - .text('/pref/bg', { - body: xhr.form({ bg: c }), - method: 'post', - }) - .then(reloadAllTheThings, announceFail); - }, - ), - getImage: () => data.image, - setImage(i: string) { - data.image = i; - xhr - .text('/pref/bgImg', { - body: xhr.form({ bgImg: i }), - method: 'post', - }) - .then(reloadAllTheThings, announceFail); - applyBackground(data, list); - redraw(); + get = () => this.data.current; + set = throttlePromiseDelay( + () => 700, + (c: string) => { + this.data.current = c; + applyBackground(this.data, this.list); + this.redraw(); + return xhr + .text('/pref/bg', { body: xhr.form({ bg: c }), method: 'post' }) + .then(this.reloadAllTheThings, this.announceFail); }, - close, - data, + ); + getImage = () => this.data.image; + setImage = (i: string) => { + this.data.image = i; + xhr + .textRaw('/pref/bgImg', { body: xhr.form({ bgImg: i }), method: 'post' }) + .then(res => (res.ok ? res.text() : Promise.reject(res.text()))) + .then(this.reloadAllTheThings, err => err.then(this.announceFail)); + applyBackground(this.data, this.list); + this.redraw(); }; } @@ -112,11 +100,7 @@ function imageInput(ctrl: BackgroundCtrl) { return h('div.image', [ h('p', ctrl.trans.noarg('backgroundImageUrl')), h('input', { - attrs: { - type: 'text', - placeholder: 'https://', - value: ctrl.getImage(), - }, + attrs: { type: 'text', placeholder: 'https://', value: ctrl.getImage() }, hook: { insert: vnode => { $(vnode.elm as HTMLElement).on( @@ -199,7 +183,7 @@ function galleryInput(ctrl: BackgroundCtrl) { const gallery = ctrl.data.gallery!; const cols = window.matchMedia('(min-width: 650px)').matches ? 4 : 2; // $mq-x-small - const montageUrl = lichess.assetUrl(gallery[`montage${cols}`], { noVersion: true }); + const montageUrl = lichess.asset.url(gallery[`montage${cols}`], { noVersion: true }); // our layout is static due to the single image gallery optimization. set width here // and allow for the possibility of non-overlaid scrollbars const width = cols * (160 + 2) + (gallery.images.length > cols * 4 ? elementScrollBarWidth() : 0); @@ -211,11 +195,9 @@ function galleryInput(ctrl: BackgroundCtrl) { 'div#images-grid', { attrs: { style: `background-image: url(${montageUrl});` } }, gallery.images.map(img => { - const assetUrl = lichess.assetUrl(img, { noVersion: true }); + const assetUrl = lichess.asset.url(img, { noVersion: true }); const divClass = ctrl.data.image.endsWith(assetUrl) ? '.selected' : ''; - return h(`div#${urlId(assetUrl)}${divClass}`, { - hook: bind('click', () => setImg(assetUrl)), - }); + return h(`div#${urlId(assetUrl)}${divClass}`, { hook: bind('click', () => setImg(assetUrl)) }); }), ), ), diff --git a/ui/dasher/src/board.ts b/ui/dasher/src/board.ts index 85af8da6e5212..9232e996e9fbe 100644 --- a/ui/dasher/src/board.ts +++ b/ui/dasher/src/board.ts @@ -1,56 +1,45 @@ import { h, VNode } from 'snabbdom'; -import { Redraw, Close, bind, header } from './util'; +import { Close, header } from './util'; import debounce from 'common/debounce'; import * as licon from 'common/licon'; import * as xhr from 'common/xhr'; - -export interface BoardCtrl { - data: BoardData; - trans: Trans; - setIs3d(v: boolean): void; - readZoom(): number; - setZoom(v: number): void; - close(): void; -} +import { bind, Redraw } from 'common/snabbdom'; export interface BoardData { is3d: boolean; } -export type PublishZoom = (v: number) => void; +export class BoardCtrl { + constructor( + readonly data: BoardData, + readonly trans: Trans, + readonly redraw: Redraw, + readonly close: Close, + ) {} -export function ctrl(data: BoardData, trans: Trans, redraw: Redraw, close: Close): BoardCtrl { - const readZoom = () => parseInt(window.getComputedStyle(document.body).getPropertyValue('--zoom')); + readZoom = () => parseInt(window.getComputedStyle(document.body).getPropertyValue('--zoom')); - const saveZoom = debounce( + saveZoom = debounce( () => xhr - .text('/pref/zoom?v=' + readZoom(), { method: 'post' }) + .text('/pref/zoom?v=' + this.readZoom(), { method: 'post' }) .catch(() => lichess.announce({ msg: 'Failed to save zoom' })), 1000, ); - return { - data, - trans, - setIs3d(v: boolean) { - data.is3d = v; - xhr - .text('/pref/is3d', { - body: xhr.form({ is3d: v }), - method: 'post', - }) - .then(lichess.reload, _ => lichess.announce({ msg: 'Failed to save geometry preference' })); - redraw(); - }, - readZoom, - setZoom(v: number) { - document.body.style.setProperty('--zoom', v.toString()); - window.dispatchEvent(new Event('resize')); - redraw(); - saveZoom(); - }, - close, + setIs3d = (v: boolean) => { + this.data.is3d = v; + xhr + .text('/pref/is3d', { body: xhr.form({ is3d: v }), method: 'post' }) + .then(lichess.reload, _ => lichess.announce({ msg: 'Failed to save geometry preference' })); + this.redraw(); + }; + + setZoom = (v: number) => { + document.body.style.setProperty('--zoom', v.toString()); + window.dispatchEvent(new Event('resize')); + this.redraw(); + this.saveZoom(); }; } @@ -86,13 +75,7 @@ export function view(ctrl: BoardCtrl): VNode { : [ h('p', [ctrl.trans.noarg('boardSize'), ': ', domZoom, '%']), h('input.range', { - attrs: { - type: 'range', - min: 0, - max: 100, - step: 1, - value: ctrl.readZoom(), - }, + attrs: { type: 'range', min: 0, max: 100, step: 1, value: ctrl.readZoom() }, hook: { insert(vnode) { const input = vnode.elm as HTMLInputElement; diff --git a/ui/dasher/src/dasher.ts b/ui/dasher/src/dasher.ts index 479b5943a49ff..7d29a0a27a7fb 100644 --- a/ui/dasher/src/dasher.ts +++ b/ui/dasher/src/dasher.ts @@ -1,11 +1,12 @@ import { PingCtrl } from './ping'; -import { LangsCtrl, LangsData, ctrl as langsCtrl } from './langs'; -import { SoundCtrl, ctrl as soundCtrl } from './sound'; -import { BackgroundCtrl, BackgroundData, ctrl as backgroundCtrl } from './background'; -import { BoardCtrl, BoardData, ctrl as boardCtrl } from './board'; -import { ThemeCtrl, ThemeData, ctrl as themeCtrl } from './theme'; -import { PieceCtrl, PieceData, ctrl as pieceCtrl } from './piece'; -import { Redraw, Prop, prop } from './util'; +import { LangsCtrl, LangsData } from './langs'; +import { SoundCtrl } from './sound'; +import { BackgroundCtrl, BackgroundData } from './background'; +import { BoardCtrl, BoardData } from './board'; +import { ThemeCtrl, ThemeData } from './theme'; +import { PieceCtrl, PieceData } from './piece'; +import { Redraw } from 'common/snabbdom'; +import { Prop, prop } from 'common'; export interface DasherData { user?: LightUser; @@ -26,63 +27,46 @@ export type Mode = 'links' | 'langs' | 'sound' | 'background' | 'board' | 'theme const defaultMode = 'links'; -export interface DasherCtrl { - mode: Prop; - setMode(m: Mode): void; - data: DasherData; - trans: Trans; - ping: PingCtrl; - subs: { - langs: LangsCtrl; - sound: SoundCtrl; - background: BackgroundCtrl; - board: BoardCtrl; - theme: ThemeCtrl; - piece: PieceCtrl; - }; - opts: DasherOpts; -} - export interface DasherOpts { playing: boolean; zenable: boolean; } -export function makeCtrl(data: DasherData, redraw: Redraw): DasherCtrl { - const trans = lichess.trans(data.i18n); - const opts = { +export default class DasherCtrl { + trans: Trans; + ping: PingCtrl; + langs: LangsCtrl; + sound: SoundCtrl; + background: BackgroundCtrl; + board: BoardCtrl; + theme: ThemeCtrl; + piece: PieceCtrl; + opts = { playing: $('body').hasClass('playing'), zenable: $('body').hasClass('zenable'), }; - const mode: Prop = prop(defaultMode as Mode); - - const setMode = (m: Mode) => { - mode(m); - redraw(); - }; - const close = () => setMode(defaultMode); - - const ping = new PingCtrl(trans, redraw); - - const subs = { - langs: langsCtrl(data.lang, trans, close), - sound: soundCtrl(data.sound.list, trans, redraw, close), - background: backgroundCtrl(data.background, trans, redraw, close), - board: boardCtrl(data.board, trans, redraw, close), - theme: themeCtrl(data.theme, trans, () => (data.board.is3d ? 'd3' : 'd2'), redraw, close), - piece: pieceCtrl(data.piece, trans, () => (data.board.is3d ? 'd3' : 'd2'), redraw, close), - }; + constructor( + readonly data: DasherData, + readonly redraw: Redraw, + ) { + this.trans = lichess.trans(data.i18n); + this.ping = new PingCtrl(this.trans, this.redraw); + const dimension = () => (this.data.board.is3d ? 'd3' : 'd2'); + this.langs = new LangsCtrl(this.data.lang, this.trans, this.close); + this.sound = new SoundCtrl(this.data.sound.list, this.trans, this.redraw, this.close); + this.background = new BackgroundCtrl(this.data.background, this.trans, this.redraw, this.close); + this.board = new BoardCtrl(this.data.board, this.trans, this.redraw, this.close); + this.theme = new ThemeCtrl(this.data.theme, this.trans, dimension, this.redraw, this.close); + this.piece = new PieceCtrl(this.data.piece, this.trans, dimension, this.redraw, this.close); + lichess.pubsub.on('top.toggle.user_tag', () => this.setMode(defaultMode)); + } - lichess.pubsub.on('top.toggle.user_tag', () => setMode(defaultMode)); + mode: Prop = prop(defaultMode as Mode); - return { - mode, - setMode, - data, - trans, - ping, - subs, - opts, + setMode = (m: Mode) => { + this.mode(m); + this.redraw(); }; + close = () => this.setMode(defaultMode); } diff --git a/ui/dasher/src/langs.ts b/ui/dasher/src/langs.ts index 650efc3ef1f4d..ef370a045401f 100644 --- a/ui/dasher/src/langs.ts +++ b/ui/dasher/src/langs.ts @@ -13,62 +13,38 @@ export interface LangsData { list: Lang[]; } -export interface LangsCtrl { - list(): Lang[]; - current: Code; +export class LangsCtrl { accepted: Set; - trans: Trans; - close: Close; -} - -export function ctrl(data: LangsData, trans: Trans, close: Close): LangsCtrl { - const accepted = new Set(data.accepted); - return { - list() { - return [...data.list.filter(lang => accepted.has(lang[0])), ...data.list]; - }, - current: data.current, - accepted, - trans, - close, - }; + constructor( + readonly data: LangsData, + readonly trans: Trans, + readonly close: Close, + ) { + this.accepted = new Set(data.accepted); + } + list = () => [...this.data.list.filter(lang => this.accepted.has(lang[0])), ...this.data.list]; } -export function view(ctrl: LangsCtrl): VNode { - return h('div.sub.langs', [ +export const view = (ctrl: LangsCtrl): VNode => + h('div.sub.langs', [ header(ctrl.trans.noarg('language'), ctrl.close), h( 'form', - { - attrs: { method: 'post', action: '/translation/select' }, - }, - ctrl.list().map(langView(ctrl.current, ctrl.accepted)), + { attrs: { method: 'post', action: '/translation/select' } }, + ctrl.list().map(langView(ctrl.data.current, ctrl.accepted)), ), h( 'a.help.text', - { - attrs: { - href: 'https://crowdin.com/project/lichess', - 'data-icon': licon.Heart, - }, - }, + { attrs: { href: 'https://crowdin.com/project/lichess', 'data-icon': licon.Heart } }, 'Help translate Lichess', ), ]); -} const langView = (current: Code, accepted: Set) => ([code, name]: Lang) => h( 'button' + (current === code ? '.current' : '') + (accepted.has(code) ? '.accepted' : ''), - { - attrs: { - type: 'submit', - name: 'lang', - value: code, - title: code, - }, - }, + { attrs: { type: 'submit', name: 'lang', value: code, title: code } }, name, ); diff --git a/ui/dasher/src/links.ts b/ui/dasher/src/links.ts index 8e06483c8114e..d7ba3ab266cff 100644 --- a/ui/dasher/src/links.ts +++ b/ui/dasher/src/links.ts @@ -1,8 +1,8 @@ import { Attrs, h, VNode } from 'snabbdom'; import * as licon from 'common/licon'; -import { DasherCtrl, Mode } from './dasher'; +import DasherCtrl, { Mode } from './dasher'; import { view as pingView } from './ping'; -import { bind } from './util'; +import { bind } from 'common/snabbdom'; export default function (ctrl: DasherCtrl): VNode { const d = ctrl.data, @@ -34,24 +34,9 @@ export default function (ctrl: DasherCtrl): VNode { !d.streamer ? null : h('a.text', linkCfg('/streamer/edit', licon.Mic), noarg('streamerManager')), - h( - 'form.logout', - { - attrs: { method: 'post', action: '/logout' }, - }, - [ - h( - 'button.text', - { - attrs: { - type: 'submit', - 'data-icon': licon.Power, - }, - }, - noarg('logOut'), - ), - ], - ), + h('form.logout', { attrs: { method: 'post', action: '/logout' } }, [ + h('button.text', { attrs: { type: 'submit', 'data-icon': licon.Power } }, noarg('logOut')), + ]), ]) : null; } @@ -73,11 +58,7 @@ export default function (ctrl: DasherCtrl): VNode { h( 'button.text', { - attrs: { - 'data-icon': licon.DiscBigOutline, - title: 'Keyboard: z', - type: 'button', - }, + attrs: { 'data-icon': licon.DiscBigOutline, title: 'Keyboard: z', type: 'button' }, hook: bind('click', () => lichess.pubsub.emit('zen')), }, noarg('zenMode'), @@ -93,11 +74,7 @@ export default function (ctrl: DasherCtrl): VNode { } const linkCfg = (href: string, icon: string, more?: Attrs) => ({ - attrs: { - href, - 'data-icon': icon, - ...(more || {}), - }, + attrs: { href, 'data-icon': icon, ...(more || {}) }, }); function modeCfg(ctrl: DasherCtrl, m: Mode): any { diff --git a/ui/dasher/src/main.ts b/ui/dasher/src/main.ts index a710f97b70d2f..85a2f0301a6bb 100644 --- a/ui/dasher/src/main.ts +++ b/ui/dasher/src/main.ts @@ -1,5 +1,5 @@ -import { Redraw } from './util'; -import { DasherCtrl, makeCtrl } from './dasher'; +import { Redraw } from 'common/snabbdom'; +import DasherCtrl from './dasher'; import { loading, loaded } from './view'; import * as xhr from 'common/xhr'; import { init as initSnabbdom, VNode, classModule, attributesModule } from 'snabbdom'; @@ -7,7 +7,7 @@ import { init as initSnabbdom, VNode, classModule, attributesModule } from 'snab const patch = initSnabbdom([classModule, attributesModule]); export function load() { - return lichess.loadEsm('dasher'); + return lichess.asset.loadEsm('dasher'); } export async function initModule() { @@ -25,7 +25,7 @@ export async function initModule() { redraw(); const data = await xhr.json('/dasher'); - ctrl = makeCtrl(data, redraw); + ctrl = new DasherCtrl(data, redraw); redraw(); new MutationObserver(_ => lichess.pubsub.emit('dasher.toggle', toggle.classList.contains('shown'))).observe( diff --git a/ui/dasher/src/piece.ts b/ui/dasher/src/piece.ts index a3467e6ceae47..3e8c5f85efd2e 100644 --- a/ui/dasher/src/piece.ts +++ b/ui/dasher/src/piece.ts @@ -1,7 +1,7 @@ import { h, VNode } from 'snabbdom'; - import * as xhr from 'common/xhr'; -import { Redraw, bind, header, Close } from './util'; +import { header, Close } from './util'; +import { bind, Redraw } from 'common/snabbdom'; type Piece = string; @@ -15,48 +15,29 @@ export interface PieceData { d3: PieceDimData; } -export interface PieceCtrl { - dimension: () => keyof PieceData; - data: () => PieceDimData; - trans: Trans; - set(t: Piece): void; - close: Close; -} - -export function ctrl( - data: PieceData, - trans: Trans, - dimension: () => keyof PieceData, - redraw: Redraw, - close: Close, -): PieceCtrl { - function dimensionData() { - return data[dimension()]; - } - - return { - dimension, - trans, - data: dimensionData, - set(t: Piece) { - const d = dimensionData(); - d.current = t; - applyPiece(t, d.list, dimension() === 'd3'); - const field = `pieceSet${dimension() === 'd3' ? '3d' : ''}`; - xhr - .text(`/pref/${field}`, { - body: xhr.form({ [field]: t }), - method: 'post', - }) - .catch(() => lichess.announce({ msg: 'Failed to save piece set preference' })); - redraw(); - }, - close: close, +export class PieceCtrl { + constructor( + private readonly data: PieceData, + readonly trans: Trans, + readonly dimension: () => keyof PieceData, + readonly redraw: Redraw, + readonly close: Close, + ) {} + dimensionData = () => this.data[this.dimension()]; + set = (t: Piece) => { + const d = this.dimensionData(); + d.current = t; + applyPiece(t, d.list, this.dimension() === 'd3'); + const field = `pieceSet${this.dimension() === 'd3' ? '3d' : ''}`; + xhr + .text(`/pref/${field}`, { body: xhr.form({ [field]: t }), method: 'post' }) + .catch(() => lichess.announce({ msg: 'Failed to save piece set preference' })); + this.redraw(); }; } export function view(ctrl: PieceCtrl): VNode { - const d = ctrl.data(); + const d = ctrl.dimensionData(); return h('div.sub.piece.' + ctrl.dimension(), [ header(ctrl.trans.noarg('pieceSet'), () => ctrl.close()), @@ -72,22 +53,16 @@ function pieceImage(t: Piece, is3d: boolean) { return `piece/${t}/wN.svg`; } -function pieceView(current: Piece, set: (t: Piece) => void, is3d: boolean) { - return (t: Piece) => - h( - 'button.no-square', - { - attrs: { title: t, type: 'button' }, - hook: bind('click', () => set(t)), - class: { active: current === t }, - }, - [ - h('piece', { - attrs: { style: `background-image:url(${lichess.assetUrl(pieceImage(t, is3d))})` }, - }), - ], - ); -} +const pieceView = (current: Piece, set: (t: Piece) => void, is3d: boolean) => (t: Piece) => + h( + 'button.no-square', + { + attrs: { title: t, type: 'button' }, + hook: bind('click', () => set(t)), + class: { active: current === t }, + }, + [h('piece', { attrs: { style: `background-image:url(${lichess.asset.url(pieceImage(t, is3d))})` } })], + ); function applyPiece(t: Piece, list: Piece[], is3d: boolean) { if (is3d) { diff --git a/ui/dasher/src/ping.ts b/ui/dasher/src/ping.ts index c9bbdcb60de19..7da9e721782f0 100644 --- a/ui/dasher/src/ping.ts +++ b/ui/dasher/src/ping.ts @@ -1,6 +1,6 @@ import { h, VNode } from 'snabbdom'; - -import { Redraw, defined } from './util'; +import { Redraw } from 'common/snabbdom'; +import { defined } from 'common'; export class PingCtrl { ping: number | undefined; @@ -48,27 +48,16 @@ const showMillis = (name: string, m?: number) => [ ]; export const view = (ctrl: PingCtrl): VNode => - h( - 'a.status', - { - attrs: { href: '/lag' }, - hook: { insert: ctrl.connect, destroy: ctrl.disconnect }, - }, - [ - signalBars(ctrl), - h( - 'span.ping', - { - attrs: { title: 'PING: ' + ctrl.trans.noarg('networkLagBetweenYouAndLichess') }, - }, - showMillis('PING', ctrl.ping), - ), - h( - 'span.server', - { - attrs: { title: 'SERVER: ' + ctrl.trans.noarg('timeToProcessAMoveOnLichessServer') }, - }, - showMillis('SERVER', ctrl.server), - ), - ], - ); + h('a.status', { attrs: { href: '/lag' }, hook: { insert: ctrl.connect, destroy: ctrl.disconnect } }, [ + signalBars(ctrl), + h( + 'span.ping', + { attrs: { title: 'PING: ' + ctrl.trans.noarg('networkLagBetweenYouAndLichess') } }, + showMillis('PING', ctrl.ping), + ), + h( + 'span.server', + { attrs: { title: 'SERVER: ' + ctrl.trans.noarg('timeToProcessAMoveOnLichessServer') } }, + showMillis('SERVER', ctrl.server), + ), + ]); diff --git a/ui/dasher/src/sound.ts b/ui/dasher/src/sound.ts index 900b657909f3f..7001c2307579a 100644 --- a/ui/dasher/src/sound.ts +++ b/ui/dasher/src/sound.ts @@ -2,7 +2,8 @@ import * as licon from 'common/licon'; import * as xhr from 'common/xhr'; import throttle, { throttlePromiseDelay } from 'common/throttle'; import { h, VNode } from 'snabbdom'; -import { Redraw, Close, bind, header } from './util'; +import { Close, header } from './util'; +import { bind, Redraw } from 'common/snabbdom'; type Key = string; @@ -13,60 +14,49 @@ export interface SoundData { list: Sound[]; } -export interface SoundCtrl { - makeList(): Sound[]; - api: SoundI; - set(k: Key): void; - volume(v: number): void; - redraw: Redraw; - trans: Trans; - close: Close; -} - -export function ctrl(raw: string[], trans: Trans, redraw: Redraw, close: Close): SoundCtrl { - const list: Sound[] = raw.map(s => s.split(' ')); +export class SoundCtrl { + list: Sound[]; + api: SoundI = lichess.sound; // ??? - const api = lichess.sound; + constructor( + raw: string[], + readonly trans: Trans, + readonly redraw: Redraw, + readonly close: Close, + ) { + this.list = raw.map(s => s.split(' ')); + } - const postSet = throttlePromiseDelay( + private postSet = throttlePromiseDelay( () => 1000, (soundSet: string) => xhr - .text('/pref/soundSet', { - body: xhr.form({ soundSet }), - method: 'post', - }) + .text('/pref/soundSet', { body: xhr.form({ soundSet }), method: 'post' }) .catch(() => lichess.announce({ msg: 'Failed to save sound preference' })), ); - return { - makeList() { - const canSpeech = window.speechSynthesis?.getVoices().length; - return list.filter(s => s[0] != 'speech' || canSpeech); - }, - api, - set(k: Key) { - api.speech(k == 'speech'); - lichess.pubsub.emit('speech.enabled', api.speech()); - if (api.speech()) { - api.changeSet('standard'); - postSet('standard'); - api.say('Speech synthesis ready'); - } else { - api.changeSet(k); - api.play('genericNotify'); - postSet(k); - } - redraw(); - }, - volume(v: number) { - api.setVolume(v); - // plays a move sound if speech is off - api.sayOrPlay('move', 'knight F 7'); - }, - redraw, - trans, - close, + makeList = () => { + const canSpeech = window.speechSynthesis?.getVoices().length; + return this.list.filter(s => s[0] != 'speech' || canSpeech); + }; + set = (k: Key) => { + this.api.speech(k == 'speech'); + lichess.pubsub.emit('speech.enabled', this.api.speech()); + if (this.api.speech()) { + this.api.changeSet('standard'); + this.postSet('standard'); + this.api.say('Speech synthesis ready'); + } else { + this.api.changeSet(k); + this.api.play('genericNotify'); + this.postSet(k); + } + this.redraw(); + }; + volume = (v: number) => { + this.api.setVolume(v); + // plays a move sound if speech is off + this.api.sayOrPlay('move', 'knight F 7'); }; } diff --git a/ui/dasher/src/theme.ts b/ui/dasher/src/theme.ts index 01d63d74e26db..44733b9b36808 100644 --- a/ui/dasher/src/theme.ts +++ b/ui/dasher/src/theme.ts @@ -1,7 +1,7 @@ import { h, VNode } from 'snabbdom'; import * as xhr from 'common/xhr'; - -import { Redraw, bind, header, Close } from './util'; +import { header, Close } from './util'; +import { bind, Redraw } from 'common/snabbdom'; type Theme = string; @@ -15,48 +15,29 @@ export interface ThemeData { d3: ThemeDimData; } -export interface ThemeCtrl { - dimension: () => keyof ThemeData; - data: () => ThemeDimData; - trans: Trans; - set(t: Theme): void; - close: Close; -} - -export function ctrl( - data: ThemeData, - trans: Trans, - dimension: () => keyof ThemeData, - redraw: Redraw, - close: Close, -): ThemeCtrl { - function dimensionData() { - return data[dimension()]; - } - - return { - dimension, - trans, - data: dimensionData, - set(t: Theme) { - const d = dimensionData(); - d.current = t; - applyTheme(t, d.list, dimension() === 'd3'); - const field = `theme${dimension() === 'd3' ? '3d' : ''}`; - xhr - .text(`/pref/${field}`, { - body: xhr.form({ [field]: t }), - method: 'post', - }) - .catch(() => lichess.announce({ msg: 'Failed to save theme preference' })); - redraw(); - }, - close, +export class ThemeCtrl { + constructor( + private readonly data: ThemeData, + readonly trans: Trans, + readonly dimension: () => keyof ThemeData, + readonly redraw: Redraw, + readonly close: Close, + ) {} + dimensionData = () => this.data[this.dimension()]; + set = (t: Theme) => { + const d = this.dimensionData(); + d.current = t; + applyTheme(t, d.list, this.dimension() === 'd3'); + const field = `theme${this.dimension() === 'd3' ? '3d' : ''}`; + xhr + .text(`/pref/${field}`, { body: xhr.form({ [field]: t }), method: 'post' }) + .catch(() => lichess.announce({ msg: 'Failed to save theme preference' })); + this.redraw(); }; } export function view(ctrl: ThemeCtrl): VNode { - const d = ctrl.data(); + const d = ctrl.dimensionData(); return h('div.sub.theme.' + ctrl.dimension(), [ header(ctrl.trans.noarg('boardTheme'), () => ctrl.close()), @@ -64,18 +45,16 @@ export function view(ctrl: ThemeCtrl): VNode { ]); } -function themeView(current: Theme, set: (t: Theme) => void) { - return (t: Theme) => - h( - 'button', - { - hook: bind('click', () => set(t)), - attrs: { title: t, type: 'button' }, - class: { active: current === t }, - }, - [h('span.' + t)], - ); -} +const themeView = (current: Theme, set: (t: Theme) => void) => (t: Theme) => + h( + 'button', + { + hook: bind('click', () => set(t)), + attrs: { title: t, type: 'button' }, + class: { active: current === t }, + }, + h('span.' + t), + ); function applyTheme(t: Theme, list: Theme[], is3d: boolean) { $('body').removeClass(list.join(' ')).addClass(t); diff --git a/ui/dasher/src/util.ts b/ui/dasher/src/util.ts index fb8abc7f0bd77..4fc1dc258f316 100644 --- a/ui/dasher/src/util.ts +++ b/ui/dasher/src/util.ts @@ -1,48 +1,12 @@ -import { h, VNode } from 'snabbdom'; +import { h } from 'snabbdom'; import * as licon from 'common/licon'; +import { bind } from 'common/snabbdom'; -export type Redraw = () => void; export type Close = () => void; -export interface Prop { - (): T; - (v: T): T; -} - -export function defined(v: A | undefined): v is A { - return typeof v !== 'undefined'; -} - -// like mithril prop but with type safety -export function prop(initialValue: A): Prop { - let value = initialValue; - const fun = function (v: A | undefined) { - if (typeof v !== 'undefined') value = v; - return value; - }; - return fun as Prop; -} - -export function bind(eventName: string, f: (e: Event) => void, redraw: Redraw | undefined = undefined) { - return { - insert: (vnode: VNode) => { - (vnode.elm as HTMLElement).addEventListener(eventName, e => { - e.stopPropagation(); - f(e); - if (redraw) redraw(); - return false; - }); - }, - }; -} - -export function header(name: string, close: Close) { - return h( +export const header = (name: string, close: Close) => + h( 'button.head.text', - { - attrs: { 'data-icon': licon.LessThan, type: 'button' }, - hook: bind('click', close), - }, + { attrs: { 'data-icon': licon.LessThan, type: 'button' }, hook: bind('click', close) }, name, ); -} diff --git a/ui/dasher/src/view.ts b/ui/dasher/src/view.ts index c3b067d7fe8ea..45a617a133125 100644 --- a/ui/dasher/src/view.ts +++ b/ui/dasher/src/view.ts @@ -1,7 +1,7 @@ import { h, VNode } from 'snabbdom'; import { spinnerVdom as spinner } from 'common/spinner'; -import { DasherCtrl } from './dasher'; +import DasherCtrl from './dasher'; import links from './links'; import { view as langsView } from './langs'; import { view as soundView } from './sound'; @@ -16,22 +16,22 @@ export function loaded(ctrl: DasherCtrl): VNode { let content: VNode | undefined; switch (ctrl.mode()) { case 'langs': - content = langsView(ctrl.subs.langs); + content = langsView(ctrl.langs); break; case 'sound': - content = soundView(ctrl.subs.sound); + content = soundView(ctrl.sound); break; case 'background': - content = backgroundView(ctrl.subs.background); + content = backgroundView(ctrl.background); break; case 'board': - content = boardView(ctrl.subs.board); + content = boardView(ctrl.board); break; case 'theme': - content = themeView(ctrl.subs.theme); + content = themeView(ctrl.theme); break; case 'piece': - content = pieceView(ctrl.subs.piece); + content = pieceView(ctrl.piece); break; default: content = links(ctrl); diff --git a/ui/dgt/package.json b/ui/dgt/package.json index fc1413dda53aa..844babe0cc04c 100644 --- a/ui/dgt/package.json +++ b/ui/dgt/package.json @@ -6,7 +6,7 @@ "author": "Thibault Duplessis", "license": "AGPL-3.0-or-later", "dependencies": { - "chessops": "^0.12.7" + "chessops": "^0.13.0" }, "scripts": { "compile": "tsc", diff --git a/ui/dgt/src/config.ts b/ui/dgt/src/config.ts index 5e2c7961097f3..c905a5743eac3 100644 --- a/ui/dgt/src/config.ts +++ b/ui/dgt/src/config.ts @@ -3,7 +3,7 @@ export default function () { voiceSelector = document.getElementById('dgt-speech-voice') as HTMLSelectElement; (function populateVoiceList() { - if (typeof speechSynthesis === 'undefined') return; + if (!voiceSelector || typeof speechSynthesis === 'undefined') return; speechSynthesis.getVoices().forEach((voice, i) => { const option = document.createElement('option'); option.value = voice.name; @@ -67,11 +67,13 @@ export default function () { }); } - ensureDefaults(); - populateForm(); + if (form) { + ensureDefaults(); + populateForm(); - form.addEventListener('submit', (e: Event) => { - e.preventDefault(); - Array.from(new FormData(form).entries()).forEach(([k, v]) => localStorage.setItem(k, v.toString())); - }); + form.addEventListener('submit', (e: Event) => { + e.preventDefault(); + Array.from(new FormData(form).entries()).forEach(([k, v]) => localStorage.setItem(k, v.toString())); + }); + } } diff --git a/ui/editor/css/_tools.scss b/ui/editor/css/_tools.scss index 8bd14f96b9f77..7454f4e3a7e70 100644 --- a/ui/editor/css/_tools.scss +++ b/ui/editor/css/_tools.scss @@ -42,6 +42,17 @@ vertical-align: middle; } } + + .enpassant { + @extend %flex-between; + margin-top: 1em; + label { + font-weight: bold; + } + select { + width: 9ch; + } + } } .actions { diff --git a/ui/editor/package.json b/ui/editor/package.json index b36b24dd1e3b9..0eec333dfa059 100644 --- a/ui/editor/package.json +++ b/ui/editor/package.json @@ -12,7 +12,7 @@ "author": "Thibault Duplessis", "license": "AGPL-3.0-or-later", "dependencies": { - "chessops": "^0.12.7", + "chessops": "^0.13.0", "common": "workspace:*", "snabbdom": "^3.5.1" }, diff --git a/ui/editor/src/chessground.ts b/ui/editor/src/chessground.ts index f92d74db592aa..834be5e41007e 100644 --- a/ui/editor/src/chessground.ts +++ b/ui/editor/src/chessground.ts @@ -12,7 +12,7 @@ export default function (ctrl: EditorCtrl): VNode { ctrl.chessground = lichess.makeChessground(el, makeConfig(ctrl)); bindEvents(el, ctrl); }, - destroy: _ => ctrl.chessground!.destroy(), + destroy: () => ctrl.chessground!.destroy(), }, }); } @@ -120,7 +120,7 @@ function makeConfig(ctrl: EditorCtrl): CgConfig { return { fen: ctrl.initialFen, orientation: ctrl.options.orientation || 'white', - coordinates: !ctrl.cfg.embed, + coordinates: ctrl.options.coordinates !== false, autoCastle: false, addPieceZIndex: ctrl.cfg.is3d, movable: { diff --git a/ui/editor/src/ctrl.ts b/ui/editor/src/ctrl.ts index 70256d7c176b0..e59587ea46747 100644 --- a/ui/editor/src/ctrl.ts +++ b/ui/editor/src/ctrl.ts @@ -17,18 +17,16 @@ import { lichessVariant, lichessRules } from 'chessops/compat'; import { defined, prop, Prop } from 'common'; export default class EditorCtrl { - cfg: Editor.Config; options: Editor.Options; trans: Trans; chessground: CgApi | undefined; - redraw: Redraw; selected: Prop; initialFen: string; pockets: Material | undefined; turn: Color; - unmovedRooks: SquareSet | undefined; + castlingRights: SquareSet | undefined; castlingToggles: CastlingToggles; epSquare: Square | undefined; remainingChecks: RemainingChecks | undefined; @@ -36,21 +34,20 @@ export default class EditorCtrl { halfmoves: number; fullmoves: number; - constructor(cfg: Editor.Config, redraw: Redraw) { - this.cfg = cfg; + constructor( + readonly cfg: Editor.Config, + readonly redraw: Redraw, + ) { this.options = cfg.options || {}; this.trans = lichess.trans(this.cfg.i18n); this.selected = prop('pointer'); - if (cfg.positions) { - cfg.positions.forEach(p => (p.epd = p.fen.split(' ').splice(0, 4).join(' '))); - } + if (cfg.positions) cfg.positions.forEach(p => (p.epd = p.fen.split(' ').splice(0, 4).join(' '))); - if (cfg.endgamePositions) { + if (cfg.endgamePositions) cfg.endgamePositions.forEach(p => (p.epd = p.fen.split(' ').splice(0, 4).join(' '))); - } lichess.mousetrap.bind('f', () => { if (this.chessground) this.chessground.toggleOrientation(); @@ -62,16 +59,35 @@ export default class EditorCtrl { this.rules = this.cfg.embed ? 'chess' : lichessRules((params.get('variant') || 'standard') as VariantKey); this.initialFen = (cfg.fen || params.get('fen') || INITIAL_FEN).replace(/_/g, ' '); - this.redraw = () => {}; - if (!this.cfg.embed) { - this.options.orientation = params.get('color') === 'black' ? 'black' : 'white'; + if (!this.cfg.embed) this.options.orientation = params.get('color') === 'black' ? 'black' : 'white'; + + parseFen(this.initialFen).unwrap(this.setSetup); + } + + private nthIndexOf = (haystack: string, needle: string, n: number): number => { + let index = haystack.indexOf(needle); + while (n-- > 0) { + if (index === -1) break; + index = haystack.indexOf(needle, index + needle.length); } - this.setFen(this.initialFen); - this.redraw = redraw; + return index; + }; + + // Ideally to be replaced when something like parseCastlingFen exists in chessops but for epSquare (@getSetup) + private fenFixedEp(fen: string) { + let enPassant = fen.split(' ')[3]; + if (enPassant !== '-' && !this.getEnPassantOptions(fen).includes(enPassant)) { + this.epSquare = undefined; + enPassant = '-'; + } + + const epIndex = this.nthIndexOf(fen, ' ', 2) + 1; + const epEndIndex = fen.indexOf(' ', epIndex); + return `${fen.substring(0, epIndex)}${enPassant}${fen.substring(epEndIndex)}`; } onChange(): void { - const fen = this.getFen(); + const fen = this.fenFixedEp(this.getFen()); if (!this.cfg.embed) { window.history.replaceState(null, '', this.makeEditorUrl(fen, this.bottomColor())); } @@ -97,8 +113,7 @@ export default class EditorCtrl { board, pockets: this.pockets, turn: this.turn, - //@ts-ignore - unmovedRooks: this.unmovedRooks || parseCastlingFen(board, this.castlingToggleFen()).unwrap(), + castlingRights: this.castlingRights || parseCastlingFen(board, this.castlingToggleFen()).unwrap(), epSquare: this.epSquare, remainingChecks: this.remainingChecks, halfmoves: this.halfmoves, @@ -124,11 +139,42 @@ export default class EditorCtrl { ); } + // hopefully moved to chessops soon + // https://github.com/niklasf/chessops/issues/154 + private getEnPassantOptions(fen: string): string[] { + const unpackRank = (packedRank: string) => + [...packedRank].reduce((accumulator, current) => { + const parsedInt = parseInt(current); + return accumulator + (parsedInt >= 1 ? 'x'.repeat(parsedInt) : current); + }, ''); + const checkRank = (rank: string, regex: RegExp, offset: number, filesEnPassant: Set) => { + let match: RegExpExecArray | null; + while ((match = regex.exec(rank)) != null) { + filesEnPassant.add(match.index + offset); + } + }; + const filesEnPassant: Set = new Set(); + const [positions, turn] = fen.split(' '); + const ranks = positions.split('/'); + const unpackedRank = unpackRank(ranks[turn === 'w' ? 3 : 4]); + checkRank(unpackedRank, /pP/g, turn === 'w' ? 0 : 1, filesEnPassant); + checkRank(unpackedRank, /Pp/g, turn === 'w' ? 1 : 0, filesEnPassant); + const [rank1, rank2] = + filesEnPassant.size >= 1 + ? [unpackRank(ranks[turn === 'w' ? 1 : 6]), unpackRank(ranks[turn === 'w' ? 2 : 5])] + : [null, null]; + return Array.from(filesEnPassant) + .filter(e => rank1![e] === 'x' && rank2![e] === 'x') + .map(e => String.fromCharCode('a'.charCodeAt(0) + e) + (turn === 'w' ? '6' : '3')); + } + getState(): EditorState { + const legalFen = this.getLegalFen(); return { fen: this.getFen(), - legalFen: this.getLegalFen(), + legalFen: legalFen, playable: this.rules == 'chess' && this.isPlayable(), + enPassantOptions: legalFen ? this.getEnPassantOptions(legalFen) : [], }; } @@ -149,13 +195,19 @@ export default class EditorCtrl { } setCastlingToggle(id: CastlingToggle, value: boolean): void { - if (this.castlingToggles[id] != value) this.unmovedRooks = undefined; + if (this.castlingToggles[id] != value) this.castlingRights = undefined; this.castlingToggles[id] = value; this.onChange(); } setTurn(turn: Color): void { this.turn = turn; + this.epSquare = undefined; + this.onChange(); + } + + setEnPassant(epSquare: Square | undefined): void { + this.epSquare = epSquare; this.onChange(); } @@ -171,31 +223,32 @@ export default class EditorCtrl { this.setFen(fen); } - setFen(fen: string): boolean { - return parseFen(fen).unwrap( + private setSetup = (setup: Setup): void => { + this.pockets = setup.pockets; + this.turn = setup.turn; + this.castlingRights = setup.castlingRights; + this.epSquare = setup.epSquare; + this.remainingChecks = setup.remainingChecks; + this.halfmoves = setup.halfmoves; + this.fullmoves = setup.fullmoves; + + const castles = Castles.fromSetup(setup); + this.castlingToggles['K'] = defined(castles.rook.white.h); + this.castlingToggles['Q'] = defined(castles.rook.white.a); + this.castlingToggles['k'] = defined(castles.rook.black.h); + this.castlingToggles['q'] = defined(castles.rook.black.a); + }; + + setFen = (fen: string): boolean => + parseFen(fen).unwrap( setup => { if (this.chessground) this.chessground.set({ fen }); - this.pockets = setup.pockets; - this.turn = setup.turn; - //@ts-ignore - this.unmovedRooks = setup.unmovedRooks; - this.epSquare = setup.epSquare; - this.remainingChecks = setup.remainingChecks; - this.halfmoves = setup.halfmoves; - this.fullmoves = setup.fullmoves; - - const castles = Castles.fromSetup(setup); - this.castlingToggles['K'] = defined(castles.rook.white.h); - this.castlingToggles['Q'] = defined(castles.rook.white.a); - this.castlingToggles['k'] = defined(castles.rook.black.h); - this.castlingToggles['q'] = defined(castles.rook.black.a); - + this.setSetup(setup); this.onChange(); return true; }, _ => false, ); - } setRules(rules: Rules): void { this.rules = rules; diff --git a/ui/editor/src/interfaces.ts b/ui/editor/src/interfaces.ts index e2a48ee2fe01a..a3fa22e1b89ee 100644 --- a/ui/editor/src/interfaces.ts +++ b/ui/editor/src/interfaces.ts @@ -12,6 +12,7 @@ export interface EditorState { fen: string; legalFen: string | undefined; playable: boolean; + enPassantOptions: string[]; } export type Redraw = () => void; diff --git a/ui/editor/src/view.ts b/ui/editor/src/view.ts index 16cb98a0e16c4..2694ca25794e1 100644 --- a/ui/editor/src/view.ts +++ b/ui/editor/src/view.ts @@ -5,6 +5,7 @@ import { dragNewPiece } from 'chessground/drag'; import { eventPosition, opposite } from 'chessground/util'; import { Rules } from 'chessops/types'; import { parseFen } from 'chessops/fen'; +import { parseSquare, makeSquare } from 'chessops/util'; import { domDialog } from 'common/dialog'; import EditorCtrl from './ctrl'; import chessground from './chessground'; @@ -13,12 +14,8 @@ import { dataIcon } from 'common/snabbdom'; function castleCheckBox(ctrl: EditorCtrl, id: CastlingToggle, label: string, reversed: boolean): VNode { const input = h('input', { - attrs: { - type: 'checkbox', - }, - props: { - checked: ctrl.castlingToggles[id], - }, + attrs: { type: 'checkbox' }, + props: { checked: ctrl.castlingToggles[id] }, on: { change(e) { ctrl.setCastlingToggle(id, (e.target as HTMLInputElement).checked); @@ -33,48 +30,25 @@ function optgroup(name: string, opts: VNode[]): VNode { } function studyButton(ctrl: EditorCtrl, state: EditorState): VNode { - return h( - 'form', - { - attrs: { - method: 'post', - action: '/study/as', + return h('form', { attrs: { method: 'post', action: '/study/as' } }, [ + h('input', { attrs: { type: 'hidden', name: 'orientation', value: ctrl.bottomColor() } }), + h('input', { attrs: { type: 'hidden', name: 'variant', value: ctrl.rules } }), + h('input', { attrs: { type: 'hidden', name: 'fen', value: state.legalFen || '' } }), + h( + 'button', + { + attrs: { type: 'submit', 'data-icon': licon.StudyBoard, disabled: !state.legalFen }, + class: { button: true, 'button-empty': true, text: true, disabled: !state.legalFen }, }, - }, - [ - h('input', { attrs: { type: 'hidden', name: 'orientation', value: ctrl.bottomColor() } }), - h('input', { attrs: { type: 'hidden', name: 'variant', value: ctrl.rules } }), - h('input', { attrs: { type: 'hidden', name: 'fen', value: state.legalFen || '' } }), - h( - 'button', - { - attrs: { - type: 'submit', - 'data-icon': licon.StudyBoard, - disabled: !state.legalFen, - }, - class: { - button: true, - 'button-empty': true, - text: true, - disabled: !state.legalFen, - }, - }, - ctrl.trans.noarg('toStudy'), - ), - ], - ); + ctrl.trans.noarg('toStudy'), + ), + ]); } function variant2option(key: Rules, name: string, ctrl: EditorCtrl): VNode { return h( 'option', - { - attrs: { - value: key, - selected: key == ctrl.rules, - }, - }, + { attrs: { value: key, selected: key == ctrl.rules } }, `${ctrl.trans.noarg('variant')} | ${name}`, ); } @@ -92,34 +66,19 @@ const allVariants: Array<[Rules, string]> = [ function controls(ctrl: EditorCtrl, state: EditorState): VNode { const endgamePosition2option = function (pos: Editor.EndgamePosition): VNode { - return h( - 'option', - { - attrs: { - value: pos.epd || pos.fen, - 'data-fen': pos.fen, - }, - }, - pos.name, - ); + return h('option', { attrs: { value: pos.epd || pos.fen, 'data-fen': pos.fen } }, pos.name); }; const buttonStart = (icon?: string) => h( `a.button.button-empty${icon ? '.text' : ''}`, - { - on: { click: ctrl.startPosition }, - attrs: icon ? dataIcon(icon) : {}, - }, + { on: { click: ctrl.startPosition }, attrs: icon ? dataIcon(icon) : {} }, ctrl.trans.noarg('startPosition'), ); const buttonClear = (icon?: string) => h( `a.button.button-empty${icon ? '.text' : ''}`, - { - on: { click: ctrl.clearBoard }, - attrs: icon ? dataIcon(icon) : {}, - }, + { on: { click: ctrl.clearBoard }, attrs: icon ? dataIcon(icon) : {} }, ctrl.trans.noarg('clearBoard'), ); @@ -135,18 +94,13 @@ function controls(ctrl: EditorCtrl, state: EditorState): VNode { ctrl.setTurn((e.target as HTMLSelectElement).value as Color); }, }, - props: { - value: ctrl.turn, - }, + props: { value: ctrl.turn }, }, ['whitePlays', 'blackPlays'].map(function (key) { return h( 'option', { - attrs: { - value: key[0] === 'w' ? 'white' : 'black', - selected: key[0] === ctrl.turn[0], - }, + attrs: { value: key[0] === 'w' ? 'white' : 'black', selected: key[0] === ctrl.turn[0] }, }, ctrl.trans(key), ); @@ -164,6 +118,35 @@ function controls(ctrl: EditorCtrl, state: EditorState): VNode { castleCheckBox(ctrl, 'q', 'O-O-O', true), ]), ]), + h('div.enpassant', [ + h('label', { attrs: { for: 'enpassant-select' } }, 'En passant'), + h( + 'select#enpassant-select', + { + on: { + change(e) { + ctrl.setEnPassant(parseSquare((e.target as HTMLSelectElement).value)); + }, + }, + props: { value: ctrl.epSquare ? makeSquare(ctrl.epSquare) : '' }, + }, + ['', ...[ctrl.turn === 'black' ? 3 : 6].flatMap(r => 'abcdefgh'.split('').map(f => f + r))].map( + key => + h( + 'option', + { + attrs: { + value: key, + selected: (key ? parseSquare(key) : undefined) === ctrl.epSquare, + hidden: Boolean(key && !state.enPassantOptions.includes(key)), + disabled: Boolean(key && !state.enPassantOptions.includes(key)) /*Safari*/, + }, + }, + key, + ), + ), + ), + ]), ]), ...(ctrl.cfg.embed || !ctrl.cfg.positions || !ctrl.cfg.endgamePositions ? [] @@ -172,12 +155,7 @@ function controls(ctrl: EditorCtrl, state: EditorState): VNode { const positionOption = (pos: Editor.OpeningPosition): VNode => h( 'option', - { - attrs: { - value: pos.epd || pos.fen, - 'data-fen': pos.fen, - }, - }, + { attrs: { value: pos.epd || pos.fen, 'data-fen': pos.fen } }, pos.eco ? `${pos.eco} ${pos.name}` : pos.name, ); const epd = state.fen.split(' ').slice(0, 4).join(' '); @@ -267,11 +245,7 @@ function controls(ctrl: EditorCtrl, state: EditorState): VNode { h( 'button', { - class: { - button: true, - 'button-empty': true, - disabled: !state.playable, - }, + class: { button: true, 'button-empty': true, disabled: !state.playable }, on: { click: () => { if (state.playable) domDialog({ cash: $('.continue-with'), show: 'modal' }); @@ -291,22 +265,12 @@ function controls(ctrl: EditorCtrl, state: EditorState): VNode { h('div.continue-with.none', [ h( 'a.button', - { - attrs: { - href: '/?fen=' + state.legalFen + '#ai', - rel: 'nofollow', - }, - }, + { attrs: { href: '/?fen=' + state.legalFen + '#ai', rel: 'nofollow' } }, ctrl.trans.noarg('playWithTheMachine'), ), h( 'a.button', - { - attrs: { - href: '/?fen=' + state.legalFen + '#friend', - rel: 'nofollow', - }, - }, + { attrs: { href: '/?fen=' + state.legalFen + '#friend', rel: 'nofollow' } }, ctrl.trans.noarg('playWithAFriend'), ), ]), @@ -320,13 +284,8 @@ function inputs(ctrl: EditorCtrl, fen: string): VNode | undefined { h('p', [ h('strong', 'FEN'), h('input.copyable', { - attrs: { - spellcheck: 'false', - enterkeyhint: 'done', - }, - props: { - value: fen, - }, + attrs: { spellcheck: 'false', enterkeyhint: 'done' }, + props: { value: fen }, on: { change(e) { const el = e.target as HTMLInputElement; @@ -355,11 +314,7 @@ function inputs(ctrl: EditorCtrl, fen: string): VNode | undefined { h('p', [ h('strong.name', 'URL'), h('input.copyable.autoselect', { - attrs: { - readonly: true, - spellcheck: 'false', - value: ctrl.makeEditorUrl(fen, ctrl.bottomColor()), - }, + attrs: { readonly: true, spellcheck: 'false', value: ctrl.makeEditorUrl(fen, ctrl.bottomColor()) }, }), ]), ]); @@ -381,11 +336,7 @@ function sparePieces(ctrl: EditorCtrl, color: Color, _orientation: Color, positi return h( 'div', - { - attrs: { - class: ['spare', 'spare-' + position, 'spare-' + color].join(' '), - }, - }, + { attrs: { class: ['spare', 'spare-' + position, 'spare-' + color].join(' ') } }, ['pointer', ...pieces, 'trash'].map((s: Selected) => { const className = selectedToClass(s); const attrs = { @@ -434,15 +385,7 @@ function onSelectSparePiece(ctrl: EditorCtrl, s: Selected, upEvent: string): (e: } else { ctrl.selected('pointer'); - dragNewPiece( - ctrl.chessground!.state, - { - color: s[0], - role: s[1], - }, - e, - true, - ); + dragNewPiece(ctrl.chessground!.state, { color: s[0], role: s[1] }, e, true); document.addEventListener( upEvent, @@ -462,7 +405,7 @@ function makeCursor(selected: Selected): string { if (selected === 'pointer') return 'pointer'; const name = selected === 'trash' ? 'trash' : selected.join('-'); - const url = lichess.assetUrl('cursors/' + name + '.cur'); + const url = lichess.asset.url('cursors/' + name + '.cur'); return `url('${url}'), default !important`; } @@ -471,19 +414,11 @@ export default function (ctrl: EditorCtrl): VNode { const state = ctrl.getState(); const color = ctrl.bottomColor(); - return h( - 'div.board-editor', - { - attrs: { - style: `cursor: ${makeCursor(ctrl.selected())}`, - }, - }, - [ - sparePieces(ctrl, opposite(color), color, 'top'), - h('div.main-board', [chessground(ctrl)]), - sparePieces(ctrl, color, color, 'bottom'), - controls(ctrl, state), - inputs(ctrl, state.fen), - ], - ); + return h('div.board-editor', { attrs: { style: `cursor: ${makeCursor(ctrl.selected())}` } }, [ + sparePieces(ctrl, opposite(color), color, 'top'), + h('div.main-board', [chessground(ctrl)]), + sparePieces(ctrl, color, color, 'bottom'), + controls(ctrl, state), + inputs(ctrl, state.legalFen || state.fen), + ]); } diff --git a/ui/gameSetup/src/view/components/ratingDifferenceSliders.ts b/ui/gameSetup/src/view/components/ratingDifferenceSliders.ts index 16d32b5a0b328..086c97f9b11ee 100644 --- a/ui/gameSetup/src/view/components/ratingDifferenceSliders.ts +++ b/ui/gameSetup/src/view/components/ratingDifferenceSliders.ts @@ -16,9 +16,7 @@ export const ratingDifferenceSliders = (ctrl: SetupCtrl) => { `div.rating-range-config.optional-config${disabled}`, { attrs: isProvisional - ? { - title: 'Your rating is still provisional, play some rated games to use the rating range.', - } + ? { title: 'Your rating is still provisional, play some rated games to use the rating range.' } : undefined, }, [ diff --git a/ui/gameSetup/src/view/components/timePickerAndSliders.ts b/ui/gameSetup/src/view/components/timePickerAndSliders.ts index 8bf302e4727a8..567df5899766e 100644 --- a/ui/gameSetup/src/view/components/timePickerAndSliders.ts +++ b/ui/gameSetup/src/view/components/timePickerAndSliders.ts @@ -1,5 +1,5 @@ import { Prop } from 'common'; -import { h } from 'snabbdom'; +import { looseH as h } from 'common/snabbdom'; import { SetupCtrl } from '../../ctrl'; import { InputValue, TimeMode } from '../../interfaces'; import { daysVToDays, incrementVToIncrement, sliderTimes, timeModes } from '../../options'; @@ -15,90 +15,70 @@ const showTime = (v: number) => { const renderBlindModeTimePickers = (ctrl: SetupCtrl, allowAnonymous: boolean) => { return [ renderTimeModePicker(ctrl, allowAnonymous), - ctrl.timeMode() === 'realTime' - ? h('div.time-choice', [ - h('label', { attrs: { for: 'sf_time' } }, ctrl.root.trans('minutesPerSide')), - h( - 'select#sf_time', - { - on: { - change: (e: Event) => ctrl.timeV(parseFloat((e.target as HTMLSelectElement).value)), - }, - }, - sliderTimes.map((sliderTime, timeV) => - option({ key: timeV.toString(), name: showTime(sliderTime) }, ctrl.timeV().toString()), - ), + ctrl.timeMode() === 'realTime' && + h('div.time-choice', [ + h('label', { attrs: { for: 'sf_time' } }, ctrl.root.trans('minutesPerSide')), + h( + 'select#sf_time', + { on: { change: (e: Event) => ctrl.timeV(parseFloat((e.target as HTMLSelectElement).value)) } }, + sliderTimes.map((sliderTime, timeV) => + option({ key: timeV.toString(), name: showTime(sliderTime) }, ctrl.timeV().toString()), ), - ]) - : null, - ctrl.timeMode() === 'realTime' - ? h('div.increment-choice', [ - h('label', { attrs: { for: 'sf_increment' } }, ctrl.root.trans('incrementInSeconds')), - h( - 'select#sf_increment', - { - on: { - change: (e: Event) => ctrl.incrementV(parseInt((e.target as HTMLSelectElement).value)), - }, - }, - // 31 because the range below goes from 0 to 30 - Array.from(Array(31).keys()).map(incrementV => - option( - { key: incrementV.toString(), name: incrementVToIncrement(incrementV).toString() }, - ctrl.incrementV().toString(), - ), + ), + ]), + ctrl.timeMode() === 'realTime' && + h('div.increment-choice', [ + h('label', { attrs: { for: 'sf_increment' } }, ctrl.root.trans('incrementInSeconds')), + h( + 'select#sf_increment', + { on: { change: (e: Event) => ctrl.incrementV(parseInt((e.target as HTMLSelectElement).value)) } }, + // 31 because the range below goes from 0 to 30 + Array.from(Array(31).keys()).map(incrementV => + option( + { key: incrementV.toString(), name: incrementVToIncrement(incrementV).toString() }, + ctrl.incrementV().toString(), ), ), - ]) - : null, - ctrl.timeMode() === 'correspondence' - ? h('div.days-choice', [ - h('label', { attrs: { for: 'sf_days' } }, ctrl.root.trans('daysPerTurn')), - h( - 'select#sf_days', - { - on: { - change: (e: Event) => ctrl.daysV(parseInt((e.target as HTMLSelectElement).value)), - }, - }, - // 7 because the range below goes from 1 to 7 - Array.from(Array(7).keys()).map(daysV => - option( - { key: (daysV + 1).toString(), name: daysVToDays(daysV + 1).toString() }, - ctrl.daysV().toString(), - ), + ), + ]), + ctrl.timeMode() === 'correspondence' && + h('div.days-choice', [ + h('label', { attrs: { for: 'sf_days' } }, ctrl.root.trans('daysPerTurn')), + h( + 'select#sf_days', + { on: { change: (e: Event) => ctrl.daysV(parseInt((e.target as HTMLSelectElement).value)) } }, + // 7 because the range below goes from 1 to 7 + Array.from(Array(7).keys()).map(daysV => + option( + { key: (daysV + 1).toString(), name: daysVToDays(daysV + 1).toString() }, + ctrl.daysV().toString(), ), ), - ]) - : null, + ), + ]), ]; }; const renderTimeModePicker = (ctrl: SetupCtrl, allowAnonymous = false) => { const trans = ctrl.root.trans; - return ctrl.root.user || allowAnonymous - ? h('div.label-select', [ - h('label', { attrs: { for: 'sf_timeMode' } }, trans('timeControl')), - h( - 'select#sf_timeMode', - { - on: { - change: (e: Event) => ctrl.timeMode((e.target as HTMLSelectElement).value as TimeMode), - }, - }, - timeModes(trans).map(timeMode => option(timeMode, ctrl.timeMode())), - ), - ]) - : null; + return ( + (ctrl.root.user || allowAnonymous) && + h('div.label-select', [ + h('label', { attrs: { for: 'sf_timeMode' } }, trans('timeControl')), + h( + 'select#sf_timeMode', + { on: { change: (e: Event) => ctrl.timeMode((e.target as HTMLSelectElement).value as TimeMode) } }, + timeModes(trans).map(timeMode => option(timeMode, ctrl.timeMode())), + ), + ]) + ); }; const inputRange = (min: number, max: number, prop: Prop, classes?: Record) => h('input.range', { class: classes, attrs: { type: 'range', min, max, value: prop() }, - on: { - input: (e: Event) => prop(parseFloat((e.target as HTMLInputElement).value)), - }, + on: { input: (e: Event) => prop(parseFloat((e.target as HTMLInputElement).value)) }, }); export const timePickerAndSliders = (ctrl: SetupCtrl, allowAnonymous = false) => { @@ -109,31 +89,29 @@ export const timePickerAndSliders = (ctrl: SetupCtrl, allowAnonymous = false) => ? renderBlindModeTimePickers(ctrl, allowAnonymous) : [ renderTimeModePicker(ctrl, allowAnonymous), - ctrl.timeMode() === 'realTime' - ? h('div.time-choice.range', [ - `${trans('minutesPerSide')}: `, - h('span', showTime(ctrl.time())), - inputRange(0, 38, ctrl.timeV, { - failure: !ctrl.validTime() || !ctrl.validAiTime(), - }), - ]) - : null, + ctrl.timeMode() === 'realTime' && + h('div.time-choice.range', [ + `${trans('minutesPerSide')}: `, + h('span', showTime(ctrl.time())), + inputRange(0, 38, ctrl.timeV, { + failure: !ctrl.validTime() || !ctrl.validAiTime(), + }), + ]), ctrl.timeMode() === 'realTime' ? h('div.increment-choice.range', [ `${trans('incrementInSeconds')}: `, - h('span', ctrl.increment()), + h('span', `${ctrl.increment()}`), inputRange(0, 30, ctrl.incrementV, { failure: !ctrl.validTime() }), ]) - : ctrl.timeMode() === 'correspondence' - ? h( + : ctrl.timeMode() === 'correspondence' && + h( 'div.correspondence', h('div.days-choice.range', [ `${trans('daysPerTurn')}: `, - h('span', ctrl.days()), + h('span', `${ctrl.days()}`), inputRange(1, 7, ctrl.daysV), ]), - ) - : null, + ), ], ); }; diff --git a/ui/gameSetup/src/view/localContent.ts b/ui/gameSetup/src/view/localContent.ts index bc459ac1e9436..a37769f71f882 100644 --- a/ui/gameSetup/src/view/localContent.ts +++ b/ui/gameSetup/src/view/localContent.ts @@ -15,7 +15,7 @@ export default function localContent(ctrl: SetupCtrl): MaybeVNodes { 'div#bot-view', { key: 'bot-view', - hook: onInsert(el => new BotDeck(el as HTMLDivElement)), + hook: onInsert(el => new BotDeck(el as HTMLDivElement, bot => console.log(bot))), }, [spinnerVdom()], ), @@ -32,11 +32,16 @@ class BotDeck { userMidY: number; startAngle = 0; startMag = 0; - handRotation: number = 0; + dragMag = 0; + dragAngle: number = 0; + rect: DOMRect; selectedCard: HTMLDivElement | null = null; - constructor(readonly view: HTMLDivElement) { - lichess.loadEsm('libot', { init: { stubs: true } }).then(bots => { + constructor( + readonly view: HTMLDivElement, + readonly select: (bot: Libot) => void, + ) { + lichess.asset.loadEsm('libot', { init: { stubs: true } }).then(bots => { this.view.innerHTML = ''; this.bots = bots; for (const bot of this.bots.sort()) this.createCard(bot); @@ -46,6 +51,7 @@ class BotDeck { createCard(bot: Libot) { const card = document.createElement('div'); + card.id = bot.uid; card.classList.add('card'); const img = document.createElement('img'); img.src = bot.imageUrl; @@ -64,6 +70,7 @@ class BotDeck { } placeCards() { + this.rect = this.view.getBoundingClientRect(); const radius = this.view.offsetWidth; const containerHeight = this.view.offsetHeight; @@ -75,28 +82,34 @@ class BotDeck { const hovered = $as($('.card.pull')); const hoveredIndex = this.cards.findIndex(x => x == hovered); this.cards.forEach((card, cardIndex) => { - const angleNudge = - !hovered || cardIndex == hoveredIndex - ? 0 - : cardIndex < hoveredIndex - ? 0 //(-Math.PI * 0.25) / visibleCards - : (Math.PI * 0.5) / visibleCards; - let angle = - beginAngle + angleNudge + this.handRotation + ((Math.PI / 4) * (cardIndex + 0.5)) / visibleCards; - const mag = 15 + radius + ($(card).hasClass('pull') ? 40 : 0); - const x = this.userMidX + mag * Math.sin(angle) - 64; - const y = this.userMidY - mag * Math.cos(angle); - if (cardIndex === hoveredIndex) angle -= Math.PI / 8; - card.style.transform = `translate(${x}px, ${y}px) rotate(${angle}rad)`; + const angleNudge = !hovered || cardIndex <= hoveredIndex ? 0 : (Math.PI * 0.5) / visibleCards; + const angle = + beginAngle + angleNudge + this.dragAngle + ((Math.PI / 4) * (cardIndex + 0.5)) / visibleCards; + this.transform(card, angle); }); } + transform(card: HTMLDivElement, angle: number) { + const hovered = card.classList.contains('pull'); + const mag = 15 + this.view.offsetWidth + (hovered ? 40 + this.dragMag - this.startMag : 0); + const x = this.userMidX + mag * Math.sin(angle) - 64; + const y = this.userMidY - mag * Math.cos(angle); + if (hovered) angle -= Math.PI / 8; + card.style.transform = `translate(${x}px, ${y}px) rotate(${angle}rad)`; + } + clientToOrigin(client: [number, number]): [number, number] { - const originX = client[0] - this.view.offsetLeft - this.userMidX; - const originY = this.view.offsetTop + this.userMidY - client[1]; + const originX = client[0] - (this.rect.left + window.scrollX) - this.userMidX; + const originY = this.rect.top + window.scrollY + this.userMidY - client[1]; return [originX, originY]; } + originToClient(origin: [number, number]): [number, number] { + const clientX = this.rect.left + window.scrollX + this.userMidX + origin[0]; + const clientY = this.rect.top + window.scrollY + this.userMidY - origin[1]; + return [clientX, clientY]; + } + getAngle(client: [number, number]): number { const translated = this.clientToOrigin(client); return Math.atan2(translated[0], translated[1]); @@ -118,8 +131,8 @@ class BotDeck { startDrag(e: PointerEvent): void { e.preventDefault(); - this.startAngle = this.getAngle([e.clientX, e.clientY]) - this.handRotation; - this.startMag = this.getMag([e.clientX, e.clientY]); + this.startAngle = this.getAngle([e.clientX, e.clientY]) - this.dragAngle; + this.dragMag = this.startMag = this.getMag([e.clientX, e.clientY]); this.selectedCard = e.currentTarget as HTMLDivElement; @@ -132,15 +145,22 @@ class BotDeck { const newAngle = this.getAngle([e.clientX, e.clientY]); - this.handRotation = newAngle - this.startAngle; + this.dragMag = this.getMag([e.clientX, e.clientY]); + this.dragAngle = newAngle - this.startAngle; this.placeCards(); } endDrag(e: PointerEvent): void { if (this.selectedCard) { this.selectedCard.releasePointerCapture(e.pointerId); + if (this.dragMag - this.startMag > 20) { + console.log(this.selectedCard); + this.select(this.bots.bots[this.selectedCard!.id.slice(1)]); + } } + this.startMag = this.dragMag = this.startAngle = this.dragAngle = 0; this.selectedCard = null; + this.placeCards(); } animate() { diff --git a/ui/insight/src/axis.ts b/ui/insight/src/axis.ts index e09ab12b699ec..2368a01920db0 100644 --- a/ui/insight/src/axis.ts +++ b/ui/insight/src/axis.ts @@ -5,12 +5,7 @@ import { Categ, Dimension, Metric } from './interfaces'; const selectData = (onClick: (v: { value: string }) => void, getValue: () => string): VNodeData => ({ hook: { - insert: vnode => - $(vnode.elm).multipleSelect({ - width: 'var(--drop-menu-width)', - single: true, - onClick, - }), + insert: vnode => $(vnode.elm).multipleSelect({ width: 'var(--drop-menu-width)', single: true, onClick }), update: vnode => $(vnode.elm).multipleSelect('setSelects', [getValue()]), }, }); diff --git a/ui/insight/src/boards.ts b/ui/insight/src/boards.ts index 52e1fd6c3558e..d5f25628ba580 100644 --- a/ui/insight/src/boards.ts +++ b/ui/insight/src/boards.ts @@ -3,42 +3,33 @@ import { h } from 'snabbdom'; import { Game } from './interfaces'; const miniGame = (game: Game) => - h( - 'a', - { - attrs: { - key: game.id, - href: `/${game.id}/${game.color}`, - }, - }, - [ - h('span.mini-board.is2d', { - attrs: { 'data-state': `${game.fen},${game.color},${game.lastMove}` }, - hook: { - insert(vnode) { - lichess.miniBoard.init(vnode.elm as HTMLElement); - }, - update(vnode) { - lichess.miniBoard.init(vnode.elm as HTMLElement); - }, + h('a', { attrs: { key: game.id, href: `/${game.id}/${game.color}` } }, [ + h('span.mini-board.is2d', { + attrs: { 'data-state': `${game.fen},${game.color},${game.lastMove}` }, + hook: { + insert(vnode) { + lichess.miniBoard.init(vnode.elm as HTMLElement); + }, + update(vnode) { + lichess.miniBoard.init(vnode.elm as HTMLElement); }, - }), - h('span.vstext', [ - h('span.vstext__pl', [ - game.user1.name, - h('br'), - game.user1.title ? game.user1.title + ' ' : '', - h('rating', game.user1.rating), - ]), - h('span.vstext__op', [ - game.user2.name, - h('br'), - h('rating', game.user2.rating), - game.user2.title ? ' ' + game.user2.title : '', - ]), + }, + }), + h('span.vstext', [ + h('span.vstext__pl', [ + game.user1.name, + h('br'), + game.user1.title ? game.user1.title + ' ' : '', + h('rating', game.user1.rating), ]), - ], - ); + h('span.vstext__op', [ + game.user2.name, + h('br'), + h('rating', game.user2.rating), + game.user2.title ? ' ' + game.user2.title : '', + ]), + ]), + ]); export default function (ctrl: Ctrl, attrs: any = null) { if (!ctrl.vm.answer) return; diff --git a/ui/insight/src/chart.ts b/ui/insight/src/chart.ts index 2025272956942..127ce13a5ed12 100644 --- a/ui/insight/src/chart.ts +++ b/ui/insight/src/chart.ts @@ -15,10 +15,11 @@ import { ChartOptions, } from 'chart.js'; import { currentTheme } from 'common/theme'; -import { gridColor, tooltipBgColor, fontFamily, maybeChart } from 'chart'; +import { gridColor, tooltipBgColor, fontFamily, maybeChart, resizePolyfill } from 'chart'; import ChartDataLabels from 'chartjs-plugin-datalabels'; import { formatNumber } from './table'; +resizePolyfill(); Chart.register(BarController, CategoryScale, LinearScale, BarElement, Tooltip, Legend, ChartDataLabels); Chart.defaults.font = fontFamily(); diff --git a/ui/insight/src/filters.ts b/ui/insight/src/filters.ts index f7efc22571c95..7f8f33930fdca 100644 --- a/ui/insight/src/filters.ts +++ b/ui/insight/src/filters.ts @@ -32,12 +32,7 @@ const select = (ctrl: Ctrl) => (dimension: Dimension) => { dimension.values.map(value => h( 'option', - { - attrs: { - value: value.key, - selected: ctrl.vm.filters[dimension.key]?.includes(value.key), - }, - }, + { attrs: { value: value.key, selected: ctrl.vm.filters[dimension.key]?.includes(value.key) } }, value.name, ), ), diff --git a/ui/insight/src/info.ts b/ui/insight/src/info.ts index a58ce8c853964..868dbb09463f4 100644 --- a/ui/insight/src/info.ts +++ b/ui/insight/src/info.ts @@ -18,13 +18,7 @@ export default function (ctrl: Ctrl) { ctrl.own ? h( 'a', - { - attrs: { - href: '/account/preferences/site', - target: '_blank', - rel: 'noopener', - }, - }, + { attrs: { href: '/account/preferences/site', target: '_blank', rel: 'noopener' } }, shareText, ) : shareText, @@ -38,19 +32,14 @@ export default function (ctrl: Ctrl) { h( 'form.insight-refresh', { - attrs: { - action: `/insights/refresh/${ctrl.env.user.id}`, - method: 'post', - }, + attrs: { action: `/insights/refresh/${ctrl.env.user.id}`, method: 'post' }, hook: onInsert(_el => lichess.refreshInsightForm()), }, [ h('button.button.text', { attrs: { 'data-icon': licon.Checkmark } }, 'Update insights'), h( 'div.crunching.none', - { - hook: onInsert(el => el.insertAdjacentHTML('afterbegin', lichess.spinnerHtml)), - }, + { hook: onInsert(el => el.insertAdjacentHTML('afterbegin', lichess.spinnerHtml)) }, [h('br'), h('p', h('strong', 'Now crunching data just for you!'))], ), ], diff --git a/ui/insight/src/view.ts b/ui/insight/src/view.ts index a088c51e101a5..e993b75aff0b0 100644 --- a/ui/insight/src/view.ts +++ b/ui/insight/src/view.ts @@ -1,4 +1,4 @@ -import { h, thunk } from 'snabbdom'; +import { thunk } from 'snabbdom'; import debounce from 'common/debounce'; import * as licon from 'common/licon'; import axis from './axis'; @@ -11,7 +11,7 @@ import info from './info'; import boards from './boards'; import Ctrl from './ctrl'; import { ViewTab } from './interfaces'; -import { bind } from 'common/snabbdom'; +import { bind, looseH as h } from 'common/snabbdom'; let forceRender = false; @@ -70,17 +70,13 @@ const viewTabData = (ctrl: Ctrl, view: ViewTab) => ({ hook: bind('click', () => ctrl.setView(view)), }); -// we can't use css media queries for most sizing decisions due to differences in the -// landscape vs portrait layouts, sorry for all the js formatting. function header(ctrl: Ctrl) { return h('header', widthStyle(mainW()), [ isAtLeastXSmall(mainW()) ? h('h2.text', { attrs: { 'data-icon': licon.Target } }, 'Chess Insights') : isAtLeastXXSmall(mainW()) ? h('h2.text', { attrs: { 'data-icon': licon.Target } }, 'Insights') - : mainW() >= 460 - ? h('h2.text', 'Insights') - : null, + : mainW() >= 460 && h('h2.text', 'Insights'), axis(ctrl, mainW() < 460 ? { attrs: { style: 'justify-content: space-evenly;' } } : null), ]); } @@ -93,10 +89,10 @@ function landscapeView(ctrl: Ctrl) { h('div.panel-tabs', [ h('a.tab.preset', panelTabData(ctrl, 'preset'), 'Presets'), h('a.tab.filter', panelTabData(ctrl, 'filter'), 'Filters'), - clearBtn(ctrl), + Object.keys(ctrl.vm.filters).length && clearBtn(ctrl), ]), - ctrl.vm.panel === 'filter' ? filters(ctrl) : null, - ctrl.vm.panel === 'preset' ? presets(ctrl) : null, + ctrl.vm.panel === 'filter' && filters(ctrl), + ctrl.vm.panel === 'preset' && presets(ctrl), help(ctrl), ]), spacer(), @@ -122,7 +118,7 @@ function portraitView(ctrl: Ctrl) { ? [header(ctrl), thunk('div.insight__main.box', renderMain, [ctrl, cacheKey(ctrl)])] : h('div.left-side', [ info(ctrl), - ctrl.vm.view === 'filters' ? clearBtn(ctrl) : null, + ctrl.vm.view === 'filters' && clearBtn(ctrl), ctrl.vm.view === 'presets' ? presets(ctrl) : filters(ctrl), ]), ), diff --git a/ui/keyboardMove/src/main.ts b/ui/keyboardMove/src/main.ts index 9bf89da375888..ff537d7eb59ca 100644 --- a/ui/keyboardMove/src/main.ts +++ b/ui/keyboardMove/src/main.ts @@ -154,10 +154,7 @@ export function ctrl(root: RootController, step: Step): KeyboardMove { export function render(ctrl: KeyboardMove) { return h('div.keyboard-move', [ h('input', { - attrs: { - spellcheck: 'false', - autocomplete: 'off', - }, + attrs: { spellcheck: 'false', autocomplete: 'off' }, hook: onInsert((input: HTMLInputElement) => loadKeyboardMove({ input, ctrl }).then((m: KeyboardMoveHandler) => ctrl.registerHandler(m)), ), diff --git a/ui/keyboardMove/src/plugins/keyboardMove.ts b/ui/keyboardMove/src/plugins/keyboardMove.ts index 11e6a993a02a4..86c1de51c7fad 100644 --- a/ui/keyboardMove/src/plugins/keyboardMove.ts +++ b/ui/keyboardMove/src/plugins/keyboardMove.ts @@ -24,7 +24,7 @@ interface Opts { } export function load(opts: Opts): Promise { - return lichess.loadEsm('keyboardMove', { init: opts }); + return lichess.asset.loadEsm('keyboardMove', { init: opts }); } export function initModule(opts: Opts) { diff --git a/ui/libot/defs/bots.json b/ui/libot/defs/bots.json index 18235a68a4cf6..06a5f5b0d9882 100644 --- a/ui/libot/defs/bots.json +++ b/ui/libot/defs/bots.json @@ -2,28 +2,28 @@ { "uid": "#babyhoward", "name": "Baby Howard", - "description": "Baby Howard is a bot that plays random moves.", + "description": "Baby Howard just learned how the pieces move. Now he wants blood.", "image": "baby-howard.webp", "netName": "maia-1100.pb" }, { "uid": "#coral", "name": "Coral", - "description": "Coral is a simple bot that plays random moves.", + "description": "Coral may slither like a snake, but she's actually a sweetheart.", "image": "coral.webp", "netName": "maia-1100.pb" }, { "uid": "#beatrice", "name": "Beatrice", - "description": "Beatrice is a bot that plays random moves.", + "description": "If you dip your pieces in honey, Beatrice will be a problem.", "image": "beatrice.webp", "netName": "maia-1200.pb" }, { "uid": "#nacho", "name": "Nacho", - "description": "", + "description": "Nacho is a verified feline grandmaster. If he tries to eat your head, do not let him.", "image": "nacho.webp", "netName": "maia-1200.pb" }, diff --git a/ui/libot/src/ctrl.ts b/ui/libot/src/ctrl.ts index 239d764090d74..c845aaa66d22c 100644 --- a/ui/libot/src/ctrl.ts +++ b/ui/libot/src/ctrl.ts @@ -34,7 +34,7 @@ export async function makeCtrl(libots: Libots, zf: Zerofish): Promise { } async function fetchNet(netName: string): Promise { - return fetch(lichess.assetUrl(`lifat/bots/weights/${netName}`, { noVersion: true })) + return fetch(lichess.asset.url(`lifat/bots/weights/${netName}`, { noVersion: true })) .then(res => res.arrayBuffer()) .then(buf => new Uint8Array(buf)); } diff --git a/ui/libot/src/main.ts b/ui/libot/src/main.ts index 0a376a92b8887..0c6a11ba6c82f 100644 --- a/ui/libot/src/main.ts +++ b/ui/libot/src/main.ts @@ -11,15 +11,17 @@ export async function initModule(stubs = false) { bots: {}, }; - const zfAsync = !stubs ? makeZerofish({ root: lichess.assetUrl('npm') }) : Promise.resolve(undefined); + const zfAsync = !stubs ? makeZerofish({ root: lichess.asset.url('npm') }) : Promise.resolve(undefined); - const botsAsync = fetch(lichess.assetUrl('bots.json')).then(x => x.json()); + const botsAsync = fetch(lichess.asset.url('bots.json')).then(x => x.json()); const [zf, bots] = await Promise.all([zfAsync, botsAsync]); + console.log(bots); for (const bot of bots) { - if (zf) libots.bots[bot.uid.slice(1)] = new ZfBot(bot, zf); - else libots.bots[bot.uid.slice(1)] = bot; + libots.bots[bot.uid.slice(1)] = zf + ? new ZfBot(bot, zf) + : { ...bot, imageUrl: lichess.asset.url(`lifat/bots/images/${bot.image}`, { noVersion: true }) }; } return zf ? makeCtrl(libots, zf) : libots; } diff --git a/ui/libot/src/zfbot.ts b/ui/libot/src/zfbot.ts index 25b1cd69420e7..34cc9f955e60e 100644 --- a/ui/libot/src/zfbot.ts +++ b/ui/libot/src/zfbot.ts @@ -16,7 +16,7 @@ export class ZfBot implements Libot { zf: Zerofish; get imageUrl() { - return lichess.assetUrl(`lifat/bots/images/${this.image}`, { noVersion: true }); + return lichess.asset.url(`lifat/bots/images/${this.image}`, { noVersion: true }); } constructor(info: BotInfo, zf: Zerofish) { diff --git a/ui/lobby/css/_feed.scss b/ui/lobby/css/_feed.scss new file mode 100644 index 0000000000000..907db985f80a2 --- /dev/null +++ b/ui/lobby/css/_feed.scss @@ -0,0 +1,72 @@ +$c-contours: mix($c-brag, $c-shade, 80%); + +.lobby__feed { + @extend %box-neat; + @include hoverflow; + background: $c-bg-box; + overflow-y: auto; +} +.daily-feed { + &__updates { + margin-left: 24px; + border-#{$start-direction}: 2px solid $c-contours; + margin-bottom: 1.5em; + } + + &__update { + padding-#{$start-direction}: 25px; + margin-top: 1.5em; + + table { + display: none; + } + + &__marker { + float: $start-direction; + width: 32px; + height: 32px; + padding: 5px; + margin-top: -5px; + margin-#{$start-direction}: -42px; + background-color: $c-contours; + border: 3px solid $c-bg-box; + border-radius: 50%; + + &.nobg { + background: $c-bg-box; + border: 2px solid $c-contours; + } + } + + &__day { + @extend %flex-center; + display: inline; + font-size: 1.15em; + font-weight: bold; + color: $c-contours; + time { + font-size: 1em; + opacity: 1; + } + &:hover { + color: $c-brag; + } + } + @include rendered-markdown( + $element-margin: 0.6em, + $h1: false, + $h2: false, + $table: false, + $img: false, + $list: false + ); + h3 { + font-size: 1.2em; + color: $c-font-dim; + border-bottom: $border; + } + h4 { + font-size: 1.15em; + } + } +} diff --git a/ui/lobby/css/_layout.scss b/ui/lobby/css/_layout.scss index 47ae35a13b9b7..ded5a11c83b1b 100644 --- a/ui/lobby/css/_layout.scss +++ b/ui/lobby/css/_layout.scss @@ -2,7 +2,7 @@ --cols: 1; // ui/lobby/src/main.ts grid-area: main; display: grid; - grid-template-areas: 'app' 'table' 'side' 'blog' 'support' 'tv' 'puzzle' 'leader' 'winner' 'tours' 'timeline' 'about'; + grid-template-areas: 'app' 'table' 'side' 'blog' 'support' 'feed' 'tv' 'puzzle' 'tours' 'leader' 'winner' 'timeline' 'about'; grid-gap: $block-gap; &__tournaments, @@ -11,20 +11,26 @@ &__winners { max-height: 20em; } + // this helps in empty dev mode + &__leaderboard, + &__winners { + min-height: 20em; + } @include breakpoint($mq-col2) { --cols: 2; // ui/lobby/src/main.ts grid-template-columns: repeat(2, 1fr); - grid-template-rows: auto repeat(3, fit-content(0)); + grid-template-rows: auto repeat(4, fit-content(0)) 20em; grid-template-areas: 'app app' 'side table' 'blog blog' 'tv puzzle' 'support support' + 'feed feed' 'tours tours' - 'leader winner' 'timeline timeline' + 'leader winner' 'about about'; &__support { @@ -37,16 +43,19 @@ @include breakpoint($mq-col3) { --cols: 3; // ui/lobby/src/main.ts grid-template-columns: repeat(3, 1fr); - grid-template-rows: 12em repeat(2, fit-content(0)) auto auto fit-content(0); + grid-template-rows: 12em repeat(2, fit-content(0)) auto fit-content(15em) repeat(2, fit-content(0)) minmax( + 0, + 1fr + ); grid-template-areas: 'table app app' 'side app app' 'tv blog blog' 'tv support support' - 'puzzle tours tours' - 'puzzle leader winner' - '. leader winner' - 'about about about'; + 'puzzle feed feed' + 'leader tours tours' + 'winner tours tours' + 'winner about about'; &__start { margin: 2em 0 0 0; @@ -68,13 +77,14 @@ @include breakpoint($mq-col4) { --cols: 4; // ui/lobby/src/main.ts grid-template-columns: repeat(4, 1fr); - grid-template-rows: repeat(2, fit-content(0)) auto minmax(0, 1fr); + grid-template-rows: repeat(2, fit-content(0)) auto repeat(6, fit-content(0)); grid-template-areas: 'side app app table' 'tv blog blog puzzle' 'tv support support puzzle' - 'leader winner tours tours' - 'about about about about'; + 'feed feed tours tours' + 'feed feed leader winner' + 'about about leader winner'; &__start { justify-content: center; @@ -150,6 +160,10 @@ grid-area: support; } + &__feed { + grid-area: feed; + } + &__about { grid-area: about; } diff --git a/ui/lobby/css/_lobby.scss b/ui/lobby/css/_lobby.scss index abd788cea8267..e01946cc0f21e 100644 --- a/ui/lobby/css/_lobby.scss +++ b/ui/lobby/css/_lobby.scss @@ -7,6 +7,7 @@ @import 'box'; @import 'support'; @import 'about'; +@import 'feed'; @import 'layout'; body { diff --git a/ui/lobby/css/build/_lobby.scss b/ui/lobby/css/build/_lobby.scss index 2a1c84775c5ea..a66cd79a26ffe 100644 --- a/ui/lobby/css/build/_lobby.scss +++ b/ui/lobby/css/build/_lobby.scss @@ -3,6 +3,7 @@ @import '../../../common/css/component/color-icon'; @import '../../../common/css/component/glowing'; @import '../../../common/css/component/tabs-horiz'; +@import '../../../common/css/component/markdown'; @import '../../../common/css/base/scrollbar'; @import '../../../site/css/ublog/card'; @import '../lobby'; diff --git a/ui/lobby/src/ctrl.ts b/ui/lobby/src/ctrl.ts index f37f54af12ad9..edbc89186bdc8 100644 --- a/ui/lobby/src/ctrl.ts +++ b/ui/lobby/src/ctrl.ts @@ -277,7 +277,7 @@ export default class LobbyController implements ParentCtrl { hasPool = (id: string) => this.pools.some(p => p.id === id); showSetupModal = async (gameType: GameType, opts?: SetupConstraints, friendUser?: string) => { - if (!this.setupCtrl) this.setupCtrl = await lichess.loadEsm('gameSetup', { init: this }); + if (!this.setupCtrl) this.setupCtrl = await lichess.asset.loadEsm('gameSetup', { init: this }); this.leavePool(); this.setupCtrl.openModal(gameType, opts, friendUser); this.redraw(); diff --git a/ui/lobby/src/disableDarkBoard.ts b/ui/lobby/src/disableDarkBoard.ts index 61fdd78e68a01..2b6343f17cc88 100644 --- a/ui/lobby/src/disableDarkBoard.ts +++ b/ui/lobby/src/disableDarkBoard.ts @@ -5,7 +5,7 @@ import { load as loadDasher } from 'dasher'; export default function disableDarkBoard() { if (!document.body.classList.contains('dark-board') || !isChrome() || !lichess.once('disableDarkBoard')) return; - loadDasher().then(m => m.subs.background.set('dark')); + loadDasher().then(m => m.background.set('dark')); domDialog({ htmlText: '

Dark board theme disabled

' + diff --git a/ui/lobby/src/view/correspondence.ts b/ui/lobby/src/view/correspondence.ts index 1cbcdbec24aa1..e7090c559626e 100644 --- a/ui/lobby/src/view/correspondence.ts +++ b/ui/lobby/src/view/correspondence.ts @@ -23,20 +23,12 @@ function renderSeek(ctrl: LobbyController, seek: Seek): VNode { tds([ h('span.is.color-icon.' + (seek.color || 'random')), seek.rating - ? h( - 'span.ulpt', - { - attrs: { 'data-href': '/@/' + seek.username }, - }, - seek.username, - ) + ? h('span.ulpt', { attrs: { 'data-href': '/@/' + seek.username } }, seek.username) : 'Anonymous', seek.rating && !ctrl.opts.hideRatings ? seek.rating + (seek.provisional ? '?' : '') : '', seek.days ? ctrl.trans.pluralSame('nbDays', seek.days) : '∞', h('span', [ - h('span.varicon', { - attrs: { 'data-icon': perfIcons[seek.perf.key] }, - }), + h('span.varicon', { attrs: { 'data-icon': perfIcons[seek.perf.key] } }), noarg(seek.mode === 1 ? 'rated' : 'casual'), ]), ]), @@ -67,12 +59,13 @@ function createSeek(ctrl: LobbyController): VNode | undefined { export default function (ctrl: LobbyController): MaybeVNodes { return [ h('table.hooks__list', [ - h('thead', [ + h( + 'thead', h( 'tr', ['', 'player', 'rating', 'time', 'mode'].map(header => h('th', ctrl.trans(header))), ), - ]), + ), h( 'tbody', { diff --git a/ui/lobby/src/view/playing.ts b/ui/lobby/src/view/playing.ts index c5b461e620630..bfaff84a5b64e 100644 --- a/ui/lobby/src/view/playing.ts +++ b/ui/lobby/src/view/playing.ts @@ -21,38 +21,29 @@ export default function (ctrl: LobbyController) { return h( 'div.now-playing', ctrl.data.nowPlaying.map(pov => - h( - 'a.' + pov.variant.key, - { - key: `${pov.gameId}${pov.lastMove}`, - attrs: { href: '/' + pov.fullId }, - }, - [ - h('span.mini-board.cg-wrap.is2d', { - attrs: { - 'data-state': `${pov.fen},${pov.orientation || pov.color},${pov.lastMove}`, - }, - hook: { - insert(vnode) { - lichess.miniBoard.init(vnode.elm as HTMLElement); - }, + h('a.' + pov.variant.key, { key: `${pov.gameId}${pov.lastMove}`, attrs: { href: '/' + pov.fullId } }, [ + h('span.mini-board.cg-wrap.is2d', { + attrs: { 'data-state': `${pov.fen},${pov.orientation || pov.color},${pov.lastMove}` }, + hook: { + insert(vnode) { + lichess.miniBoard.init(vnode.elm as HTMLElement); }, - }), - h('span.meta', [ - pov.opponent.ai - ? ctrl.trans('aiNameLevelAiLevel', 'Stockfish', pov.opponent.ai) - : pov.opponent.username, - h( - 'span.indicator', - pov.isMyTurn - ? pov.secondsLeft && pov.hasMoved - ? timer(pov) - : [ctrl.trans.noarg('yourTurn')] - : h('span', '\xa0'), - ), //   - ]), - ], - ), + }, + }), + h('span.meta', [ + pov.opponent.ai + ? ctrl.trans('aiNameLevelAiLevel', 'Stockfish', pov.opponent.ai) + : pov.opponent.username, + h( + 'span.indicator', + pov.isMyTurn + ? pov.secondsLeft && pov.hasMoved + ? timer(pov) + : [ctrl.trans.noarg('yourTurn')] + : h('span', '\xa0'), + ), //   + ]), + ]), ), ); } diff --git a/ui/lobby/src/view/pools.ts b/ui/lobby/src/view/pools.ts index 4ea4fdc7a3dc2..9895d02a375a5 100644 --- a/ui/lobby/src/view/pools.ts +++ b/ui/lobby/src/view/pools.ts @@ -25,31 +25,18 @@ export function render(ctrl: LobbyController) { .map(pool => { const active = !!member && member.id === pool.id, transp = !!member && !active; - return h( - 'div', - { - class: { - active, - transp: !active && transp, - }, - attrs: { 'data-id': pool.id }, - }, - [ - h('div.clock', `${pool.lim}+${pool.inc}`), - active && member!.range && !ctrl.opts.hideRatings - ? h('div.range', member.range.replace('-', '–')) - : h('div.perf', pool.perf), - active ? spinner() : null, - ], - ); + return h('div', { class: { active, transp: !active && transp }, attrs: { 'data-id': pool.id } }, [ + h('div.clock', `${pool.lim}+${pool.inc}`), + active && member!.range && !ctrl.opts.hideRatings + ? h('div.range', member.range.replace('-', '–')) + : h('div.perf', pool.perf), + active ? spinner() : null, + ]); }) .concat( h( 'div.custom', - { - class: { transp: !!member }, - attrs: { 'data-id': 'custom' }, - }, + { class: { transp: !!member }, attrs: { 'data-id': 'custom' } }, ctrl.trans.noarg('custom'), ), ); diff --git a/ui/lobby/src/view/realTime/chart.ts b/ui/lobby/src/view/realTime/chart.ts index bf31eb661a6ba..e29378c7dfc58 100644 --- a/ui/lobby/src/view/realTime/chart.ts +++ b/ui/lobby/src/view/realTime/chart.ts @@ -41,10 +41,7 @@ function renderPlot(ctrl: LobbyController, hook: Hook) { ].join('.'); return h('span#' + klass, { key: hook.id, - attrs: { - 'data-icon': perfIcons[hook.perf], - style: `bottom:${percents(bottom)};left:${percents(left)}`, - }, + attrs: { 'data-icon': perfIcons[hook.perf], style: `bottom:${percents(bottom)};left:${percents(left)}` }, hook: { insert(vnode) { $(vnode.elm as HTMLElement).powerTip({ @@ -94,20 +91,8 @@ function renderXAxis() { const tags: VNode[] = []; xMarks.forEach(v => { const l = clockX(v * 60); - tags.push( - h( - 'span.x.label', - { - attrs: { style: 'left:' + percents(l - 1.5) }, - }, - '' + v, - ), - ); - tags.push( - h('div.grid.vert', { - attrs: { style: 'width:' + percents(l) }, - }), - ); + tags.push(h('span.x.label', { attrs: { style: 'left:' + percents(l - 1.5) } }, '' + v)); + tags.push(h('div.grid.vert', { attrs: { style: 'width:' + percents(l) } })); }); return tags; } @@ -118,20 +103,8 @@ function renderYAxis() { const tags: VNode[] = []; yMarks.forEach(function (v) { const b = ratingY(v); - tags.push( - h( - 'span.y.label', - { - attrs: { style: 'bottom:' + percents(b + 1) }, - }, - '' + v, - ), - ); - tags.push( - h('div.grid.horiz', { - attrs: { style: 'height:' + percents(b + 0.8) }, - }), - ); + tags.push(h('span.y.label', { attrs: { style: 'bottom:' + percents(b + 1) } }, '' + v)); + tags.push(h('div.grid.horiz', { attrs: { style: 'height:' + percents(b + 0.8) } })); }); return tags; } diff --git a/ui/lobby/src/view/realTime/filter.ts b/ui/lobby/src/view/realTime/filter.ts index a81abdd2dade9..a0e0f36271cf6 100644 --- a/ui/lobby/src/view/realTime/filter.ts +++ b/ui/lobby/src/view/realTime/filter.ts @@ -49,17 +49,11 @@ function initialize(ctrl: LobbyController, el: HTMLElement) { const rangeValues = $rangeInput.val() ? ($rangeInput.val() as string).split('-') : []; $minInput - .attr({ - step: '50', - value: rangeValues[0] || $minInput.attr('min')!, - }) + .attr({ step: '50', value: rangeValues[0] || $minInput.attr('min')! }) .on('input', changeRatingRange); $maxInput - .attr({ - step: '50', - value: rangeValues[1] || $maxInput.attr('max')!, - }) + .attr({ step: '50', value: rangeValues[1] || $maxInput.attr('max')! }) .on('input', changeRatingRange); changeRatingRange(); @@ -70,10 +64,7 @@ export function toggle(ctrl: LobbyController, nbFiltered: number) { return h('i.toggle.toggle-filter', { class: { gamesFiltered: nbFiltered > 0, active: filter.open }, hook: bind('mousedown', filter.toggle, ctrl.redraw), - attrs: { - 'data-icon': filter.open ? licon.X : licon.Gear, - title: ctrl.trans.noarg('filterGames'), - }, + attrs: { 'data-icon': filter.open ? licon.X : licon.Gear, title: ctrl.trans.noarg('filterGames') }, }); } diff --git a/ui/lobby/src/view/realTime/list.ts b/ui/lobby/src/view/realTime/list.ts index 49bc719d647ad..aae858ca685fe 100644 --- a/ui/lobby/src/view/realTime/list.ts +++ b/ui/lobby/src/view/realTime/list.ts @@ -26,23 +26,11 @@ function renderHook(ctrl: LobbyController, hook: Hook) { tds([ h('span.is.color-icon.' + (hook.c || 'random')), hook.rating - ? h( - 'span.ulink.ulpt', - { - attrs: { 'data-href': '/@/' + hook.u }, - }, - hook.u, - ) + ? h('span.ulink.ulpt', { attrs: { 'data-href': '/@/' + hook.u } }, hook.u) : noarg('anonymous'), hook.rating && !ctrl.opts.hideRatings ? hook.rating + (hook.prov ? '?' : '') : '', hook.clock, - h( - 'span', - { - attrs: { 'data-icon': perfIcons[hook.perf] }, - }, - noarg(hook.ra ? 'rated' : 'casual'), - ), + h('span', { attrs: { 'data-icon': perfIcons[hook.perf] } }, noarg(hook.ra ? 'rated' : 'casual')), ]), ); } @@ -75,21 +63,9 @@ export const render = (ctrl: LobbyController, allHooks: Hook[]) => { const renderedHooks = [ ...standards.map(render), variants.length - ? h( - 'tr.variants', - { - key: 'variants', - }, - [ - h( - 'td', - { - attrs: { colspan: 5 }, - }, - '— ' + ctrl.trans('variant') + ' —', - ), - ], - ) + ? h('tr.variants', { key: 'variants' }, [ + h('td', { attrs: { colspan: 5 } }, '— ' + ctrl.trans('variant') + ' —'), + ]) : null, ...variants.map(render), ]; @@ -103,10 +79,7 @@ export const render = (ctrl: LobbyController, allHooks: Hook[]) => { h( 'th', { - class: { - sortable: true, - sort: ctrl.sort === 'rating', - }, + class: { sortable: true, sort: ctrl.sort === 'rating' }, hook: bind('click', _ => ctrl.setSort('rating'), ctrl.redraw), }, [h('i.is'), ctrl.trans('rating')], @@ -114,10 +87,7 @@ export const render = (ctrl: LobbyController, allHooks: Hook[]) => { h( 'th', { - class: { - sortable: true, - sort: ctrl.sort === 'time', - }, + class: { sortable: true, sort: ctrl.sort === 'time' }, hook: bind('click', _ => ctrl.setSort('time'), ctrl.redraw), }, [h('i.is'), ctrl.trans('time')], diff --git a/ui/lobby/src/view/tabs.ts b/ui/lobby/src/view/tabs.ts index dde576661b900..17a79517df30f 100644 --- a/ui/lobby/src/view/tabs.ts +++ b/ui/lobby/src/view/tabs.ts @@ -8,10 +8,7 @@ function tab(ctrl: LobbyController, key: Tab, active: Tab, content: MaybeVNodes) 'span', { attrs: { role: 'tab' }, - class: { - active: key === active, - glowing: key !== active && key === 'pools' && !!ctrl.poolMember, - }, + class: { active: key === active, glowing: key !== active && key === 'pools' && !!ctrl.poolMember }, hook: bind('mousedown', _ => ctrl.setTab(key), ctrl.redraw), }, content, diff --git a/ui/localPlay/src/ctrl.ts b/ui/localPlay/src/ctrl.ts index 3cca179e1f0a6..4d0fbd49e3e5e 100644 --- a/ui/localPlay/src/ctrl.ts +++ b/ui/localPlay/src/ctrl.ts @@ -18,7 +18,7 @@ export class Ctrl { readonly opts: LocalPlayOpts, readonly redraw: () => void, ) { - this.loaded = lichess.loadEsm('libot').then(libot => { + this.loaded = lichess.asset.loadEsm('libot').then(libot => { this.libot = libot; this.libot.setBot('coral'); }); diff --git a/ui/localPlay/src/main.ts b/ui/localPlay/src/main.ts index d16a223cd8eb9..3ee71b270b6c4 100644 --- a/ui/localPlay/src/main.ts +++ b/ui/localPlay/src/main.ts @@ -44,5 +44,5 @@ export async function makeRound(ctrl: LocalPlayCtrl): Promise { onChange: (d: RoundData) => d, //console.log(d), local: true, }; - return lichess.loadEsm('round', { init: opts }); + return lichess.asset.loadEsm('round', { init: opts }); } diff --git a/ui/mod/src/teamAdmin.ts b/ui/mod/src/teamAdmin.ts index 2801eb7570f86..da2b486482703 100644 --- a/ui/mod/src/teamAdmin.ts +++ b/ui/mod/src/teamAdmin.ts @@ -10,7 +10,7 @@ lichess.load.then(() => { initTagify(this, 100); }); $('form.team-add-leader input[name="name"]').each(function (this: HTMLInputElement) { - lichess.userComplete({ + lichess.asset.userComplete({ input: this, team: this.dataset.teamId, tag: 'span', diff --git a/ui/msg/src/view/actions.ts b/ui/msg/src/view/actions.ts index 1431f2fa5290e..946e27f8dcd3d 100644 --- a/ui/msg/src/view/actions.ts +++ b/ui/msg/src/view/actions.ts @@ -47,11 +47,7 @@ export default function renderActions(ctrl: MsgCtrl, convo: Convo): VNode[] { nodes.push( h(`button.${cls}.bad`, { key: 'delete', - attrs: { - 'data-icon': licon.Trash, - type: 'button', - title: ctrl.trans.noarg('delete'), - }, + attrs: { 'data-icon': licon.Trash, type: 'button', title: ctrl.trans.noarg('delete') }, hook: bind('click', withConfirm(ctrl.delete)), }), ); diff --git a/ui/msg/src/view/contact.ts b/ui/msg/src/view/contact.ts index 86f919a782a7a..e0a866796547e 100644 --- a/ui/msg/src/view/contact.ts +++ b/ui/msg/src/view/contact.ts @@ -26,16 +26,10 @@ export default function renderContact(ctrl: MsgCtrl, contact: Contact, active?: h('div.msg-app__side__contact__body', [ h( 'div.msg-app__side__contact__msg', - { - class: { 'msg-app__side__contact__msg--new': isNew }, - }, + { class: { 'msg-app__side__contact__msg--new': isNew } }, msg.text, ), - isNew - ? h('i.msg-app__side__contact__new', { - attrs: { 'data-icon': licon.BellOutline }, - }) - : null, + isNew ? h('i.msg-app__side__contact__new', { attrs: { 'data-icon': licon.BellOutline } }) : null, ]), ]), ], @@ -43,25 +37,11 @@ export default function renderContact(ctrl: MsgCtrl, contact: Contact, active?: } export const userIcon = (user: User, cls: string): VNode => - h( - 'div.user-link.' + cls, - { - class: { - online: user.online, - }, - }, - userLine(user), - ); + h('div.user-link.' + cls, { class: { online: user.online } }, userLine(user)); const renderDate = (msg: LastMsg): VNode => h( 'time.timeago', - { - key: msg.date.getTime(), - attrs: { - title: msg.date.toLocaleString(), - datetime: msg.date.getTime(), - }, - }, + { key: msg.date.getTime(), attrs: { title: msg.date.toLocaleString(), datetime: msg.date.getTime() } }, lichess.timeago(msg.date), ); diff --git a/ui/msg/src/view/convo.ts b/ui/msg/src/view/convo.ts index 202a6dc39a597..66530d702d426 100644 --- a/ui/msg/src/view/convo.ts +++ b/ui/msg/src/view/convo.ts @@ -10,41 +10,29 @@ import { userLink } from 'common/userLink'; export default function renderConvo(ctrl: MsgCtrl, convo: Convo): VNode { const user = convo.user; - return h( - 'div.msg-app__convo', - { - key: user.id, - }, - [ - h('div.msg-app__convo__head', [ - h('div.msg-app__convo__head__left', [ - h('span.msg-app__convo__head__back', { - attrs: { 'data-icon': licon.LessThan }, - hook: hookMobileMousedown(ctrl.showSide), - }), - userLink({ ...user, moderator: user.id == 'lichess' }), - ]), - h('div.msg-app__convo__head__actions', renderActions(ctrl, convo)), + return h('div.msg-app__convo', { key: user.id }, [ + h('div.msg-app__convo__head', [ + h('div.msg-app__convo__head__left', [ + h('span.msg-app__convo__head__back', { + attrs: { 'data-icon': licon.LessThan }, + hook: hookMobileMousedown(ctrl.showSide), + }), + userLink({ ...user, moderator: user.id == 'lichess' }), ]), - renderMsgs(ctrl, convo), - h('div.msg-app__convo__reply', [ - convo.relations.out === false || convo.relations.in === false - ? blocked('This conversation is blocked.') - : ctrl.data.me.bot - ? blocked('Bot accounts cannot send nor receive messages.') - : convo.postable - ? renderInteract(ctrl, user) - : blocked(`${user.name} doesn't accept new messages.`), - ]), - ], - ); + h('div.msg-app__convo__head__actions', renderActions(ctrl, convo)), + ]), + renderMsgs(ctrl, convo), + h('div.msg-app__convo__reply', [ + convo.relations.out === false || convo.relations.in === false + ? blocked('This conversation is blocked.') + : ctrl.data.me.bot + ? blocked('Bot accounts cannot send nor receive messages.') + : convo.postable + ? renderInteract(ctrl, user) + : blocked(`${user.name} doesn't accept new messages.`), + ]), + ]); } const blocked = (msg: string) => - h( - 'div.msg-app__convo__reply__block.text', - { - attrs: { 'data-icon': licon.NotAllowed }, - }, - msg, - ); + h('div.msg-app__convo__reply__block.text', { attrs: { 'data-icon': licon.NotAllowed } }, msg); diff --git a/ui/msg/src/view/enhance.ts b/ui/msg/src/view/enhance.ts index 56a73d463d269..62621bb4b9777 100644 --- a/ui/msg/src/view/enhance.ts +++ b/ui/msg/src/view/enhance.ts @@ -116,7 +116,7 @@ const expandGame = async (exp: Expandable) => { const $lpv = $('
'); $(exp.element).parent().parent().addClass('has-embed'); $(exp.element).replaceWith($('
').prepend($lpv)); - await lichess.loadEsm('lpv', { + await lichess.asset.loadEsm('lpv', { init: { el: $lpv[0] as HTMLElement, url: exp.link.src, lpvOpts: exp.link.opts }, }); scroller.auto(); diff --git a/ui/msg/src/view/interact.ts b/ui/msg/src/view/interact.ts index 1995607cf8218..256320eefee6b 100644 --- a/ui/msg/src/view/interact.ts +++ b/ui/msg/src/view/interact.ts @@ -22,11 +22,7 @@ export default function renderInteract(ctrl: MsgCtrl, user: User): VNode { renderTextarea(ctrl, user), h('button.msg-app__convo__post__submit.button', { class: { connected }, - attrs: { - type: 'submit', - 'data-icon': licon.PlayTriangle, - disabled: !connected, - }, + attrs: { type: 'submit', 'data-icon': licon.PlayTriangle, disabled: !connected }, }), ], ); @@ -34,10 +30,7 @@ export default function renderInteract(ctrl: MsgCtrl, user: User): VNode { function renderTextarea(ctrl: MsgCtrl, user: User): VNode { return h('textarea.msg-app__convo__post__text', { - attrs: { - rows: 1, - enterkeyhint: 'send', - }, + attrs: { rows: 1, enterkeyhint: 'send' }, hook: { insert(vnode) { setupTextarea(vnode.elm as HTMLTextAreaElement, user.id, ctrl); diff --git a/ui/msg/src/view/main.ts b/ui/msg/src/view/main.ts index c701cdad41bd1..78ad2e1e4f042 100644 --- a/ui/msg/src/view/main.ts +++ b/ui/msg/src/view/main.ts @@ -7,28 +7,20 @@ import * as search from './search'; export default function (ctrl: MsgCtrl): VNode { const activeId = ctrl.data.convo?.user.id; - return h( - 'main.box.msg-app', - { - class: { - [`pane-${ctrl.pane}`]: true, - }, - }, - [ - h('div.msg-app__side', [ - search.renderInput(ctrl), - ctrl.search.result - ? search.renderResults(ctrl, ctrl.search.result) - : h( - 'div.msg-app__contacts.msg-app__side__content', - ctrl.data.contacts.map(t => renderContact(ctrl, t, activeId)), - ), - ]), - ctrl.data.convo - ? renderConvo(ctrl, ctrl.data.convo) - : ctrl.loading - ? h('div.msg-app__convo', { key: ':' }, [h('div.msg-app__convo__head'), spinner()]) - : '', - ], - ); + return h('main.box.msg-app', { class: { [`pane-${ctrl.pane}`]: true } }, [ + h('div.msg-app__side', [ + search.renderInput(ctrl), + ctrl.search.result + ? search.renderResults(ctrl, ctrl.search.result) + : h( + 'div.msg-app__contacts.msg-app__side__content', + ctrl.data.contacts.map(t => renderContact(ctrl, t, activeId)), + ), + ]), + ctrl.data.convo + ? renderConvo(ctrl, ctrl.data.convo) + : ctrl.loading + ? h('div.msg-app__convo', { key: ':' }, [h('div.msg-app__convo__head'), spinner()]) + : '', + ]); } diff --git a/ui/msg/src/view/msgs.ts b/ui/msg/src/view/msgs.ts index 748ca3e74c41d..3b7db5d9a7228 100644 --- a/ui/msg/src/view/msgs.ts +++ b/ui/msg/src/view/msgs.ts @@ -10,12 +10,7 @@ import MsgCtrl from '../ctrl'; export default function renderMsgs(ctrl: MsgCtrl, convo: Convo): VNode { return h( 'div.msg-app__convo__msgs', - { - hook: { - insert: setupMsgs(ctrl, true), - postpatch: setupMsgs(ctrl, false), - }, - }, + { hook: { insert: setupMsgs(ctrl, true), postpatch: setupMsgs(ctrl, false) } }, [ h('div.msg-app__convo__msgs__init'), h('div.msg-app__convo__msgs__content', [ @@ -24,9 +19,7 @@ export default function renderMsgs(ctrl: MsgCtrl, convo: Convo): VNode { 'button.msg-app__convo__msgs__more.button.button-empty', { key: 'more', - attrs: { - type: 'button', - }, + attrs: { type: 'button' }, hook: bind('click', _ => { scroller.setMarker(); ctrl.getMore(); @@ -70,21 +63,12 @@ const pad2 = (num: number): string => (num < 10 ? '0' : '') + num; function groupMsgs(msgs: Msg[]): Daily[] { let prev: Msg = msgs[0]; if (!prev) return [{ date: new Date(), msgs: [] }]; - const dailies: Daily[] = [ - { - date: prev.date, - msgs: [[prev]], - }, - ]; + const dailies: Daily[] = [{ date: prev.date, msgs: [[prev]] }]; msgs.slice(1).forEach(msg => { if (sameDay(msg.date, prev.date)) { if (msg.user == prev.user) dailies[0].msgs[0].unshift(msg); else dailies[0].msgs.unshift([msg]); - } else - dailies.unshift({ - date: msg.date, - msgs: [[msg]], - }); + } else dailies.unshift({ date: msg.date, msgs: [[msg]] }); prev = msg; }); return dailies; diff --git a/ui/msg/src/view/search.ts b/ui/msg/src/view/search.ts index 7e70713ed69b4..29dda72b9a0c9 100644 --- a/ui/msg/src/view/search.ts +++ b/ui/msg/src/view/search.ts @@ -9,10 +9,7 @@ import { hookMobileMousedown } from 'common/device'; export const renderInput = (ctrl: MsgCtrl): VNode => h('div.msg-app__side__search', [ h('input', { - attrs: { - value: '', - placeholder: ctrl.trans.noarg('searchOrStartNewDiscussion'), - }, + attrs: { value: '', placeholder: ctrl.trans.noarg('searchOrStartNewDiscussion') }, hook: { insert(vnode) { const input = vnode.elm as HTMLInputElement; @@ -63,10 +60,7 @@ export function renderResults(ctrl: MsgCtrl, res: SearchResult): VNode { function renderUser(ctrl: MsgCtrl, user: User): VNode { return h( 'div.msg-app__side__contact', - { - key: user.id, - hook: hookMobileMousedown(_ => ctrl.openConvo(user.id)), - }, + { key: user.id, hook: hookMobileMousedown(_ => ctrl.openConvo(user.id)) }, [ userIcon(user, 'msg-app__side__contact__icon'), h('div.msg-app__side__contact__user', [ diff --git a/ui/notify/src/renderers.ts b/ui/notify/src/renderers.ts index 8d2f6b71f1420..def6bd8ac8f2c 100644 --- a/ui/notify/src/renderers.ts +++ b/ui/notify/src/renderers.ts @@ -149,22 +149,10 @@ function generic(n: Notification, url: string | undefined, icon: string, content return h( url ? 'a' : 'span', { - class: { - site_notification: true, - [n.type]: true, - new: !n.read, - }, - attrs: { - key: n.date, - ...(url ? { href: url } : {}), - }, + class: { site_notification: true, [n.type]: true, new: !n.read }, + attrs: { key: n.date, ...(url ? { href: url } : {}) }, }, - [ - h('i', { - attrs: { 'data-icon': icon }, - }), - h('span.content', content), - ], + [h('i', { attrs: { 'data-icon': icon } }), h('span.content', content)], ); } @@ -172,12 +160,7 @@ function drawTime(n: Notification) { const date = new Date(n.date); return h( 'time.timeago', - { - attrs: { - title: date.toLocaleString(), - datetime: n.date, - }, - }, + { attrs: { title: date.toLocaleString(), datetime: n.date } }, lichess.timeago(date), ); } diff --git a/ui/notify/src/view.ts b/ui/notify/src/view.ts index 393a8de4ca4cb..2403840be0a2b 100644 --- a/ui/notify/src/view.ts +++ b/ui/notify/src/view.ts @@ -28,10 +28,7 @@ function renderContent(ctrl: Ctrl, d: NotifyData): VNode[] { if (nb > 0) nodes.push( h('button.delete.button.button-empty', { - attrs: { - 'data-icon': licon.Trash, - title: 'Clear', - }, + attrs: { 'data-icon': licon.Trash, title: 'Clear' }, hook: clickHook(ctrl.clear), }), ); @@ -40,10 +37,7 @@ function renderContent(ctrl: Ctrl, d: NotifyData): VNode[] { if (pager.nextPage) nodes.push( - h('div.pager.next', { - attrs: { 'data-icon': licon.DownTriangle }, - hook: clickHook(ctrl.nextPage), - }), + h('div.pager.next', { attrs: { 'data-icon': licon.DownTriangle }, hook: clickHook(ctrl.nextPage) }), ); if (!('Notification' in window)) @@ -61,13 +55,7 @@ export function asText(n: Notification, trans: Trans): string | undefined { function notificationDenied(): VNode { return h( 'a.browser-notification.denied', - { - attrs: { - href: '/faq#browser-notifications', - target: '_blank', - rel: 'noopener', - }, - }, + { attrs: { href: '/faq#browser-notifications', target: '_blank', rel: 'noopener' } }, 'Notification popups disabled by browser setting', ); } @@ -92,14 +80,8 @@ function recentNotifications(d: NotifyData, scrolling: boolean): VNode { return h( 'div', { - class: { - notifications: true, - scrolling, - }, - hook: { - insert: contentLoaded, - postpatch: contentLoaded, - }, + class: { notifications: true, scrolling }, + hook: { insert: contentLoaded, postpatch: contentLoaded }, }, d.pager.currentPageResults.map(n => asHtml(n, trans)) as VNode[], ); diff --git a/ui/nvui/package.json b/ui/nvui/package.json index 5bf11c8322d88..7cab6931c6938 100644 --- a/ui/nvui/package.json +++ b/ui/nvui/package.json @@ -24,7 +24,7 @@ "license": "AGPL-3.0-or-later", "dependencies": { "chess": "workspace:*", - "chessops": "^0.12.7", + "chessops": "^0.13.0", "snabbdom": "^3.5.1" }, "scripts": { diff --git a/ui/nvui/src/chess.ts b/ui/nvui/src/chess.ts index 266e931f6b31a..cebf5c9423724 100644 --- a/ui/nvui/src/chess.ts +++ b/ui/nvui/src/chess.ts @@ -710,16 +710,7 @@ export function renderMainline(nodes: Tree.Node[], currentPath: Tree.Path, style node.ply & 1 ? plyToTurn(node.ply) + ' ' : null, renderSan(node.san, node.uci, style), ]; - res.push( - h( - 'move', - { - attrs: { p: path }, - class: { active: path === currentPath }, - }, - content, - ), - ); + res.push(h('move', { attrs: { p: path }, class: { active: path === currentPath } }, content)); res.push(renderComments(node, style)); res.push(', '); if (node.ply % 2 === 0) res.push(h('br')); diff --git a/ui/nvui/src/notify.ts b/ui/nvui/src/notify.ts index 1a79402ec433f..33a8744ce77d9 100644 --- a/ui/nvui/src/notify.ts +++ b/ui/nvui/src/notify.ts @@ -24,14 +24,5 @@ export class Notify { : ''; render = () => - h( - 'div.notify', - { - attrs: { - 'aria-live': 'assertive', - 'aria-atomic': 'true', - }, - }, - this.currentText(), - ); + h('div.notify', { attrs: { 'aria-live': 'assertive', 'aria-atomic': 'true' } }, this.currentText()); } diff --git a/ui/nvui/src/setting.ts b/ui/nvui/src/setting.ts index 2dc55d7db0ced..3e8e6973a7f48 100644 --- a/ui/nvui/src/setting.ts +++ b/ui/nvui/src/setting.ts @@ -42,16 +42,7 @@ export function renderSetting(setting: Setting, redraw: () => void): VNode }, setting.choices.map(choice => { const [key, name] = choice; - return h( - 'option', - { - attrs: { - value: '' + key, - selected: key === v, - }, - }, - name, - ); + return h('option', { attrs: { value: '' + key, selected: key === v } }, name); }), ); } diff --git a/ui/opening/package.json b/ui/opening/package.json index 96c451f9b6122..83c47695bd5e4 100644 --- a/ui/opening/package.json +++ b/ui/opening/package.json @@ -7,10 +7,10 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@types/debounce-promise": "^3.1.6", - "chart.js": "=3.9.1", - "chartjs-adapter-date-fns": "=2.0.1", + "chart.js": "=4.4.0", + "chartjs-adapter-dayjs-4": "^1.0.4", "common": "workspace:*", - "date-fns": "^2.29.3", + "dayjs": "^1.11.10", "debounce-promise": "^3.1.2", "lichess-pgn-viewer": "^2.0.1" }, diff --git a/ui/opening/src/chart.ts b/ui/opening/src/chart.ts index fb02826af9458..4dcd071daf623 100644 --- a/ui/opening/src/chart.ts +++ b/ui/opening/src/chart.ts @@ -1,11 +1,10 @@ import * as chart from 'chart.js'; -import 'chartjs-adapter-date-fns'; -import { addMonths } from 'date-fns'; +import 'chartjs-adapter-dayjs-4'; +import dayjs from 'dayjs'; import { OpeningPage } from './interfaces'; chart.Chart.register( chart.LineController, - chart.CategoryScale, chart.LinearScale, chart.PointElement, chart.LineElement, @@ -15,18 +14,17 @@ chart.Chart.register( chart.TimeScale, ); -const firstDate = new Date('2017-01-01'); +const firstDate = dayjs('2017-01-01'); export const renderHistoryChart = (data: OpeningPage) => { if (!data.history.find(p => p > 0)) return; - const canvas = document.querySelector('.opening__popularity__chart') as HTMLCanvasElement; + const canvas = $('.opening__popularity__chart')[0] as HTMLCanvasElement; new chart.Chart(canvas, { type: 'line', data: { - labels: data.history.map((_, i) => addMonths(firstDate, i)), datasets: [ { - data: data.history, + data: data.history.map((n, i) => ({ x: firstDate.add(i, 'M').valueOf(), y: n })), borderColor: 'hsla(37,74%,43%,1)', backgroundColor: 'hsla(37,74%,43%,0.5)', fill: true, @@ -39,7 +37,7 @@ export const renderHistoryChart = (data: OpeningPage) => { x: { type: 'time', time: { - tooltipFormat: 'MMMM yyyy', + tooltipFormat: 'MMMM YYYY', }, display: false, }, @@ -52,16 +50,12 @@ export const renderHistoryChart = (data: OpeningPage) => { }, // https://www.chartjs.org/docs/latest/configuration/responsive.html // responsive: false, // just doesn't work - hover: { + interaction: { mode: 'index', intersect: false, }, - plugins: { - tooltip: { - mode: 'index', - intersect: false, - }, - }, + parsing: false, + normalized: true, }, }); }; diff --git a/ui/puz/package.json b/ui/puz/package.json index 3e38a67e4629a..97e66ac779b9f 100644 --- a/ui/puz/package.json +++ b/ui/puz/package.json @@ -20,7 +20,7 @@ "author": "Thibault Duplessis", "license": "AGPL-3.0-or-later", "dependencies": { - "chessops": "^0.12.7", + "chessops": "^0.13.0", "common": "workspace:*", "snabbdom": "^3.5.1" }, diff --git a/ui/puz/src/view/history.ts b/ui/puz/src/view/history.ts index bb46ffa86cced..51ee821d682d0 100644 --- a/ui/puz/src/view/history.ts +++ b/ui/puz/src/view/history.ts @@ -18,10 +18,7 @@ const toggleButton = (prop: Toggle, title: string): VNode => h( 'button.puz-history__filter.button', { - class: { - active: prop(), - 'button-empty': !prop, - }, + class: { active: prop(), 'button-empty': !prop }, hook: onInsert(e => e.addEventListener('click', prop.toggle)), }, title, @@ -48,34 +45,28 @@ export default (ctrl: PuzCtrl): VNode => { (!filters.skip || !filters.skip() || r.puzzle.id === ctrl.run.skipId), ) .map(round => - h( - 'div.puz-history__round', - { - key: round.puzzle.id, - }, - [ - h('a.puz-history__round__puzzle.mini-board.cg-wrap.is2d', { - attrs: { - href: `/training/${round.puzzle.id}`, - target: '_blank', - rel: 'noopener', - }, - hook: onInsert(e => { - const pos = Chess.fromSetup(parseFen(round.puzzle.fen).unwrap()).unwrap(); - const uci = round.puzzle.line.split(' ')[0]; - pos.play(parseUci(uci)!); - miniBoard.initWith(e, makeFen(pos.toSetup()), pos.turn, uci); - }), + h('div.puz-history__round', { key: round.puzzle.id }, [ + h('a.puz-history__round__puzzle.mini-board.cg-wrap.is2d', { + attrs: { + href: `/training/${round.puzzle.id}`, + target: '_blank', + rel: 'noopener', + }, + hook: onInsert(e => { + const pos = Chess.fromSetup(parseFen(round.puzzle.fen).unwrap()).unwrap(); + const uci = round.puzzle.line.split(' ')[0]; + pos.play(parseUci(uci)!); + miniBoard.initWith(e, makeFen(pos.toSetup()), pos.turn, uci); }), - h('span.puz-history__round__meta', [ - h('span.puz-history__round__result', [ - h(round.win ? 'good' : 'bad', Math.round(round.millis / 1000) + 's'), - h('rating', round.puzzle.rating), - ]), - h('span.puz-history__round__id', '#' + round.puzzle.id), + }), + h('span.puz-history__round__meta', [ + h('span.puz-history__round__result', [ + h(round.win ? 'good' : 'bad', Math.round(round.millis / 1000) + 's'), + h('rating', round.puzzle.rating), ]), - ], - ), + h('span.puz-history__round__id', '#' + round.puzzle.id), + ]), + ]), ), ), ]); diff --git a/ui/puz/src/view/util.ts b/ui/puz/src/view/util.ts index 5335eead2b1c1..4a715b1777014 100644 --- a/ui/puz/src/view/util.ts +++ b/ui/puz/src/view/util.ts @@ -25,9 +25,7 @@ export const renderCombo = ]), h('div.puz-combo__bars', [ h('div.puz-combo__bar', [ - h('div.puz-combo__bar__in', { - attrs: { style: `width:${run.combo.percent()}%` }, - }), + h('div.puz-combo__bar__in', { attrs: { style: `width:${run.combo.percent()}%` } }), h('div.puz-combo__bar__in-full'), ]), h( @@ -35,11 +33,7 @@ export const renderCombo = [0, 1, 2, 3].map(l => h( 'div.puz-combo__level', - { - class: { - active: l < level, - }, - }, + { class: { active: l < level } }, h('span', renderBonus(config.combo.levels[l + 1][1])), ), ), diff --git a/ui/puzzle/package.json b/ui/puzzle/package.json index a80ebbc32cf1a..196c9122507bb 100644 --- a/ui/puzzle/package.json +++ b/ui/puzzle/package.json @@ -12,17 +12,17 @@ "author": "Thibault Duplessis", "license": "AGPL-3.0-or-later", "dependencies": { + "board": "workspace:*", "ceval": "workspace:*", - "chart.js": "=3.9.1", + "chart.js": "=4.4.0", "chess": "workspace:*", - "chessops": "^0.12.7", + "chessops": "^0.13.0", "common": "workspace:*", "keyboardMove": "workspace:*", - "voice": "workspace:*", "nvui": "workspace:*", - "board": "workspace:*", "snabbdom": "^3.5.1", - "tree": "workspace:*" + "tree": "workspace:*", + "voice": "workspace:*" }, "scripts": { "compile": "tsc", diff --git a/ui/puzzle/src/autoShape.ts b/ui/puzzle/src/autoShape.ts index 8b018dc2977c8..5e98eca04c4c1 100644 --- a/ui/puzzle/src/autoShape.ts +++ b/ui/puzzle/src/autoShape.ts @@ -1,16 +1,16 @@ import { winningChances, CevalCtrl } from 'ceval'; import { DrawModifiers, DrawShape } from 'chessground/draw'; -import { Vm } from './interfaces'; import { Api as CgApi } from 'chessground/api'; import { opposite, parseUci, makeSquare } from 'chessops/util'; import { NormalMove } from 'chessops/types'; interface Opts { - vm: Vm; + node: Tree.Node; + showComputer(): boolean; ceval: CevalCtrl; ground: CgApi; - nextNodeBest?: Uci; - threatMode: boolean; + nextNodeBest(): Uci | undefined; + threatMode(): boolean; } function makeAutoShapesFromUci( @@ -30,16 +30,16 @@ function makeAutoShapesFromUci( } export default function (opts: Opts): DrawShape[] { - const n = opts.vm.node, + const n = opts.node, hovering = opts.ceval.hovering(), color = n.fen.includes(' w ') ? 'white' : 'black'; let shapes: DrawShape[] = []; if (hovering && hovering.fen === n.fen) shapes = shapes.concat(makeAutoShapesFromUci(color, hovering.uci, 'paleBlue')); - if (opts.vm.showAutoShapes() && opts.vm.showComputer()) { + if (opts.showComputer()) { if (n.eval) shapes = shapes.concat(makeAutoShapesFromUci(color, n.eval.best!, 'paleGreen')); if (!hovering) { - let nextBest: Uci | undefined = opts.nextNodeBest; + let nextBest: Uci | undefined = opts.nextNodeBest(); if (!nextBest && opts.ceval.enabled() && n.ceval) nextBest = n.ceval.pvs[0].moves[0]; if (nextBest) shapes = shapes.concat(makeAutoShapesFromUci(color, nextBest, 'paleBlue')); if ( @@ -47,7 +47,7 @@ export default function (opts: Opts): DrawShape[] { n.ceval && n.ceval.pvs && n.ceval.pvs[1] && - !(opts.threatMode && n.threat && n.threat.pvs[2]) + !(opts.threatMode() && n.threat && n.threat.pvs[2]) ) { n.ceval.pvs.forEach(function (pv) { if (pv.moves[0] === nextBest) return; @@ -62,7 +62,7 @@ export default function (opts: Opts): DrawShape[] { } } } - if (opts.ceval.enabled() && opts.threatMode && n.threat) { + if (opts.ceval.enabled() && opts.threatMode() && n.threat) { if (n.threat.pvs[1]) { shapes = shapes.concat(makeAutoShapesFromUci(opposite(color), n.threat.pvs[0].moves[0], 'paleRed')); n.threat.pvs.slice(1).forEach(function (pv) { diff --git a/ui/puzzle/src/control.ts b/ui/puzzle/src/control.ts index ce060c90f69ad..36578220460f2 100644 --- a/ui/puzzle/src/control.ts +++ b/ui/puzzle/src/control.ts @@ -1,26 +1,26 @@ import { path as treePath } from 'tree'; -import { KeyboardController } from './interfaces'; +import PuzzleCtrl from './ctrl'; -export function canGoForward(ctrl: KeyboardController): boolean { - return ctrl.vm.node.children.length > 0; +export function canGoForward(ctrl: PuzzleCtrl): boolean { + return ctrl.node.children.length > 0; } -export function next(ctrl: KeyboardController): void { - const child = ctrl.vm.node.children[0]; +export function next(ctrl: PuzzleCtrl): void { + const child = ctrl.node.children[0]; if (!child) return; - ctrl.userJump(ctrl.vm.path + child.id); + ctrl.userJump(ctrl.path + child.id); } -export function prev(ctrl: KeyboardController): void { - ctrl.userJump(treePath.init(ctrl.vm.path)); +export function prev(ctrl: PuzzleCtrl): void { + ctrl.userJump(treePath.init(ctrl.path)); } -export function last(ctrl: KeyboardController): void { - const toInit = !treePath.contains(ctrl.vm.path, ctrl.vm.initialPath); - ctrl.userJump(toInit ? ctrl.vm.initialPath : treePath.fromNodeList(ctrl.vm.mainline)); +export function last(ctrl: PuzzleCtrl): void { + const toInit = !treePath.contains(ctrl.path, ctrl.initialPath); + ctrl.userJump(toInit ? ctrl.initialPath : treePath.fromNodeList(ctrl.mainline)); } -export function first(ctrl: KeyboardController): void { - const toInit = ctrl.vm.path !== ctrl.vm.initialPath && treePath.contains(ctrl.vm.path, ctrl.vm.initialPath); - ctrl.userJump(toInit ? ctrl.vm.initialPath : treePath.root); +export function first(ctrl: PuzzleCtrl): void { + const toInit = ctrl.path !== ctrl.initialPath && treePath.contains(ctrl.path, ctrl.initialPath); + ctrl.userJump(toInit ? ctrl.initialPath : treePath.root); } diff --git a/ui/puzzle/src/ctrl.ts b/ui/puzzle/src/ctrl.ts index 32cd87f528f18..0c029d7a5b32a 100644 --- a/ui/puzzle/src/ctrl.ts +++ b/ui/puzzle/src/ctrl.ts @@ -6,7 +6,7 @@ import moveTest from './moveTest'; import PuzzleSession from './session'; import PuzzleStreak from './streak'; import throttle from 'common/throttle'; -import { Vm, Controller, PuzzleOpts, PuzzleData, MoveTest, ThemeKey, ReplayEnd } from './interfaces'; +import { PuzzleOpts, PuzzleData, MoveTest, ThemeKey, ReplayEnd, NvuiPlugin, PuzzleRound } from './interfaces'; import { Api as CgApi } from 'chessground/api'; import { build as treeBuild, ops as treeOps, path as treePath, TreeWrapper } from 'tree'; import { Chess, normalizeMove } from 'chessops/chess'; @@ -15,156 +15,204 @@ import { Config as CgConfig } from 'chessground/config'; import { CevalCtrl } from 'ceval'; import { makeVoiceMove, VoiceMove, RootCtrl as VoiceRoot } from 'voice'; import { ctrl as makeKeyboardMove, KeyboardMove, RootController as KeyboardRoot } from 'keyboardMove'; -import { defer } from 'common/defer'; -import { defined, prop, Prop, propWithEffect, toggle } from 'common'; +import { Deferred, defer } from 'common/defer'; +import { defined, prop, Prop, propWithEffect, Toggle, toggle } from 'common'; import { makeSanAndPlay } from 'chessops/san'; import { parseFen, makeFen } from 'chessops/fen'; import { parseSquare, parseUci, makeSquare, makeUci, opposite } from 'chessops/util'; import { pgnToTree, mergeSolution } from './moveTree'; import { PromotionCtrl } from 'chess/promotion'; import { Role, Move, Outcome } from 'chessops/types'; -import { storedBooleanProp } from 'common/storage'; +import { StoredProp, storedBooleanProp, storedBooleanPropWithEffect } from 'common/storage'; import { fromNodeList } from 'tree/dist/path'; import { last } from 'tree/dist/ops'; import { uciToMove } from 'chessground/util'; import { Redraw } from 'common/snabbdom'; +import { ParentCtrl } from 'ceval/src/types'; + +export default class PuzzleCtrl implements ParentCtrl { + data: PuzzleData; + next: Deferred = defer(); + trans: Trans; + tree: TreeWrapper; + ceval: CevalCtrl; + autoNext: StoredProp; + rated: StoredProp; + ground: Prop = prop(undefined) as Prop; + threatMode: Toggle = toggle(false); + streak?: PuzzleStreak; + streakFailStorage = lichess.storage.make('puzzle.streak.fail'); + session: PuzzleSession; + menu: Toggle; + flipped = toggle(false); + keyboardMove?: KeyboardMove; + voiceMove?: VoiceMove; + promotion: PromotionCtrl; + keyboardHelp: Prop; + cgConfig?: CgConfig; + path: Tree.Path; + node: Tree.Node; + nodeList: Tree.Node[]; + mainline: Tree.Node[]; + initialPath: Tree.Path; + initialNode: Tree.Node; + pov: Color; + mode: 'play' | 'view' | 'try'; + round?: PuzzleRound; + justPlayed?: Key; + resultSent: boolean; + lastFeedback: 'init' | 'fail' | 'win' | 'good' | 'retry'; + canViewSolution = toggle(false); + autoScrollRequested: boolean; + autoScrollNow: boolean; + voteDisabled?: boolean; + isDaily: boolean; + + constructor( + readonly opts: PuzzleOpts, + readonly redraw: Redraw, + readonly nvui?: NvuiPlugin, + ) { + this.trans = lichess.trans(opts.i18n); + this.rated = storedBooleanPropWithEffect('puzzle.rated', true, this.redraw); + this.autoNext = storedBooleanProp( + `puzzle.autoNext${opts.data.streak ? '.streak' : ''}`, + !!opts.data.streak, + ); + this.streak = opts.data.streak ? new PuzzleStreak(opts.data) : undefined; + if (this.streak) { + opts.data = { ...opts.data, ...this.streak.data.current }; + this.streakFailStorage.listen(_ => this.failStreak(this.streak!)); + } + this.session = new PuzzleSession(opts.data.angle.key, opts.data.user?.id, !!opts.data.streak); + this.menu = toggle(false, redraw); -export default function (opts: PuzzleOpts, redraw: Redraw): Controller { - const vm: Vm = { - next: defer(), - } as Vm; - let data: PuzzleData, tree: TreeWrapper, ceval: CevalCtrl; - const hasStreak = !!opts.data.streak; - const autoNext = storedBooleanProp(`puzzle.autoNext${hasStreak ? '.streak' : ''}`, hasStreak); - const rated = storedBooleanProp('puzzle.rated', true); - const ground = prop(undefined) as Prop; - const threatMode = prop(false); - const streak = opts.data.streak ? new PuzzleStreak(opts.data) : undefined; - const streakFailStorage = lichess.storage.make('puzzle.streak.fail'); - if (streak) { - opts.data = { - ...opts.data, - ...streak.data.current, - }; - streakFailStorage.listen(_ => failStreak(streak)); - } - const session = new PuzzleSession(opts.data.angle.key, opts.data.user?.id, hasStreak); + this.initiate(opts.data); + this.promotion = new PromotionCtrl( + this.withGround, + () => this.withGround(g => g.set(this.cgConfig!)), + redraw, + ); - const menu = toggle(false, redraw); + this.keyboardHelp = propWithEffect(location.hash === '#keyboard', this.redraw); + keyboard(this); - // required by ceval - vm.showComputer = () => vm.mode === 'view'; - vm.showAutoShapes = () => true; + // If the page loads while being hidden (like when changing settings), + // chessground is not displayed, and the first move is not fully applied. + // Make sure chessground is fully shown when the page goes back to being visible. + document.addEventListener('visibilitychange', () => + lichess.requestIdleCallback(() => this.jump(this.path), 500), + ); + + lichess.pubsub.on('zen', () => { + const zen = $('body').toggleClass('zen').hasClass('zen'); + window.dispatchEvent(new Event('resize')); + if (!$('body').hasClass('zen-auto')) xhr.setZen(zen); + }); + $('body').addClass('playing'); // for zen + $('#zentog').on('click', () => lichess.pubsub.emit('zen')); + } - const loadSound = (file: string, volume?: number) => { + private loadSound = (file: string, volume?: number) => { lichess.sound.load(file, `${lichess.sound.baseUrl}/${file}`); return () => lichess.sound.play(file, volume); }; - const sound = { - good: loadSound('lisp/PuzzleStormGood', 0.7), - end: loadSound('lisp/PuzzleStormEnd', 1), + sound = { + good: this.loadSound('lisp/PuzzleStormGood', 0.7), + end: this.loadSound('lisp/PuzzleStormEnd', 1), }; - let flipped = false; - - function setPath(path: Tree.Path): void { - vm.path = path; - vm.nodeList = tree.getNodeList(path); - vm.node = treeOps.last(vm.nodeList)!; - vm.mainline = treeOps.mainlineNodeList(tree.root); - } - - let keyboardMove: KeyboardMove | undefined; - let voiceMove: VoiceMove | undefined; + setPath = (path: Tree.Path): void => { + this.path = path; + this.nodeList = this.tree.getNodeList(path); + this.node = treeOps.last(this.nodeList)!; + this.mainline = treeOps.mainlineNodeList(this.tree.root); + }; - function setChessground(this: Controller, cg: CgApi): void { - ground(cg); + setChessground = (cg: CgApi): void => { + this.ground(cg); const makeRoot = () => ({ data: { game: { variant: { key: 'standard' } }, - player: { color: vm.pov }, + player: { color: this.pov }, }, chessground: cg, - sendMove: playUserMove, - auxMove: auxMove, + sendMove: this.playUserMove, + auxMove: this.auxMove, redraw: this.redraw, - flipNow: flip, - userJumpPlyDelta, - next: nextPuzzle, - vote, - solve: viewSolution, + flipNow: this.flip, + userJumpPlyDelta: this.userJumpPlyDelta, + next: this.nextPuzzle, + vote: this.vote, + solve: this.viewSolution, }); - if (opts.pref.voiceMove) - this.voiceMove = voiceMove = makeVoiceMove(makeRoot() as VoiceRoot, this.vm.node.fen); - if (opts.pref.keyboardMove) - this.keyboardMove = keyboardMove = makeKeyboardMove(makeRoot() as KeyboardRoot, { - fen: this.vm.node.fen, - }); + if (this.opts.pref.voiceMove) this.voiceMove = makeVoiceMove(makeRoot() as VoiceRoot, this.node.fen); + if (this.opts.pref.keyboardMove) + this.keyboardMove = makeKeyboardMove(makeRoot() as KeyboardRoot, { fen: this.node.fen }); requestAnimationFrame(() => this.redraw()); - } + }; + + pref = this.opts.pref; - function withGround(f: (cg: CgApi) => A): A | undefined { - const g = ground(); + withGround = (f: (cg: CgApi) => A): A | undefined => { + const g = this.ground(); return g && f(g); - } + }; - function initiate(fromData: PuzzleData): void { - data = fromData; - tree = treeBuild(pgnToTree(data.game.pgn.split(' '))); - const initialPath = treePath.fromNodeList(treeOps.mainlineNodeList(tree.root)); - vm.mode = 'play'; - vm.next = defer(); - vm.round = undefined; - vm.justPlayed = undefined; - vm.resultSent = false; - vm.lastFeedback = 'init'; - vm.initialPath = initialPath; - vm.initialNode = tree.nodeAtPath(initialPath); - vm.pov = vm.initialNode.ply % 2 == 1 ? 'black' : 'white'; - vm.isDaily = location.href.endsWith('/daily'); - - setPath(lichess.blindMode ? initialPath : treePath.init(initialPath)); + initiate = (fromData: PuzzleData): void => { + this.data = fromData; + this.tree = treeBuild(pgnToTree(this.data.game.pgn.split(' '))); + const initialPath = treePath.fromNodeList(treeOps.mainlineNodeList(this.tree.root)); + this.mode = 'play'; + this.next = defer(); + this.round = undefined; + this.justPlayed = undefined; + this.resultSent = false; + this.lastFeedback = 'init'; + this.initialPath = initialPath; + this.initialNode = this.tree.nodeAtPath(initialPath); + this.pov = this.initialNode.ply % 2 == 1 ? 'black' : 'white'; + this.isDaily = location.href.endsWith('/daily'); + + this.setPath(lichess.blindMode ? initialPath : treePath.init(initialPath)); setTimeout( () => { - jump(initialPath); - redraw(); + this.jump(initialPath); + this.redraw(); }, - opts.pref.animation.duration > 0 ? 500 : 0, + this.opts.pref.animation.duration > 0 ? 500 : 0, ); // just to delay button display - vm.canViewSolution = false; - if (!vm.canViewSolution) { - setTimeout( - () => { - vm.canViewSolution = true; - redraw(); - }, - rated() ? 4000 : 1000, - ); - } + setTimeout( + () => { + this.canViewSolution(true); + this.redraw(); + }, + this.rated() ? 4000 : 1000, + ); - withGround(g => { + this.withGround(g => { g.selectSquare(null); g.setAutoShapes([]); g.setShapes([]); - showGround(g); + this.showGround(g); }); - instanciateCeval(); - } + this.instanciateCeval(); + }; - function position(): Chess { - const setup = parseFen(vm.node.fen).unwrap(); + position = (): Chess => { + const setup = parseFen(this.node.fen).unwrap(); return Chess.fromSetup(setup).unwrap(); - } + }; - function makeCgOpts(): CgConfig { - const node = vm.node; + makeCgOpts = (): CgConfig => { + const node = this.node; const color: Color = node.ply % 2 === 0 ? 'white' : 'black'; - const dests = chessgroundDests(position()); - const nextNode = vm.node.children[0]; - const canMove = vm.mode === 'view' || (color === vm.pov && (!nextNode || nextNode.puzzle == 'fail')); + const dests = chessgroundDests(this.position()); + const nextNode = this.node.children[0]; + const canMove = this.mode === 'view' || (color === this.pov && (!nextNode || nextNode.puzzle == 'fail')); const movable = canMove ? { color: dests.size > 0 ? color : undefined, @@ -176,7 +224,7 @@ export default function (opts: PuzzleOpts, redraw: Redraw): Controller { }; const config = { fen: node.fen, - orientation: flipped ? opposite(vm.pov) : vm.pov, + orientation: this.flipped() ? opposite(this.pov) : this.pov, turnColor: color, movable: movable, premovable: { @@ -185,63 +233,56 @@ export default function (opts: PuzzleOpts, redraw: Redraw): Controller { check: !!node.check, lastMove: uciToMove(node.uci), }; - if (node.ply >= vm.initialNode.ply) { - if (vm.mode !== 'view' && color !== vm.pov && !nextNode) { - config.movable.color = vm.pov; + if (node.ply >= this.initialNode.ply) { + if (this.mode !== 'view' && color !== this.pov && !nextNode) { + config.movable.color = this.pov; config.premovable.enabled = true; } } - vm.cgConfig = config; + this.cgConfig = config; return config; - } + }; - function showGround(g: CgApi): void { - g.set(makeCgOpts()); - } + showGround = (g: CgApi): void => g.set(this.makeCgOpts()); - function auxMove(orig: Key, dest: Key, role?: Role) { - if (role) playUserMove(orig, dest, role); + auxMove = (orig: Key, dest: Key, role?: Role) => { + if (role) this.playUserMove(orig, dest, role); else - withGround(g => { + this.withGround(g => { g.move(orig, dest); g.state.movable.dests = undefined; g.state.turnColor = opposite(g.state.turnColor); }); - } + }; - function userMove(orig: Key, dest: Key): void { - vm.justPlayed = orig; - if (!promotion.start(orig, dest, { submit: playUserMove, show: voiceMove?.promotionHook() })) - playUserMove(orig, dest); - voiceMove?.update(vm.node.fen, true); - keyboardMove?.update({ fen: vm.node.fen }); - } + userMove = (orig: Key, dest: Key): void => { + this.justPlayed = orig; + if ( + !this.promotion.start(orig, dest, { submit: this.playUserMove, show: this.voiceMove?.promotionHook() }) + ) + this.playUserMove(orig, dest); + this.voiceMove?.update(this.node.fen, true); + this.keyboardMove?.update({ fen: this.node.fen }); + }; - function playUci(uci: Uci): void { - sendMove(parseUci(uci)!); - } + playUci = (uci: Uci): void => this.sendMove(parseUci(uci)!); - function playUciList(uciList: Uci[]): void { - uciList.forEach(playUci); - } + playUciList = (uciList: Uci[]): void => uciList.forEach(this.playUci); - function playUserMove(orig: Key, dest: Key, promotion?: Role): void { - sendMove({ + playUserMove = (orig: Key, dest: Key, promotion?: Role): void => + this.sendMove({ from: parseSquare(orig)!, to: parseSquare(dest)!, promotion, }); - } - function sendMove(move: Move): void { - sendMoveAt(vm.path, position(), move); - } + sendMove = (move: Move): void => this.sendMoveAt(this.path, this.position(), move); - function sendMoveAt(path: Tree.Path, pos: Chess, move: Move): void { + sendMoveAt = (path: Tree.Path, pos: Chess, move: Move): void => { move = normalizeMove(pos, move); const san = makeSanAndPlay(pos, move); const check = pos.isCheck() ? pos.board.kingOf(pos.turn) : undefined; - addNode( + this.addNode( { ply: 2 * (pos.fullmoves - 1) + (pos.turn == 'white' ? 0 : 1), fen: makeFen(pos.toSetup()), @@ -253,149 +294,149 @@ export default function (opts: PuzzleOpts, redraw: Redraw): Controller { }, path, ); - } + }; - function addNode(node: Tree.Node, path: Tree.Path): void { - const newPath = tree.addNode(node, path)!; - jump(newPath); - withGround(g => g.playPremove()); + addNode = (node: Tree.Node, path: Tree.Path): void => { + const newPath = this.tree.addNode(node, path)!; + this.jump(newPath); + this.withGround(g => g.playPremove()); - const progress = moveTest(vm, data.puzzle); + const progress = moveTest(this); if (progress === 'fail') lichess.sound.say('incorrect'); - if (progress) applyProgress(progress); - reorderChildren(path); - redraw(); - } + if (progress) this.applyProgress(progress); + this.reorderChildren(path); + this.redraw(); + }; - function reorderChildren(path: Tree.Path, recursive?: boolean): void { - const node = tree.nodeAtPath(path); + reorderChildren = (path: Tree.Path, recursive?: boolean): void => { + const node = this.tree.nodeAtPath(path); node.children.sort((c1, _) => { const p = c1.puzzle; if (p == 'fail') return 1; if (p == 'good' || p == 'win') return -1; return 0; }); - if (recursive) node.children.forEach(child => reorderChildren(path + child.id, true)); - } + if (recursive) node.children.forEach(child => this.reorderChildren(path + child.id, true)); + }; - function instantRevertUserMove(): void { - withGround(g => { + private instantRevertUserMove = (): void => { + this.withGround(g => { g.cancelPremove(); g.selectSquare(null); }); - jump(treePath.init(vm.path)); - redraw(); - } + this.jump(treePath.init(this.path)); + this.redraw(); + }; - function revertUserMove(): void { - if (lichess.blindMode) instantRevertUserMove(); - else setTimeout(instantRevertUserMove, 100); - } + revertUserMove = (): void => { + if (lichess.blindMode) this.instantRevertUserMove(); + else setTimeout(this.instantRevertUserMove, 100); + }; - function applyProgress(progress: undefined | 'fail' | 'win' | MoveTest): void { + applyProgress = (progress: undefined | 'fail' | 'win' | MoveTest): void => { if (progress === 'fail') { - vm.lastFeedback = 'fail'; - revertUserMove(); - if (vm.mode === 'play') { - if (streak) { - failStreak(streak); - streakFailStorage.fire(); + this.lastFeedback = 'fail'; + this.revertUserMove(); + if (this.mode === 'play') { + if (this.streak) { + this.failStreak(this.streak); + this.streakFailStorage.fire(); } else { - vm.canViewSolution = true; - vm.mode = 'try'; - sendResult(false); + this.canViewSolution(true); + this.mode = 'try'; + this.sendResult(false); } } } else if (progress == 'win') { - if (streak) sound.good(); - vm.lastFeedback = 'win'; - if (vm.mode != 'view') { - const sent = vm.mode == 'play' ? sendResult(true) : Promise.resolve(); - vm.mode = 'view'; - withGround(showGround); - sent.then(_ => (autoNext() ? nextPuzzle() : startCeval())); + if (this.streak) this.sound.good(); + this.lastFeedback = 'win'; + if (this.mode != 'view') { + const sent = this.mode == 'play' ? this.sendResult(true) : Promise.resolve(); + this.mode = 'view'; + this.withGround(this.showGround); + sent.then(_ => (this.autoNext() ? this.nextPuzzle() : this.startCeval())); } } else if (progress) { - vm.lastFeedback = 'good'; + this.lastFeedback = 'good'; setTimeout( () => { const pos = Chess.fromSetup(parseFen(progress.fen).unwrap()).unwrap(); - sendMoveAt(progress.path, pos, progress.move); + this.sendMoveAt(progress.path, pos, progress.move); }, - opts.pref.animation.duration * (autoNext() ? 1 : 1.5), + this.opts.pref.animation.duration * (this.autoNext() ? 1 : 1.5), ); } - } + }; - function failStreak(streak: PuzzleStreak): void { - vm.mode = 'view'; + failStreak = (streak: PuzzleStreak): void => { + this.mode = 'view'; streak.onComplete(false); - setTimeout(viewSolution, 500); - sound.end(); - } + setTimeout(this.viewSolution, 500); + this.sound.end(); + }; - async function sendResult(win: boolean): Promise { - if (vm.resultSent) return Promise.resolve(); - vm.resultSent = true; - session.complete(data.puzzle.id, win); + sendResult = async (win: boolean): Promise => { + if (this.resultSent) return Promise.resolve(); + this.resultSent = true; + this.session.complete(this.data.puzzle.id, win); const res = await xhr.complete( - data.puzzle.id, - data.angle.key, + this.data.puzzle.id, + this.data.angle.key, win, - rated, - data.replay, - streak, - opts.settings.color, + this.rated, + this.data.replay, + this.streak, + this.opts.settings.color, ); const next = res.next; - if (next?.user && data.user) { - data.user.rating = next.user.rating; - data.user.provisional = next.user.provisional; - vm.round = res.round; - if (res.round?.ratingDiff) session.setRatingDiff(data.puzzle.id, res.round.ratingDiff); + if (next?.user && this.data.user) { + this.data.user.rating = next.user.rating; + this.data.user.provisional = next.user.provisional; + this.round = res.round; + if (res.round?.ratingDiff) this.session.setRatingDiff(this.data.puzzle.id, res.round.ratingDiff); } if (win) lichess.sound.say('Success!'); if (next) { - vm.next.resolve(data.replay && res.replayComplete ? data.replay : next); - if (streak && win) streak.onComplete(true, res.next); + this.next.resolve(this.data.replay && res.replayComplete ? this.data.replay : next); + if (this.streak && win) this.streak.onComplete(true, res.next); } - redraw(); + this.redraw(); if (!next) { - if (!data.replay) { + if (!this.data.replay) { alert('No more puzzles available! Try another theme.'); lichess.redirect('/training/themes'); } } - } + }; - const isPuzzleData = (d: PuzzleData | ReplayEnd): d is PuzzleData => 'puzzle' in d; + private isPuzzleData = (d: PuzzleData | ReplayEnd): d is PuzzleData => 'puzzle' in d; - function nextPuzzle(): void { - if (streak && vm.lastFeedback != 'win') return; - if (vm.mode !== 'view') return; + nextPuzzle = (): void => { + if (this.streak && this.lastFeedback != 'win') return; + if (this.mode !== 'view') return; - ceval.stop(); - vm.next.promise.then(n => { - if (isPuzzleData(n)) { - initiate(n); - redraw(); + this.ceval.stop(); + this.next.promise.then(n => { + if (this.isPuzzleData(n)) { + this.initiate(n); + this.redraw(); } }); - if (data.replay && vm.round === undefined) { - lichess.redirect(`/training/dashboard/${data.replay.days}`); + if (this.data.replay && this.round === undefined) { + lichess.redirect(`/training/dashboard/${this.data.replay.days}`); } - if (!streak && !data.replay) { - const path = router.withLang(`/training/${data.angle.key}`); + if (!this.streak && !this.data.replay) { + const path = router.withLang(`/training/${this.data.angle.key}`); if (location.pathname != path) history.replaceState(null, '', path); } - } + }; - function instanciateCeval(): void { - if (ceval) ceval.destroy(); - ceval = new CevalCtrl({ - redraw, + instanciateCeval = (): void => { + this.ceval?.destroy(); + this.ceval = new CevalCtrl({ + redraw: this.redraw, variant: { short: 'Std', name: 'Standard', @@ -403,285 +444,184 @@ export default function (opts: PuzzleOpts, redraw: Redraw): Controller { }, initialFen: undefined, // always standard starting position possible: true, - emit: function (ev, work) { - tree.updateAt(work.path, function (node) { + emit: (ev, work) => { + this.tree.updateAt(work.path, node => { if (work.threatMode) { const threat = ev as Tree.LocalEval; if (!node.threat || node.threat.depth <= threat.depth) node.threat = threat; } else if (!node.ceval || node.ceval.depth <= ev.depth) node.ceval = ev; - if (work.path === vm.path) { - setAutoShapes(); - redraw(); + if (work.path === this.path) { + this.setAutoShapes(); + this.redraw(); } }); }, - setAutoShapes: setAutoShapes, + setAutoShapes: this.setAutoShapes, }); - } + }; - function setAutoShapes(): void { - withGround(g => { + setAutoShapes = (): void => + this.withGround(g => g.setAutoShapes( computeAutoShapes({ - vm: vm, - ceval: ceval, + ...this, ground: g, - threatMode: threatMode(), - nextNodeBest: nextNodeBest(), + node: this.node, }), - ); - }); - } + ), + ); - function canUseCeval(): boolean { - return vm.mode === 'view' && !outcome(); - } + canUseCeval = (): boolean => this.mode === 'view' && !this.outcome(); - function startCeval(): void { - if (ceval.enabled() && canUseCeval()) doStartCeval(); - } - - const doStartCeval = throttle(800, function () { - ceval.start(vm.path, vm.nodeList, threatMode()); - }); + startCeval = (): void => { + if (this.ceval.enabled() && this.canUseCeval()) this.doStartCeval(); + }; - const nextNodeBest = () => treeOps.withMainlineChild(vm.node, n => n.eval?.best); + private doStartCeval = throttle(800, () => this.ceval.start(this.path, this.nodeList, this.threatMode())); - const getCeval = () => ceval; + nextNodeBest = () => treeOps.withMainlineChild(this.node, n => n.eval?.best); - function toggleCeval(): void { - ceval.toggle(); - setAutoShapes(); - startCeval(); - if (!ceval.enabled()) threatMode(false); - vm.autoScrollRequested = true; - redraw(); - } + toggleCeval = (): void => { + this.ceval.toggle(); + this.setAutoShapes(); + this.startCeval(); + if (!this.ceval.enabled()) this.threatMode(false); + this.autoScrollRequested = true; + this.redraw(); + }; - function restartCeval(): void { - ceval.stop(); - startCeval(); - redraw(); - } + restartCeval = (): void => { + this.ceval.stop(); + this.startCeval(); + this.redraw(); + }; - function toggleThreatMode(): void { - if (vm.node.check) return; - if (!ceval.enabled()) ceval.toggle(); - if (!ceval.enabled()) return; - threatMode(!threatMode()); - setAutoShapes(); - startCeval(); - redraw(); - } + toggleThreatMode = (): void => { + if (this.node.check) return; + if (!this.ceval.enabled()) this.ceval.toggle(); + if (!this.ceval.enabled()) return; + this.threatMode.toggle(); + this.setAutoShapes(); + this.startCeval(); + this.redraw(); + }; - function outcome(): Outcome | undefined { - return position().outcome(); - } + outcome = (): Outcome | undefined => this.position().outcome(); - function jump(path: Tree.Path): void { - const pathChanged = path !== vm.path, - isForwardStep = pathChanged && path.length === vm.path.length + 2; - setPath(path); - withGround(showGround); + jump = (path: Tree.Path): void => { + const pathChanged = path !== this.path, + isForwardStep = pathChanged && path.length === this.path.length + 2; + this.setPath(path); + this.withGround(this.showGround); if (pathChanged) { if (isForwardStep) { - lichess.sound.saySan(vm.node.san); - lichess.sound.move(vm.node); + lichess.sound.saySan(this.node.san); + lichess.sound.move(this.node); } - threatMode(false); - ceval.stop(); - startCeval(); + this.threatMode(false); + this.ceval.stop(); + this.startCeval(); } - promotion.cancel(); - vm.justPlayed = undefined; - vm.autoScrollRequested = true; - keyboardMove?.update({ fen: vm.node.fen }); - voiceMove?.update(vm.node.fen, true); - lichess.pubsub.emit('ply', vm.node.ply); - } + this.promotion.cancel(); + this.justPlayed = undefined; + this.autoScrollRequested = true; + this.keyboardMove?.update({ fen: this.node.fen }); + this.voiceMove?.update(this.node.fen, true); + lichess.pubsub.emit('ply', this.node.ply); + }; - function userJump(path: Tree.Path): void { - if (tree.nodeAtPath(path)?.puzzle == 'fail' && vm.mode != 'view') return; - withGround(g => g.selectSquare(null)); - jump(path); - } + userJump = (path: Tree.Path): void => { + if (this.tree.nodeAtPath(path)?.puzzle == 'fail' && this.mode != 'view') return; + this.withGround(g => g.selectSquare(null)); + this.jump(path); + }; - function userJumpPlyDelta(plyDelta: Ply) { + userJumpPlyDelta = (plyDelta: Ply) => { // ensure we are jumping to a valid ply - let maxValidPly = vm.mainline.length - 1; - if (last(vm.mainline)?.puzzle == 'fail' && vm.mode != 'view') maxValidPly -= 1; - const newPly = Math.min(Math.max(vm.node.ply + plyDelta, 0), maxValidPly); - userJump(fromNodeList(vm.mainline.slice(0, newPly + 1))); - } + let maxValidPly = this.mainline.length - 1; + if (last(this.mainline)?.puzzle == 'fail' && this.mode != 'view') maxValidPly -= 1; + const newPly = Math.min(Math.max(this.node.ply + plyDelta, 0), maxValidPly); + this.userJump(fromNodeList(this.mainline.slice(0, newPly + 1))); + }; - function viewSolution(): void { - sendResult(false); - vm.mode = 'view'; - mergeSolution(tree, vm.initialPath, data.puzzle.solution, vm.pov); - reorderChildren(vm.initialPath, true); + viewSolution = (): void => { + this.sendResult(false); + this.mode = 'view'; + mergeSolution(this.tree, this.initialPath, this.data.puzzle.solution, this.pov); + this.reorderChildren(this.initialPath, true); // try to play the solution next move - const next = vm.node.children[0]; - if (next && next.puzzle === 'good') userJump(vm.path + next.id); + const next = this.node.children[0]; + if (next && next.puzzle === 'good') this.userJump(this.path + next.id); else { - const firstGoodPath = treeOps.takePathWhile(vm.mainline, node => node.puzzle != 'good'); - if (firstGoodPath) userJump(firstGoodPath + tree.nodeAtPath(firstGoodPath).children[0].id); + const firstGoodPath = treeOps.takePathWhile(this.mainline, node => node.puzzle != 'good'); + if (firstGoodPath) this.userJump(firstGoodPath + this.tree.nodeAtPath(firstGoodPath).children[0].id); } - vm.autoScrollRequested = true; - vm.voteDisabled = true; - redraw(); - startCeval(); + this.autoScrollRequested = true; + this.voteDisabled = true; + this.redraw(); + this.startCeval(); setTimeout(() => { - vm.voteDisabled = false; - redraw(); + this.voteDisabled = false; + this.redraw(); }, 500); - } + }; - const skip = () => { - if (!streak || !streak.data.skip || vm.mode != 'play') return; - streak.skip(); - userJump(treePath.fromNodeList(vm.mainline)); - const moveIndex = treePath.size(vm.path) - treePath.size(vm.initialPath); - const solution = data.puzzle.solution[moveIndex]; - playUci(solution); - playBestMove(); + skip = () => { + if (!this.streak || !this.streak.data.skip || this.mode != 'play') return; + this.streak.skip(); + this.userJump(treePath.fromNodeList(this.mainline)); + const moveIndex = treePath.size(this.path) - treePath.size(this.initialPath); + const solution = this.data.puzzle.solution[moveIndex]; + this.playUci(solution); + this.playBestMove(); }; - const flip = () => { - flipped = !flipped; - withGround(g => g.toggleOrientation()); - redraw(); + flip = () => { + this.flipped.toggle(); + this.withGround(g => g.toggleOrientation()); + this.redraw(); }; - const vote = (v: boolean) => { - if (!vm.voteDisabled) { - xhr.vote(data.puzzle.id, v); - nextPuzzle(); + vote = (v: boolean) => { + if (!this.voteDisabled) { + xhr.vote(this.data.puzzle.id, v); + this.nextPuzzle(); } }; - const voteTheme = (theme: ThemeKey, v: boolean) => { - if (vm.round) { - vm.round.themes = vm.round.themes || {}; - if (v === vm.round.themes[theme]) { - delete vm.round.themes[theme]; - xhr.voteTheme(data.puzzle.id, theme, undefined); + voteTheme = (theme: ThemeKey, v: boolean) => { + if (this.round) { + this.round.themes = this.round.themes || {}; + if (v === this.round.themes[theme]) { + delete this.round.themes[theme]; + xhr.voteTheme(this.data.puzzle.id, theme, undefined); } else { - if (v || data.puzzle.themes.includes(theme)) vm.round.themes[theme] = v; - else delete vm.round.themes[theme]; - xhr.voteTheme(data.puzzle.id, theme, v); + if (v || this.data.puzzle.themes.includes(theme)) this.round.themes[theme] = v; + else delete this.round.themes[theme]; + xhr.voteTheme(this.data.puzzle.id, theme, v); } - redraw(); + this.redraw(); } }; - initiate(opts.data); - - const promotion = new PromotionCtrl(withGround, () => withGround(g => g.set(vm.cgConfig)), redraw); - - function playBestMove(): void { - const uci = nextNodeBest() || (vm.node.ceval && vm.node.ceval.pvs[0].moves[0]); - if (uci) playUci(uci); - } - - const keyboardHelp = propWithEffect(location.hash === '#keyboard', redraw); - keyboard({ - vm, - userJump, - getCeval, - toggleCeval, - toggleThreatMode, - redraw, - playBestMove, - flip, - flipped: () => flipped, - nextPuzzle, - keyboardHelp, - }); - - // If the page loads while being hidden (like when changing settings), - // chessground is not displayed, and the first move is not fully applied. - // Make sure chessground is fully shown when the page goes back to being visible. - document.addEventListener('visibilitychange', () => lichess.requestIdleCallback(() => jump(vm.path), 500)); - - lichess.pubsub.on('zen', () => { - const zen = $('body').toggleClass('zen').hasClass('zen'); - window.dispatchEvent(new Event('resize')); - if (!$('body').hasClass('zen-auto')) { - xhr.setZen(zen); - } - }); - $('body').addClass('playing'); // for zen - $('#zentog').on('click', () => lichess.pubsub.emit('zen')); - - return { - vm, - getData() { - return data; - }, - getTree() { - return tree; - }, - setChessground, - ground, - makeCgOpts, - voiceMove, - keyboardMove, - keyboardHelp, - userJump, - viewSolution, - nextPuzzle, - vote, - voteTheme, - getCeval, - pref: opts.pref, - settings: opts.settings, - trans: lichess.trans(opts.i18n), - autoNext, - autoNexting: () => vm.lastFeedback == 'win' && autoNext(), - rated, - toggleRated: () => { - rated(!rated()); - redraw(); - }, - outcome, - toggleCeval, - toggleThreatMode, - threatMode, - currentEvals() { - return { client: vm.node.ceval }; - }, - nextNodeBest, - userMove, - playUci, - playUciList, - showEvalGauge() { - return vm.showComputer() && ceval.enabled() && !outcome(); - }, - getOrientation() { - return withGround(g => g.state.orientation)!; - }, - getNode() { - return vm.node; - }, - showComputer: vm.showComputer, - promotion, - redraw, - ongoing: false, - playBestMove, - session, - allThemes: opts.themes && { - dynamic: opts.themes.dynamic.split(' '), - static: new Set(opts.themes.static.split(' ')), - }, - streak, - skip, - flip, - flipped: () => flipped, - showRatings: opts.showRatings, - menu, - restartCeval: restartCeval, - clearCeval: restartCeval, + playBestMove = (): void => { + const uci = this.nextNodeBest() || (this.node.ceval && this.node.ceval.pvs[0].moves[0]); + if (uci) this.playUci(uci); + }; + autoNexting = () => this.lastFeedback == 'win' && this.autoNext(); + currentEvals = () => ({ client: this.node.ceval }); + showEvalGauge = () => this.showComputer() && this.ceval.enabled() && !this.outcome(); + getOrientation = () => this.withGround(g => g.state.orientation)!; + allThemes = this.opts.themes && { + dynamic: this.opts.themes.dynamic.split(' '), + static: new Set(this.opts.themes.static.split(' ')), }; + toggleRated = () => this.rated(!this.rated()); + // implement cetal ParentCtrl: + getCeval = () => this.ceval; + ongoing = false; + getNode = () => this.node; + showComputer = () => this.mode === 'view'; } diff --git a/ui/puzzle/src/dashboard.ts b/ui/puzzle/src/dashboard.ts index 6154377dc33de..da5d7e7e41a28 100644 --- a/ui/puzzle/src/dashboard.ts +++ b/ui/puzzle/src/dashboard.ts @@ -29,7 +29,7 @@ export function initModule(data: RadarData) { ...{ backgroundColor: 'rgba(189,130,35,0.2)', borderColor: 'rgba(189,130,35,1)', - pointBackgroundColor: 'rgb(189,130,35,1)', + pointBackgroundColor: 'rgba(189,130,35,1)', }, }; const fontColor = currentTheme() === 'dark' ? '#bababa' : '#4d4d4d'; @@ -47,6 +47,9 @@ export function initModule(data: RadarData) { ticks: { color: fontColor, showLabelBackdrop: false, // hide square behind text + format: { + useGrouping: false, + }, }, pointLabels: { color: fontColor, diff --git a/ui/puzzle/src/interfaces.ts b/ui/puzzle/src/interfaces.ts index 325e43912e555..44d709e5a3656 100644 --- a/ui/puzzle/src/interfaces.ts +++ b/ui/puzzle/src/interfaces.ts @@ -1,123 +1,23 @@ -import PuzzleSession from './session'; -import { Api as CgApi } from 'chessground/api'; -import { CevalCtrl, NodeEvals } from 'ceval'; -import { Config as CgConfig } from 'chessground/config'; -import { Deferred } from 'common/defer'; -import { Outcome, Move } from 'chessops/types'; -import { Prop, Toggle } from 'common'; -import { StoredProp } from 'common/storage'; -import { TreeWrapper } from 'tree'; +import { Move } from 'chessops/types'; import { VNode } from 'snabbdom'; -import PuzzleStreak from './streak'; -import { PromotionCtrl } from 'chess/promotion'; -import { KeyboardMove } from 'keyboardMove'; -import { VoiceMove } from 'voice'; import * as Prefs from 'common/prefs'; import perfIcons from 'common/perfIcons'; -import { Redraw } from 'common/snabbdom'; +import PuzzleCtrl from './ctrl'; export type PuzzleId = string; -export interface KeyboardController { - vm: Vm; - redraw: Redraw; - userJump(path: Tree.Path): void; - getCeval(): CevalCtrl; - toggleCeval(): void; - toggleThreatMode(): void; - playBestMove(): void; - flip(): void; - flipped(): boolean; - nextPuzzle(): void; - keyboardHelp: Prop; -} - export type ThemeKey = string; export interface AllThemes { dynamic: ThemeKey[]; static: Set; } -export interface Controller extends KeyboardController { - nextNodeBest(): string | undefined; - disableThreatMode?: Prop; - outcome(): Outcome | undefined; - mandatoryCeval?: Prop; - showEvalGauge: Prop; - currentEvals(): NodeEvals; - ongoing: boolean; - playUci(uci: string): void; - playUciList(uciList: string[]): void; - getOrientation(): Color; - threatMode: Prop; - getNode(): Tree.Node; - showComputer(): boolean; - trans: Trans; - getData(): PuzzleData; - getTree(): TreeWrapper; - ground: Prop; - setChessground(cg: CgApi): void; - makeCgOpts(): CgConfig; - viewSolution(): void; - nextPuzzle(): void; - vote(v: boolean): void; - voteTheme(theme: ThemeKey, v: boolean): void; - pref: PuzzlePrefs; - settings: PuzzleSettings; - userMove(orig: Key, dest: Key): void; - promotion: PromotionCtrl; - autoNext: StoredProp; - autoNexting: () => boolean; - rated: StoredProp; - toggleRated: () => void; - session: PuzzleSession; - allThemes?: AllThemes; - showRatings: boolean; - keyboardMove?: KeyboardMove; - voiceMove?: VoiceMove; - - streak?: PuzzleStreak; - skip(): void; - - path?: Tree.Path; - autoScrollRequested?: boolean; - - nvui?: NvuiPlugin; - menu: Toggle; - restartCeval(): void; - clearCeval(): void; -} - export interface NvuiPlugin { - render(ctrl: Controller): VNode; + render(ctrl: PuzzleCtrl): VNode; } export type ReplayEnd = PuzzleReplay; -export interface Vm { - path: Tree.Path; - nodeList: Tree.Node[]; - node: Tree.Node; - mainline: Tree.Node[]; - pov: Color; - mode: 'play' | 'view' | 'try'; - round?: PuzzleRound; - next: Deferred; - justPlayed?: Key; - resultSent: boolean; - lastFeedback: 'init' | 'fail' | 'win' | 'good' | 'retry'; - initialPath: Tree.Path; - initialNode: Tree.Node; - canViewSolution: boolean; - autoScrollRequested: boolean; - autoScrollNow: boolean; - voteDisabled?: boolean; - cgConfig: CgConfig; - showComputer(): boolean; - showAutoShapes(): boolean; - isDaily: boolean; -} - export type PuzzleDifficulty = 'easiest' | 'easier' | 'normal' | 'harder' | 'hardest'; export interface PuzzleSettings { diff --git a/ui/puzzle/src/keyboard.ts b/ui/puzzle/src/keyboard.ts index 59ec666fd01da..6c9b8187e9274 100644 --- a/ui/puzzle/src/keyboard.ts +++ b/ui/puzzle/src/keyboard.ts @@ -1,8 +1,8 @@ import * as control from './control'; -import { Controller, KeyboardController } from './interfaces'; +import PuzzleCtrl from './ctrl'; import { snabDialog } from 'common/dialog'; -export default (ctrl: KeyboardController) => +export default (ctrl: PuzzleCtrl) => lichess.mousetrap .bind(['left', 'k'], () => { control.prev(ctrl); @@ -23,8 +23,8 @@ export default (ctrl: KeyboardController) => .bind('l', ctrl.toggleCeval) .bind('x', ctrl.toggleThreatMode) .bind('space', () => { - if (ctrl.vm.mode === 'view') { - if (ctrl.getCeval().enabled()) ctrl.playBestMove(); + if (ctrl.mode === 'view') { + if (ctrl.ceval.enabled()) ctrl.playBestMove(); else ctrl.toggleCeval(); } }) @@ -33,7 +33,7 @@ export default (ctrl: KeyboardController) => .bind('f', ctrl.flip) .bind('n', ctrl.nextPuzzle); -export const view = (ctrl: Controller) => +export const view = (ctrl: PuzzleCtrl) => snabDialog({ class: 'help', htmlUrl: '/training/help', diff --git a/ui/puzzle/src/main.ts b/ui/puzzle/src/main.ts index 1e81ae8123d22..f439c2d7cdecd 100644 --- a/ui/puzzle/src/main.ts +++ b/ui/puzzle/src/main.ts @@ -1,5 +1,5 @@ import { attributesModule, classModule, init } from 'snabbdom'; -import makeCtrl from './ctrl'; +import PuzzleCtrl from './ctrl'; import menuHover from 'common/menuHover'; import view from './view/main'; import { PuzzleOpts, NvuiPlugin } from './interfaces'; @@ -8,8 +8,8 @@ const patch = init([classModule, attributesModule]); export async function initModule(opts: PuzzleOpts) { const element = document.querySelector('main.puzzle') as HTMLElement; - const ctrl = makeCtrl(opts, redraw); - ctrl.nvui = lichess.blindMode ? await lichess.loadEsm('puzzle.nvui') : undefined; + const nvui = lichess.blindMode ? await lichess.asset.loadEsm('puzzle.nvui') : undefined; + const ctrl = new PuzzleCtrl(opts, redraw, nvui); const blueprint = view(ctrl); element.innerHTML = ''; diff --git a/ui/puzzle/src/moveTest.ts b/ui/puzzle/src/moveTest.ts index 0dee8f4ad1888..282a509832b7d 100644 --- a/ui/puzzle/src/moveTest.ts +++ b/ui/puzzle/src/moveTest.ts @@ -1,7 +1,8 @@ import { altCastles } from 'chess'; import { parseUci } from 'chessops/util'; import { path as pathOps } from 'tree'; -import { Vm, Puzzle, MoveTest } from './interfaces'; +import { MoveTest } from './interfaces'; +import PuzzleCtrl from './ctrl'; type MoveTestReturn = undefined | 'fail' | 'win' | MoveTest; @@ -11,36 +12,36 @@ function isAltCastle(str: string): str is AltCastle { return str in altCastles; } -export default function moveTest(vm: Vm, puzzle: Puzzle): MoveTestReturn { - if (vm.mode === 'view') return; - if (!pathOps.contains(vm.path, vm.initialPath)) return; +export default function moveTest(ctrl: PuzzleCtrl): MoveTestReturn { + if (ctrl.mode === 'view') return; + if (!pathOps.contains(ctrl.path, ctrl.initialPath)) return; - const playedByColor = vm.node.ply % 2 === 1 ? 'white' : 'black'; - if (playedByColor !== vm.pov) return; + const playedByColor = ctrl.node.ply % 2 === 1 ? 'white' : 'black'; + if (playedByColor !== ctrl.pov) return; - const nodes = vm.nodeList.slice(pathOps.size(vm.initialPath) + 1).map(node => ({ + const nodes = ctrl.nodeList.slice(pathOps.size(ctrl.initialPath) + 1).map(node => ({ uci: node.uci, castle: node.san!.startsWith('O-O'), checkmate: node.san!.endsWith('#'), })); for (const i in nodes) { - if (nodes[i].checkmate) return (vm.node.puzzle = 'win'); + if (nodes[i].checkmate) return (ctrl.node.puzzle = 'win'); const uci = nodes[i].uci!, - solUci = puzzle.solution[i]; + solUci = ctrl.data.puzzle.solution[i]; if (uci != solUci && (!nodes[i].castle || !isAltCastle(uci) || altCastles[uci] != solUci)) - return (vm.node.puzzle = 'fail'); + return (ctrl.node.puzzle = 'fail'); } - const nextUci = puzzle.solution[nodes.length]; - if (!nextUci) return (vm.node.puzzle = 'win'); + const nextUci = ctrl.data.puzzle.solution[nodes.length]; + if (!nextUci) return (ctrl.node.puzzle = 'win'); // from here we have a next move - vm.node.puzzle = 'good'; + ctrl.node.puzzle = 'good'; return { move: parseUci(nextUci)!, - fen: vm.node.fen, - path: vm.path, + fen: ctrl.node.fen, + path: ctrl.path, }; } diff --git a/ui/puzzle/src/plugins/nvui.ts b/ui/puzzle/src/plugins/nvui.ts index 468788d4da605..2b74fa80afa62 100644 --- a/ui/puzzle/src/plugins/nvui.ts +++ b/ui/puzzle/src/plugins/nvui.ts @@ -1,5 +1,4 @@ import { h, VNode } from 'snabbdom'; -import { Controller } from '../interfaces'; import { puzzleBox, renderDifficultyForm, userBox } from '../view/side'; import theme from '../view/theme'; import { @@ -31,6 +30,7 @@ import * as control from '../control'; import { bind, onInsert } from 'common/snabbdom'; import { Api } from 'chessground/api'; import throttle from 'common/throttle'; +import PuzzleCtrl from '../ctrl'; const throttled = (sound: string) => throttle(100, () => lichess.sound.play(sound)); const selectSound = throttled('select'); @@ -45,7 +45,7 @@ export function initModule() { positionStyle = positionSetting(), boardStyle = boardSetting(); return { - render(ctrl: Controller): VNode { + render(ctrl: PuzzleCtrl): VNode { notify.redraw = ctrl.redraw; const ground = ctrl.ground() || @@ -58,11 +58,11 @@ export function initModule() { ctrl.ground(ground); return h( - `main.puzzle.puzzle--nvui.puzzle-${ctrl.getData().replay ? 'replay' : 'play'}${ + `main.puzzle.puzzle--nvui.puzzle-${ctrl.data.replay ? 'replay' : 'play'}${ ctrl.streak ? '.puzzle--streak' : '' }`, h('div.nvui', [ - h('h1', `Puzzle: ${ctrl.vm.pov} to play.`), + h('h1', `Puzzle: ${ctrl.pov} to play.`), h('h2', 'Puzzle info'), puzzleBox(ctrl), theme(ctrl), @@ -70,26 +70,15 @@ export function initModule() { h('h2', 'Moves'), h( 'p.moves', - { - attrs: { - role: 'log', - 'aria-live': 'off', - }, - }, - renderMainline(ctrl.vm.mainline, ctrl.vm.path, moveStyle.get()), + { attrs: { role: 'log', 'aria-live': 'off' } }, + renderMainline(ctrl.mainline, ctrl.path, moveStyle.get()), ), h('h2', 'Pieces'), h('div.pieces', renderPieces(ground.state.pieces, moveStyle.get())), h('h2', 'Puzzle status'), h( 'div.status', - { - attrs: { - role: 'status', - 'aria-live': 'polite', - 'aria-atomic': 'true', - }, - }, + { attrs: { role: 'status', 'aria-live': 'polite', 'aria-atomic': 'true' } }, renderStatus(ctrl), ), h('div.replay', renderReplay(ctrl)), @@ -97,12 +86,7 @@ export function initModule() { h('h2', 'Last move'), h( 'p.lastMove', - { - attrs: { - 'aria-live': 'assertive', - 'aria-atomic': 'true', - }, - }, + { attrs: { 'aria-live': 'assertive', 'aria-atomic': 'true' } }, lastMove(ctrl, moveStyle.get()), ), h('h2', 'Move form'), @@ -118,21 +102,16 @@ export function initModule() { }, [ h('label', [ - ctrl.vm.mode === 'view' ? 'Command input' : `Find the best move for ${ctrl.vm.pov}.`, + ctrl.mode === 'view' ? 'Command input' : `Find the best move for ${ctrl.pov}.`, h('input.move.mousetrap', { - attrs: { - name: 'move', - type: 'text', - autocomplete: 'off', - autofocus: true, - }, + attrs: { name: 'move', type: 'text', autocomplete: 'off', autofocus: true }, }), ]), ], ), notify.render(), h('h2', 'Actions'), - ctrl.vm.mode === 'view' ? afterActions(ctrl) : playActions(ctrl), + ctrl.mode === 'view' ? afterActions(ctrl) : playActions(ctrl), h('h2', 'Board'), h( 'div.board', @@ -140,15 +119,15 @@ export function initModule() { hook: onInsert(el => { const $board = $(el); const $buttons = $board.find('button'); - const steps = () => ctrl.getTree().getNodeList(ctrl.vm.path); + const steps = () => ctrl.tree.getNodeList(ctrl.path); const uciSteps = () => steps().filter(hasUci); const fenSteps = () => steps().map(step => step.fen); - const opponentColor = ctrl.vm.pov === 'white' ? 'black' : 'white'; + const opponentColor = ctrl.pov === 'white' ? 'black' : 'white'; $board.on( 'click', selectionHandler(() => opponentColor, selectSound), ); - $board.on('keydown', arrowKeyHandler(ctrl.vm.pov, borderSound)); + $board.on('keydown', arrowKeyHandler(ctrl.pov, borderSound)); $board.on('keypress', boardCommandsHandler()); $buttons.on( 'keypress', @@ -157,7 +136,7 @@ export function initModule() { $buttons.on( 'keypress', possibleMovesHandler( - ctrl.vm.pov, + ctrl.pov, () => ground.state.turnColor, ground.getFen, () => ground.state.pieces, @@ -172,23 +151,14 @@ export function initModule() { }, renderBoard( ground.state.pieces, - ctrl.vm.pov, + ctrl.pov, pieceStyle.get(), prefixStyle.get(), positionStyle.get(), boardStyle.get(), ), ), - h( - 'div.boardstatus', - { - attrs: { - 'aria-live': 'polite', - 'aria-atomic': 'true', - }, - }, - '', - ), + h('div.boardstatus', { attrs: { 'aria-live': 'polite', 'aria-atomic': 'true' } }, ''), h('h2', 'Settings'), h('label', ['Move notation', renderSetting(moveStyle, ctrl.redraw)]), h('h3', 'Board settings'), @@ -196,7 +166,7 @@ export function initModule() { h('label', ['Piece prefix style', renderSetting(prefixStyle, ctrl.redraw)]), h('label', ['Show position', renderSetting(positionStyle, ctrl.redraw)]), h('label', ['Board layout', renderSetting(boardStyle, ctrl.redraw)]), - ...(!ctrl.getData().replay && !ctrl.streak + ...(!ctrl.data.replay && !ctrl.streak ? [h('h3', 'Puzzle Settings'), renderDifficultyForm(ctrl)] : []), h('h2', 'Keyboard shortcuts'), @@ -263,15 +233,15 @@ function hasUci(step: Tree.Node): step is StepWithUci { return step.uci !== undefined; } -function lastMove(ctrl: Controller, style: Style): string { - const node = ctrl.vm.node; +function lastMove(ctrl: PuzzleCtrl, style: Style): string { + const node = ctrl.node; if (node.ply === 0) return 'Initial position'; // make sure consecutive moves are different so that they get re-read return renderSan(node.san || '', node.uci, style) + (node.ply % 2 === 0 ? '' : ' '); } function onSubmit( - ctrl: Controller, + ctrl: PuzzleCtrl, notify: (txt: string) => void, style: () => Style, $input: Cash, @@ -282,10 +252,10 @@ function onSubmit( if (isShortCommand(input)) input = '/' + input; if (input[0] === '/') onCommand(ctrl, notify, input.slice(1), style()); else { - const uci = inputToLegalUci(input, ctrl.vm.node.fen, ground); + const uci = inputToLegalUci(input, ctrl.node.fen, ground); if (uci) { ctrl.playUci(uci); - switch (ctrl.vm.lastFeedback) { + switch (ctrl.lastFeedback) { case 'fail': notify(ctrl.trans.noarg('notTheMove')); break; @@ -304,12 +274,12 @@ function onSubmit( }; } -function isYourMove(ctrl: Controller) { - return ctrl.vm.node.children.length === 0 || ctrl.vm.node.children[0].puzzle === 'fail'; +function isYourMove(ctrl: PuzzleCtrl) { + return ctrl.node.children.length === 0 || ctrl.node.children[0].puzzle === 'fail'; } -function browseHint(ctrl: Controller): string[] { - if (ctrl.vm.mode !== 'view' && !isYourMove(ctrl)) return ['You browsed away from the latest position.']; +function browseHint(ctrl: PuzzleCtrl): string[] { + if (ctrl.mode !== 'view' && !isYourMove(ctrl)) return ['You browsed away from the latest position.']; else return []; } @@ -319,7 +289,7 @@ function isShortCommand(input: string): boolean { return shortCommands.includes(input.split(' ')[0].toLowerCase()); } -function onCommand(ctrl: Controller, notify: (txt: string) => void, c: string, style: Style): void { +function onCommand(ctrl: PuzzleCtrl, notify: (txt: string) => void, c: string, style: Style): void { const lowered = c.toLowerCase(); const pieces = ctrl.ground()!.state.pieces; if (lowered === 'l' || lowered === 'last') notify($('.lastMove').text()); @@ -332,9 +302,9 @@ function onCommand(ctrl: Controller, notify: (txt: string) => void, c: string, s ); } -function viewOrAdvanceSolution(ctrl: Controller, notify: (txt: string) => void): void { - if (ctrl.vm.mode === 'view') { - const node = ctrl.vm.node, +function viewOrAdvanceSolution(ctrl: PuzzleCtrl, notify: (txt: string) => void): void { + if (ctrl.mode === 'view') { + const node = ctrl.node, next = nextNode(node), nextNext = nextNode(next); if (isInSolution(next) || (isInSolution(node) && isInSolution(nextNext))) { @@ -359,26 +329,26 @@ function nextNode(node?: Tree.Node): Tree.Node | undefined { else return; } -function renderStreak(ctrl: Controller): VNode[] { +function renderStreak(ctrl: PuzzleCtrl): VNode[] { if (!ctrl.streak) return []; return [h('h2', 'Puzzle streak'), h('p', ctrl.streak.data.index || ctrl.trans.noarg('streakDescription'))]; } -function renderStatus(ctrl: Controller): string { - if (ctrl.vm.mode !== 'view') return 'Solving'; +function renderStatus(ctrl: PuzzleCtrl): string { + if (ctrl.mode !== 'view') return 'Solving'; else if (ctrl.streak) return `GAME OVER. Your streak: ${ctrl.streak.data.index}`; - else if (ctrl.vm.lastFeedback === 'win') return 'Puzzle solved!'; + else if (ctrl.lastFeedback === 'win') return 'Puzzle solved!'; else return 'Puzzle complete.'; } -function renderReplay(ctrl: Controller): string { - const replay = ctrl.getData().replay; +function renderReplay(ctrl: PuzzleCtrl): string { + const replay = ctrl.data.replay; if (!replay) return ''; - const i = replay.i + (ctrl.vm.mode === 'play' ? 0 : 1); - return `Replaying ${ctrl.trans.noarg(ctrl.getData().angle.key)} puzzles: ${i} of ${replay.of}`; + const i = replay.i + (ctrl.mode === 'play' ? 0 : 1); + return `Replaying ${ctrl.trans.noarg(ctrl.data.angle.key)} puzzles: ${i} of ${replay.of}`; } -function playActions(ctrl: Controller): VNode { +function playActions(ctrl: PuzzleCtrl): VNode { if (ctrl.streak) return button( ctrl.trans.noarg('skip'), @@ -389,8 +359,8 @@ function playActions(ctrl: Controller): VNode { else return h('div.actions_play', button('View the solution', ctrl.viewSolution)); } -function afterActions(ctrl: Controller): VNode { - const win = ctrl.vm.lastFeedback === 'win'; +function afterActions(ctrl: PuzzleCtrl): VNode { + const win = ctrl.lastFeedback === 'win'; return h( 'div.actions_after', ctrl.streak && !win @@ -399,40 +369,28 @@ function afterActions(ctrl: Controller): VNode { ); } -const renderVoteTutorial = (ctrl: Controller): VNode[] => - ctrl.session.isNew() && ctrl.getData().user?.provisional +const renderVoteTutorial = (ctrl: PuzzleCtrl): VNode[] => + ctrl.session.isNew() && ctrl.data.user?.provisional ? [h('p', ctrl.trans.noarg('didYouLikeThisPuzzle')), h('p', ctrl.trans.noarg('voteToLoadNextOne'))] : []; -function renderVote(ctrl: Controller): VNode[] { - if (!ctrl.getData().user || ctrl.autoNexting()) return []; +function renderVote(ctrl: PuzzleCtrl): VNode[] { + if (!ctrl.data.user || ctrl.autoNexting()) return []; return [ ...renderVoteTutorial(ctrl), - button('Thumbs up', () => ctrl.vote(true), undefined, ctrl.vm.voteDisabled), - button('Thumbs down', () => ctrl.vote(false), undefined, ctrl.vm.voteDisabled), + button('Thumbs up', () => ctrl.vote(true), undefined, ctrl.voteDisabled), + button('Thumbs down', () => ctrl.vote(false), undefined, ctrl.voteDisabled), ]; } function anchor(text: string, href: string): VNode { - return h( - 'a', - { - attrs: { href }, - }, - text, - ); + return h('a', { attrs: { href } }, text); } function button(text: string, action: (e: Event) => void, title?: string, disabled?: boolean): VNode { return h( 'button', - { - hook: bind('click', action), - attrs: { - ...(title ? { title } : {}), - disabled: !!disabled, - }, - }, + { hook: bind('click', action), attrs: { ...(title ? { title } : {}), disabled: !!disabled } }, text, ); } diff --git a/ui/puzzle/src/view/after.ts b/ui/puzzle/src/view/after.ts index 66efd770142bd..85be6244750aa 100644 --- a/ui/puzzle/src/view/after.ts +++ b/ui/puzzle/src/view/after.ts @@ -1,66 +1,47 @@ import * as licon from 'common/licon'; -import { MaybeVNodes, bind, dataIcon } from 'common/snabbdom'; -import { Controller } from '../interfaces'; -import { h, VNode } from 'snabbdom'; +import { MaybeVNodes, bind, dataIcon, looseH as h } from 'common/snabbdom'; +import { VNode } from 'snabbdom'; import * as router from 'common/router'; +import PuzzleCtrl from '../ctrl'; -const renderVote = (ctrl: Controller): VNode => +const renderVote = (ctrl: PuzzleCtrl): VNode => h( 'div.puzzle__vote', - ctrl.autoNexting() - ? [] - : [ - ctrl.session.isNew() && ctrl.getData().user?.provisional - ? h('div.puzzle__vote__help', [ - h('p', ctrl.trans.noarg('didYouLikeThisPuzzle')), - h('p', ctrl.trans.noarg('voteToLoadNextOne')), - ]) - : null, - h( - 'div.puzzle__vote__buttons', - { - class: { - enabled: !ctrl.vm.voteDisabled, - }, - }, - [ - h('div.vote.vote-up', { - hook: bind('click', () => ctrl.vote(true)), - }), - h('div.vote.vote-down', { - hook: bind('click', () => ctrl.vote(false)), - }), - ], - ), - ], + {}, + !ctrl.autoNexting() && [ + ctrl.session.isNew() && + ctrl.data.user?.provisional && + h('div.puzzle__vote__help', [ + h('p', ctrl.trans.noarg('didYouLikeThisPuzzle')), + h('p', ctrl.trans.noarg('voteToLoadNextOne')), + ]), + h('div.puzzle__vote__buttons', { class: { enabled: !ctrl.voteDisabled } }, [ + h('div.vote.vote-up', { hook: bind('click', () => ctrl.vote(true)) }), + h('div.vote.vote-down', { hook: bind('click', () => ctrl.vote(false)) }), + ]), + ], ); -const renderContinue = (ctrl: Controller) => - h( - 'a.continue', - { - hook: bind('click', ctrl.nextPuzzle), - }, - [h('i', { attrs: dataIcon(licon.PlayTriangle) }), ctrl.trans.noarg('continueTraining')], - ); +const renderContinue = (ctrl: PuzzleCtrl) => + h('a.continue', { hook: bind('click', ctrl.nextPuzzle) }, [ + h('i', { attrs: dataIcon(licon.PlayTriangle) }), + ctrl.trans.noarg('continueTraining'), + ]); -const renderStreak = (ctrl: Controller): MaybeVNodes => [ +const renderStreak = (ctrl: PuzzleCtrl): MaybeVNodes => [ h('div.complete', [ h('span.game-over', 'GAME OVER'), - h('span', ctrl.trans.vdom('yourStreakX', h('strong', ctrl.streak?.data.index))), + h('span', ctrl.trans.vdom('yourStreakX', h('strong', `${ctrl.streak?.data.index ?? 0}`))), + ]), + h('a.continue', { attrs: { href: router.withLang('/streak') } }, [ + h('i', { attrs: dataIcon(licon.PlayTriangle) }), + ctrl.trans('newStreak'), ]), - h( - 'a.continue', - { - attrs: { href: router.withLang('/streak') }, - }, - [h('i', { attrs: dataIcon(licon.PlayTriangle) }), ctrl.trans('newStreak')], - ), ]; -export default function (ctrl: Controller): VNode { - const data = ctrl.getData(); - const win = ctrl.vm.lastFeedback == 'win'; +export default function (ctrl: PuzzleCtrl): VNode { + const data = ctrl.data; + const win = ctrl.lastFeedback == 'win'; return h( 'div.puzzle__feedback.after', ctrl.streak && !win @@ -72,21 +53,19 @@ export default function (ctrl: Controller): VNode { h('a', { attrs: { 'data-icon': licon.Bullseye, - href: `/analysis/${ctrl.vm.node.fen.replace(/ /g, '_')}?color=${ctrl.vm.pov}#practice`, + href: `/analysis/${ctrl.node.fen.replace(/ /g, '_')}?color=${ctrl.pov}#practice`, title: ctrl.trans.noarg('playWithTheMachine'), target: '_blank', rel: 'noopener', }, }), - data.user && !ctrl.autoNexting() - ? h( - 'a', - { - hook: bind('click', ctrl.nextPuzzle), - }, - ctrl.trans.noarg(ctrl.streak ? 'continueTheStreak' : 'continueTraining'), - ) - : undefined, + data.user && + !ctrl.autoNexting() && + h( + 'a', + { hook: bind('click', ctrl.nextPuzzle) }, + ctrl.trans.noarg(ctrl.streak ? 'continueTheStreak' : 'continueTraining'), + ), ]), ], ); diff --git a/ui/puzzle/src/view/boardMenu.ts b/ui/puzzle/src/view/boardMenu.ts index ee69dfb0bc53f..467474172d60a 100644 --- a/ui/puzzle/src/view/boardMenu.ts +++ b/ui/puzzle/src/view/boardMenu.ts @@ -1,9 +1,9 @@ import { h } from 'snabbdom'; import { menu as menuDropdown } from 'board/menu'; -import { Controller } from '../interfaces'; import { boolPrefXhrToggle } from 'common/controls'; +import PuzzleCtrl from '../ctrl'; -export default function (ctrl: Controller) { +export default function (ctrl: PuzzleCtrl) { return menuDropdown(ctrl.trans, ctrl.redraw, ctrl.menu, menu => [ h('section', [menu.flip(ctrl.trans.noarg('flipBoard'), ctrl.flipped(), ctrl.flip)]), h('section', [ diff --git a/ui/puzzle/src/view/chessground.ts b/ui/puzzle/src/view/chessground.ts index 3274e1f544e7c..1bba0f5543e3b 100644 --- a/ui/puzzle/src/view/chessground.ts +++ b/ui/puzzle/src/view/chessground.ts @@ -1,20 +1,20 @@ import resizeHandle from 'common/resize'; import { Config as CgConfig } from 'chessground/config'; -import { Controller } from '../interfaces'; import { h, VNode } from 'snabbdom'; import * as Prefs from 'common/prefs'; +import PuzzleCtrl from '../ctrl'; -export default function (ctrl: Controller): VNode { +export default function (ctrl: PuzzleCtrl): VNode { return h('div.cg-wrap', { hook: { insert: vnode => ctrl.setChessground(lichess.makeChessground(vnode.elm as HTMLElement, makeConfig(ctrl))), - destroy: _ => ctrl.ground()!.destroy(), + destroy: () => ctrl.ground()!.destroy(), }, }); } -export function makeConfig(ctrl: Controller): CgConfig { +export function makeConfig(ctrl: PuzzleCtrl): CgConfig { const opts = ctrl.makeCgOpts(); return { fen: opts.fen, @@ -42,7 +42,7 @@ export function makeConfig(ctrl: Controller): CgConfig { events: { move: ctrl.userMove, insert(elements) { - resizeHandle(elements, Prefs.ShowResizeHandle.Always, ctrl.vm.node.ply); + resizeHandle(elements, Prefs.ShowResizeHandle.Always, ctrl.node.ply); }, }, premovable: { diff --git a/ui/puzzle/src/view/feedback.ts b/ui/puzzle/src/view/feedback.ts index 0c75f5d65c609..ef16b5b368af5 100644 --- a/ui/puzzle/src/view/feedback.ts +++ b/ui/puzzle/src/view/feedback.ts @@ -1,60 +1,41 @@ import { bind, MaybeVNode } from 'common/snabbdom'; import { h, VNode } from 'snabbdom'; -import { Controller } from '../interfaces'; import afterView from './after'; +import PuzzleCtrl from '../ctrl'; -const viewSolution = (ctrl: Controller): VNode => +const viewSolution = (ctrl: PuzzleCtrl): VNode => ctrl.streak - ? h( - 'div.view_solution.skip', - { - class: { show: !!ctrl.streak?.data.skip }, - }, - [ - h( - 'a.button.button-empty', - { - hook: bind('click', ctrl.skip), - attrs: { - title: ctrl.trans.noarg('streakSkipExplanation'), - }, - }, - ctrl.trans.noarg('skip'), - ), - ], - ) - : h( - 'div.view_solution', - { - class: { show: ctrl.vm.canViewSolution }, - }, - [ - h( - 'a.button.button-empty', - { - hook: bind('click', ctrl.viewSolution), - }, - ctrl.trans.noarg('viewTheSolution'), - ), - ], - ); + ? h('div.view_solution.skip', { class: { show: !!ctrl.streak?.data.skip } }, [ + h( + 'a.button.button-empty', + { hook: bind('click', ctrl.skip), attrs: { title: ctrl.trans.noarg('streakSkipExplanation') } }, + ctrl.trans.noarg('skip'), + ), + ]) + : h('div.view_solution', { class: { show: ctrl.canViewSolution() } }, [ + h( + 'a.button.button-empty', + { hook: bind('click', ctrl.viewSolution) }, + ctrl.trans.noarg('viewTheSolution'), + ), + ]); -const initial = (ctrl: Controller): VNode => +const initial = (ctrl: PuzzleCtrl): VNode => h('div.puzzle__feedback.play', [ h('div.player', [ - h('div.no-square', h('piece.king.' + ctrl.vm.pov)), + h('div.no-square', h('piece.king.' + ctrl.pov)), h('div.instruction', [ h('strong', ctrl.trans.noarg('yourTurn')), h( 'em', - ctrl.trans.noarg(ctrl.vm.pov === 'white' ? 'findTheBestMoveForWhite' : 'findTheBestMoveForBlack'), + ctrl.trans.noarg(ctrl.pov === 'white' ? 'findTheBestMoveForWhite' : 'findTheBestMoveForBlack'), ), ]), ]), viewSolution(ctrl), ]); -const good = (ctrl: Controller): VNode => +const good = (ctrl: PuzzleCtrl): VNode => h('div.puzzle__feedback.good', [ h('div.player', [ h('div.icon', '✓'), @@ -66,7 +47,7 @@ const good = (ctrl: Controller): VNode => viewSolution(ctrl), ]); -const fail = (ctrl: Controller): VNode => +const fail = (ctrl: PuzzleCtrl): VNode => h('div.puzzle__feedback.fail', [ h('div.player', [ h('div.icon', '✗'), @@ -78,9 +59,9 @@ const fail = (ctrl: Controller): VNode => viewSolution(ctrl), ]); -export default function (ctrl: Controller): MaybeVNode { - if (ctrl.vm.mode === 'view') return afterView(ctrl); - switch (ctrl.vm.lastFeedback) { +export default function (ctrl: PuzzleCtrl): MaybeVNode { + if (ctrl.mode === 'view') return afterView(ctrl); + switch (ctrl.lastFeedback) { case 'init': return initial(ctrl); case 'good': diff --git a/ui/puzzle/src/view/main.ts b/ui/puzzle/src/view/main.ts index 54f4ef4a14418..99ee9efe8366c 100644 --- a/ui/puzzle/src/view/main.ts +++ b/ui/puzzle/src/view/main.ts @@ -6,9 +6,8 @@ import chessground from './chessground'; import feedbackView from './feedback'; import * as licon from 'common/licon'; import { stepwiseScroll } from 'common/scroll'; -import { Controller } from '../interfaces'; -import { h, VNode } from 'snabbdom'; -import { onInsert, bindNonPassive } from 'common/snabbdom'; +import { VNode } from 'snabbdom'; +import { onInsert, bindNonPassive, looseH as h } from 'common/snabbdom'; import { bindMobileMousedown } from 'common/device'; import { render as treeView } from './tree'; import { view as cevalView } from 'ceval'; @@ -16,10 +15,10 @@ import { renderVoiceBar } from 'voice'; import { render as renderKeyboardMove } from 'keyboardMove'; import { toggleButton as boardMenuToggleButton } from 'board/menu'; import boardMenu from './boardMenu'; - import * as Prefs from 'common/prefs'; +import PuzzleCtrl from '../ctrl'; -const renderAnalyse = (ctrl: Controller): VNode => h('div.puzzle__moves.areplay', [treeView(ctrl)]); +const renderAnalyse = (ctrl: PuzzleCtrl): VNode => h('div.puzzle__moves.areplay', [treeView(ctrl)]); function dataAct(e: Event): string | null { const target = e.target as HTMLElement; @@ -27,19 +26,13 @@ function dataAct(e: Event): string | null { } function jumpButton(icon: string, effect: string, disabled: boolean, glowing = false): VNode { - return h('button.fbt', { - class: { disabled, glowing }, - attrs: { - 'data-act': effect, - 'data-icon': icon, - }, - }); + return h('button.fbt', { class: { disabled, glowing }, attrs: { 'data-act': effect, 'data-icon': icon } }); } -function controls(ctrl: Controller): VNode { - const node = ctrl.vm.node; +function controls(ctrl: PuzzleCtrl): VNode { + const node = ctrl.node; const nextNode = node.children[0]; - const notOnLastMove = ctrl.vm.mode == 'play' && nextNode && nextNode.puzzle != 'fail'; + const notOnLastMove = ctrl.mode == 'play' && nextNode && nextNode.puzzle != 'fail'; return h('div.puzzle__controls.analyse-controls', [ h( 'div.jumps', @@ -68,16 +61,16 @@ function controls(ctrl: Controller): VNode { let cevalShown = false; -export default function (ctrl: Controller): VNode { +export default function (ctrl: PuzzleCtrl): VNode { if (ctrl.nvui) return ctrl.nvui.render(ctrl); - const showCeval = ctrl.vm.showComputer(), + const showCeval = ctrl.showComputer(), gaugeOn = ctrl.showEvalGauge(); if (cevalShown !== showCeval) { - if (!cevalShown) ctrl.vm.autoScrollNow = true; + if (!cevalShown) ctrl.autoScrollNow = true; cevalShown = showCeval; } return h( - `main.puzzle.puzzle-${ctrl.getData().replay ? 'replay' : 'play'}${ctrl.streak ? '.puzzle--streak' : ''}`, + `main.puzzle.puzzle-${ctrl.data.replay ? 'replay' : 'play'}${ctrl.streak ? '.puzzle--streak' : ''}`, { class: { 'gauge-on': gaugeOn }, hook: { @@ -132,68 +125,49 @@ export default function (ctrl: Controller): VNode { // so the siblings are only updated when ceval is added h( 'div.ceval-wrap', - { - class: { none: !showCeval }, - }, + { class: { none: !showCeval } }, showCeval ? [...cevalView.renderCeval(ctrl), cevalView.renderPvs(ctrl)] : [], ), renderAnalyse(ctrl), feedbackView(ctrl), ]), controls(ctrl), - ctrl.keyboardMove ? renderKeyboardMove(ctrl.keyboardMove) : null, + ctrl.keyboardMove && renderKeyboardMove(ctrl.keyboardMove), session(ctrl), - ctrl.keyboardHelp() ? keyboard.view(ctrl) : null, + ctrl.keyboardHelp() && keyboard.view(ctrl), ], ); } -function session(ctrl: Controller) { +function session(ctrl: PuzzleCtrl) { const rounds = ctrl.session.get().rounds, - current = ctrl.getData().puzzle.id; + current = ctrl.data.puzzle.id; return h('div.puzzle__session', [ ...rounds.map(round => { - const rd = - round.ratingDiff && ctrl.showRatings - ? round.ratingDiff > 0 - ? '+' + round.ratingDiff - : round.ratingDiff - : null; + const rd = !ctrl.opts.showRatings + ? '' + : round.ratingDiff && round.ratingDiff > 0 + ? '+' + round.ratingDiff + : round.ratingDiff; return h( `a.result-${round.result}${rd ? '' : '.result-empty'}`, { key: round.id, - class: { - current: current == round.id, - }, + class: { current: current == round.id }, attrs: { href: `/training/${ctrl.session.theme}/${round.id}`, ...(ctrl.streak ? { target: '_blank', rel: 'noopener' } : {}), }, }, - rd, + `${rd}`, ); }), rounds.find(r => r.id == current) - ? ctrl.streak - ? null - : h('a.session-new', { - key: 'new', - attrs: { - href: `/training/${ctrl.session.theme}`, - }, - }) + ? !ctrl.streak && h('a.session-new', { key: 'new', attrs: { href: `/training/${ctrl.session.theme}` } }) : h( 'a.result-cursor.current', - { - key: current, - attrs: ctrl.streak - ? {} - : { - href: `/training/${ctrl.session.theme}/${current}`, - }, - }, - ctrl.streak?.data.index, + { key: current, attrs: ctrl.streak ? {} : { href: `/training/${ctrl.session.theme}/${current}` } }, + `${ctrl.streak?.data.index ?? 0}`, ), ]); } diff --git a/ui/puzzle/src/view/side.ts b/ui/puzzle/src/view/side.ts index 186e96c952464..8f957262f22b0 100644 --- a/ui/puzzle/src/view/side.ts +++ b/ui/puzzle/src/view/side.ts @@ -1,41 +1,37 @@ -import { Controller, Puzzle, PuzzleGame, PuzzleDifficulty } from '../interfaces'; +import { Puzzle, PuzzleGame, PuzzleDifficulty } from '../interfaces'; import * as licon from 'common/licon'; -import { dataIcon, onInsert, MaybeVNode } from 'common/snabbdom'; -import { h, VNode } from 'snabbdom'; +import { dataIcon, onInsert, MaybeVNode, looseH as h } from 'common/snabbdom'; +import { VNode } from 'snabbdom'; import { numberFormat } from 'common/number'; import perfIcons from 'common/perfIcons'; import * as router from 'common/router'; import { userLink } from 'common/userLink'; import PuzzleStreak from '../streak'; +import PuzzleCtrl from '../ctrl'; -export function puzzleBox(ctrl: Controller): VNode { - const data = ctrl.getData(); +export function puzzleBox(ctrl: PuzzleCtrl): VNode { + const data = ctrl.data; return h('div.puzzle__side__metas', [ puzzleInfos(ctrl, data.puzzle), gameInfos(ctrl, data.game, data.puzzle), ]); } -const angleImg = (ctrl: Controller): string => { - const angle = ctrl.getData().angle; +const angleImg = (ctrl: PuzzleCtrl): string => { + const angle = ctrl.data.angle; const name = angle.opening ? 'opening' : angle.key.startsWith('mateIn') ? 'mate' : angle.key; - return lichess.assetUrl(`images/puzzle-themes/${name}.svg`); + return lichess.asset.url(`images/puzzle-themes/${name}.svg`); }; -const puzzleInfos = (ctrl: Controller, puzzle: Puzzle): VNode => +const puzzleInfos = (ctrl: PuzzleCtrl, puzzle: Puzzle): VNode => h('div.infos.puzzle', [ - h('img.infos__angle-img', { - attrs: { - src: angleImg(ctrl), - alt: ctrl.getData().angle.name, - }, - }), + h('img.infos__angle-img', { attrs: { src: angleImg(ctrl), alt: ctrl.data.angle.name } }), h('div', [ h( 'p', ctrl.trans.vdom( 'puzzleId', - ctrl.streak && ctrl.vm.mode === 'play' + ctrl.streak && ctrl.mode === 'play' ? h('span.hidden', ctrl.trans.noarg('hidden')) : h( 'a', @@ -49,22 +45,21 @@ const puzzleInfos = (ctrl: Controller, puzzle: Puzzle): VNode => ), ), ), - ctrl.showRatings - ? h( - 'p', - ctrl.trans.vdom( - 'ratingX', - !ctrl.streak && ctrl.vm.mode === 'play' - ? h('span.hidden', ctrl.trans.noarg('hidden')) - : h('strong', puzzle.rating), - ), - ) - : null, + ctrl.opts.showRatings && + h( + 'p', + ctrl.trans.vdom( + 'ratingX', + !ctrl.streak && ctrl.mode === 'play' + ? h('span.hidden', ctrl.trans.noarg('hidden')) + : h('strong', `${puzzle.rating}`), + ), + ), h('p', ctrl.trans.vdomPlural('playedXTimes', puzzle.plays, h('strong', numberFormat(puzzle.plays)))), ]), ]); -function gameInfos(ctrl: Controller, game: PuzzleGame, puzzle: Puzzle): VNode { +function gameInfos(ctrl: PuzzleCtrl, game: PuzzleGame, puzzle: Puzzle): VNode { const gameName = `${game.clock} • ${game.perf.name}`; return h('div.infos', { attrs: dataIcon(perfIcons[game.perf.key]) }, [ h('div', [ @@ -72,25 +67,15 @@ function gameInfos(ctrl: Controller, game: PuzzleGame, puzzle: Puzzle): VNode { 'p', ctrl.trans.vdom( 'fromGameLink', - ctrl.vm.mode == 'play' + ctrl.mode == 'play' ? h('span', gameName) - : h( - 'a', - { - attrs: { href: `/${game.id}/${ctrl.vm.pov}#${puzzle.initialPly}` }, - }, - gameName, - ), + : h('a', { attrs: { href: `/${game.id}/${ctrl.pov}#${puzzle.initialPly}` } }, gameName), ), ), h( 'div.players', game.players.map(p => { - const user = { - ...p, - rating: ctrl.showRatings ? p.rating : undefined, - line: false, - }; + const user = { ...p, rating: ctrl.opts.showRatings ? p.rating : undefined, line: false }; return h('div.player.color-icon.is.text.' + p.color, userLink(user)); }), ), @@ -103,69 +88,57 @@ const renderStreak = (streak: PuzzleStreak, noarg: TransNoArg) => 'div.puzzle__side__streak', streak.data.index == 0 ? h('div.puzzle__side__streak__info', [ - h( - 'h1.text', - { - attrs: dataIcon(licon.ArrowThruApple), - }, - 'Puzzle Streak', - ), + h('h1.text', { attrs: dataIcon(licon.ArrowThruApple) }, 'Puzzle Streak'), h('p', noarg('streakDescription')), ]) : h( 'div.puzzle__side__streak__score.text', - { - attrs: dataIcon(licon.ArrowThruApple), - }, - streak.data.index, + { attrs: dataIcon(licon.ArrowThruApple) }, + `${streak.data.index}`, ), ); -export const userBox = (ctrl: Controller): VNode => { - const data = ctrl.getData(), +export const userBox = (ctrl: PuzzleCtrl): VNode => { + const data = ctrl.data, noarg = ctrl.trans.noarg; if (!data.user) return h('div.puzzle__side__user', [ h('p', noarg('toGetPersonalizedPuzzles')), h('a.button', { attrs: { href: router.withLang('/signup') } }, noarg('signUp')), ]); - const diff = ctrl.vm.round?.ratingDiff, + const diff = ctrl.round?.ratingDiff, ratedId = 'puzzle-toggle-rated'; return h('div.puzzle__side__user', [ - !data.replay && !ctrl.streak && data.user - ? h('div.puzzle__side__config__toggle', [ - h('div.switch', [ - h(`input#${ratedId}.cmn-toggle.cmn-toggle--subtle`, { - attrs: { - type: 'checkbox', - checked: ctrl.rated(), - disabled: ctrl.vm.lastFeedback != 'init', - }, - hook: { - insert: vnode => (vnode.elm as HTMLElement).addEventListener('change', ctrl.toggleRated), - }, - }), - h('label', { attrs: { for: ratedId } }), - ]), - h('label', { attrs: { for: ratedId } }, noarg('rated')), - ]) - : undefined, + !data.replay && + !ctrl.streak && + data.user && + h('div.puzzle__side__config__toggle', [ + h('div.switch', [ + h(`input#${ratedId}.cmn-toggle.cmn-toggle--subtle`, { + attrs: { type: 'checkbox', checked: ctrl.rated(), disabled: ctrl.lastFeedback != 'init' }, + hook: { + insert: vnode => (vnode.elm as HTMLElement).addEventListener('change', ctrl.toggleRated), + }, + }), + h('label', { attrs: { for: ratedId } }), + ]), + h('label', { attrs: { for: ratedId } }, noarg('rated')), + ]), h( 'div.puzzle__side__user__rating', ctrl.rated() - ? ctrl.showRatings - ? h('strong', [ + ? ctrl.opts.showRatings && + h('strong', [ data.user.rating - (diff || 0), ...(diff && diff > 0 ? [' ', h('good.rp', '+' + diff)] : []), ...(diff && diff < 0 ? [' ', h('bad.rp', '−' + -diff)] : []), ]) - : null : h('p.puzzle__side__user__rating__casual', noarg('yourPuzzleRatingWillNotChange')), ), ]); }; -export const streakBox = (ctrl: Controller) => +export const streakBox = (ctrl: PuzzleCtrl) => h('div.puzzle__side__user', renderStreak(ctrl.streak!, ctrl.trans.noarg)); const difficulties: [PuzzleDifficulty, number][] = [ @@ -181,20 +154,15 @@ const colors = [ ['white', 'asWhite'], ]; -export function replay(ctrl: Controller): MaybeVNode { - const replay = ctrl.getData().replay; +export function replay(ctrl: PuzzleCtrl): MaybeVNode { + const replay = ctrl.data.replay; if (!replay) return; - const i = replay.i + (ctrl.vm.mode == 'play' ? 0 : 1); + const i = replay.i + (ctrl.mode == 'play' ? 0 : 1); return h('div.puzzle__side__replay', [ - h( - 'a', - { - attrs: { - href: `/training/dashboard/${replay.days}`, - }, - }, - ['« ', `Replaying ${ctrl.trans.noarg(ctrl.getData().angle.key)} puzzles`], - ), + h('a', { attrs: { href: `/training/dashboard/${replay.days}` } }, [ + '« ', + `Replaying ${ctrl.trans.noarg(ctrl.data.angle.key)} puzzles`, + ]), h('div.puzzle__side__replay__bar', { attrs: { style: `--p:${replay.of ? Math.round((100 * i) / replay.of) : 1}%`, @@ -204,23 +172,20 @@ export function replay(ctrl: Controller): MaybeVNode { ]); } -export function config(ctrl: Controller): MaybeVNode { +export function config(ctrl: PuzzleCtrl): MaybeVNode { const autoNextId = 'puzzle-toggle-autonext', noarg = ctrl.trans.noarg, - data = ctrl.getData(); + data = ctrl.data; return h('div.puzzle__side__config', [ h('div.puzzle__side__config__toggle', [ h('div.switch', [ h(`input#${autoNextId}.cmn-toggle.cmn-toggle--subtle`, { - attrs: { - type: 'checkbox', - checked: ctrl.autoNext(), - }, + attrs: { type: 'checkbox', checked: ctrl.autoNext() }, hook: { insert: vnode => (vnode.elm as HTMLElement).addEventListener('change', () => { ctrl.autoNext(!ctrl.autoNext()); - if (ctrl.autoNext() && ctrl.vm.resultSent && !ctrl.streak) { + if (ctrl.autoNext() && ctrl.resultSent && !ctrl.streak) { ctrl.nextPuzzle(); } }), @@ -234,23 +199,12 @@ export function config(ctrl: Controller): MaybeVNode { ]); } -export const renderDifficultyForm = (ctrl: Controller): VNode => +export const renderDifficultyForm = (ctrl: PuzzleCtrl): VNode => h( 'form.puzzle__side__config__difficulty', - { - attrs: { - action: `/training/difficulty/${ctrl.getData().angle.key}`, - method: 'post', - }, - }, + { attrs: { action: `/training/difficulty/${ctrl.data.angle.key}`, method: 'post' } }, [ - h( - 'label', - { - attrs: { for: 'puzzle-difficulty' }, - }, - ctrl.trans.noarg('difficultyLevel'), - ), + h('label', { attrs: { for: 'puzzle-difficulty' } }, ctrl.trans.noarg('difficultyLevel')), h( 'select#puzzle-difficulty.puzzle__difficulty__selector', { @@ -265,7 +219,7 @@ export const renderDifficultyForm = (ctrl: Controller): VNode => { attrs: { value: key, - selected: key == ctrl.settings.difficulty, + selected: key == ctrl.opts.settings.difficulty, title: !!delta && ctrl.trans.pluralSame( @@ -281,7 +235,7 @@ export const renderDifficultyForm = (ctrl: Controller): VNode => ], ); -export const renderColorForm = (ctrl: Controller): VNode => +export const renderColorForm = (ctrl: PuzzleCtrl): VNode => h( 'div.puzzle__side__config__color', h( @@ -289,12 +243,9 @@ export const renderColorForm = (ctrl: Controller): VNode => colors.map(([key, i18n]) => h('div', [ h( - `a.label.color-${key}${key === (ctrl.settings.color || 'random') ? '.active' : ''}`, + `a.label.color-${key}${key === (ctrl.opts.settings.color || 'random') ? '.active' : ''}`, { - attrs: { - href: `/training/${ctrl.getData().angle.key}/${key}`, - title: ctrl.trans.noarg(i18n), - }, + attrs: { href: `/training/${ctrl.data.angle.key}/${key}`, title: ctrl.trans.noarg(i18n) }, }, h('i'), ), diff --git a/ui/puzzle/src/view/theme.ts b/ui/puzzle/src/view/theme.ts index 60caaad1f53ab..909c4c9b53a76 100644 --- a/ui/puzzle/src/view/theme.ts +++ b/ui/puzzle/src/view/theme.ts @@ -1,38 +1,26 @@ import * as licon from 'common/licon'; import * as router from 'common/router'; -import { MaybeVNode, bind, dataIcon } from 'common/snabbdom'; -import { Controller } from '../interfaces'; -import { h, VNode } from 'snabbdom'; +import { MaybeVNode, bind, dataIcon, looseH as h } from 'common/snabbdom'; +import { VNode } from 'snabbdom'; import { renderColorForm } from './side'; +import PuzzleCtrl from '../ctrl'; const studyUrl = 'https://lichess.org/study/viiWlKjv'; -export default function theme(ctrl: Controller): MaybeVNode { - const data = ctrl.getData(), +export default function theme(ctrl: PuzzleCtrl): MaybeVNode { + const data = ctrl.data, angle = data.angle; - const showEditor = ctrl.vm.mode == 'view' && !ctrl.autoNexting(); + const showEditor = ctrl.mode == 'view' && !ctrl.autoNexting(); if (data.replay) return showEditor ? h('div.puzzle__side__theme', editor(ctrl)) : null; const puzzleMenu = (v: VNode): VNode => h('a', { attrs: { href: router.withLang(`/training/${angle.opening ? 'openings' : 'themes'}`) } }, v); - return ctrl.streak - ? null - : ctrl.vm.isDaily + return !ctrl.streak && ctrl.isDaily ? h( 'div.puzzle__side__theme.puzzle__side__theme--daily', puzzleMenu(h('h2', ctrl.trans.noarg('dailyPuzzle'))), ) : h('div.puzzle__side__theme', [ - puzzleMenu( - h( - 'h2', - { - class: { - long: angle.name.length > 20, - }, - }, - ['« ', angle.name], - ), - ), + puzzleMenu(h('h2', { class: { long: angle.name.length > 20 } }, ['« ', angle.name])), angle.opening ? h('a', { attrs: { href: `/opening/${angle.opening.key}` } }, [ 'Learn more about ', @@ -43,30 +31,22 @@ export default function theme(ctrl: Controller): MaybeVNode { angle.chapter && h( 'a.puzzle__side__theme__chapter.text', - { - attrs: { - href: `${studyUrl}/${angle.chapter}`, - target: '_blank', - rel: 'noopener', - }, - }, + { attrs: { href: `${studyUrl}/${angle.chapter}`, target: '_blank', rel: 'noopener' } }, [' ', ctrl.trans.noarg('example')], ), ]), showEditor ? h('div.puzzle__themes', editor(ctrl)) - : !data.replay && !ctrl.streak && angle.opening - ? renderColorForm(ctrl) - : null, + : !data.replay && !ctrl.streak && angle.opening && renderColorForm(ctrl), ]); } const invisibleThemes = new Set(['master', 'masterVsMaster', 'superGM']); -const editor = (ctrl: Controller): VNode[] => { - const data = ctrl.getData(), +const editor = (ctrl: PuzzleCtrl): VNode[] => { + const data = ctrl.data, trans = ctrl.trans.noarg, - votedThemes = ctrl.vm.round?.themes || {}; + votedThemes = ctrl.round?.themes || {}; const visibleThemes: string[] = data.puzzle.themes .filter(t => !invisibleThemes.has(t)) .concat(Object.keys(votedThemes).filter(t => votedThemes[t] && !data.puzzle.themes.includes(t))) @@ -85,50 +65,25 @@ const editor = (ctrl: Controller): VNode[] => { }), }, visibleThemes.map(key => - h( - 'div.puzzle__themes__list__entry', - { - class: { - strike: votedThemes[key] === false, - }, - }, - [ + h('div.puzzle__themes__list__entry', { class: { strike: votedThemes[key] === false } }, [ + h('a', { attrs: { href: `/training/${key}`, title: trans(`${key}Description`) } }, trans(key)), + allThemes && h( - 'a', - { - attrs: { - href: `/training/${key}`, - title: trans(`${key}Description`), - }, - }, - trans(key), + 'div.puzzle__themes__votes', + allThemes.static.has(key) + ? [h('div.puzzle__themes__lock', h('i', { attrs: dataIcon(licon.Padlock) }))] + : [ + h('span.puzzle__themes__vote.vote-up', { + class: { active: votedThemes[key] }, + attrs: { 'data-theme': key }, + }), + h('span.puzzle__themes__vote.vote-down', { + class: { active: votedThemes[key] === false }, + attrs: { 'data-theme': key }, + }), + ], ), - !allThemes - ? null - : h( - 'div.puzzle__themes__votes', - allThemes.static.has(key) - ? [ - h( - 'div.puzzle__themes__lock', - h('i', { - attrs: dataIcon(licon.Padlock), - }), - ), - ] - : [ - h('span.puzzle__themes__vote.vote-up', { - class: { active: votedThemes[key] }, - attrs: { 'data-theme': key }, - }), - h('span.puzzle__themes__vote.vote-down', { - class: { active: votedThemes[key] === false }, - attrs: { 'data-theme': key }, - }), - ], - ), - ], - ), + ]), ), ), ...(availableThemes @@ -147,37 +102,15 @@ const editor = (ctrl: Controller): VNode[] => { }, }, [ - h( - 'option', - { - attrs: { value: '', selected: true }, - }, - trans('addAnotherTheme'), - ), + h('option', { attrs: { value: '', selected: true } }, trans('addAnotherTheme')), ...availableThemes.map(theme => - h( - 'option', - { - attrs: { - value: theme, - title: trans(`${theme}Description`), - }, - }, - trans(theme), - ), + h('option', { attrs: { value: theme, title: trans(`${theme}Description`) } }, trans(theme)), ), ], ), h( 'a.puzzle__themes__study.text', - { - attrs: { - 'data-icon': licon.InfoCircle, - href: studyUrl, - target: '_blank', - rel: 'noopener', - }, - }, + { attrs: { 'data-icon': licon.InfoCircle, href: studyUrl, target: '_blank', rel: 'noopener' } }, 'About puzzle themes', ), ] diff --git a/ui/puzzle/src/view/tree.ts b/ui/puzzle/src/view/tree.ts index 1f5a5be8023af..9aaf8def95f54 100644 --- a/ui/puzzle/src/view/tree.ts +++ b/ui/puzzle/src/view/tree.ts @@ -1,13 +1,13 @@ -import { h, VNode, Classes } from 'snabbdom'; +import { VNode, Classes } from 'snabbdom'; import { defined } from 'common'; import throttle from 'common/throttle'; import { renderEval as normalizeEval } from 'ceval'; import { path as treePath } from 'tree'; -import { Controller } from '../interfaces'; -import { MaybeVNode, MaybeVNodes } from 'common/snabbdom'; +import { MaybeVNode, LooseVNodes, looseH as h } from 'common/snabbdom'; +import PuzzleCtrl from '../ctrl'; interface Ctx { - ctrl: Controller; + ctrl: PuzzleCtrl; } interface RenderOpts { @@ -21,18 +21,18 @@ interface Glyph { symbol: string; } -const autoScroll = throttle(150, (ctrl: Controller, el) => { +const autoScroll = throttle(150, (ctrl: PuzzleCtrl, el) => { const cont = el.parentNode; const target = el.querySelector('.active'); if (!target) { - cont.scrollTop = ctrl.vm.path === treePath.root ? 0 : 99999; + cont.scrollTop = ctrl.path === treePath.root ? 0 : 99999; return; } cont.scrollTop = target.offsetTop - cont.offsetHeight / 2 + target.offsetHeight; }); function pathContains(ctx: Ctx, path: Tree.Path): boolean { - return treePath.contains(ctx.ctrl.vm.path, path); + return treePath.contains(ctx.ctrl.path, path); } function plyToTurn(ply: number): number { @@ -43,7 +43,7 @@ export function renderIndex(ply: number, withDots: boolean): VNode { return h('index', plyToTurn(ply) + (withDots ? (ply % 2 === 1 ? '.' : '...') : '')); } -function renderChildrenOf(ctx: Ctx, node: Tree.Node, opts: RenderOpts): MaybeVNodes { +function renderChildrenOf(ctx: Ctx, node: Tree.Node, opts: RenderOpts): LooseVNodes { const cs = node.children, main = cs[0]; if (!main) return []; @@ -51,31 +51,19 @@ function renderChildrenOf(ctx: Ctx, node: Tree.Node, opts: RenderOpts): MaybeVNo const isWhite = main.ply % 2 === 1; if (!cs[1]) return [ - isWhite ? renderIndex(main.ply, false) : null, - ...renderMoveAndChildrenOf(ctx, main, { - parentPath: opts.parentPath, - isMainline: true, - }), + isWhite && renderIndex(main.ply, false), + ...renderMoveAndChildrenOf(ctx, main, { parentPath: opts.parentPath, isMainline: true }), ]; const mainChildren = renderChildrenOf(ctx, main, { parentPath: opts.parentPath + main.id, isMainline: true, }), - passOpts = { - parentPath: opts.parentPath, - isMainline: true, - }; + passOpts = { parentPath: opts.parentPath, isMainline: true }; return [ - isWhite ? renderIndex(main.ply, false) : null, + isWhite && renderIndex(main.ply, false), renderMoveOf(ctx, main, passOpts), - isWhite ? emptyMove() : null, - h( - 'interrupt', - renderLines(ctx, cs.slice(1), { - parentPath: opts.parentPath, - isMainline: true, - }), - ), + isWhite && emptyMove(), + h('interrupt', renderLines(ctx, cs.slice(1), { parentPath: opts.parentPath, isMainline: true })), ...(isWhite && mainChildren ? [renderIndex(main.ply, false), emptyMove()] : []), ...mainChildren, ]; @@ -86,17 +74,11 @@ function renderChildrenOf(ctx: Ctx, node: Tree.Node, opts: RenderOpts): MaybeVNo function renderLines(ctx: Ctx, nodes: Tree.Node[], opts: RenderOpts): VNode { return h( 'lines', - { - class: { single: !!nodes[1] }, - }, + { class: { single: !!nodes[1] } }, nodes.map(function (n) { return h( 'line', - renderMoveAndChildrenOf(ctx, n, { - parentPath: opts.parentPath, - isMainline: false, - withIndex: true, - }), + renderMoveAndChildrenOf(ctx, n, { parentPath: opts.parentPath, isMainline: false, withIndex: true }), ); }), ); @@ -109,64 +91,37 @@ function renderMoveOf(ctx: Ctx, node: Tree.Node, opts: RenderOpts): VNode { function renderMainlineMoveOf(ctx: Ctx, node: Tree.Node, opts: RenderOpts): VNode { const path = opts.parentPath + node.id; const classes: Classes = { - active: path === ctx.ctrl.vm.path, - current: path === ctx.ctrl.vm.initialPath, - hist: node.ply < ctx.ctrl.vm.initialNode.ply, + active: path === ctx.ctrl.path, + current: path === ctx.ctrl.initialPath, + hist: node.ply < ctx.ctrl.initialNode.ply, }; if (node.puzzle) classes[node.puzzle] = true; - return h( - 'move', - { - attrs: { p: path }, - class: classes, - }, - renderMove(ctx, node), - ); + return h('move', { attrs: { p: path }, class: classes }, renderMove(ctx, node)); } function renderGlyph(glyph: Glyph): VNode { - return h( - 'glyph', - { - attrs: { title: glyph.name }, - }, - glyph.symbol, - ); + return h('glyph', { attrs: { title: glyph.name } }, glyph.symbol); } function puzzleGlyph(ctx: Ctx, node: Tree.Node): MaybeVNode { switch (node.puzzle) { case 'good': case 'win': - return renderGlyph({ - name: ctx.ctrl.trans.noarg('bestMove'), - symbol: '✓', - }); + return renderGlyph({ name: ctx.ctrl.trans.noarg('bestMove'), symbol: '✓' }); case 'fail': - return renderGlyph({ - name: ctx.ctrl.trans.noarg('puzzleFailed'), - symbol: '✗', - }); + return renderGlyph({ name: ctx.ctrl.trans.noarg('puzzleFailed'), symbol: '✗' }); case 'retry': - return renderGlyph({ - name: ctx.ctrl.trans.noarg('goodMove'), - symbol: '?!', - }); + return renderGlyph({ name: ctx.ctrl.trans.noarg('goodMove'), symbol: '?!' }); default: return; } } -export function renderMove(ctx: Ctx, node: Tree.Node): MaybeVNodes { +function renderMove(ctx: Ctx, node: Tree.Node): LooseVNodes { const ev = node.eval || node.ceval; return [ node.san, - ev && - (defined(ev.cp) - ? renderEval(normalizeEval(ev.cp)) - : defined(ev.mate) - ? renderEval('#' + ev.mate) - : undefined), + ev && (defined(ev.cp) ? renderEval(normalizeEval(ev.cp)) : defined(ev.mate) && renderEval('#' + ev.mate)), puzzleGlyph(ctx, node), ]; } @@ -174,29 +129,20 @@ export function renderMove(ctx: Ctx, node: Tree.Node): MaybeVNodes { function renderVariationMoveOf(ctx: Ctx, node: Tree.Node, opts: RenderOpts): VNode { const withIndex = opts.withIndex || node.ply % 2 === 1; const path = opts.parentPath + node.id; - const active = path === ctx.ctrl.vm.path; - const classes: Classes = { - active, - parent: !active && pathContains(ctx, path), - }; + const active = path === ctx.ctrl.path; + const classes: Classes = { active, parent: !active && pathContains(ctx, path) }; if (node.puzzle) classes[node.puzzle] = true; - return h( - 'move', - { - attrs: { p: path }, - class: classes, - }, - [withIndex ? renderIndex(node.ply, true) : null, node.san, puzzleGlyph(ctx, node)], - ); + return h('move', { attrs: { p: path }, class: classes }, [ + withIndex && renderIndex(node.ply, true), + node.san, + puzzleGlyph(ctx, node), + ]); } -function renderMoveAndChildrenOf(ctx: Ctx, node: Tree.Node, opts: RenderOpts): MaybeVNodes { +function renderMoveAndChildrenOf(ctx: Ctx, node: Tree.Node, opts: RenderOpts): LooseVNodes { return [ renderMoveOf(ctx, node, opts), - ...renderChildrenOf(ctx, node, { - parentPath: opts.parentPath + node.id, - isMainline: opts.isMainline, - }), + ...renderChildrenOf(ctx, node, { parentPath: opts.parentPath + node.id, isMainline: opts.isMainline }), ]; } @@ -213,12 +159,9 @@ function eventPath(e: Event): Tree.Path | null { return target.getAttribute('p') || (target.parentNode as HTMLElement).getAttribute('p'); } -export function render(ctrl: Controller): VNode { - const root = ctrl.getTree().root; - const ctx = { - ctrl: ctrl, - showComputer: false, - }; +export function render(ctrl: PuzzleCtrl): VNode { + const root = ctrl.tree.root; + const ctx = { ctrl: ctrl, showComputer: false }; return h( 'div.tview2.tview2-column', { @@ -234,23 +177,20 @@ export function render(ctrl: Controller): VNode { }); }, postpatch: (_, vnode) => { - if (ctrl.vm.autoScrollNow) { + if (ctrl.autoScrollNow) { autoScroll(ctrl, vnode.elm as HTMLElement); - ctrl.vm.autoScrollNow = false; + ctrl.autoScrollNow = false; + ctrl.autoScrollRequested = false; + } else if (ctrl.autoScrollRequested) { + if (ctrl.path !== treePath.root) autoScroll(ctrl, vnode.elm as HTMLElement); ctrl.autoScrollRequested = false; - } else if (ctrl.vm.autoScrollRequested) { - if (ctrl.vm.path !== treePath.root) autoScroll(ctrl, vnode.elm as HTMLElement); - ctrl.vm.autoScrollRequested = false; } }, }, }, [ ...(root.ply % 2 === 1 ? [renderIndex(root.ply, false), emptyMove()] : []), - ...renderChildrenOf(ctx, root, { - parentPath: '', - isMainline: true, - }), + ...renderChildrenOf(ctx, root, { parentPath: '', isMainline: true }), ], ); } diff --git a/ui/racer/package.json b/ui/racer/package.json index 4c45c4322d04c..a5bc0af91a9d7 100644 --- a/ui/racer/package.json +++ b/ui/racer/package.json @@ -13,7 +13,7 @@ "license": "AGPL-3.0-or-later", "dependencies": { "chess": "workspace:*", - "chessops": "^0.12.7", + "chessops": "^0.13.0", "common": "workspace:*", "puz": "workspace:*", "snabbdom": "^3.5.1" diff --git a/ui/racer/src/view/board.ts b/ui/racer/src/view/board.ts index c028ac4b882ea..934d0f057065e 100644 --- a/ui/racer/src/view/board.ts +++ b/ui/racer/src/view/board.ts @@ -23,11 +23,7 @@ const renderGround = (ctrl: RacerCtrl): VNode => makeCgConfig( ctrl.isRacing() && ctrl.isPlayer() ? makeCgOpts(ctrl.run, true, ctrl.flipped) - : { - fen: INITIAL_BOARD_FEN, - orientation: ctrl.run.pov, - movable: { color: ctrl.run.pov }, - }, + : { fen: INITIAL_BOARD_FEN, orientation: ctrl.run.pov, movable: { color: ctrl.run.pov } }, ctrl.pref, ctrl.userMove, ), @@ -39,15 +35,9 @@ const renderGround = (ctrl: RacerCtrl): VNode => const renderCountdown = (seconds: number) => h('div.racer__countdown', [ h('div.racer__countdown__lights', [ - h('light.red', { - class: { active: seconds > 4 }, - }), - h('light.orange', { - class: { active: seconds == 3 || seconds == 4 }, - }), - h('light.green', { - class: { active: seconds <= 2 }, - }), + h('light.red', { class: { active: seconds > 4 } }), + h('light.orange', { class: { active: seconds == 3 || seconds == 4 } }), + h('light.green', { class: { active: seconds <= 2 } }), ]), h('div.racer__countdown__seconds', seconds), ]); diff --git a/ui/racer/src/view/main.ts b/ui/racer/src/view/main.ts index 85ca1be37cbf1..cc800bbbfbe70 100644 --- a/ui/racer/src/view/main.ts +++ b/ui/racer/src/view/main.ts @@ -3,8 +3,8 @@ import RacerCtrl from '../ctrl'; import renderClock from 'puz/view/clock'; import renderHistory from 'puz/view/history'; import * as licon from 'common/licon'; -import { MaybeVNodes, bind } from 'common/snabbdom'; -import { h, VNode } from 'snabbdom'; +import { MaybeVNodes, bind, looseH as h } from 'common/snabbdom'; +import { VNode } from 'snabbdom'; import { playModifiers, renderCombo } from 'puz/view/util'; import { renderRace } from './race'; import { renderBoard } from './board'; @@ -13,12 +13,7 @@ import { povMessage } from 'puz/run'; export default function (ctrl: RacerCtrl): VNode { return h( 'div.racer.racer-app.racer--play', - { - class: { - ...playModifiers(ctrl.run), - [`racer--${ctrl.status()}`]: true, - }, - }, + { class: { ...playModifiers(ctrl.run), [`racer--${ctrl.status()}`]: true } }, [ renderBoard(ctrl), h('div.puz-side', selectScreen(ctrl)), @@ -46,7 +41,7 @@ const selectScreen = (ctrl: RacerCtrl): MaybeVNodes => { ), povMsg, ]), - ctrl.knowsSkip() ? null : renderSkip(ctrl), + !ctrl.knowsSkip() && renderSkip(ctrl), ]), comboZone(ctrl), ] @@ -92,12 +87,8 @@ const renderSkip = (ctrl: RacerCtrl) => h( 'button.racer__skip.button.button-red', { - class: { - disabled: !ctrl.canSkip(), - }, - attrs: { - title: ctrl.trans.noarg('skipExplanation'), - }, + class: { disabled: !ctrl.canSkip() }, + attrs: { title: ctrl.trans.noarg('skipExplanation') }, hook: bind('click', ctrl.skip), }, ctrl.trans.noarg('skip'), @@ -125,14 +116,8 @@ const renderControls = (ctrl: RacerCtrl): VNode => h( 'div.puz-side__control', h('a.puz-side__control__flip.button', { - class: { - active: ctrl.flipped, - 'button-empty': !ctrl.flipped, - }, - attrs: { - 'data-icon': licon.ChasingArrows, - title: ctrl.trans.noarg('flipBoard') + ' (Keyboard: f)', - }, + class: { active: ctrl.flipped, 'button-empty': !ctrl.flipped }, + attrs: { 'data-icon': licon.ChasingArrows, title: ctrl.trans.noarg('flipBoard') + ' (Keyboard: f)' }, hook: bind('click', ctrl.flip), }), ); @@ -141,7 +126,7 @@ const comboZone = (ctrl: RacerCtrl) => h('div.puz-side__table', [renderControls(ctrl), renderCombo(config, renderBonus)(ctrl.run)]); const playerScore = (ctrl: RacerCtrl): VNode => - h('div.puz-side__top.puz-side__solved', [h('div.puz-side__solved__text', ctrl.myScore() || 0)]); + h('div.puz-side__top.puz-side__solved', [h('div.puz-side__solved__text', `${ctrl.myScore() || 0}`)]); const renderLink = (ctrl: RacerCtrl) => h('div.puz-side__link', [ @@ -155,47 +140,33 @@ const renderLink = (ctrl: RacerCtrl) => }, }), h('button.copy.button', { - attrs: { - title: 'Copy URL', - 'data-rel': `racer-url-${ctrl.race.id}`, - 'data-icon': licon.Link, - }, + attrs: { title: 'Copy URL', 'data-rel': `racer-url-${ctrl.race.id}`, 'data-icon': licon.Link }, }), ]), ]); const renderStart = (ctrl: RacerCtrl) => - ctrl.isOwner() && !ctrl.vm.startsAt - ? h( - 'div.puz-side__start', - h( - 'button.button.button-fat', - { - class: { - disabled: ctrl.players().length < 2, - }, - hook: bind('click', ctrl.start), - attrs: { - disabled: ctrl.players().length < 2, - }, - }, - ctrl.trans.noarg('startTheRace'), - ), - ) - : null; - -const renderJoin = (ctrl: RacerCtrl) => + ctrl.isOwner() && + !ctrl.vm.startsAt && h( - 'div.puz-side__join', + 'div.puz-side__start', h( 'button.button.button-fat', { - hook: bind('click', ctrl.join), + class: { disabled: ctrl.players().length < 2 }, + hook: bind('click', ctrl.start), + attrs: { disabled: ctrl.players().length < 2 }, }, - ctrl.trans.noarg('joinTheRace'), + ctrl.trans.noarg('startTheRace'), ), ); +const renderJoin = (ctrl: RacerCtrl) => + h( + 'div.puz-side__join', + h('button.button.button-fat', { hook: bind('click', ctrl.join) }, ctrl.trans.noarg('joinTheRace')), + ); + const yourRank = (ctrl: RacerCtrl) => { const score = ctrl.myScore(); if (!score) return; @@ -207,53 +178,31 @@ const yourRank = (ctrl: RacerCtrl) => { const waitForRematch = (noarg: TransNoArg) => h( `a.racer__new-race.button.button-fat.button-navaway.disabled`, - { - attrs: { disabled: true }, - }, + { attrs: { disabled: true } }, noarg('waitForRematch'), ); const lobbyNext = (ctrl: RacerCtrl) => - h( - 'form', - { - attrs: { - action: '/racer/lobby', - method: 'post', - }, - }, - [ - h( - `button.racer__new-race.button.button-navaway${ctrl.race.lobby ? '.button-fat' : '.button-empty'}`, - ctrl.trans.noarg('nextRace'), - ), - ], - ); + h('form', { attrs: { action: '/racer/lobby', method: 'post' } }, [ + h( + `button.racer__new-race.button.button-navaway${ctrl.race.lobby ? '.button-fat' : '.button-empty'}`, + ctrl.trans.noarg('nextRace'), + ), + ]); const friendNext = (ctrl: RacerCtrl) => h('div.racer__post__next', [ h( `a.racer__rematch.button.button-fat.button-navaway`, - { - attrs: { href: `/racer/${ctrl.race.id}/rematch` }, - }, + { attrs: { href: `/racer/${ctrl.race.id}/rematch` } }, ctrl.trans.noarg('joinRematch'), ), h( 'form.racer__post__next__new', - { - attrs: { - action: '/racer', - method: 'post', - }, - }, + { attrs: { action: '/racer', method: 'post' } }, h( 'button.racer__post__next__button.button.button-empty', - { - attrs: { - type: 'submit', - }, - }, + { attrs: { type: 'submit' } }, ctrl.trans.noarg('createNewGame'), ), ), diff --git a/ui/racer/src/view/race.ts b/ui/racer/src/view/race.ts index e606422229988..d5bfc5b55447d 100644 --- a/ui/racer/src/view/race.ts +++ b/ui/racer/src/view/race.ts @@ -26,11 +26,7 @@ export const renderRace = (ctrl: RacerCtrl) => { }); return h( 'div.racer__race', - { - attrs: { - style: `height:${players.length * trackHeight + 14}px`, - }, - }, + { attrs: { style: `height:${players.length * trackHeight + 14}px` } }, h('div.racer__race__tracks', tracks), ); }; @@ -75,12 +71,4 @@ const renderTrack = ( export const playerLink = (player: PlayerWithScore, isMe: boolean) => player.id ? userLink({ ...player, line: false }) - : h( - 'anonymous', - { - attrs: { - title: 'Anonymous player', - }, - }, - [player.name, isMe ? ' (you)' : undefined], - ); + : h('anonymous', { attrs: { title: 'Anonymous player' } }, [player.name, isMe ? ' (you)' : undefined]); diff --git a/ui/round/src/boot.ts b/ui/round/src/boot.ts index 0efc79ef4eeb2..609e7da7516d4 100644 --- a/ui/round/src/boot.ts +++ b/ui/round/src/boot.ts @@ -72,7 +72,7 @@ export default async function (opts: RoundOpts, roundMain: (opts: RoundOpts, nvu const round: RoundApi = roundMain( opts, - lichess.blindMode ? await lichess.loadEsm('round.nvui') : undefined, + lichess.blindMode ? await lichess.asset.loadEsm('round.nvui') : undefined, ); const chatOpts = opts.chat; if (chatOpts) { diff --git a/ui/round/src/clock/clockView.ts b/ui/round/src/clock/clockView.ts index c8b305d419df0..e3c0221df7287 100644 --- a/ui/round/src/clock/clockView.ts +++ b/ui/round/src/clock/clockView.ts @@ -4,7 +4,8 @@ import * as game from 'game'; import RoundController from '../ctrl'; import { bind, justIcon } from '../util'; import { ClockElements, ClockController, Millis } from './clockCtrl'; -import { h, Hooks } from 'snabbdom'; +import { Hooks } from 'snabbdom'; +import { looseH as h } from 'common/snabbdom'; import { Position } from '../interfaces'; export function renderClock(ctrl: RoundController, player: game.Player, position: Position) { @@ -28,28 +29,12 @@ export function renderClock(ctrl: RoundController, player: game.Player, position // the player.color class ensures that when the board is flipped, the clock is redrawn. solves bug where clock // would be incorrectly latched to red color: https://github.com/lichess-org/lila/issues/10774 `div.rclock.rclock-${position}.rclock-${player.color}`, - { - class: { - outoftime: millis <= 0, - running: isRunning, - emerg: millis < clock.emergMs, - }, - }, + { class: { outoftime: millis <= 0, running: isRunning, emerg: millis < clock.emergMs } }, clock.opts.nvui - ? [ - h('div.time', { - attrs: { role: 'timer' }, - hook: timeHook, - }), - ] + ? [h('div.time', { attrs: { role: 'timer' }, hook: timeHook })] : [ clock.showBar && game.bothPlayersHavePlayed(ctrl.data) ? showBar(ctrl, player.color) : undefined, - h('div.time', { - class: { - hour: millis > 3600 * 1000, - }, - hook: timeHook, - }), + h('div.time', { class: { hour: millis > 3600 * 1000 }, hook: timeHook }), renderBerserk(ctrl, player.color, position), isPlayer ? goBerserk(ctrl) : button.moretime(ctrl), tourRank(ctrl, player.color, position), @@ -141,10 +126,7 @@ const goBerserk = (ctrl: RoundController) => { if (!game.berserkableBy(ctrl.data)) return; if (ctrl.goneBerserk[ctrl.data.player.color]) return; return h('button.fbt.go-berserk', { - attrs: { - title: 'GO BERSERK! Half the time, no increment, bonus point', - 'data-icon': licon.Berserk, - }, + attrs: { title: 'GO BERSERK! Half the time, no increment, bonus point', 'data-icon': licon.Berserk }, hook: bind('click', ctrl.goBerserk), }); }; @@ -152,13 +134,9 @@ const goBerserk = (ctrl: RoundController) => { const tourRank = (ctrl: RoundController, color: Color, position: Position) => { const d = ctrl.data, ranks = d.tournament?.ranks || d.swiss?.ranks; - return ranks && !showBerserk(ctrl, color) - ? h( - 'div.tour-rank.' + position, - { - attrs: { title: 'Current tournament rank' }, - }, - '#' + ranks[color], - ) - : null; + return ( + ranks && + !showBerserk(ctrl, color) && + h('div.tour-rank.' + position, { attrs: { title: 'Current tournament rank' } }, '#' + ranks[color]) + ); }; diff --git a/ui/round/src/corresClock/corresClockCtrl.ts b/ui/round/src/corresClock/corresClockCtrl.ts index 7bd179dfd98eb..b6e9d15b9d48a 100644 --- a/ui/round/src/corresClock/corresClockCtrl.ts +++ b/ui/round/src/corresClock/corresClockCtrl.ts @@ -9,14 +9,14 @@ export interface CorresClockData { showBar: boolean; } -export interface CorresClockController { - root: RoundController; - data: CorresClockData; - timePercent(color: Color): number; - update(white: Seconds, black: Seconds): void; - tick(color: Color): void; - millisOf(color: Color): Millis; -} +// export interface CorresClockController { +// root: RoundController; +// data: CorresClockData; +// timePercent(color: Color): number; +// update(white: Seconds, black: Seconds): void; +// tick(color: Color): void; +// millisOf(color: Color): Millis; +// } interface Times { white: Millis; @@ -24,41 +24,36 @@ interface Times { lastUpdate: Millis; } -export function ctrl( - root: RoundController, - data: CorresClockData, - onFlag: () => void, -): CorresClockController { - const timePercentDivisor = 0.1 / data.increment; - - const timePercent = (color: Color): number => Math.max(0, Math.min(100, times[color] * timePercentDivisor)); +export class CorresClockController { + timePercentDivisor: number; + times: Times; + + constructor( + readonly root: RoundController, + readonly data: CorresClockData, + private readonly onFlag: () => void, + ) { + this.timePercentDivisor = 0.1 / data.increment; + this.update(data.white, data.black); + } - let times: Times; + timePercent = (color: Color): number => + Math.max(0, Math.min(100, this.times[color] * this.timePercentDivisor)); - function update(white: Seconds, black: Seconds): void { - times = { + update = (white: Seconds, black: Seconds): void => { + this.times = { white: white * 1000, black: black * 1000, lastUpdate: performance.now(), }; - } - update(data.white, data.black); + }; - function tick(color: Color): void { + tick = (color: Color): void => { const now = performance.now(); - times[color] -= now - times.lastUpdate; - times.lastUpdate = now; - if (times[color] <= 0) onFlag(); - } - - const millisOf = (color: Color): Millis => Math.max(0, times[color]); - - return { - root, - data, - timePercent, - millisOf, - update, - tick, + this.times[color] -= now - this.times.lastUpdate; + this.times.lastUpdate = now; + if (this.times[color] <= 0) this.onFlag(); }; + + millisOf = (color: Color): Millis => Math.max(0, this.times[color]); } diff --git a/ui/round/src/corresClock/corresClockView.ts b/ui/round/src/corresClock/corresClockView.ts index 6438be30d107f..f38cd4e9d75d1 100644 --- a/ui/round/src/corresClock/corresClockView.ts +++ b/ui/round/src/corresClock/corresClockView.ts @@ -1,11 +1,11 @@ -import { h } from 'snabbdom'; +import { looseH as h } from 'common/snabbdom'; import { Millis } from '../clock/clockCtrl'; import { Position } from '../interfaces'; import { CorresClockController } from './corresClockCtrl'; import { moretime } from '../view/button'; const prefixInteger = (num: number, length: number): string => - (num / Math.pow(10, length)).toFixed(length).substr(2); + (num / Math.pow(10, length)).toFixed(length).slice(2); const bold = (x: string) => `${x}`; @@ -47,20 +47,10 @@ export default function ( direction = document.dir == 'rtl' && millis < 86400 * 1000 ? 'ltr' : undefined; return h( 'div.rclock.rclock-correspondence.rclock-' + position, - { - class: { - outoftime: millis <= 0, - running: runningColor === color, - }, - }, + { class: { outoftime: millis <= 0, running: runningColor === color } }, [ - ctrl.data.showBar - ? h('div.bar', [ - h('span', { - attrs: { style: `width: ${ctrl.timePercent(color)}%` }, - }), - ]) - : null, + ctrl.data.showBar && + h('div.bar', [h('span', { attrs: { style: `width: ${ctrl.timePercent(color)}%` } })]), h('div.time', { attrs: direction && { style: `direction: ${direction}` }, hook: { @@ -68,7 +58,7 @@ export default function ( postpatch: (_, vnode) => update(vnode.elm as HTMLElement), }, }), - isPlayer ? null : moretime(ctrl.root), + !isPlayer && moretime(ctrl.root), ], ); } diff --git a/ui/round/src/crazy/crazyCtrl.ts b/ui/round/src/crazy/crazyCtrl.ts index 241de06e840a6..b38fba1dd687e 100644 --- a/ui/round/src/crazy/crazyCtrl.ts +++ b/ui/round/src/crazy/crazyCtrl.ts @@ -138,6 +138,6 @@ export function init(ctrl: RoundController) { // Images are used in _zh.scss, which should be kept in sync. function preloadMouseIcons(data: RoundData) { const colorKey = data.player.color[0]; - for (const pKey of 'PNBRQ') fetch(lichess.assetUrl(`piece/cburnett/${colorKey}${pKey}.svg`)); + for (const pKey of 'PNBRQ') fetch(lichess.asset.url(`piece/cburnett/${colorKey}${pKey}.svg`)); mouseIconsLoaded = true; } diff --git a/ui/round/src/crazy/crazyView.ts b/ui/round/src/crazy/crazyView.ts index 598eb10393b7c..877d89121dee5 100644 --- a/ui/round/src/crazy/crazyView.ts +++ b/ui/round/src/crazy/crazyView.ts @@ -43,11 +43,7 @@ export default function pocket(ctrl: RoundController, color: Color, position: Po 'div.pocket-c2', h('piece.' + role + '.' + color, { class: { premove: activeColor && preDropRole === role }, - attrs: { - 'data-role': role, - 'data-color': color, - 'data-nb': nb, - }, + attrs: { 'data-role': role, 'data-color': color, 'data-nb': nb }, }), ), ); diff --git a/ui/round/src/ctrl.ts b/ui/round/src/ctrl.ts index c303d4476632e..eda73e2a4e176 100644 --- a/ui/round/src/ctrl.ts +++ b/ui/round/src/ctrl.ts @@ -16,7 +16,7 @@ import * as cg from 'chessground/types'; import { Config as CgConfig } from 'chessground/config'; import { Api as CgApi } from 'chessground/api'; import { ClockController } from './clock/clockCtrl'; -import { CorresClockController, ctrl as makeCorresClock } from './corresClock/corresClockCtrl'; +import { CorresClockController } from './corresClock/corresClockCtrl'; import MoveOn from './moveOn'; import TransientMove from './transientMove'; import * as atomic from './atomic'; @@ -616,7 +616,7 @@ export default class RoundController { lichess.pubsub.emit('challenge-app.open'); if (lichess.once('rematch-challenge')) setTimeout(() => { - lichess.hopscotch(function () { + lichess.asset.hopscotch(function () { window.hopscotch .configure({ i18n: { doneBtn: 'OK, got it' }, @@ -639,7 +639,7 @@ export default class RoundController { private makeCorrespondenceClock = (): void => { if (this.data.correspondence && !this.corresClock) - this.corresClock = makeCorresClock(this, this.data.correspondence, this.socket.outoftime); + this.corresClock = new CorresClockController(this, this.data.correspondence, this.socket.outoftime); }; private corresClockTick = (): void => { diff --git a/ui/round/src/plugins/nvui.ts b/ui/round/src/plugins/nvui.ts index 253f81be3fbf0..dcbd8ed97f9da 100644 --- a/ui/round/src/plugins/nvui.ts +++ b/ui/round/src/plugins/nvui.ts @@ -1,4 +1,5 @@ -import { h, VNode } from 'snabbdom'; +import { VNode } from 'snabbdom'; +import { looseH as h } from 'common/snabbdom'; import RoundController from '../ctrl'; import { renderClock } from '../clock/clockView'; import { renderTableWatch, renderTablePlay, renderTableEnd } from '../view/table'; @@ -80,221 +81,182 @@ export function initModule(): NvuiPlugin { ); if (variantNope) setTimeout(() => notify.set(variantNope), 3000); } - return h( - 'div.nvui', - { - hook: onInsert(_ => setTimeout(() => notify.set(gameText(ctrl)), 2000)), - }, - [ - h('h1', gameText(ctrl)), - h('h2', 'Game info'), - ...['white', 'black'].map((color: Color) => - h('p', [color + ' player: ', playerHtml(ctrl, ctrl.playerByColor(color))]), - ), - h('p', `${d.game.rated ? 'Rated' : 'Casual'} ${d.game.perf}`), - d.clock ? h('p', `Clock: ${d.clock.initial / 60} + ${d.clock.increment}`) : null, - h('h2', 'Moves'), - h( - 'p.moves', - { - attrs: { - role: 'log', - 'aria-live': 'off', - }, - }, - renderMoves(d.steps.slice(1), style), - ), - h('h2', 'Pieces'), - h('div.pieces', renderPieces(ctrl.chessground.state.pieces, style)), - h('h2', 'Game status'), - h( - 'div.status', - { - attrs: { - role: 'status', - 'aria-live': 'assertive', - 'aria-atomic': 'true', - }, - }, - [ctrl.data.game.status.name === 'started' ? 'Playing' : renderResult(ctrl)], - ), - h('h2', 'Last move'), - h( - 'p.lastMove', - { - attrs: { - 'aria-live': 'assertive', - 'aria-atomic': 'true', - }, - }, - // make sure consecutive moves are different so that they get re-read - renderSan(step.san, step.uci, style) + (ctrl.ply % 2 === 0 ? '' : ' '), - ), - ...(ctrl.isPlaying() - ? [ - h('h2', 'Move form'), - h( - 'form', - { - hook: onInsert(el => { - const $form = $(el as HTMLFormElement), - $input = $form.find('.move').val(''); - $input[0]!.focus(); - nvui.submitMove = createSubmitHandler(ctrl, notify.set, moveStyle.get, $input); - $form.on('submit', () => nvui.submitMove?.()); - }), - }, - [ - h('label', [ - d.player.color === d.game.player ? 'Your move' : 'Waiting', - h('input.move.mousetrap', { - attrs: { - name: 'move', - type: 'text', - autocomplete: 'off', - autofocus: true, - disabled: !!variantNope, - title: variantNope, - }, - }), - ]), - ], - ), - ] - : []), - h('h2', 'Your clock'), - h('div.botc', anyClock(ctrl, 'bottom')), - h('h2', 'Opponent clock'), - h('div.topc', anyClock(ctrl, 'top')), - notify.render(), - h('h2', 'Actions'), - ...(ctrl.data.player.spectator - ? renderTableWatch(ctrl) - : game.playable(ctrl.data) - ? renderTablePlay(ctrl) - : renderTableEnd(ctrl)), - h('h2', 'Board'), + return h('div.nvui', { hook: onInsert(_ => setTimeout(() => notify.set(gameText(ctrl)), 2000)) }, [ + h('h1', gameText(ctrl)), + h('h2', 'Game info'), + ...['white', 'black'].map((color: Color) => + h('p', [color + ' player: ', playerHtml(ctrl, ctrl.playerByColor(color))]), + ), + h('p', `${d.game.rated ? 'Rated' : 'Casual'} ${d.game.perf}`), + d.clock ? h('p', `Clock: ${d.clock.initial / 60} + ${d.clock.increment}`) : null, + h('h2', 'Moves'), + h('p.moves', { attrs: { role: 'log', 'aria-live': 'off' } }, renderMoves(d.steps.slice(1), style)), + h('h2', 'Pieces'), + h('div.pieces', renderPieces(ctrl.chessground.state.pieces, style)), + h('h2', 'Game status'), + h('div.status', { attrs: { role: 'status', 'aria-live': 'assertive', 'aria-atomic': 'true' } }, [ + ctrl.data.game.status.name === 'started' ? 'Playing' : renderResult(ctrl), + ]), + h('h2', 'Last move'), + h( + 'p.lastMove', + { attrs: { 'aria-live': 'assertive', 'aria-atomic': 'true' } }, + // make sure consecutive moves are different so that they get re-read + renderSan(step.san, step.uci, style) + (ctrl.ply % 2 === 0 ? '' : ' '), + ), + ctrl.isPlaying() && h('h2', 'Move form'), + ctrl.isPlaying() && h( - 'div.board', + 'form', { hook: onInsert(el => { - const $board = $(el as HTMLElement); - $board.on('keypress', () => console.log(ctrl)); - // NOTE: This is the only line different from analysis board listener setup - const $buttons = $board.find('button'); - $buttons.on( - 'click', - selectionHandler(() => ctrl.data.opponent.color, selectSound), - ); - $buttons.on('keydown', arrowKeyHandler(ctrl.data.player.color, borderSound)); - $buttons.on('keypress', boardCommandsHandler()); - $buttons.on( - 'keypress', - lastCapturedCommandHandler( - () => ctrl.data.steps.map(step => step.fen), - pieceStyle.get(), - prefixStyle.get(), - ), - ); - $buttons.on( - 'keypress', - possibleMovesHandler( - ctrl.data.player.color, - () => ctrl.chessground.state.turnColor, - ctrl.chessground.getFen, - () => ctrl.chessground.state.pieces, - ctrl.data.game.variant.key, - () => ctrl.chessground.state.movable.dests, - () => ctrl.data.steps, - ), - ); - $buttons.on('keypress', positionJumpHandler()); - $buttons.on('keypress', pieceJumpingHandler(selectSound, errorSound)); + const $form = $(el as HTMLFormElement), + $input = $form.find('.move').val(''); + $input[0]!.focus(); + nvui.submitMove = createSubmitHandler(ctrl, notify.set, moveStyle.get, $input); + $form.on('submit', () => nvui.submitMove?.()); }), }, - renderBoard( - ctrl.chessground.state.pieces, - ctrl.data.player.color, - pieceStyle.get(), - prefixStyle.get(), - positionStyle.get(), - boardStyle.get(), - ), + [ + h('label', [ + d.player.color === d.game.player ? 'Your move' : 'Waiting', + h('input.move.mousetrap', { + attrs: { + name: 'move', + type: 'text', + autocomplete: 'off', + autofocus: true, + disabled: !!variantNope, + title: variantNope, + }, + }), + ]), + ], ), - h( - 'div.boardstatus', - { - attrs: { - 'aria-live': 'polite', - 'aria-atomic': 'true', - }, - }, - '', + + h('h2', 'Your clock'), + h('div.botc', anyClock(ctrl, 'bottom')), + h('h2', 'Opponent clock'), + h('div.topc', anyClock(ctrl, 'top')), + notify.render(), + h('h2', 'Actions'), + ...(ctrl.data.player.spectator + ? renderTableWatch(ctrl) + : game.playable(ctrl.data) + ? renderTablePlay(ctrl) + : renderTableEnd(ctrl)), + h('h2', 'Board'), + h( + 'div.board', + { + hook: onInsert(el => { + const $board = $(el as HTMLElement); + $board.on('keypress', () => console.log(ctrl)); + // NOTE: This is the only line different from analysis board listener setup + const $buttons = $board.find('button'); + $buttons.on( + 'click', + selectionHandler(() => ctrl.data.opponent.color, selectSound), + ); + $buttons.on('keydown', arrowKeyHandler(ctrl.data.player.color, borderSound)); + $buttons.on('keypress', boardCommandsHandler()); + $buttons.on( + 'keypress', + lastCapturedCommandHandler( + () => ctrl.data.steps.map(step => step.fen), + pieceStyle.get(), + prefixStyle.get(), + ), + ); + $buttons.on( + 'keypress', + possibleMovesHandler( + ctrl.data.player.color, + () => ctrl.chessground.state.turnColor, + ctrl.chessground.getFen, + () => ctrl.chessground.state.pieces, + ctrl.data.game.variant.key, + () => ctrl.chessground.state.movable.dests, + () => ctrl.data.steps, + ), + ); + $buttons.on('keypress', positionJumpHandler()); + $buttons.on('keypress', pieceJumpingHandler(selectSound, errorSound)); + }), + }, + renderBoard( + ctrl.chessground.state.pieces, + ctrl.data.player.color, + pieceStyle.get(), + prefixStyle.get(), + positionStyle.get(), + boardStyle.get(), ), - // h('p', takes(ctrl.data.steps.map(data => data.fen))), - h('h2', 'Settings'), - h('label', ['Move notation', renderSetting(moveStyle, ctrl.redraw)]), - h('h3', 'Board settings'), - h('label', ['Piece style', renderSetting(pieceStyle, ctrl.redraw)]), - h('label', ['Piece prefix style', renderSetting(prefixStyle, ctrl.redraw)]), - h('label', ['Show position', renderSetting(positionStyle, ctrl.redraw)]), - h('label', ['Board layout', renderSetting(boardStyle, ctrl.redraw)]), - h('h2', 'Commands'), - h('p', [ - 'Type these commands in the move input.', - h('br'), - 'c: Read clocks.', - h('br'), - 'l: Read last move.', - h('br'), - 'o: Read name and rating of the opponent.', - h('br'), - commands.piece.help, - h('br'), - commands.scan.help, - h('br'), - 'abort: Abort game.', - h('br'), - 'resign: Resign game.', - h('br'), - 'draw: Offer or accept draw.', - h('br'), - 'takeback: Offer or accept take back.', - h('br'), - ]), - h('h2', 'Board mode commands'), - h('p', [ - 'Use these commands when focused on the board itself.', - h('br'), - 'o: announce current position.', - h('br'), - "c: announce last move's captured piece.", - h('br'), - 'l: announce last move.', - h('br'), - 't: announce clocks.', - h('br'), - 'm: announce possible moves for the selected piece.', - h('br'), - 'shift+m: announce possible moves for the selected pieces which capture..', - h('br'), - 'arrow keys: move left, right, up or down.', - h('br'), - 'kqrbnp/KQRBNP: move forward/backward to a piece.', - h('br'), - '1-8: move to rank 1-8.', - h('br'), - 'Shift+1-8: move to file a-h.', - h('br'), - ]), - h('h2', 'Promotion'), - h('p', [ - 'Standard PGN notation selects the piece to promote to. Example: a8=n promotes to a knight.', - h('br'), - 'Omission results in promotion to queen', - ]), - ], - ); + ), + h('div.boardstatus', { attrs: { 'aria-live': 'polite', 'aria-atomic': 'true' } }, ''), + // h('p', takes(ctrl.data.steps.map(data => data.fen))), + h('h2', 'Settings'), + h('label', ['Move notation', renderSetting(moveStyle, ctrl.redraw)]), + h('h3', 'Board settings'), + h('label', ['Piece style', renderSetting(pieceStyle, ctrl.redraw)]), + h('label', ['Piece prefix style', renderSetting(prefixStyle, ctrl.redraw)]), + h('label', ['Show position', renderSetting(positionStyle, ctrl.redraw)]), + h('label', ['Board layout', renderSetting(boardStyle, ctrl.redraw)]), + h('h2', 'Commands'), + h('p', [ + 'Type these commands in the move input.', + h('br'), + 'c: Read clocks.', + h('br'), + 'l: Read last move.', + h('br'), + 'o: Read name and rating of the opponent.', + h('br'), + commands.piece.help, + h('br'), + commands.scan.help, + h('br'), + 'abort: Abort game.', + h('br'), + 'resign: Resign game.', + h('br'), + 'draw: Offer or accept draw.', + h('br'), + 'takeback: Offer or accept take back.', + h('br'), + ]), + h('h2', 'Board mode commands'), + h('p', [ + 'Use these commands when focused on the board itself.', + h('br'), + 'o: announce current position.', + h('br'), + "c: announce last move's captured piece.", + h('br'), + 'l: announce last move.', + h('br'), + 't: announce clocks.', + h('br'), + 'm: announce possible moves for the selected piece.', + h('br'), + 'shift+m: announce possible moves for the selected pieces which capture..', + h('br'), + 'arrow keys: move left, right, up or down.', + h('br'), + 'kqrbnp/KQRBNP: move forward/backward to a piece.', + h('br'), + '1-8: move to rank 1-8.', + h('br'), + 'Shift+1-8: move to file a-h.', + h('br'), + ]), + h('h2', 'Promotion'), + h('p', [ + 'Standard PGN notation selects the piece to promote to. Example: a8=n promotes to a knight.', + h('br'), + 'Omission results in promotion to queen', + ]), + ]); }, }; } @@ -417,9 +379,7 @@ function playerHtml(ctrl: RoundController, player: game.Player) { ? h('span', [ h( 'a', - { - attrs: { href: '/@/' + user.username }, - }, + { attrs: { href: '/@/' + user.username } }, user.title ? `${user.title} ${user.username}` : user.username, ), rating ? ` ${rating}` : ``, diff --git a/ui/round/src/tourStanding.ts b/ui/round/src/tourStanding.ts index a6ecb7ab2b012..d8aa65d59d9da 100644 --- a/ui/round/src/tourStanding.ts +++ b/ui/round/src/tourStanding.ts @@ -21,51 +21,26 @@ export const tourStandingCtrl = ( name: name, }, view(): VNode { - return h( - 'div', - { - hook: onInsert(_ => lichess.loadCssPath('round.tour-standing')), - }, - [ - team - ? h( - 'h3.text', - { - attrs: { 'data-icon': licon.Group }, - }, - team.name, - ) - : null, - h('table.slist', [ - h( - 'tbody', - players.map((p: TourPlayer, i: number) => { - return h('tr.' + p.n, [ - h('td.name', [ - h('span.rank', '' + (i + 1)), - h( - 'a.user-link.ulpt', - { - attrs: { href: `/@/${p.n}` }, - }, - (p.t ? p.t + ' ' : '') + p.n, - ), - ]), - h( - 'td.total', - p.f - ? { - class: { 'is-gold': true }, - attrs: { 'data-icon': licon.Fire }, - } - : {}, - '' + p.s, - ), - ]); - }), - ), - ]), - ], - ); + return h('div', { hook: onInsert(_ => lichess.asset.loadCssPath('round.tour-standing')) }, [ + team ? h('h3.text', { attrs: { 'data-icon': licon.Group } }, team.name) : null, + h('table.slist', [ + h( + 'tbody', + players.map((p: TourPlayer, i: number) => { + return h('tr.' + p.n, [ + h('td.name', [ + h('span.rank', '' + (i + 1)), + h('a.user-link.ulpt', { attrs: { href: `/@/${p.n}` } }, (p.t ? p.t + ' ' : '') + p.n), + ]), + h( + 'td.total', + p.f ? { class: { 'is-gold': true }, attrs: { 'data-icon': licon.Fire } } : {}, + '' + p.s, + ), + ]); + }), + ), + ]), + ]); }, }); diff --git a/ui/round/src/view/button.ts b/ui/round/src/view/button.ts index 6bb3af79e1766..f47981acd0937 100644 --- a/ui/round/src/view/button.ts +++ b/ui/round/src/view/button.ts @@ -1,4 +1,4 @@ -import { h, VNode, Hooks } from 'snabbdom'; +import { VNode, Hooks } from 'snabbdom'; import * as licon from 'common/licon'; import { spinnerVdom as spinner } from 'common/spinner'; import * as util from '../util'; @@ -8,7 +8,7 @@ import { game as gameRoute } from 'game/router'; import { RoundData } from '../interfaces'; import { ClockData } from '../clock/clockCtrl'; import RoundController from '../ctrl'; -import { MaybeVNodes } from 'common/snabbdom'; +import { LooseVNodes, looseH as h } from 'common/snabbdom'; export interface ButtonState { enabled: boolean; @@ -23,25 +23,26 @@ function poolUrl(clock: ClockData, blocking?: game.PlayerUser) { return '/#pool/' + clock.initial / 60 + '+' + clock.increment + (blocking ? '/' + blocking.id : ''); } -function analysisButton(ctrl: RoundController): VNode | null { +function analysisButton(ctrl: RoundController): VNode | false { const d = ctrl.data, url = gameRoute(d, analysisBoardOrientation(d)) + '#' + ctrl.ply; - return game.replayable(d) - ? h( - 'a.fbt', - { - attrs: { href: url }, - hook: util.bind('click', _ => { - // force page load in case the URL is the same - if (location.pathname === url.split('#')[0]) location.reload(); - }), - }, - ctrl.noarg('analysis'), - ) - : null; + return ( + game.replayable(d) && + h( + 'a.fbt', + { + attrs: { href: url }, + hook: util.bind('click', _ => { + // force page load in case the URL is the same + if (location.pathname === url.split('#')[0]) location.reload(); + }), + }, + ctrl.noarg('analysis'), + ) + ); } -function rematchButtons(ctrl: RoundController): MaybeVNodes { +function rematchButtons(ctrl: RoundController): LooseVNodes { const d = ctrl.data, me = !!d.player.offeringRematch, disabled = !me && !d.opponent.onGame && (!!d.clock || !d.player.user || !d.opponent.user), @@ -49,27 +50,19 @@ function rematchButtons(ctrl: RoundController): MaybeVNodes { noarg = ctrl.noarg; if (!game.rematchable(d)) return []; return [ - them - ? h( - 'button.rematch-decline', - { - attrs: { - 'data-icon': licon.X, - title: noarg('decline'), - }, - hook: util.bind('click', () => ctrl.socket.send('rematch-no')), - }, - ctrl.nvui ? noarg('decline') : '', - ) - : null, + them && + h( + 'button.rematch-decline', + { + attrs: { 'data-icon': licon.X, title: noarg('decline') }, + hook: util.bind('click', () => ctrl.socket.send('rematch-no')), + }, + ctrl.nvui ? noarg('decline') : '', + ), h( 'button.fbt.rematch.white', { - class: { - me, - glowing: them, - disabled, - }, + class: { me, glowing: them, disabled }, attrs: { title: them ? noarg('yourOpponentWantsToPlayANewGameWithYou') : me ? noarg('rematchOfferSent') : '', }, @@ -83,8 +76,8 @@ function rematchButtons(ctrl: RoundController): MaybeVNodes { ctrl.socket.send('rematch-no'); } else if (d.opponent.onGame || !d.clock) { d.player.offeringRematch = true; - ctrl.socket.send('rematch-yes'); - if (!disabled && !d.opponent.onGame) ctrl.challengeRematch(); + if (d.opponent.onGame) ctrl.socket.send('rematch-yes'); + else if (!disabled && !d.opponent.onGame) ctrl.challengeRematch(); } }, ctrl.redraw, @@ -109,11 +102,8 @@ export function standard( return h( 'button.fbt.' + socketMsg, { - attrs: { - disabled: !enabled(), - title: ctrl.noarg(hintFn()), - }, - hook: util.bind('click', _ => { + attrs: { disabled: !enabled(), title: ctrl.noarg(hintFn()) }, + hook: util.bind('click', () => { if (enabled()) onclick ? onclick() : ctrl.socket.sendLoading(socketMsg); }), }, @@ -129,24 +119,20 @@ export function opponentGone(ctrl: RoundController) { h('p', { hook: onSuggestionHook }, ctrl.noarg('opponentLeftChoices')), h( 'button.button', - { - hook: util.bind('click', () => ctrl.socket.sendLoading('resign-force')), - }, + { hook: util.bind('click', () => ctrl.socket.sendLoading('resign-force')) }, ctrl.noarg('forceResignation'), ), h( 'button.button', - { - hook: util.bind('click', () => ctrl.socket.sendLoading('draw-force')), - }, + { hook: util.bind('click', () => ctrl.socket.sendLoading('draw-force')) }, ctrl.noarg('forceDraw'), ), ]) - : gone - ? h('div.suggestion', [ - h('p', ctrl.trans.vdomPlural('opponentLeftCounter', gone, h('strong', '' + gone))), - ]) - : null; + : gone && + h( + 'div.suggestion', + h('p', ctrl.trans.vdomPlural('opponentLeftCounter', gone, h('strong', '' + gone))), + ); } const fbtCancel = (ctrl: RoundController, f: (v: boolean) => void) => @@ -184,89 +170,70 @@ export const claimThreefold = (ctrl: RoundController, condition: (d: RoundData) title: ctrl.noarg(condition(ctrl.data)?.overrideHint || 'claimADraw'), disabled: !condition(ctrl.data).enabled, }, - class: { - disabled: !condition(ctrl.data).enabled, - }, + class: { disabled: !condition(ctrl.data).enabled }, }, h('span', '½'), ); export function threefoldSuggestion(ctrl: RoundController) { - return ctrl.data.game.threefold - ? h('div.suggestion', [ - h( - 'p', - { - hook: onSuggestionHook, - }, - ctrl.noarg('threefoldRepetition'), - ), - ]) - : null; + return ( + ctrl.data.game.threefold && + h('div.suggestion', [h('p', { hook: onSuggestionHook }, ctrl.noarg('threefoldRepetition'))]) + ); } -export function backToTournament(ctrl: RoundController): VNode | undefined { +export function backToTournament(ctrl: RoundController) { const d = ctrl.data; - return d.tournament?.running - ? h('div.follow-up', [ - h( - 'a.text.fbt.strong.glowing', - { - attrs: { - 'data-icon': licon.PlayTriangle, - href: '/tournament/' + d.tournament.id, - }, - hook: util.bind('click', ctrl.setRedirecting), - }, - ctrl.noarg('backToTournament'), - ), - h( - 'form', - { - attrs: { - method: 'post', - action: '/tournament/' + d.tournament.id + '/withdraw', - }, - }, - [h('button.text.fbt.weak', util.justIcon(licon.Pause), 'Pause')], - ), - analysisButton(ctrl), - ]) - : undefined; + return ( + d.tournament?.running && + h('div.follow-up', [ + h( + 'a.text.fbt.strong.glowing', + { + attrs: { 'data-icon': licon.PlayTriangle, href: '/tournament/' + d.tournament.id }, + hook: util.bind('click', ctrl.setRedirecting), + }, + ctrl.noarg('backToTournament'), + ), + h('form', { attrs: { method: 'post', action: '/tournament/' + d.tournament.id + '/withdraw' } }, [ + h('button.text.fbt.weak', util.justIcon(licon.Pause), 'Pause'), + ]), + analysisButton(ctrl), + ]) + ); } -export function backToSwiss(ctrl: RoundController): VNode | undefined { +export function backToSwiss(ctrl: RoundController) { const d = ctrl.data; - return d.swiss?.running - ? h('div.follow-up', [ - h( - 'a.text.fbt.strong.glowing', - { - attrs: { - 'data-icon': licon.PlayTriangle, - href: '/swiss/' + d.swiss.id, - }, - hook: util.bind('click', ctrl.setRedirecting), - }, - ctrl.noarg('backToTournament'), - ), - analysisButton(ctrl), - ]) - : undefined; + return ( + d.swiss?.running && + h('div.follow-up', [ + h( + 'a.text.fbt.strong.glowing', + { + attrs: { 'data-icon': licon.PlayTriangle, href: '/swiss/' + d.swiss.id }, + hook: util.bind('click', ctrl.setRedirecting), + }, + ctrl.noarg('backToTournament'), + ), + analysisButton(ctrl), + ]) + ); } export function moretime(ctrl: RoundController) { - return game.moretimeable(ctrl.data) - ? h('a.moretime', { - attrs: { - title: ctrl.data.clock - ? ctrl.trans('giveNbSeconds', ctrl.data.clock.moretime) - : ctrl.noarg('giveMoreTime'), - 'data-icon': licon.PlusButton, - }, - hook: util.bind('click', ctrl.socket.moreTime), - }) - : null; + return ( + game.moretimeable(ctrl.data) && + h('a.moretime', { + attrs: { + title: ctrl.data.clock + ? ctrl.trans('giveNbSeconds', ctrl.data.clock.moretime) + : ctrl.noarg('giveMoreTime'), + 'data-icon': licon.PlusButton, + }, + hook: util.bind('click', ctrl.socket.moreTime), + }) + ); } export function followUp(ctrl: RoundController): VNode { @@ -284,75 +251,39 @@ export function followUp(ctrl: RoundController): VNode { rematchZone = rematchable || d.game.rematch ? rematchButtons(ctrl) : []; return h('div.follow-up', [ ...rematchZone, - d.tournament - ? h( - 'a.fbt', - { - attrs: { href: '/tournament/' + d.tournament.id }, - }, - ctrl.noarg('viewTournament'), - ) - : null, - d.swiss - ? h( - 'a.fbt', - { - attrs: { href: '/swiss/' + d.swiss.id }, - }, - ctrl.noarg('viewTournament'), - ) - : null, - newable - ? h( - 'a.fbt', - { - attrs: { - href: - d.game.source === 'pool' ? poolUrl(d.clock!, d.opponent.user) : '/?hook_like=' + d.game.id, - }, + d.tournament && + h('a.fbt', { attrs: { href: '/tournament/' + d.tournament.id } }, ctrl.noarg('viewTournament')), + d.swiss && h('a.fbt', { attrs: { href: '/swiss/' + d.swiss.id } }, ctrl.noarg('viewTournament')), + newable && + h( + 'a.fbt', + { + attrs: { + href: d.game.source === 'pool' ? poolUrl(d.clock!, d.opponent.user) : '/?hook_like=' + d.game.id, }, - ctrl.noarg('newOpponent'), - ) - : null, + }, + ctrl.noarg('newOpponent'), + ), analysisButton(ctrl), ]); } -export function watcherFollowUp(ctrl: RoundController): VNode | null { +export function watcherFollowUp(ctrl: RoundController) { const d = ctrl.data, content = [ - d.game.rematch - ? h( - 'a.fbt.text', - { - attrs: { - href: `/${d.game.rematch}/${d.opponent.color}`, - }, - }, - ctrl.noarg('viewRematch'), - ) - : null, - d.tournament - ? h( - 'a.fbt', - { - attrs: { href: '/tournament/' + d.tournament.id }, - }, - ctrl.noarg('viewTournament'), - ) - : null, - d.swiss - ? h( - 'a.fbt', - { - attrs: { href: '/swiss/' + d.swiss.id }, - }, - ctrl.noarg('viewTournament'), - ) - : null, + d.game.rematch && + h( + 'a.fbt.text', + { attrs: { href: `/${d.game.rematch}/${d.opponent.color}` } }, + ctrl.noarg('viewRematch'), + ), + d.tournament && + h('a.fbt', { attrs: { href: '/tournament/' + d.tournament.id } }, ctrl.noarg('viewTournament')), + + d.swiss && h('a.fbt', { attrs: { href: '/swiss/' + d.swiss.id } }, ctrl.noarg('viewTournament')), analysisButton(ctrl), ]; - return content.find(x => !!x) ? h('div.follow-up', content) : null; + return content.find(x => !!x) && h('div.follow-up', content); } const onSuggestionHook: Hooks = util.onInsert(el => lichess.pubsub.emit('round.suggestion', el.textContent)); diff --git a/ui/round/src/view/expiration.ts b/ui/round/src/view/expiration.ts index b06ccaaae574a..6d9415d249e3f 100644 --- a/ui/round/src/view/expiration.ts +++ b/ui/round/src/view/expiration.ts @@ -19,12 +19,7 @@ export default function (ctrl: RoundController): MaybeVNode { const side = myTurn != ctrl.flip ? 'bottom' : 'top'; return h( 'div.expiration.expiration-' + side, - { - class: { - emerg, - 'bar-glider': myTurn, - }, - }, + { class: { emerg, 'bar-glider': myTurn } }, ctrl.trans.vdomPlural('nbSecondsToPlayTheFirstMove', secondsLeft, h('strong', '' + secondsLeft)), ); } diff --git a/ui/round/src/view/main.ts b/ui/round/src/view/main.ts index 030a13b23fa2b..2a69ef6faaaaf 100644 --- a/ui/round/src/view/main.ts +++ b/ui/round/src/view/main.ts @@ -3,7 +3,8 @@ import * as util from '../util'; import crazyView from '../crazy/crazyView'; import RoundController from '../ctrl'; import { stepwiseScroll } from 'common/scroll'; -import { h, VNode } from 'snabbdom'; +import { VNode } from 'snabbdom'; +import { looseH as h } from 'common/snabbdom'; import { render as renderKeyboardMove } from 'keyboardMove'; import { render as renderGround } from '../ground'; import { renderTable } from './table'; @@ -48,8 +49,8 @@ export function main(ctrl: RoundController): VNode { }, [renderGround(ctrl), ctrl.promotion.view(ctrl.data.game.variant.key === 'antichess')], ), - ctrl.voiceMove ? renderVoiceBar(ctrl.voiceMove.ui, ctrl.redraw) : null, - ctrl.keyboardHelp ? keyboard.view(ctrl) : null, + ctrl.voiceMove && renderVoiceBar(ctrl.voiceMove.ui, ctrl.redraw), + ctrl.keyboardHelp && keyboard.view(ctrl), crazyView(ctrl, topColor, 'top') || materialDiffs[0], ...renderTable(ctrl), crazyView(ctrl, bottomColor, 'bottom') || materialDiffs[1], diff --git a/ui/round/src/view/replay.ts b/ui/round/src/view/replay.ts index f544742edada1..5600fc46c6322 100644 --- a/ui/round/src/view/replay.ts +++ b/ui/round/src/view/replay.ts @@ -8,10 +8,10 @@ import RoundController from '../ctrl'; import throttle from 'common/throttle'; import viewStatus from 'game/view/status'; import { game as gameRoute } from 'game/router'; -import { h, VNode } from 'snabbdom'; +import { VNode } from 'snabbdom'; import { Step } from '../interfaces'; import { toggleButton as boardMenuToggleButton } from 'board/menu'; -import { MaybeVNodes } from 'common/snabbdom'; +import { LooseVNodes, looseH as h } from 'common/snabbdom'; import boardMenu from './boardMenu'; const scrollMax = 99999, @@ -42,34 +42,15 @@ const autoScroll = throttle(100, (movesEl: HTMLElement, ctrl: RoundController) = }), ); -const renderDrawOffer = () => - h( - 'draw', - { - attrs: { - title: 'Draw offer', - }, - }, - '½?', - ); +const renderDrawOffer = () => h('draw', { attrs: { title: 'Draw offer' } }, '½?'); const renderMove = (step: Step, curPly: number, orEmpty: boolean, drawOffers: Set) => step - ? h( - moveTag, - { - class: { - a1t: step.ply === curPly, - }, - }, - [ - step.san[0] === 'P' ? step.san.slice(1) : step.san, - drawOffers.has(step.ply) ? renderDrawOffer() : undefined, - ], - ) - : orEmpty - ? h(moveTag, '…') - : undefined; + ? h(moveTag, { class: { a1t: step.ply === curPly } }, [ + step.san[0] === 'P' ? step.san.slice(1) : step.san, + drawOffers.has(step.ply) ? renderDrawOffer() : undefined, + ]) + : orEmpty && h(moveTag, '…'); export function renderResult(ctrl: RoundController): VNode | undefined { let result: string | undefined; @@ -102,7 +83,7 @@ export function renderResult(ctrl: RoundController): VNode | undefined { return; } -function renderMoves(ctrl: RoundController): MaybeVNodes { +function renderMoves(ctrl: RoundController): LooseVNodes { const steps = ctrl.data.steps, firstPly = round.firstPly(ctrl.data), lastPly = round.lastPly(ctrl.data), @@ -119,7 +100,7 @@ function renderMoves(ctrl: RoundController): MaybeVNodes { } for (let i = startAt; i < steps.length; i += 2) pairs.push([steps[i], steps[i + 1]]); - const els: MaybeVNodes = [], + const els: LooseVNodes = [], curPly = ctrl.ply; for (let i = 0; i < pairs.length; i++) { els.push(h(indexTag, i + indexOffset + '')); @@ -131,24 +112,23 @@ function renderMoves(ctrl: RoundController): MaybeVNodes { return els; } -export function analysisButton(ctrl: RoundController): VNode | undefined { +export function analysisButton(ctrl: RoundController) { const forecastCount = ctrl.data.forecastCount; - return game.userAnalysable(ctrl.data) - ? h( - 'a.fbt.analysis', - { - class: { - text: !!forecastCount, - }, - attrs: { - title: ctrl.noarg('analysis'), - href: gameRoute(ctrl.data, ctrl.data.player.color) + '/analysis#' + ctrl.ply, - 'data-icon': licon.Microscope, - }, + return ( + game.userAnalysable(ctrl.data) && + h( + 'a.fbt.analysis', + { + class: { text: !!forecastCount }, + attrs: { + title: ctrl.noarg('analysis'), + href: gameRoute(ctrl.data, ctrl.data.player.color) + '/analysis#' + ctrl.ply, + 'data-icon': licon.Microscope, }, - forecastCount ? ['' + forecastCount] : [], - ) - : undefined; + }, + forecastCount ? ['' + forecastCount] : [], + ) + ); } function renderButtons(ctrl: RoundController) { @@ -180,11 +160,7 @@ function renderButtons(ctrl: RoundController) { const enabled = ctrl.ply !== b[1] && (b[1] as number) >= firstPly && (b[1] as number) <= lastPly; return h('button.fbt', { class: { glowing: i === 3 && ctrl.isLate() }, - attrs: { - disabled: !enabled, - 'data-icon': b[0], - 'data-ply': enabled ? b[1] : '-', - }, + attrs: { disabled: !enabled, 'data-icon': b[0], 'data-ply': enabled ? b[1] : '-' }, }); }), boardMenuToggleButton(ctrl.menu, ctrl.noarg('menu')), @@ -194,26 +170,23 @@ function renderButtons(ctrl: RoundController) { function initMessage(ctrl: RoundController) { const d = ctrl.data; - return (ctrl.replayEnabledByPref() || !isCol1()) && + return ( + (ctrl.replayEnabledByPref() || !isCol1()) && game.playable(d) && d.game.turns === 0 && - !d.player.spectator - ? h('div.message', util.justIcon(licon.InfoCircle), [ - h('div', [ - ctrl.trans(d.player.color === 'white' ? 'youPlayTheWhitePieces' : 'youPlayTheBlackPieces'), - ...(d.player.color === 'white' ? [h('br'), h('strong', ctrl.trans('itsYourTurn'))] : []), - ]), - ]) - : null; + !d.player.spectator && + h('div.message', util.justIcon(licon.InfoCircle), [ + h('div', [ + ctrl.trans(d.player.color === 'white' ? 'youPlayTheWhitePieces' : 'youPlayTheBlackPieces'), + ...(d.player.color === 'white' ? [h('br'), h('strong', ctrl.trans('itsYourTurn'))] : []), + ]), + ]) + ); } const col1Button = (ctrl: RoundController, dir: number, icon: string, disabled: boolean) => h('button.fbt', { - attrs: { - disabled: disabled, - 'data-icon': icon, - 'data-ply': ctrl.ply + dir, - }, + attrs: { disabled: disabled, 'data-icon': icon, 'data-ply': ctrl.ply + dir }, hook: util.bind('mousedown', e => { e.preventDefault(); ctrl.userJump(ctrl.ply + dir); @@ -221,7 +194,7 @@ const col1Button = (ctrl: RoundController, dir: number, icon: string, disabled: }), }); -export function render(ctrl: RoundController): VNode | undefined { +export function render(ctrl: RoundController) { const d = ctrl.data, moves = ctrl.replayEnabledByPref() && @@ -249,18 +222,19 @@ export function render(ctrl: RoundController): VNode | undefined { renderMoves(ctrl), ); const renderMovesOrResult = moves ? moves : renderResult(ctrl); - return ctrl.nvui - ? undefined - : h(rmovesTag, [ - renderButtons(ctrl), - boardMenu(ctrl), - initMessage(ctrl) || - (isCol1() - ? h('div.col1-moves', [ - col1Button(ctrl, -1, licon.JumpPrev, ctrl.ply == round.firstPly(d)), - renderMovesOrResult, - col1Button(ctrl, 1, licon.JumpNext, ctrl.ply == round.lastPly(d)), - ]) - : renderMovesOrResult), - ]); + return ( + !ctrl.nvui && + h(rmovesTag, [ + renderButtons(ctrl), + boardMenu(ctrl), + initMessage(ctrl) || + (isCol1() + ? h('div.col1-moves', [ + col1Button(ctrl, -1, licon.JumpPrev, ctrl.ply == round.firstPly(d)), + renderMovesOrResult, + col1Button(ctrl, 1, licon.JumpNext, ctrl.ply == round.lastPly(d)), + ]) + : renderMovesOrResult), + ]) + ); } diff --git a/ui/round/src/view/table.ts b/ui/round/src/view/table.ts index dbb1d79fa24c9..c8609255ef073 100644 --- a/ui/round/src/view/table.ts +++ b/ui/round/src/view/table.ts @@ -1,5 +1,4 @@ import * as licon from 'common/licon'; -import { h } from 'snabbdom'; import { Position } from '../interfaces'; import { bind } from '../util'; import * as game from 'game'; @@ -11,7 +10,7 @@ import renderExpiration from './expiration'; import * as renderUser from './user'; import * as button from './button'; import RoundController from '../ctrl'; -import { MaybeVNodes } from 'common/snabbdom'; +import { LooseVNodes, looseH as h } from 'common/snabbdom'; import { toggleButton as boardMenuToggleButton } from 'board/menu'; function renderPlayer(ctrl: RoundController, position: Position) { @@ -32,9 +31,9 @@ const isLoading = (ctrl: RoundController): boolean => ctrl.loading || ctrl.redir const loader = () => h('i.ddloader'); -const renderTableWith = (ctrl: RoundController, buttons: MaybeVNodes) => [ +const renderTableWith = (ctrl: RoundController, buttons: LooseVNodes) => [ replay.render(ctrl), - buttons.find(x => !!x) ? h('div.rcontrols', buttons) : null, + buttons.find(x => !!x) && h('div.rcontrols', buttons), ]; export const renderTableEnd = (ctrl: RoundController) => @@ -119,19 +118,13 @@ export const renderTablePlay = (ctrl: RoundController) => { replay.analysisButton(ctrl), boardMenuToggleButton(ctrl.menu, ctrl.noarg('menu')), ], - buttons: MaybeVNodes = loading + buttons = loading ? [loader()] : [promptVNode, button.opponentGone(ctrl), button.threefoldSuggestion(ctrl)]; return [ replay.render(ctrl), h('div.rcontrols', [ - h( - 'div.ricons', - { - class: { confirm: !!(ctrl.drawConfirm || ctrl.resignConfirm) }, - }, - icons, - ), + h('div.ricons', { class: { confirm: !!(ctrl.drawConfirm || ctrl.resignConfirm) } }, icons), ...buttons, ]), ]; @@ -140,16 +133,16 @@ export const renderTablePlay = (ctrl: RoundController) => { function whosTurn(ctrl: RoundController, color: Color, position: Position) { const d = ctrl.data; if (status.finished(d) || status.aborted(d)) return; - return h('div.rclock.rclock-turn.rclock-' + position, [ - d.game.player === color - ? h( - 'div.rclock-turn__text', - d.player.spectator - ? ctrl.trans(d.game.player + 'Plays') - : ctrl.trans(d.game.player === d.player.color ? 'yourTurn' : 'waitingForOpponent'), - ) - : null, - ]); + return h( + 'div.rclock.rclock-turn.rclock-' + position, + d.game.player === color && + h( + 'div.rclock-turn__text', + d.player.spectator + ? ctrl.trans(d.game.player + 'Plays') + : ctrl.trans(d.game.player === d.player.color ? 'yourTurn' : 'waitingForOpponent'), + ), + ); } function anyClock(ctrl: RoundController, position: Position) { @@ -160,7 +153,7 @@ function anyClock(ctrl: RoundController, position: Position) { else return whosTurn(ctrl, player.color, position); } -export const renderTable = (ctrl: RoundController): MaybeVNodes => [ +export const renderTable = (ctrl: RoundController): LooseVNodes => [ h('div.round__app__table'), renderExpiration(ctrl), renderPlayer(ctrl, 'top'), diff --git a/ui/round/src/view/user.ts b/ui/round/src/view/user.ts index f94ba4aedd0d1..49e1f3d60ce60 100644 --- a/ui/round/src/view/user.ts +++ b/ui/round/src/view/user.ts @@ -1,4 +1,4 @@ -import { h } from 'snabbdom'; +import { looseH as h } from 'common/snabbdom'; import * as licon from 'common/licon'; import { Player } from 'game'; import { Position } from '../interfaces'; @@ -21,7 +21,7 @@ export function botHtml(ctrl: RoundController, player: Player, position: Positio [ h('i.line', [h('img', { attrs: { src: player.image! } })]), h('a.text', h('name', player.name)), - h('rating', player.rating), + h('rating', `${player.rating}`), //h('rating', player.ratingDiff), ], ); @@ -62,29 +62,19 @@ export function userHtml(ctrl: RoundController, player: Player, position: Positi online: false, line: false, }), - rating ? h('rating', rating + (player.provisional ? '?' : '')) : null, - rating ? ratingDiff(player) : null, - player.engine - ? h('span', { - attrs: { - 'data-icon': licon.CautionCircle, - title: ctrl.noarg('thisAccountViolatedTos'), - }, - }) - : undefined, + !!rating && h('rating', `${rating + (player.provisional ? '?' : '')}`), + !!rating && ratingDiff(player), + player.engine && + h('span', { + attrs: { 'data-icon': licon.CautionCircle, title: ctrl.noarg('thisAccountViolatedTos') }, + }), ], ); } const connecting = !player.onGame && ctrl.firstSeconds; return h( `div.ruser-${position}.ruser.user-link`, - { - class: { - online: player.onGame, - offline: !player.onGame, - connecting, - }, - }, + { class: { online: player.onGame, offline: !player.onGame, connecting } }, [ h('i.line', { attrs: { diff --git a/ui/simul/src/view/created.ts b/ui/simul/src/view/created.ts index a9f83b17fdf96..4292b6f331916 100644 --- a/ui/simul/src/view/created.ts +++ b/ui/simul/src/view/created.ts @@ -1,13 +1,13 @@ -import { h } from 'snabbdom'; +import { VNode } from 'snabbdom'; import * as licon from 'common/licon'; -import { bind, MaybeVNode } from 'common/snabbdom'; +import { bind, looseH as h } from 'common/snabbdom'; import SimulCtrl from '../ctrl'; import { Applicant } from '../interfaces'; import xhr from '../xhr'; import * as util from './util'; import { domDialog } from 'common/dialog'; -export default function (showText: (ctrl: SimulCtrl) => MaybeVNode) { +export default function (showText: (ctrl: SimulCtrl) => VNode | false) { return (ctrl: SimulCtrl) => { const candidates = ctrl.candidates().sort(byName), accepted = ctrl.accepted().sort(byName), @@ -15,14 +15,7 @@ export default function (showText: (ctrl: SimulCtrl) => MaybeVNode) { canJoin = ctrl.data.canJoin; const variantIconFor = (a: Applicant) => { const variant = ctrl.data.variants.find(v => a.variant == v.key); - return ( - variant && - h('td.variant', { - attrs: { - 'data-icon': variant.icon, - }, - }) - ); + return variant && h('td.variant', { attrs: { 'data-icon': variant.icon } }); }; return [ h('div.box__top', [ @@ -35,18 +28,13 @@ export default function (showText: (ctrl: SimulCtrl) => MaybeVNode) { : ctrl.containsMe() ? h( 'a.button', - { - hook: bind('click', () => xhr.withdraw(ctrl.data.id)), - }, + { hook: bind('click', () => xhr.withdraw(ctrl.data.id)) }, ctrl.trans('withdraw'), ) : h( 'a.button.text' + (canJoin ? '' : '.disabled'), { - attrs: { - disabled: !canJoin, - 'data-icon': licon.PlayTriangle, - }, + attrs: { disabled: !canJoin, 'data-icon': licon.PlayTriangle }, hook: canJoin ? bind('click', () => { if (ctrl.data.variants.length === 1) @@ -81,18 +69,12 @@ export default function (showText: (ctrl: SimulCtrl) => MaybeVNode) { showText(ctrl), ctrl.acceptedContainsMe() ? h('p.instructions', 'You have been selected! Hold still, the simul is about to begin.') - : isHost && ctrl.data.applicants.length < 6 - ? h('p.instructions', 'Share this page URL to let people enter the simul!') - : null, + : isHost && + ctrl.data.applicants.length < 6 && + h('p.instructions', 'Share this page URL to let people enter the simul!'), h( 'div.halves', - { - hook: { - postpatch(_old, vnode) { - lichess.powertip.manualUserIn(vnode.elm as HTMLElement); - }, - }, - }, + { hook: { postpatch: (_old, vnode) => lichess.powertip.manualUserIn(vnode.elm as HTMLElement) } }, [ h( 'div.half.candidates', @@ -102,13 +84,10 @@ export default function (showText: (ctrl: SimulCtrl) => MaybeVNode) { 'thead', h( 'tr', - h( - 'th', - { - attrs: { colspan: 3 }, - }, - [h('strong', candidates.length), ' candidate players'], - ), + h('th', { attrs: { colspan: 3 } }, [ + h('strong', `${candidates.length}`), + ' candidate players', + ]), ), ), h( @@ -116,28 +95,17 @@ export default function (showText: (ctrl: SimulCtrl) => MaybeVNode) { candidates.map(applicant => { return h( 'tr', - { - key: applicant.player.id, - class: { - me: ctrl.opts.userId === applicant.player.id, - }, - }, + { key: applicant.player.id, class: { me: ctrl.opts.userId === applicant.player.id } }, [ h('td', util.player(applicant.player, ctrl)), variantIconFor(applicant), h( 'td.action', - isHost - ? [ - h('a.button', { - attrs: { - 'data-icon': licon.Checkmark, - title: 'Accept', - }, - hook: bind('click', () => xhr.accept(applicant.player.id)(ctrl.data.id)), - }), - ] - : [], + isHost && + h('a.button', { + attrs: { 'data-icon': licon.Checkmark, title: 'Accept' }, + hook: bind('click', () => xhr.accept(applicant.player.id)(ctrl.data.id)), + }), ), ], ); @@ -151,44 +119,32 @@ export default function (showText: (ctrl: SimulCtrl) => MaybeVNode) { h('thead', [ h( 'tr', - h( - 'th', - { - attrs: { colspan: 3 }, - }, - [h('strong', accepted.length), ' accepted players'], - ), + h('th', { attrs: { colspan: 3 } }, [ + h('strong', `${accepted.length}`), + ' accepted players', + ]), ), - isHost && candidates.length && !accepted.length - ? [h('tr.help', h('th', 'Now you get to accept some players, then start the simul'))] - : [], + isHost && + candidates.length > 0 && + !accepted.length && + h('tr.help', h('th', 'Now you get to accept some players, then start the simul')), ]), h( 'tbody', accepted.map(applicant => { return h( 'tr', - { - key: applicant.player.id, - class: { - me: ctrl.opts.userId === applicant.player.id, - }, - }, + { key: applicant.player.id, class: { me: ctrl.opts.userId === applicant.player.id } }, [ h('td', util.player(applicant.player, ctrl)), variantIconFor(applicant), h( 'td.action', - isHost - ? [ - h('a.button.button-red', { - attrs: { - 'data-icon': licon.X, - }, - hook: bind('click', () => xhr.reject(applicant.player.id)(ctrl.data.id)), - }), - ] - : [], + isHost && + h('a.button.button-red', { + attrs: { 'data-icon': licon.X }, + hook: bind('click', () => xhr.reject(applicant.player.id)(ctrl.data.id)), + }), ), ], ); @@ -198,21 +154,12 @@ export default function (showText: (ctrl: SimulCtrl) => MaybeVNode) { ]), ], ), - ctrl.data.quote - ? h('blockquote.pull-quote', [h('p', ctrl.data.quote.text), h('footer', ctrl.data.quote.author)]) - : null, + ctrl.data.quote && + h('blockquote.pull-quote', [h('p', ctrl.data.quote.text), h('footer', ctrl.data.quote.author)]), h( 'div.continue-with.none', ctrl.data.variants.map(variant => - h( - 'button.button', - { - attrs: { - 'data-variant': variant.key, - }, - }, - variant.name, - ), + h('button.button', { attrs: { 'data-variant': variant.key } }, variant.name), ), ), ]; @@ -222,41 +169,31 @@ export default function (showText: (ctrl: SimulCtrl) => MaybeVNode) { const byName = (a: Applicant, b: Applicant) => (a.player.name > b.player.name ? 1 : -1); const randomButton = (ctrl: SimulCtrl) => - ctrl.candidates().length - ? h( - 'a.button.text', - { - attrs: { - 'data-icon': licon.Checkmark, - }, - hook: bind('click', () => { - const candidates = ctrl.candidates(); - const randomCandidate = candidates[Math.floor(Math.random() * candidates.length)]; - xhr.accept(randomCandidate.player.id)(ctrl.data.id); - }), - }, - 'Accept random candidate', - ) - : null; + ctrl.candidates().length > 0 && + h( + 'a.button.text', + { + attrs: { 'data-icon': licon.Checkmark }, + hook: bind('click', () => { + const candidates = ctrl.candidates(); + const randomCandidate = candidates[Math.floor(Math.random() * candidates.length)]; + xhr.accept(randomCandidate.player.id)(ctrl.data.id); + }), + }, + 'Accept random candidate', + ); const startOrCancel = (ctrl: SimulCtrl, accepted: Applicant[]) => accepted.length > 1 ? h( 'a.button.button-green.text', - { - attrs: { - 'data-icon': licon.PlayTriangle, - }, - hook: bind('click', () => xhr.start(ctrl.data.id)), - }, + { attrs: { 'data-icon': licon.PlayTriangle }, hook: bind('click', () => xhr.start(ctrl.data.id)) }, `Start (${accepted.length})`, ) : h( 'a.button.button-red.text', { - attrs: { - 'data-icon': licon.X, - }, + attrs: { 'data-icon': licon.X }, hook: bind('click', () => { if (confirm('Delete this simul?')) xhr.abort(ctrl.data.id); }), diff --git a/ui/simul/src/view/main.ts b/ui/simul/src/view/main.ts index e517d22aadac9..c4685843ada53 100644 --- a/ui/simul/src/view/main.ts +++ b/ui/simul/src/view/main.ts @@ -1,5 +1,4 @@ -import { h } from 'snabbdom'; -import { onInsert } from 'common/snabbdom'; +import { onInsert, looseH as h } from 'common/snabbdom'; import SimulCtrl from '../ctrl'; import * as util from './util'; import created from './created'; @@ -10,49 +9,23 @@ import pairings from './pairings'; export default function (ctrl: SimulCtrl) { const handler = ctrl.data.isRunning ? started : ctrl.data.isFinished ? finished : created(showText); - return h( - 'main.simul', - { - class: { - 'simul-created': ctrl.data.isCreated, - }, - }, - [ - h('aside.simul__side', { - hook: onInsert(el => { - $(el).replaceWith(ctrl.opts.$side); - if (ctrl.opts.chat) { - ctrl.opts.chat.data.hostId = ctrl.data.host.id; - lichess.makeChat(ctrl.opts.chat); - } - }), + return h('main.simul', { class: { 'simul-created': ctrl.data.isCreated } }, [ + h('aside.simul__side', { + hook: onInsert(el => { + $(el).replaceWith(ctrl.opts.$side); + if (ctrl.opts.chat) { + ctrl.opts.chat.data.hostIds = [ctrl.data.host.id]; + lichess.makeChat(ctrl.opts.chat); + } }), - h( - 'div.simul__main.box', - { - hook: { - postpatch() { - lichess.miniGame.initAll(); - }, - }, - }, - handler(ctrl), - ), - h('div.chat__members.none', { - hook: onInsert(lichess.watchers), - }), - ], - ); + }), + h('div.simul__main.box', { hook: { postpatch: () => lichess.miniGame.initAll() } }, handler(ctrl)), + h('div.chat__members.none', { hook: onInsert(lichess.watchers) }), + ]); } const showText = (ctrl: SimulCtrl) => - ctrl.data.text.length > 0 - ? h('div.simul-text', [ - h('p', { - hook: richHTML(ctrl.data.text), - }), - ]) - : undefined; + ctrl.data.text.length > 0 && h('div.simul-text', [h('p', { hook: richHTML(ctrl.data.text) })]); const started = (ctrl: SimulCtrl) => [ h('div.box__top', util.title(ctrl)), diff --git a/ui/simul/src/view/pairings.ts b/ui/simul/src/view/pairings.ts index bb29210675c6f..1416468da4a84 100644 --- a/ui/simul/src/view/pairings.ts +++ b/ui/simul/src/view/pairings.ts @@ -17,9 +17,7 @@ const miniPairing = (ctrl: SimulCtrl) => (pairing: Pairing) => { return h( `span.mini-game.mini-game-${game.id}.mini-game--init.is2d`, { - class: { - host: ctrl.data.host.gameId === game.id, - }, + class: { host: ctrl.data.host.gameId === game.id }, attrs: { 'data-state': `${game.fen},${game.orient},${game.lastMove}`, 'data-live': game.clock ? game.id : '', @@ -28,30 +26,18 @@ const miniPairing = (ctrl: SimulCtrl) => (pairing: Pairing) => { }, [ h('span.mini-game__player', [ - h( - 'a.mini-game__user.ulpt', - { - attrs: { - href: `/@/${player.name}`, - }, - }, - [ - h( - 'span.name', - player.title ? [h('span.utitle', player.title), ' ', player.name, flair] : [player.name, flair], - ), - ...(ctrl.opts.showRatings ? [' ', h('span.rating', player.rating)] : []), - ], - ), + h('a.mini-game__user.ulpt', { attrs: { href: `/@/${player.name}` } }, [ + h( + 'span.name', + player.title ? [h('span.utitle', player.title), ' ', player.name, flair] : [player.name, flair], + ), + ...(ctrl.opts.showRatings ? [' ', h('span.rating', player.rating)] : []), + ]), game.clock ? renderClock(opposite(game.orient), game.clock[opposite(game.orient)]) : h('span.mini-game__result', game.winner ? (game.winner == game.orient ? 0 : 1) : '½'), ]), - h('a.cg-wrap', { - attrs: { - href: `/${game.id}/${game.orient}`, - }, - }), + h('a.cg-wrap', { attrs: { href: `/${game.id}/${game.orient}` } }), h('span.mini-game__player', [ h('span'), game.clock diff --git a/ui/simul/src/view/util.ts b/ui/simul/src/view/util.ts index 338a7de64ffc8..f26f453662957 100644 --- a/ui/simul/src/view/util.ts +++ b/ui/simul/src/view/util.ts @@ -9,9 +9,7 @@ export function player(p: Player, ctrl: SimulCtrl) { 'a.ulpt.user-link.' + (p.online || ctrl.data.host.id != p.id ? 'online' : 'offline'), { attrs: { href: '/@/' + p.name }, - hook: { - destroy: vnode => $.powerTip.destroy(vnode.elm as HTMLElement), - }, + hook: { destroy: vnode => $.powerTip.destroy(vnode.elm as HTMLElement) }, }, [ userLine({ line: true, ...p }), diff --git a/ui/site/css/_account.scss b/ui/site/css/_account.scss index 241feca17f38d..e00202f7c4178 100644 --- a/ui/site/css/_account.scss +++ b/ui/site/css/_account.scss @@ -274,10 +274,3 @@ .page-menu__content { overflow: hidden; // prevents pre overflow (personal-data) } - -.emoji-details { - position: relative; - // ensure the emoji picker is above the text and its licon - z-index: 1; - margin-bottom: 1em; -} diff --git a/ui/site/css/_dailyFeed.scss b/ui/site/css/_dailyFeed.scss new file mode 100644 index 0000000000000..64dfac79c93b1 --- /dev/null +++ b/ui/site/css/_dailyFeed.scss @@ -0,0 +1,77 @@ +$c-contours: mix($c-brag, $c-shade, 80%); + +.daily-feed { + &__updates { + margin: 3em 3em 0 3em; + border-#{$start-direction}: 3px solid $c-contours; + } + .atom { + font-size: 2.6em; + color: $c-font-dimmer; + &:hover { + color: $c-accent; + } + } + + &__update { + padding-#{$start-direction}: 45px; + margin-bottom: 4em; + + &__marker { + float: $start-direction; + width: 54px; + height: 54px; + padding: 7px; + margin-top: -14px; + margin-#{$start-direction}: -74px; + background-color: $c-contours; + border: 3px solid $c-bg-box; + border-radius: 50%; + + &.nobg { + background: $c-bg-box; + border: 2px solid $c-contours; + } + } + + &__day { + @extend %flex-center; + h2 { + display: inline; + font-size: 1.5em; + } + a { + font-weight: bold; + color: $c-contours; + } + time { + font-size: 1em; + opacity: 1; + } + margin-bottom: 1rem; + } + + &__markup { + @include rendered-markdown( + $element-margin: 1em, + $h1: false, + $h2: false, + $table: false, + $img: false, + $list: false + ); + h3 { + font-size: 1.3em; + color: $c-font-dim; + border-bottom: $border; + } + h4 { + font-size: 1.25em; + } + } + } + + &__delete { + text-align: $end-direction; + } +} diff --git a/ui/site/css/_importer.scss b/ui/site/css/_importer.scss index 29ecc00ce8aa7..a4e0516e5ef9a 100644 --- a/ui/site/css/_importer.scss +++ b/ui/site/css/_importer.scss @@ -1,9 +1,4 @@ main.importer { - .explanation { - margin: 3vh 0; - text-align: center; - } - form { label.pgn { display: block; diff --git a/ui/site/css/_linkPopup.scss b/ui/site/css/_linkPopup.scss index e67971bc86a78..9b99c833e59f8 100644 --- a/ui/site/css/_linkPopup.scss +++ b/ui/site/css/_linkPopup.scss @@ -1,11 +1,10 @@ .link-popup { - @extend %box-neat; font-size: 1.1em; padding: 0 !important; max-width: 600px; &__content { - @extend %flex-center-nowrap; + @extend %flex-center-nowrap, %box-radius-top; background: $c-bg-zebra; padding: 2em; diff --git a/ui/site/css/_video.scss b/ui/site/css/_video.scss index c6d3a847ca0e6..7739ea66ff206 100644 --- a/ui/site/css/_video.scss +++ b/ui/site/css/_video.scss @@ -257,11 +257,6 @@ } #video { - .not_found { - margin-top: 200px; - text-align: center; - } - .not_much { margin-top: 100px; text-align: center; diff --git a/ui/site/css/build/_account.scss b/ui/site/css/build/_account.scss index 2d1e02ceea2d5..bede02bec5ebd 100644 --- a/ui/site/css/build/_account.scss +++ b/ui/site/css/build/_account.scss @@ -2,4 +2,5 @@ @import '../../../common/css/component/slist'; @import '../../../common/css/form/form3'; @import '../../../common/css/form/radio'; +@import '../../../common/css/form/emoji-picker'; @import '../account'; diff --git a/ui/site/css/build/_dailyFeed.scss b/ui/site/css/build/_dailyFeed.scss new file mode 100644 index 0000000000000..d1c1a560b89a0 --- /dev/null +++ b/ui/site/css/build/_dailyFeed.scss @@ -0,0 +1,7 @@ +@import '../../../common/css/plugin'; +@import '../../../common/css/component/markdown'; +@import '../../../common/css/component/slist'; +@import '../../../common/css/form/form3'; +@import '../../../common/css/vendor/flatpickr'; + +@import '../dailyFeed'; diff --git a/ui/site/css/build/_team.scss b/ui/site/css/build/_team.scss index 39ec27f7a4ff4..90d5c581ce4c2 100644 --- a/ui/site/css/build/_team.scss +++ b/ui/site/css/build/_team.scss @@ -4,6 +4,7 @@ @import '../../../common/css/component/slist'; @import '../../../common/css/form/form3'; @import '../../../common/css/form/captcha'; +@import '../../../common/css/form/emoji-picker'; @import '../../../chat/css/chat'; @import '../team/team'; diff --git a/ui/site/css/build/dailyFeed.ltr.dark.scss b/ui/site/css/build/dailyFeed.ltr.dark.scss new file mode 100644 index 0000000000000..8447c4666a724 --- /dev/null +++ b/ui/site/css/build/dailyFeed.ltr.dark.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/ltr'; +@import '../../../common/css/theme/dark'; +@import 'dailyFeed'; diff --git a/ui/site/css/build/dailyFeed.ltr.light.scss b/ui/site/css/build/dailyFeed.ltr.light.scss new file mode 100644 index 0000000000000..d757786f7cd24 --- /dev/null +++ b/ui/site/css/build/dailyFeed.ltr.light.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/ltr'; +@import '../../../common/css/theme/light'; +@import 'dailyFeed'; diff --git a/ui/site/css/build/dailyFeed.ltr.transp.scss b/ui/site/css/build/dailyFeed.ltr.transp.scss new file mode 100644 index 0000000000000..fbcfe6786c91d --- /dev/null +++ b/ui/site/css/build/dailyFeed.ltr.transp.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/ltr'; +@import '../../../common/css/theme/transp'; +@import 'dailyFeed'; diff --git a/ui/site/css/build/dailyFeed.rtl.dark.scss b/ui/site/css/build/dailyFeed.rtl.dark.scss new file mode 100644 index 0000000000000..b98dc2f6f8429 --- /dev/null +++ b/ui/site/css/build/dailyFeed.rtl.dark.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/rtl'; +@import '../../../common/css/theme/dark'; +@import 'dailyFeed'; diff --git a/ui/site/css/build/dailyFeed.rtl.light.scss b/ui/site/css/build/dailyFeed.rtl.light.scss new file mode 100644 index 0000000000000..91acc0dccca4e --- /dev/null +++ b/ui/site/css/build/dailyFeed.rtl.light.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/rtl'; +@import '../../../common/css/theme/light'; +@import 'dailyFeed'; diff --git a/ui/site/css/build/dailyFeed.rtl.transp.scss b/ui/site/css/build/dailyFeed.rtl.transp.scss new file mode 100644 index 0000000000000..ab010b49e8b05 --- /dev/null +++ b/ui/site/css/build/dailyFeed.rtl.transp.scss @@ -0,0 +1,3 @@ +@import '../../../common/css/dir/rtl'; +@import '../../../common/css/theme/transp'; +@import 'dailyFeed'; diff --git a/ui/site/css/team/_show.scss b/ui/site/css/team/_show.scss index 2e2a752ec9496..66638df850a12 100644 --- a/ui/site/css/team/_show.scss +++ b/ui/site/css/team/_show.scss @@ -7,10 +7,13 @@ $section-margin-more: 5vh; overflow: hidden; h1 { - @extend %break-word-hard, %zalgoverflow; + @extend %break-word-hard, %zalgoverflow, %flex-center; unicode-bidi: plaintext; - display: flex; + + .uflair { + transform: none; + } } .mchat { @@ -58,6 +61,7 @@ $section-margin-more: 5vh; .user-link { display: inline-block; vertical-align: bottom; + padding-#{$end-direction}: 0.25em; } } diff --git a/ui/site/css/ublog/_post.scss b/ui/site/css/ublog/_post.scss index 74b8478e62316..61e6cc0218fb1 100644 --- a/ui/site/css/ublog/_post.scss +++ b/ui/site/css/ublog/_post.scss @@ -32,6 +32,10 @@ &__date { unicode-bidi: embed; } + input[type='number'] { + width: 6em; + margin-right: 1em; + } } &__topics { @extend %flex-wrap; diff --git a/ui/site/css/user/_list.scss b/ui/site/css/user/_list.scss index e674570adc295..68f15ad241671 100644 --- a/ui/site/css/user/_list.scss +++ b/ui/site/css/user/_list.scss @@ -50,10 +50,9 @@ $user-list-width: 30ch; display: grid; grid-template-columns: repeat(auto-fit, minmax($user-list-width, 1fr)); - border: 1px solid $c-lead; .user-top { - border-#{$end-direction}: 2px solid fade-out($c-lead, 0.5); + border: 1px solid fade-out($c-lead, 0.5); } h2 { diff --git a/ui/site/css/user/_rating.stats.scss b/ui/site/css/user/_rating.stats.scss index c3a23f2a0ed02..21f5386e567f2 100644 --- a/ui/site/css/user/_rating.stats.scss +++ b/ui/site/css/user/_rating.stats.scss @@ -18,6 +18,10 @@ #rating_distribution { height: 500px; margin-top: 2rem; + + #rating_distribution_chart { + max-width: 100%; + } } .mselect { diff --git a/ui/site/package.json b/ui/site/package.json index b4d6388f9046e..ab289d586f47b 100644 --- a/ui/site/package.json +++ b/ui/site/package.json @@ -69,7 +69,9 @@ "src/ublogForm.ts": "ublogForm", "src/streamer.ts": "streamer", "src/soundMove.ts": "soundMove", - "src/diagnostic.ts": "diagnostic" + "src/diagnostic.ts": "diagnostic", + "src/flairPicker.ts": "flairPicker", + "src/dailyFeed.ts": "dailyFeed" } }, "copy": [ diff --git a/ui/site/src/account.ts b/ui/site/src/account.ts index 46713d90f6064..7a75147e58e29 100644 --- a/ui/site/src/account.ts +++ b/ui/site/src/account.ts @@ -1,24 +1,10 @@ import * as licon from 'common/licon'; import * as xhr from 'common/xhr'; -import { emojiPicker } from './emojiPicker'; +import flairPicker from './component/flairPicker'; lichess.load.then(() => { $('.emoji-details').each(function (this: HTMLElement) { - const details = this; - const parent = $(details).parent(); - const close = () => details.removeAttribute('open'); - const onEmojiSelect = (i?: { id: string; src: string }) => { - parent.find('input[name="flair"]').val(i?.id ?? ''); - parent.find('.user-link .uflair').remove(); - if (i) parent.find('.user-link').append(''); - close(); - }; - parent.find('.emoji-remove').on('click', e => { - e.preventDefault(); - onEmojiSelect(); - $(e.target).remove(); - }); - $(details).on('toggle', () => emojiPicker(details.querySelector('.emoji-picker')!, close, onEmojiSelect)); + flairPicker(this); }); const localPrefs: [string, string, string, boolean][] = [ @@ -77,6 +63,24 @@ lichess.load.then(() => { return !isDanger || confirm(this.title); }); }); + + $('form.dirty-alert').each(function (this: HTMLFormElement) { + const form = this; + const serialize = () => { + const data = new FormData(form); + return Array.from(data.keys()) + .map(k => `${k}=${data.get(k)}`) + .join('&'); + }; + let clean = serialize(); + $(form).on('submit', () => { + clean = serialize(); + }); + window.addEventListener('beforeunload', e => { + if (clean != serialize() && !confirm('You have unsaved changes. Are you sure you want to leave?')) + e.preventDefault(); + }); + }); }); function computeBitChoices($form: Cash, name: string) { diff --git a/ui/site/src/challengePage.ts b/ui/site/src/challengePage.ts index 6d1f78be8ec84..aeec66b1f00e5 100644 --- a/ui/site/src/challengePage.ts +++ b/ui/site/src/challengePage.ts @@ -45,7 +45,7 @@ export function initModule(opts: ChallengeOpts) { .find('input.friend-autocomplete') .each(function (this: HTMLInputElement) { const input = this; - lichess.userComplete({ + lichess.asset.userComplete({ input: input, friend: true, tag: 'span', @@ -53,6 +53,17 @@ export function initModule(opts: ChallengeOpts) { onSelect: () => setTimeout(() => (input.parentNode as HTMLFormElement).submit(), 100), }); }); + $(selector) + .find('.invite__user__recent button') + .on('click', function (this: HTMLButtonElement) { + $(selector) + .find('input.friend-autocomplete') + .val(this.dataset.user!) + .parents('form') + .each(function (this: HTMLFormElement) { + this.submit(); + }); + }); } init(); diff --git a/ui/site/src/clas.ts b/ui/site/src/clas.ts index ed6d663c6289f..e61b954b2f0fc 100644 --- a/ui/site/src/clas.ts +++ b/ui/site/src/clas.ts @@ -23,7 +23,7 @@ lichess.load.then(() => { return textarea.value.split('\n').slice(0, -1); } - lichess.loadIife('vendor/textcomplete.min.js').then(() => { + lichess.asset.loadIife('vendor/textcomplete.min.js').then(() => { const textcomplete = new window.Textcomplete(new window.Textcomplete.editors.Textarea(textarea), { dropdown: { maxCount: 10, diff --git a/ui/site/src/component/assets.ts b/ui/site/src/component/assets.ts index 0125e8b16ddea..538b4839dfd3c 100644 --- a/ui/site/src/component/assets.ts +++ b/ui/site/src/component/assets.ts @@ -1,31 +1,37 @@ import * as xhr from 'common/xhr'; import { supportsSystemTheme } from 'common/theme'; +import { memoize } from 'common'; -export const assetUrl = (path: string, opts: AssetUrlOpts = {}) => { +export const baseUrl = memoize(() => document.body.getAttribute('data-asset-url') || ''); + +const version = memoize(() => document.body.getAttribute('data-asset-version')); + +export const url = (path: string, opts: AssetUrlOpts = {}) => { opts = opts || {}; - const baseUrl = opts.sameDomain ? '' : document.body.getAttribute('data-asset-url'), - version = opts.version || document.body.getAttribute('data-asset-version'); - return baseUrl + '/assets' + (opts.noVersion ? '' : '/_' + version) + '/' + path; + const base = opts.sameDomain ? '' : baseUrl(), + v = opts.version || version(); + return `${base}/assets${opts.noVersion ? '' : '/_' + v}/${path}`; }; -export const flairSrc = (flair: Flair) => lichess.assetUrl(`flair/img/${flair}.webp`, { noVersion: true }); +// bump flairs version if a flair is changed only (not added or removed) +export const flairSrc = (flair: Flair) => url(`flair/img/${flair}.webp`, { version: '_____2' }); const loadedCss = new Map>(); -export const loadCss = (url: string, media?: 'dark' | 'light'): Promise => { - if (!loadedCss.has(url)) { +export const loadCss = (href: string, media?: 'dark' | 'light'): Promise => { + if (!loadedCss.has(href)) { const el = document.createElement('link'); el.rel = 'stylesheet'; - el.href = assetUrl(lichess.debug ? `${url}?_=${Date.now()}` : url); + el.href = url(lichess.debug ? `${href}?_=${Date.now()}` : href); if (media) el.media = `(prefers-color-scheme: ${media})`; loadedCss.set( - url, + href, new Promise(resolve => { el.onload = () => resolve(); }), ); document.head.append(el); } - return loadedCss.get(url)!; + return loadedCss.get(href)!; }; export const loadCssPath = async (key: string): Promise => { @@ -48,16 +54,16 @@ export const jsModule = (name: string) => `compiled/${name}${document.body.datas const scriptCache = new Map>(); -export const loadIife = (url: string, opts: AssetUrlOpts = {}): Promise => { - if (!scriptCache.has(url)) scriptCache.set(url, xhr.script(assetUrl(url, opts))); - return scriptCache.get(url)!; +export const loadIife = (u: string, opts: AssetUrlOpts = {}): Promise => { + if (!scriptCache.has(u)) scriptCache.set(u, xhr.script(url(u, opts))); + return scriptCache.get(u)!; }; export async function loadEsm( name: string, opts?: { init?: ModuleOpts; url?: AssetUrlOpts }, ): Promise { - const module = await import(assetUrl(jsModule(name), opts?.url)); + const module = await import(url(jsModule(name), opts?.url)); return module.initModule ? module.initModule(opts?.init) : module.default(opts?.init); } @@ -73,4 +79,4 @@ export const hopscotch = () => { }); }; -export const embedChessground = () => import(assetUrl('npm/chessground.min.js')); +export const embedChessground = () => import(url('npm/chessground.min.js')); diff --git a/ui/site/src/component/flairPicker.ts b/ui/site/src/component/flairPicker.ts new file mode 100644 index 0000000000000..b1df37f6a0dac --- /dev/null +++ b/ui/site/src/component/flairPicker.ts @@ -0,0 +1,24 @@ +export default function flairPicker(element: HTMLElement) { + const parent = $(element).parent(); + const close = () => element.removeAttribute('open'); + const onEmojiSelect = (i?: { id: string; src: string }) => { + parent.find('input[name="flair"]').val(i?.id ?? ''); + parent.find('.uflair').remove(); + if (i) parent.find('.flair-container').append(''); + close(); + }; + parent.find('.emoji-remove').on('click', e => { + e.preventDefault(); + onEmojiSelect(); + $(e.target).remove(); + }); + $(element).on('toggle', () => + lichess.asset.loadEsm('flairPicker', { + init: { + element: element.querySelector('.flair-picker')!, + close, + onEmojiSelect, + }, + }), + ); +} diff --git a/ui/site/src/component/log.ts b/ui/site/src/component/log.ts index 14ced27b4bb5e..914d9aca81789 100644 --- a/ui/site/src/component/log.ts +++ b/ui/site/src/component/log.ts @@ -30,7 +30,7 @@ export default function makeLog(): LichessLog { } const log: any = async (...args: any[]) => { - const msg = args.map(stringify).join(' '); + const msg = `${lichess.info.commit.substr(0, 7)} - ${args.map(stringify).join(' ')}`; let nextKey = Date.now(); console.log(...args); if (nextKey === lastKey) { @@ -53,14 +53,18 @@ export default function makeLog(): LichessLog { log.get = async (): Promise => { await ready; const [keys, vals] = await Promise.all([store.list(), store.getMany()]); - return keys.map((k, i) => `${new Date(k).toISOString()} - ${vals[i]}`).join('\n'); + return keys.map((k, i) => `${new Date(k).toISOString()} ${vals[i]}`).join('\n'); }; window.addEventListener('error', async e => { - log(`${e.message} (${e.filename}:${e.lineno}:${e.colno})\n${e.error?.stack ?? ''}`.trim()); + log( + `${window.location.href} - ${e.message} (${e.filename}:${e.lineno}:${e.colno})\n${ + e.error?.stack ?? '' + }`.trim(), + ); }); window.addEventListener('unhandledrejection', async e => { - log(`${e.reason}\n${e.reason.stack ?? ''}`.trim()); + log(`${window.location.href} - ${e.reason}\n${e.reason.stack ?? ''}`.trim()); }); return log; diff --git a/ui/site/src/component/mic.ts b/ui/site/src/component/mic.ts index e5d5e8b1041cc..2bf5b06f56b53 100644 --- a/ui/site/src/component/mic.ts +++ b/ui/site/src/component/mic.ts @@ -208,11 +208,13 @@ export const mic = new (class implements Voice.Microphone { } this.broadcast('Loading...'); - const modelUrl = lichess.assetUrl(models.get(this.lang)!, { noVersion: true }); + const modelUrl = lichess.asset.url(models.get(this.lang)!, { noVersion: true }); const downloadAsync = this.downloadModel(`/vosk/${modelUrl.replace(/[\W]/g, '_')}`); const audioAsync = this.initAudio(); - this.vosk ??= await lichess.loadEsm('voice.vosk', { url: { version: VOSK_TS_VERSION } }); + this.vosk ??= await lichess.asset.loadEsm('voice.vosk', { + url: { version: VOSK_TS_VERSION }, + }); await downloadAsync; await this.vosk.initModel(modelUrl, this.lang); @@ -263,7 +265,7 @@ export const mic = new (class implements Voice.Microphone { if ((await voskStore.count(`${emscriptenPath}/extracted.ok`)) > 0) return; const modelBlob: ArrayBuffer | undefined = await new Promise((resolve, reject) => { this.download = new XMLHttpRequest(); - this.download.open('GET', lichess.assetUrl(models.get(this.lang)!, { noVersion: true }), true); + this.download.open('GET', lichess.asset.url(models.get(this.lang)!, { noVersion: true }), true); this.download.responseType = 'arraybuffer'; this.download.onerror = _ => reject('Failed. See console'); this.download.onabort = _ => reject('Aborted'); diff --git a/ui/site/src/component/serviceWorker.ts b/ui/site/src/component/serviceWorker.ts index 719c87e5611cf..2480ae936444d 100644 --- a/ui/site/src/component/serviceWorker.ts +++ b/ui/site/src/component/serviceWorker.ts @@ -1,12 +1,10 @@ -import { assetUrl, jsModule } from './assets'; +import { url as assetUrl, jsModule } from './assets'; import { storage } from './storage'; export default async function () { if (!('serviceWorker' in navigator && 'Notification' in window && 'PushManager' in window)) return; const workerUrl = new URL( - assetUrl(jsModule('serviceWorker'), { - sameDomain: true, - }), + assetUrl(jsModule('serviceWorker'), { sameDomain: true }), self.location.href, // eslint-disable-line no-restricted-globals ); workerUrl.searchParams.set('asset-url', document.body.getAttribute('data-asset-url')!); diff --git a/ui/site/src/component/socket.ts b/ui/site/src/component/socket.ts index 3c59df0c1e8e5..5e0b910fcfdab 100644 --- a/ui/site/src/component/socket.ts +++ b/ui/site/src/component/socket.ts @@ -62,9 +62,10 @@ export default class StrongSocket { tryOtherUrl = false; autoReconnect = true; nbConnects = 0; - storage: LichessStorage = makeStorage.make('surl17'); + storage: LichessStorage = makeStorage.make('surl17', 30 * 60 * 1000); private _sign?: string; private resendWhenOpen: [string, any, any][] = []; + private baseUrls = document.body.dataset.socketDomains!.split(','); static defaultOptions: Options = { idle: false, @@ -121,13 +122,7 @@ export default class StrongSocket { try { const ws = (this.ws = new WebSocket(fullUrl)); ws.onerror = e => this.onError(e); - ws.onclose = () => { - this.pubsub.emit('socket.close'); - if (this.autoReconnect) { - this.debug('Will autoreconnect in ' + this.options.autoReconnectDelay); - this.scheduleConnect(this.options.autoReconnectDelay); - } - }; + ws.onclose = e => this.onClose(e, fullUrl); ws.onopen = () => { this.debug('connected to ' + fullUrl); this.onSuccess(); @@ -148,7 +143,7 @@ export default class StrongSocket { this.handle(m); }; } catch (e) { - this.onError(e); + this.onClose({ code: 4000, reason: String(e) } as CloseEvent, fullUrl); } this.scheduleConnect(this.options.pingMaxLag); }; @@ -197,7 +192,14 @@ export default class StrongSocket { this.connectSchedule = setTimeout(() => { document.body.classList.add('offline'); document.body.classList.remove('online'); - this.tryOtherUrl = true; + if (!this.tryOtherUrl) { + // if this was set earlier, we've already logged the error + this.tryOtherUrl = true; + const sri = this.settings.params?.sri; + lichess.log( + `socket.ts:${sri ? ' sri ' + sri : ''} timeout ${delay}ms, rotating to ${this.baseUrl()}`, + ); + } this.connect(); }, delay); }; @@ -294,6 +296,17 @@ export default class StrongSocket { onError = (e: unknown) => { this.options.debug = true; this.debug(`error: ${e} ${JSON.stringify(e)}`); // e not always from lila + }; + + onClose = (e: CloseEvent, url: string) => { + this.pubsub.emit('socket.close'); + if (this.autoReconnect) { + this.debug('Will autoreconnect in ' + this.options.autoReconnectDelay); + this.scheduleConnect(this.options.autoReconnectDelay); + } + if (e.wasClean && e.code < 1002) return; + + lichess.log(`socket.ts:${sri ? ' sri ' + sri : ''} unclean close ${e.code} ${url} ${e.reason}`); this.tryOtherUrl = true; clearTimeout(this.pingSchedule); }; @@ -319,12 +332,16 @@ export default class StrongSocket { }; baseUrl = () => { - const baseUrls = document.body.dataset.socketDomains!.split(','); let url = this.storage.get(); - if (!url || this.tryOtherUrl) { - url = baseUrls[Math.floor(Math.random() * baseUrls.length)]; + if (!url) { + url = this.baseUrls[Math.floor(Math.random() * this.baseUrls.length)]; + this.storage.set(url); + } else if (this.tryOtherUrl) { + const i = this.baseUrls.findIndex(u => u === url); + url = this.baseUrls[(i + 1) % this.baseUrls.length]; this.storage.set(url); } + this.tryOtherUrl = false; return url; }; diff --git a/ui/site/src/component/sound.ts b/ui/site/src/component/sound.ts index 987a88c1c32e1..c5825f0819086 100644 --- a/ui/site/src/component/sound.ts +++ b/ui/site/src/component/sound.ts @@ -1,5 +1,5 @@ import pubsub from './pubsub'; -import { assetUrl } from './assets'; +import { url as assetUrl } from './assets'; import { storage } from './storage'; import { isIOS } from 'common/device'; import throttle from 'common/throttle'; @@ -19,7 +19,7 @@ export default new (class implements SoundI { music?: SoundMove; primerEvents = ['touchend', 'pointerup', 'pointerdown', 'mousedown', 'keydown']; primer = () => - this.ctx.resume().then(() => { + this.ctx?.resume().then(() => { setTimeout(() => $('#warn-no-autoplay').removeClass('shown'), 500); for (const e of this.primerEvents) window.removeEventListener(e, this.primer, { capture: true }); }); @@ -29,6 +29,7 @@ export default new (class implements SoundI { } async load(name: Name, path?: Path): Promise { + if (!this.ctx) return; if (path) this.paths.set(name, path); else path = this.paths.get(name) ?? this.resolvePath(name); if (!path) return; @@ -39,9 +40,9 @@ export default new (class implements SoundI { const arrayBuffer = await result.arrayBuffer(); const audioBuffer = await new Promise((resolve, reject) => { - if (this.ctx.decodeAudioData.length === 1) - this.ctx.decodeAudioData(arrayBuffer).then(resolve).catch(reject); - else this.ctx.decodeAudioData(arrayBuffer, resolve, reject); + if (this.ctx?.decodeAudioData.length === 1) + this.ctx?.decodeAudioData(arrayBuffer).then(resolve).catch(reject); + else this.ctx?.decodeAudioData(arrayBuffer, resolve, reject); }); const sound = new Sound(this.ctx, audioBuffer); this.sounds.set(path, sound); @@ -76,7 +77,7 @@ export default new (class implements SoundI { } } if (o?.filter === 'game' || this.theme !== 'music') return; - this.music ??= await lichess.loadEsm('soundMove'); + this.music ??= await lichess.asset.loadEsm('soundMove'); this.music(o); } @@ -147,7 +148,7 @@ export default new (class implements SoundI { publish = () => pubsub.emit('sound_set', this.theme); changeSet = (s: string) => { - if (isIOS()) this.ctx.resume(); + if (isIOS()) this.ctx?.resume(); this.theme = s; this.publish(); }; @@ -158,36 +159,36 @@ export default new (class implements SoundI { const text = !san ? 'Game start' : san.includes('O-O-O#') - ? 'long castle checkmate' - : san.includes('O-O-O+') - ? 'long castle check' - : san.includes('O-O-O') - ? 'long castle' - : san.includes('O-O#') - ? 'short castle checkmate' - : san.includes('O-O+') - ? 'short castle check' - : san.includes('O-O') - ? 'short castle' - : san - .split('') - .map(c => { - if (c == 'x') return 'takes'; - if (c == '+') return 'check'; - if (c == '#') return 'checkmate'; - if (c == '=') return 'promotes to'; - if (c == '@') return 'at'; - const code = c.charCodeAt(0); - if (code > 48 && code < 58) return c; // 1-8 - if (code > 96 && code < 105) return c.toUpperCase(); - return charRole(c) || c; - }) - .join(' ') - .replace(/^A /, 'A, ') // "A takes" & "A 3" are mispronounced - .replace(/(\d) E (\d)/, '$1,E $2') // Strings such as 1E5 are treated as scientific notation - .replace(/C /, 'c ') // Capital C is pronounced as "degrees celsius" when it comes after a number (e.g. R8c3) - .replace(/F /, 'f ') // Capital F is pronounced as "degrees fahrenheit" when it comes after a number (e.g. R8f3) - .replace(/(\d) H (\d)/, '$1H$2'); // "H" is pronounced as "hour" when it comes after a number with a space (e.g. Rook 5 H 3) + ? 'long castle checkmate' + : san.includes('O-O-O+') + ? 'long castle check' + : san.includes('O-O-O') + ? 'long castle' + : san.includes('O-O#') + ? 'short castle checkmate' + : san.includes('O-O+') + ? 'short castle check' + : san.includes('O-O') + ? 'short castle' + : san + .split('') + .map(c => { + if (c == 'x') return 'takes'; + if (c == '+') return 'check'; + if (c == '#') return 'checkmate'; + if (c == '=') return 'promotes to'; + if (c == '@') return 'at'; + const code = c.charCodeAt(0); + if (code > 48 && code < 58) return c; // 1-8 + if (code > 96 && code < 105) return c.toUpperCase(); + return charRole(c) || c; + }) + .join(' ') + .replace(/^A /, 'A, ') // "A takes" & "A 3" are mispronounced + .replace(/(\d) E (\d)/, '$1,E $2') // Strings such as 1E5 are treated as scientific notation + .replace(/C /, 'c ') // Capital C is pronounced as "degrees celsius" when it comes after a number (e.g. R8c3) + .replace(/F /, 'f ') // Capital F is pronounced as "degrees fahrenheit" when it comes after a number (e.g. R8f3) + .replace(/(\d) H (\d)/, '$1H$2'); // "H" is pronounced as "hour" when it comes after a number with a space (e.g. Rook 5 H 3) this.say(text, cut); } @@ -196,6 +197,7 @@ export default new (class implements SoundI { } async resumeWithTest(): Promise { + if (!this.ctx) return false; if (this.ctx.state !== 'running' && this.ctx.state !== 'suspended') { // in addition to 'closed', iOS has 'interrupted'. who knows what else is out there if (this.ctx.state !== 'closed') this.ctx.close(); @@ -205,7 +207,7 @@ export default new (class implements SoundI { } } // if suspended, try audioContext.resume() with a timeout (sometimes it never resolves) - if (this.ctx.state === 'suspended') + if (this.ctx?.state === 'suspended') await new Promise(resolve => { const resumeTimer = setTimeout(() => { $('#warn-no-autoplay').addClass('shown'); @@ -219,7 +221,7 @@ export default new (class implements SoundI { }) .catch(resolve); }); - if (this.ctx.state !== 'running') return false; + if (this.ctx?.state !== 'running') return false; $('#warn-no-autoplay').removeClass('shown'); return true; } @@ -257,6 +259,10 @@ class Sound { } } -function makeAudioContext(): AudioContext { - return window.webkitAudioContext ? new window.webkitAudioContext() : new AudioContext(); +function makeAudioContext(): AudioContext | undefined { + return window.webkitAudioContext + ? new window.webkitAudioContext() + : typeof AudioContext !== 'undefined' + ? new AudioContext() + : undefined; } diff --git a/ui/site/src/component/storage.ts b/ui/site/src/component/storage.ts index eaf055651a9a2..c22af8cbcd076 100644 --- a/ui/site/src/component/storage.ts +++ b/ui/site/src/component/storage.ts @@ -14,25 +14,41 @@ const builder = (storage: Storage): LichessStorageHelper => { }), ), remove: (k: string) => storage.removeItem(k), - make: (k: string) => ({ - get: () => api.get(k), - set: (v: any) => api.set(k, v), - fire: (v?: string) => api.fire(k, v), - remove: () => api.remove(k), - listen: (f: (e: LichessStorageEvent) => void) => - window.addEventListener('storage', e => { - if (e.key !== k || e.storageArea !== storage || e.newValue === null) return; - let parsed: LichessStorageEvent | null; - try { - parsed = JSON.parse(e.newValue); - } catch (_) { - return; - } - // check sri, because Safari fires events also in the original - // document when there are multiple tabs - if (parsed?.sri && parsed.sri !== sri) f(parsed); - }), - }), + make: (k: string, ttl?: number) => { + const bdKey = ttl && `${k}--bd`; + const remove = () => { + api.remove(k); + if (bdKey) api.remove(bdKey); + }; + return { + get: () => { + if (!bdKey) return api.get(k); + const birthday = Number(api.get(bdKey)); + if (!birthday) api.set(bdKey, String(Date.now())); + else if (Date.now() - birthday > ttl) remove(); + return api.get(k); + }, + set: (v: any) => { + api.set(k, v); + if (bdKey) api.set(bdKey, String(Date.now())); + }, + fire: (v?: string) => api.fire(k, v), + remove, + listen: (f: (e: LichessStorageEvent) => void) => + window.addEventListener('storage', e => { + if (e.key !== k || e.storageArea !== storage || e.newValue === null) return; + let parsed: LichessStorageEvent | null; + try { + parsed = JSON.parse(e.newValue); + } catch (_) { + return; + } + // check sri, because Safari fires events also in the original + // document when there are multiple tabs + if (parsed?.sri && parsed.sri !== sri) f(parsed); + }), + }; + }, boolean: (k: string) => ({ get: () => api.get(k) == '1', getOrDefault: (defaultValue: boolean) => { diff --git a/ui/site/src/component/timeago.ts b/ui/site/src/component/timeago.ts index 86edc5e8b2ef9..5af7f27a93bb6 100644 --- a/ui/site/src/component/timeago.ts +++ b/ui/site/src/component/timeago.ts @@ -8,7 +8,7 @@ interface ElementWithDate extends Element { } // past, future, divisor, at least -const units: [string | undefined, string, number, number][] = [ +const agoUnits: [string | undefined, string, number, number][] = [ ['nbYearsAgo', 'inNbYears', 60 * 60 * 24 * 365, 1], ['nbMonthsAgo', 'inNbMonths', (60 * 60 * 24 * 365) / 12, 1], ['nbWeeksAgo', 'inNbWeeks', 60 * 60 * 24 * 7, 1], @@ -24,13 +24,21 @@ const toDate = (input: DateLike): Date => input instanceof Date ? input : new Date(isNaN(input as any) ? input : parseInt(input as any)); // format the diff second to *** time ago -const formatDiff = (seconds: number): string => { +const formatAgo = (seconds: number): string => { const absSeconds = Math.abs(seconds); const strIndex = seconds < 0 ? 1 : 0; - const unit = units.find(unit => absSeconds >= unit[2] * unit[3] && unit[strIndex])!; + const unit = agoUnits.find(unit => absSeconds >= unit[2] * unit[3] && unit[strIndex])!; return siteTrans.pluralSame(unit[strIndex]!, Math.floor(absSeconds / unit[2])); }; +// format the diff second to *** time remaining +const formatRemaining = (seconds: number): string => + seconds < 1 + ? siteTrans.noarg('completed') + : seconds < 3600 + ? siteTrans.pluralSame('nbMinutesRemaining', Math.floor(seconds / 60)) + : siteTrans.pluralSame('nbHoursRemaining', Math.floor(seconds / 3600)); + export const formatter = memoize(() => window.Intl ? // for many users, using the islamic calendar is not practical on the internet @@ -48,7 +56,7 @@ export const formatter = memoize(() => : (d: Date) => d.toLocaleString(), ); -export const format = (date: DateLike) => formatDiff((Date.now() - toDate(date).getTime()) / 1000); +export const format = (date: DateLike) => formatAgo((Date.now() - toDate(date).getTime()) / 1000); export const findAndRender = (parent?: HTMLElement) => requestAnimationFrame(() => { @@ -67,9 +75,12 @@ export const findAndRender = (parent?: HTMLElement) => cl.add('set'); if (abs || cl.contains('once')) cl.remove('timeago'); } - if (!abs) { + if (cl.contains('remaining')) { + const diff = (node.lichessDate.getTime() - now) / 1000; + node.textContent = formatRemaining(diff); + } else if (!abs) { const diff = (now - node.lichessDate.getTime()) / 1000; - node.textContent = formatDiff(diff); + node.textContent = formatAgo(diff); if (Math.abs(diff) > 9999) cl.remove('timeago'); // ~3h } }); diff --git a/ui/site/src/component/top-bar.ts b/ui/site/src/component/top-bar.ts index 658a09f9f38e5..be78a7ce92c24 100644 --- a/ui/site/src/component/top-bar.ts +++ b/ui/site/src/component/top-bar.ts @@ -24,7 +24,8 @@ export default function () { $p.siblings('.shown').removeClass('shown'); setTimeout(() => { const handler = (e: Event) => { - if ($p[0]?.contains(e.target as HTMLElement)) return; + const target = e.target as HTMLElement; + if (!target.isConnected || $p[0]?.contains(target)) return; $p.removeClass('shown'); $('html').off('click', handler); }; diff --git a/ui/site/src/dailyFeed.ts b/ui/site/src/dailyFeed.ts new file mode 100644 index 0000000000000..3ecbecc53385a --- /dev/null +++ b/ui/site/src/dailyFeed.ts @@ -0,0 +1,5 @@ +import flairPicker from './component/flairPicker'; + +$('.emoji-details').each(function (this: HTMLElement) { + flairPicker(this); +}); diff --git a/ui/site/src/diagnostic.ts b/ui/site/src/diagnostic.ts index f7a3378cb2ecb..778e7fe6ca5a3 100644 --- a/ui/site/src/diagnostic.ts +++ b/ui/site/src/diagnostic.ts @@ -1,22 +1,28 @@ import { isTouchDevice } from 'common/device'; -import { domDialog, ready } from 'common/dialog'; +import { domDialog } from 'common/dialog'; +import * as licon from 'common/licon'; export default async function initModule() { - const [logs] = await Promise.all([lichess.log.get(), ready]); + const logs = await lichess.log.get(); const text = `Browser: ${navigator.userAgent}\n` + - `Cores: ${navigator.hardwareConcurrency}\n` + - `Touch: ${isTouchDevice()} ${navigator.maxTouchPoints}\n` + - `Screen: ${window.screen.width}x${window.screen.height}\n` + - `Lang: ${navigator.language}` + - (logs ? `\n\n${logs}` : ''); + `Cores: ${navigator.hardwareConcurrency}, ` + + `Touch: ${isTouchDevice()} ${navigator.maxTouchPoints}, ` + + `Screen: ${window.screen.width}x${window.screen.height}, ` + + `Lang: ${navigator.language}\n` + + `Engine: ${lichess.storage.get('ceval.engine')}, ` + + `Threads: ${lichess.storage.get('ceval.threads')}` + + `\n\n${logs ?? ''}`.trim(); const dlg = await domDialog({ class: 'diagnostic', cssPath: 'diagnostic', htmlText: `

Diagnostics

${lichess.escapeHtml(text)}
` + - (logs ? `` : ''), + '' + + (logs + ? '  ' + : ''), }); const select = () => setTimeout(() => { @@ -26,6 +32,13 @@ export default async function initModule() { window.getSelection()?.addRange(range); }, 0); $('.err', dlg.view).on('focus', select); - $('.clear', dlg.view).on('click', () => lichess.log.clear().then(lichess.reload)); + $('.clear', dlg.view).on('click', () => lichess.log.clear().then(dlg.close)); + $('.copy', dlg.view).on('click', () => + navigator.clipboard + .writeText(text) + .then(() => + $('.copy', dlg.view).replaceWith($(`COPIED `)), + ), + ); dlg.showModal(); } diff --git a/ui/site/src/expandText.ts b/ui/site/src/expandText.ts index 9cdda2bcafd4f..e9cd57e08848c 100644 --- a/ui/site/src/expandText.ts +++ b/ui/site/src/expandText.ts @@ -153,5 +153,5 @@ lichess.load.then(() => { as.filter(a => a.type === 'twitter').forEach(expandTwitter); - if ($('.lpv--autostart').length) lichess.loadEsm('lpv'); + if ($('.lpv--autostart').length) lichess.asset.loadEsm('lpv'); }); diff --git a/ui/site/src/emojiPicker.ts b/ui/site/src/flairPicker.ts similarity index 87% rename from ui/site/src/emojiPicker.ts rename to ui/site/src/flairPicker.ts index 0afe5259463c5..7c340051dd48b 100644 --- a/ui/site/src/emojiPicker.ts +++ b/ui/site/src/flairPicker.ts @@ -1,30 +1,32 @@ import * as emojis from 'emoji-mart'; -export function emojiPicker( - element: HTMLElement, - close: () => void, - onEmojiSelect: (i?: { id: string; src: string }) => void, -) { - if (element.classList.contains('emoji-done')) return; +type Config = { + element: HTMLElement; + close: () => void; + onEmojiSelect: (i?: { id: string; src: string }) => void; +}; + +export function initModule(cfg: Config) { + if (cfg.element.classList.contains('emoji-done')) return; const opts = { - onEmojiSelect, - onClickOutside: close, + ...cfg, + onClickOutside: cfg.close, data: makeEmojiData, categories: categories.map(categ => categ[0]), categoryIcons, previewEmoji: 'people.backhand-index-pointing-up', noResultsEmoji: 'smileys.crying-face', skinTonePosition: 'none', + exceptEmojis: cfg.element.dataset.exceptEmojis?.split(' '), }; const picker = new emojis.Picker(opts); - element.appendChild(picker as unknown as HTMLElement); - element.classList.add('emoji-done'); - $(element).find('em-emoji-picker').attr('trap-bypass', '1'); // disable mousetrap within the shadow DOM + cfg.element.appendChild(picker as unknown as HTMLElement); + cfg.element.classList.add('emoji-done'); + $(cfg.element).find('em-emoji-picker').attr('trap-bypass', '1'); // disable mousetrap within the shadow DOM } const makeEmojiData = async () => { - const flairUrl = lichess.assetUrl('flair', { version: '_____2' }); // bump version if a flair is changed only (not added or removed) - const res = await fetch(lichess.assetUrl('flair/list.txt')); + const res = await fetch(lichess.asset.url('flair/list.txt')); const text = await res.text(); const lines = text.split('\n').slice(0, -1); const data = { @@ -44,7 +46,7 @@ const makeEmojiData = async () => { keywords: [categ, ...name.split('-')], skins: [ { - src: `${flairUrl}/img/${key}.webp`, + src: lichess.asset.flairSrc(key), }, ], }, diff --git a/ui/site/src/forum.ts b/ui/site/src/forum.ts index d91e25276a255..2ce72d69199b1 100644 --- a/ui/site/src/forum.ts +++ b/ui/site/src/forum.ts @@ -33,7 +33,7 @@ lichess.load.then(() => { $(el).replaceWith($('.forum-post__message', el)); }); $('.forum-post__message').each(function (this: HTMLElement) { - if (this.innerText.match(/(^|\n)>/)) { + if (this.innerHTML.match(/(^|
)>/)) { const hiddenQuotes = '>'; let result = ''; let quote = []; @@ -100,7 +100,7 @@ lichess.load.then(() => { topicId = $(this).attr('data-topic'); if (topicId) - lichess.loadIife('vendor/textcomplete.min.js').then(function () { + lichess.asset.loadIife('vendor/textcomplete.min.js').then(function () { const searchCandidates = function (term: string, candidateUsers: string[]) { return candidateUsers.filter((user: string) => user.toLowerCase().startsWith(term.toLowerCase())); }; diff --git a/ui/site/src/login.ts b/ui/site/src/login.ts index d1c84972b65c4..6f134b5a08eec 100644 --- a/ui/site/src/login.ts +++ b/ui/site/src/login.ts @@ -108,5 +108,5 @@ function signupStart() { else return false; }); - lichess.loadEsm('passwordComplexity', { init: 'form3-password' }); + lichess.asset.loadEsm('passwordComplexity', { init: 'form3-password' }); } diff --git a/ui/site/src/site.lichess.globals.ts b/ui/site/src/site.lichess.globals.ts index b17f5494cbb97..fd2a73c64c864 100644 --- a/ui/site/src/site.lichess.globals.ts +++ b/ui/site/src/site.lichess.globals.ts @@ -7,17 +7,7 @@ import sri from './component/sri'; import { storage, tempStorage } from './component/storage'; import powertip from './component/powertip'; import clockWidget from './component/clock-widget'; -import { - assetUrl, - flairSrc, - loadCss, - loadCssPath, - jsModule, - loadIife, - hopscotch, - userComplete, - loadEsm, -} from './component/assets'; +import * as assets from './component/assets'; import makeLog from './component/log'; import idleTimer from './component/idle-timer'; import pubsub from './component/pubsub'; @@ -47,15 +37,7 @@ export default () => { l.powertip = powertip; l.clockWidget = clockWidget; l.spinnerHtml = spinnerHtml; - l.assetUrl = assetUrl; - l.flairSrc = flairSrc; - l.loadCss = loadCss; - l.loadCssPath = loadCssPath; - l.jsModule = jsModule; - l.loadIife = loadIife; - l.loadEsm = loadEsm; - l.hopscotch = hopscotch; - l.userComplete = userComplete; + l.asset = assets; l.idleTimer = idleTimer; l.pubsub = pubsub; l.unload = unload; @@ -73,7 +55,8 @@ export default () => { l.dateFormat = dateFormat; l.contentLoaded = (parent?: HTMLElement) => pubsub.emit('content-loaded', parent); l.blindMode = document.body.classList.contains('blind-mode'); - l.makeChat = data => lichess.loadEsm('chat', { init: { el: document.querySelector('.mchat')!, ...data } }); + l.makeChat = data => + lichess.asset.loadEsm('chat', { init: { el: document.querySelector('.mchat')!, ...data } }); l.makeChessground = Chessground; l.log = makeLog(); }; diff --git a/ui/site/src/site.ts b/ui/site/src/site.ts index 4315c56234341..441512f055b96 100644 --- a/ui/site/src/site.ts +++ b/ui/site/src/site.ts @@ -15,7 +15,6 @@ import StrongSocket from './component/socket'; import topBar from './component/top-bar'; import watchers from './component/watchers'; import { requestIdleCallback } from './component/functions'; -import { userComplete } from './component/assets'; import { siteTrans } from './component/trans'; import { isIOS } from 'common/device'; import { scrollToInnerSelector } from 'common'; @@ -99,7 +98,7 @@ lichess.load.then(() => { $('.user-autocomplete').each(function (this: HTMLInputElement) { const focus = !!this.autofocus; const start = () => - userComplete({ + lichess.asset.userComplete({ input: this, friend: !!this.dataset.friend, tag: this.dataset.tag as any, @@ -138,7 +137,7 @@ lichess.load.then(() => { if (setBlind && !lichess.blindMode) setTimeout(() => $('#blind-mode button').trigger('click'), 1500); - if (showDebug) lichess.loadEsm('diagnostic'); + if (showDebug) lichess.asset.loadEsm('diagnostic'); const pageAnnounce = document.body.getAttribute('data-announce'); if (pageAnnounce) announce(JSON.parse(pageAnnounce)); diff --git a/ui/site/src/team.ts b/ui/site/src/team.ts index edd8306fff3a1..ca455fc99b414 100644 --- a/ui/site/src/team.ts +++ b/ui/site/src/team.ts @@ -1,4 +1,5 @@ import * as xhr from 'common/xhr'; +import flairPicker from './component/flairPicker'; interface TeamOpts { id: string; @@ -26,3 +27,7 @@ $('button.explain').on('click', e => { if (why && why.length > 3) $(e.target).parents('form').find('input[name="explain"]').val(why); else return false; }); + +$('.emoji-details').each(function (this: HTMLElement) { + flairPicker(this); +}); diff --git a/ui/site/src/userComplete.ts b/ui/site/src/userComplete.ts index 13673eb093a84..2f6750813ff1e 100644 --- a/ui/site/src/userComplete.ts +++ b/ui/site/src/userComplete.ts @@ -64,7 +64,7 @@ export function initModule(opts: Opts): void { ' ' : '') + o.name + - (o.flair ? '' : '') + + (o.flair ? '' : '') + '' diff --git a/ui/storm/package.json b/ui/storm/package.json index 5b8060d40d557..c040283336cb2 100644 --- a/ui/storm/package.json +++ b/ui/storm/package.json @@ -13,7 +13,7 @@ "license": "AGPL-3.0-or-later", "dependencies": { "chess": "workspace:*", - "chessops": "^0.12.7", + "chessops": "^0.13.0", "common": "workspace:*", "puz": "workspace:*", "snabbdom": "^3.5.1" diff --git a/ui/storm/src/view/end.ts b/ui/storm/src/view/end.ts index a49dc5d380c7e..9992c5e7a75d1 100644 --- a/ui/storm/src/view/end.ts +++ b/ui/storm/src/view/end.ts @@ -1,11 +1,10 @@ import StormCtrl from '../ctrl'; import { getNow } from 'puz/util'; import renderHistory from 'puz/view/history'; -import { h, VNode } from 'snabbdom'; import { numberSpread } from 'common/number'; -import { onInsert } from 'common/snabbdom'; +import { onInsert, LooseVNodes, looseH as h } from 'common/snabbdom'; -const renderEnd = (ctrl: StormCtrl): VNode[] => [...renderSummary(ctrl), renderHistory(ctrl)]; +const renderEnd = (ctrl: StormCtrl): LooseVNodes => [...renderSummary(ctrl), renderHistory(ctrl)]; const newHighI18n = { day: 'newDailyHighscore', @@ -14,32 +13,27 @@ const newHighI18n = { allTime: 'newAllTimeHighscore', }; -const renderSummary = (ctrl: StormCtrl): VNode[] => { +const renderSummary = (ctrl: StormCtrl): LooseVNodes => { const run = ctrl.runStats(); const high = ctrl.vm.response?.newHigh; const accuracy = (100 * (run.moves - run.errors)) / run.moves; const noarg = ctrl.trans.noarg; const scoreSteps = Math.min(run.score, 50); return [ - ...(high - ? [ - h( - 'div.storm--end__high.storm--end__high-daily.bar-glider', - h('div.storm--end__high__content', [ - h('div.storm--end__high__text', [ - h('strong', noarg(newHighI18n[high.key])), - high.prev ? h('span', ctrl.trans('previousHighscoreWasX', high.prev)) : null, - ]), - ]), - ), - ] - : []), + high && + h( + 'div.storm--end__high.storm--end__high-daily.bar-glider', + h('div.storm--end__high__content', [ + h('div.storm--end__high__text', [ + h('strong', noarg(newHighI18n[high.key])), + high.prev ? h('span', ctrl.trans('previousHighscoreWasX', high.prev)) : null, + ]), + ]), + ), h('div.storm--end__score', [ h( 'span.storm--end__score__number', - { - hook: onInsert(el => numberSpread(el, scoreSteps, Math.round(scoreSteps * 50), 0)(run.score)), - }, + { hook: onInsert(el => numberSpread(el, scoreSteps, Math.round(scoreSteps * 50), 0)(run.score)) }, '0', ), h('p', noarg('puzzlesSolved')), @@ -47,23 +41,21 @@ const renderSummary = (ctrl: StormCtrl): VNode[] => { h('div.storm--end__stats.box.box-pad', [ h('table.slist', [ h('tbody', [ - h('tr', [h('th', noarg('moves')), h('td', h('number', run.moves))]), + h('tr', [h('th', noarg('moves')), h('td', h('number', `${run.moves}`))]), h('tr', [h('th', noarg('accuracy')), h('td', [h('number', Number(accuracy).toFixed(1)), '%'])]), - h('tr', [h('th', noarg('combo')), h('td', h('number', ctrl.run.combo.best))]), - h('tr', [h('th', noarg('time')), h('td', [h('number', Math.round(run.time)), 's'])]), + h('tr', [h('th', noarg('combo')), h('td', h('number', `${ctrl.run.combo.best}`))]), + h('tr', [h('th', noarg('time')), h('td', [h('number', `${Math.round(run.time)}`), 's'])]), h('tr', [ h('th', noarg('timePerMove')), h('td', [h('number', Number(run.time / run.moves).toFixed(2)), 's']), ]), - h('tr', [h('th', noarg('highestSolved')), h('td', h('number', run.highest))]), + h('tr', [h('th', noarg('highestSolved')), h('td', h('number', `${run.highest}`))]), ]), ]), ]), h( 'a.storm-play-again.button', - { - attrs: ctrl.run.endAt! < getNow() - 900 ? { href: '/storm' } : {}, - }, + { attrs: ctrl.run.endAt! < getNow() - 900 ? { href: '/storm' } : {} }, noarg('playAgain'), ), ]; diff --git a/ui/storm/src/view/main.ts b/ui/storm/src/view/main.ts index 69dbf7a8c26f5..55a564e1b643d 100644 --- a/ui/storm/src/view/main.ts +++ b/ui/storm/src/view/main.ts @@ -2,25 +2,19 @@ import config from '../config'; import renderClock from 'puz/view/clock'; import renderEnd from './end'; import StormCtrl from '../ctrl'; -import { h, VNode } from 'snabbdom'; +import { VNode } from 'snabbdom'; import { makeCgOpts, povMessage } from 'puz/run'; import { makeConfig as makeCgConfig } from 'puz/view/chessground'; import { getNow } from 'puz/util'; import { playModifiers, renderCombo } from 'puz/view/util'; import * as licon from 'common/licon'; -import { onInsert } from 'common/snabbdom'; +import { onInsert, looseH as h } from 'common/snabbdom'; export default function (ctrl: StormCtrl): VNode { if (ctrl.vm.dupTab) return renderReload(ctrl, 'thisRunWasOpenedInAnotherTab'); if (ctrl.vm.lateStart) return renderReload(ctrl, 'thisRunHasExpired'); if (!ctrl.run.endAt) - return h( - 'div.storm.storm-app.storm--play', - { - class: playModifiers(ctrl.run), - }, - renderPlay(ctrl), - ); + return h('div.storm.storm-app.storm--play', { class: playModifiers(ctrl.run) }, renderPlay(ctrl)); return h('main.storm.storm--end', renderEnd(ctrl)); } @@ -50,8 +44,8 @@ const renderPlay = (ctrl: StormCtrl): VNode[] => { run.clock.startAt ? renderSolved(ctrl) : renderStart(ctrl), h('div.puz-clock', [ renderClock(run, ctrl.endNow, true), - !!malus && malus.at > now - 900 ? h('div.puz-clock__malus', '-' + malus.seconds) : null, - !!bonus && bonus.at > now - 900 ? h('div.puz-clock__bonus', '+' + bonus.seconds) : null, + !!malus && malus.at > now - 900 && h('div.puz-clock__malus', '-' + malus.seconds), + !!bonus && bonus.at > now - 900 && h('div.puz-clock__bonus', '+' + bonus.seconds), ...(run.clock.started() ? [] : [h('span.puz-clock__pov', ctrl.trans.noarg(povMessage(run)))]), ]), h('div.puz-side__table', [renderControls(ctrl), renderCombo(config, renderBonus)(run)]), @@ -60,33 +54,20 @@ const renderPlay = (ctrl: StormCtrl): VNode[] => { }; const renderSolved = (ctrl: StormCtrl): VNode => - h('div.puz-side__top.puz-side__solved', [h('div.puz-side__solved__text', ctrl.countWins())]); + h('div.puz-side__top.puz-side__solved', [h('div.puz-side__solved__text', `${ctrl.countWins()}`)]); const renderControls = (ctrl: StormCtrl): VNode => h('div.puz-side__control', [ h('a.puz-side__control__flip.button', { - class: { - active: ctrl.flipped, - 'button-empty': !ctrl.flipped, - }, - attrs: { - 'data-icon': licon.ChasingArrows, - title: ctrl.trans.noarg('flipBoard') + ' (Keyboard: f)', - }, + class: { active: ctrl.flipped, 'button-empty': !ctrl.flipped }, + attrs: { 'data-icon': licon.ChasingArrows, title: ctrl.trans.noarg('flipBoard') + ' (Keyboard: f)' }, hook: onInsert(el => el.addEventListener('click', ctrl.flip)), }), h('a.puz-side__control__reload.button.button-empty', { - attrs: { - href: '/storm', - 'data-icon': licon.Trash, - title: ctrl.trans('newRun'), - }, + attrs: { href: '/storm', 'data-icon': licon.Trash, title: ctrl.trans('newRun') }, }), h('a.puz-side__control__end.button.button-empty', { - attrs: { - 'data-icon': licon.FlagOutline, - title: ctrl.trans('endRun'), - }, + attrs: { 'data-icon': licon.FlagOutline, title: ctrl.trans('endRun') }, hook: onInsert(el => el.addEventListener('click', ctrl.endNow)), }), ]); @@ -100,11 +81,5 @@ const renderReload = (ctrl: StormCtrl, msgKey: string) => h('div.storm.storm--reload.box.box-pad', [ h('i', { attrs: { 'data-icon': licon.Storm } }), h('p', ctrl.trans.noarg(msgKey)), - h( - 'a.storm--dup__reload.button', - { - attrs: { href: '/storm' }, - }, - ctrl.trans.noarg('clickToReload'), - ), + h('a.storm--dup__reload.button', { attrs: { href: '/storm' } }, ctrl.trans.noarg('clickToReload')), ]); diff --git a/ui/swiss/src/pagination.ts b/ui/swiss/src/pagination.ts index 80a204314b9ed..e7218a6555e56 100644 --- a/ui/swiss/src/pagination.ts +++ b/ui/swiss/src/pagination.ts @@ -9,11 +9,7 @@ export const maxPerPage = 10; function button(text: string, icon: string, click: () => void, enable: boolean, ctrl: SwissCtrl): VNode { return h('button.fbt.is', { - attrs: { - 'data-icon': icon, - disabled: !enable, - title: text, - }, + attrs: { 'data-icon': icon, disabled: !enable, title: text }, hook: bind('mousedown', click, ctrl.redraw), }); } @@ -21,10 +17,7 @@ function button(text: string, icon: string, click: () => void, enable: boolean, function scrollToMeButton(ctrl: SwissCtrl): VNode | undefined { return ctrl.data.me ? h('button.fbt' + (ctrl.focusOnMe ? '.active' : ''), { - attrs: { - 'data-icon': licon.Target, - title: 'Scroll to your player', - }, + attrs: { 'data-icon': licon.Target, title: 'Scroll to your player' }, hook: bind('mousedown', ctrl.toggleFocusOnMe, ctrl.redraw), }) : undefined; diff --git a/ui/swiss/src/search.ts b/ui/swiss/src/search.ts index 17399d0855b8c..44a581e99732a 100644 --- a/ui/swiss/src/search.ts +++ b/ui/swiss/src/search.ts @@ -6,10 +6,7 @@ import TournamentController from './ctrl'; export function button(ctrl: TournamentController): VNode { return h('button.fbt', { class: { active: ctrl.searching }, - attrs: { - 'data-icon': ctrl.searching ? licon.X : licon.Search, - title: 'Search tournament players', - }, + attrs: { 'data-icon': ctrl.searching ? licon.X : licon.Search, title: 'Search tournament players' }, hook: bind('click', ctrl.toggleSearch, ctrl.redraw), }); } @@ -18,11 +15,9 @@ export function input(ctrl: TournamentController): VNode { return h( 'div.search', h('input', { - attrs: { - spellcheck: 'false', - }, + attrs: { spellcheck: 'false' }, hook: onInsert((el: HTMLInputElement) => { - lichess + lichess.asset .userComplete({ input: el, swiss: ctrl.data.id, diff --git a/ui/swiss/src/view/boards.ts b/ui/swiss/src/view/boards.ts index 78f5f3002ba5a..eeaa95f6ecc7c 100644 --- a/ui/swiss/src/view/boards.ts +++ b/ui/swiss/src/view/boards.ts @@ -19,10 +19,7 @@ const renderBoard = `div.swiss__board.mini-game.mini-game-${board.id}.mini-game--init.is2d`, { key: board.id, - attrs: { - 'data-state': `${board.fen},${board.orientation},${board.lastMove}`, - 'data-live': board.id, - }, + attrs: { 'data-state': `${board.fen},${board.orientation},${board.lastMove}`, 'data-live': board.id }, hook: { insert(vnode) { lichess.powertip.manualUserIn(vnode.elm as HTMLElement); @@ -31,11 +28,7 @@ const renderBoard = }, [ boardPlayer(board, opposite(board.orientation), opts), - h('a.cg-wrap', { - attrs: { - href: `/${board.id}/${board.orientation}`, - }, - }), + h('a.cg-wrap', { attrs: { href: `/${board.id}/${board.orientation}` } }), boardPlayer(board, board.orientation, opts), ], ); diff --git a/ui/swiss/src/view/header.ts b/ui/swiss/src/view/header.ts index a5567c11f0be3..a89bbf442a2d3 100644 --- a/ui/swiss/src/view/header.ts +++ b/ui/swiss/src/view/header.ts @@ -15,9 +15,7 @@ function clock(ctrl: SwissCtrl): VNode | undefined { if (next.in > oneDayInSeconds) return h('div.clock', [ h('time.timeago.shy', { - attrs: { - datetime: Date.now() + next.in * 1000, - }, + attrs: { datetime: Date.now() + next.in * 1000 }, hook: { insert(vnode) { (vnode.elm as HTMLElement).setAttribute('datetime', '' + (Date.now() + next.in * 1000)); @@ -30,9 +28,7 @@ function clock(ctrl: SwissCtrl): VNode | undefined { 'span.shy', ctrl.data.status == 'created' ? ctrl.trans.noarg('startingIn') : ctrl.trans.noarg('nextRound'), ), - h('span.time.text', { - hook: startClock(next.in + 1), - }), + h('span.time.text', { hook: startClock(next.in + 1) }), ]); } @@ -51,17 +47,7 @@ export default function (ctrl: SwissCtrl): VNode { 'h1', greatPlayer ? [ - h( - 'a', - { - attrs: { - href: greatPlayer.url, - target: '_blank', - rel: 'noopener', - }, - }, - greatPlayer.name, - ), + h('a', { attrs: { href: greatPlayer.url, target: '_blank', rel: 'noopener' } }, greatPlayer.name), ' Tournament', ] : [ctrl.data.name], diff --git a/ui/swiss/src/view/main.ts b/ui/swiss/src/view/main.ts index 4b50103ec56c8..6121b64da8801 100644 --- a/ui/swiss/src/view/main.ts +++ b/ui/swiss/src/view/main.ts @@ -1,7 +1,7 @@ -import { h, VNode } from 'snabbdom'; +import { VNode } from 'snabbdom'; import * as licon from 'common/licon'; import { spinnerVdom as spinner } from 'common/spinner'; -import { dataIcon, bind, onInsert, MaybeVNodes } from 'common/snabbdom'; +import { dataIcon, bind, onInsert, LooseVNodes, looseH as h } from 'common/snabbdom'; import { numberRow } from './util'; import SwissCtrl from '../ctrl'; import * as pagination from '../pagination'; @@ -17,59 +17,46 @@ export default function (ctrl: SwissCtrl) { const d = ctrl.data; const content = d.status == 'created' ? created(ctrl) : d.status == 'started' ? started(ctrl) : finished(ctrl); - return h( - 'main.' + ctrl.opts.classes, - { - hook: { - postpatch() { - lichess.miniGame.initAll(); - }, - }, - }, - [ - h('aside.swiss__side', { - hook: onInsert(el => { - $(el).replaceWith(ctrl.opts.$side); - ctrl.opts.chat && lichess.makeChat(ctrl.opts.chat); - }), - }), - h('div.swiss__underchat', { - hook: onInsert(el => { - $(el).replaceWith($('.swiss__underchat.none').removeClass('none')); - }), + return h('main.' + ctrl.opts.classes, { hook: { postpatch: () => lichess.miniGame.initAll() } }, [ + h('aside.swiss__side', { + hook: onInsert(el => { + $(el).replaceWith(ctrl.opts.$side); + ctrl.opts.chat && lichess.makeChat(ctrl.opts.chat); }), - playerInfo(ctrl) || stats(ctrl) || boards.top(d.boards, ctrl.opts), - h('div.swiss__main', [h('div.box.swiss__main-' + d.status, content), boards.many(d.boards, ctrl.opts)]), - ctrl.opts.chat - ? h('div.chat__members.none', { - hook: onInsert(lichess.watchers), - }) - : null, - ], - ); + }), + h('div.swiss__underchat', { + hook: onInsert(el => $(el).replaceWith($('.swiss__underchat.none').removeClass('none'))), + }), + playerInfo(ctrl) || stats(ctrl) || boards.top(d.boards, ctrl.opts), + h('div.swiss__main', [h('div.box.swiss__main-' + d.status, content), boards.many(d.boards, ctrl.opts)]), + ctrl.opts.chat && h('div.chat__members.none', { hook: onInsert(lichess.watchers) }), + ]); } -function created(ctrl: SwissCtrl): MaybeVNodes { +function created(ctrl: SwissCtrl): LooseVNodes { const pag = pagination.players(ctrl); return [ header(ctrl), nextRound(ctrl), controls(ctrl, pag), standing(ctrl, pag, 'created'), - ctrl.data.quote - ? h('blockquote.pull-quote', [h('p', ctrl.data.quote.text), h('footer', ctrl.data.quote.author)]) - : undefined, + ctrl.data.quote && + h('blockquote.pull-quote', [h('p', ctrl.data.quote.text), h('footer', ctrl.data.quote.author)]), ]; } -const notice = (ctrl: SwissCtrl): VNode | undefined => { +const notice = (ctrl: SwissCtrl) => { const d = ctrl.data; - return d.me && !d.me.absent && d.status == 'started' && d.nextRound - ? h('div.swiss__notice.bar-glider', ctrl.trans('standByX', d.me.name)) - : undefined; + return ( + d.me && + !d.me.absent && + d.status == 'started' && + d.nextRound && + h('div.swiss__notice.bar-glider', ctrl.trans('standByX', d.me.name)) + ); }; -function started(ctrl: SwissCtrl): MaybeVNodes { +function started(ctrl: SwissCtrl): LooseVNodes { const pag = pagination.players(ctrl); return [ header(ctrl), @@ -80,7 +67,7 @@ function started(ctrl: SwissCtrl): MaybeVNodes { ]; } -function finished(ctrl: SwissCtrl): MaybeVNodes { +function finished(ctrl: SwissCtrl): LooseVNodes { const pag = pagination.players(ctrl); return [ h('div.podium-wrap', [confetti(ctrl.data), header(ctrl), podium(ctrl)]), @@ -98,21 +85,12 @@ function nextRound(ctrl: SwissCtrl): VNode | undefined { return h( 'form.schedule-next-round', { - class: { - required: !ctrl.data.nextRound, - }, - attrs: { - action: `/api/swiss/${ctrl.data.id}/schedule-next-round`, - method: 'post', - }, + class: { required: !ctrl.data.nextRound }, + attrs: { action: `/api/swiss/${ctrl.data.id}/schedule-next-round`, method: 'post' }, }, [ h('input', { - attrs: { - name: 'date', - placeholder: 'Schedule the next round', - value: ctrl.data.nextRound?.at || '', - }, + attrs: { name: 'date', placeholder: 'Schedule the next round', value: ctrl.data.nextRound?.at || '' }, hook: onInsert((el: HTMLInputElement) => flatpickr(el, { minDate: 'today', @@ -137,24 +115,14 @@ function joinButton(ctrl: SwissCtrl): VNode | undefined { if (!ctrl.opts.userId) return h( 'a.fbt.text.highlight', - { - attrs: { - href: '/login?referrer=' + window.location.pathname, - 'data-icon': licon.PlayTriangle, - }, - }, + { attrs: { href: '/login?referrer=' + window.location.pathname, 'data-icon': licon.PlayTriangle } }, ctrl.trans('signIn'), ); if (d.joinTeam) return h( 'a.fbt.text.highlight', - { - attrs: { - href: `/team/${d.joinTeam}`, - 'data-icon': licon.Group, - }, - }, + { attrs: { href: `/team/${d.joinTeam}`, 'data-icon': licon.Group } }, ctrl.trans.noarg('joinTeam'), ); @@ -185,20 +153,14 @@ function joinButton(ctrl: SwissCtrl): VNode | undefined { ? spinner() : h( 'button.fbt.text.highlight', - { - attrs: dataIcon(licon.PlayTriangle), - hook: bind('click', _ => ctrl.join(), ctrl.redraw), - }, + { attrs: dataIcon(licon.PlayTriangle), hook: bind('click', _ => ctrl.join(), ctrl.redraw) }, ctrl.trans.noarg('join'), ) : ctrl.joinSpinner ? spinner() : h( 'button.fbt.text', - { - attrs: dataIcon(licon.FlagOutline), - hook: bind('click', ctrl.withdraw, ctrl.redraw), - }, + { attrs: dataIcon(licon.FlagOutline), hook: bind('click', ctrl.withdraw, ctrl.redraw) }, ctrl.trans.noarg('withdraw'), ); @@ -207,110 +169,86 @@ function joinButton(ctrl: SwissCtrl): VNode | undefined { function joinTheGame(ctrl: SwissCtrl) { const gameId = ctrl.data.me?.gameId; - return gameId - ? h( - 'a.swiss__ur-playing.button.is.is-after', - { - attrs: { href: '/' + gameId }, - }, - [ctrl.trans('youArePlaying'), h('br'), ctrl.trans('joinTheGame')], - ) - : undefined; + return ( + gameId && + h('a.swiss__ur-playing.button.is.is-after', { attrs: { href: '/' + gameId } }, [ + ctrl.trans('youArePlaying'), + h('br'), + ctrl.trans('joinTheGame'), + ]) + ); } -function confetti(data: SwissData): VNode | undefined { - return data.me && data.isRecentlyFinished && lichess.once('tournament.end.canvas.' + data.id) - ? h('canvas#confetti', { - hook: { - insert: _ => lichess.loadIife('javascripts/confetti.js'), - }, - }) - : undefined; +function confetti(data: SwissData) { + return ( + data.me && + data.isRecentlyFinished && + lichess.once('tournament.end.canvas.' + data.id) && + h('canvas#confetti', { + hook: { + insert: _ => lichess.asset.loadIife('javascripts/confetti.js'), + }, + }) + ); } -function stats(ctrl: SwissCtrl): VNode | undefined { +function stats(ctrl: SwissCtrl) { const s = ctrl.data.stats, noarg = ctrl.trans.noarg, slots = ctrl.data.round * ctrl.data.nbPlayers; - return s - ? h('div.swiss__stats', [ - h('h2', noarg('tournamentComplete')), - h('table', [ - ctrl.opts.showRatings ? numberRow(noarg('averageElo'), s.averageRating, 'raw') : null, - numberRow(noarg('gamesPlayed'), s.games), - numberRow(noarg('whiteWins'), [s.whiteWins, slots], 'percent'), - numberRow(noarg('blackWins'), [s.blackWins, slots], 'percent'), - numberRow(noarg('draws'), [s.draws, slots], 'percent'), - numberRow('Byes', [s.byes, slots], 'percent'), - numberRow('Absences', [s.absences, slots], 'percent'), - ]), - h('div.swiss__stats__links', [ - h( - 'a', - { - attrs: { - href: `/swiss/${ctrl.data.id}/round/1`, - }, - }, - ctrl.trans('viewAllXRounds', ctrl.data.round), - ), - h('br'), - h( - 'a.text', - { - attrs: { - 'data-icon': licon.Download, - href: `/swiss/${ctrl.data.id}.trf`, - download: true, - }, - }, - 'Download TRF file', - ), - h( - 'a.text', - { - attrs: { - 'data-icon': licon.Download, - href: `/api/swiss/${ctrl.data.id}/games`, - download: true, - }, - }, - 'Download all games', - ), - h( - 'a.text', - { - attrs: { - 'data-icon': licon.Download, - href: `/api/swiss/${ctrl.data.id}/results`, - download: true, - }, - }, - 'Download results as NDJSON', - ), - h( - 'a.text', - { - attrs: { - 'data-icon': licon.Download, - href: `/api/swiss/${ctrl.data.id}/results?as=csv`, - download: true, - }, - }, - 'Download results as CSV', - ), - h('br'), - h( - 'a.text', - { - attrs: { - 'data-icon': licon.InfoCircle, - href: 'https://lichess.org/api#tag/Swiss-tournaments', - }, - }, - 'Swiss API documentation', - ), - ]), - ]) - : undefined; + if (!s) return undefined; + return h('div.swiss__stats', [ + h('h2', noarg('tournamentComplete')), + h('table', [ + ctrl.opts.showRatings ? numberRow(noarg('averageElo'), s.averageRating, 'raw') : null, + numberRow(noarg('gamesPlayed'), s.games), + numberRow(noarg('whiteWins'), [s.whiteWins, slots], 'percent'), + numberRow(noarg('blackWins'), [s.blackWins, slots], 'percent'), + numberRow(noarg('drawRate'), [s.draws, slots], 'percent'), + numberRow('Byes', [s.byes, slots], 'percent'), + numberRow('Absences', [s.absences, slots], 'percent'), + ]), + h('div.swiss__stats__links', [ + h( + 'a', + { attrs: { href: `/swiss/${ctrl.data.id}/round/1` } }, + ctrl.trans('viewAllXRounds', ctrl.data.round), + ), + h('br'), + h( + 'a.text', + { attrs: { 'data-icon': licon.Download, href: `/swiss/${ctrl.data.id}.trf`, download: true } }, + 'Download TRF file', + ), + h( + 'a.text', + { attrs: { 'data-icon': licon.Download, href: `/api/swiss/${ctrl.data.id}/games`, download: true } }, + 'Download all games', + ), + h( + 'a.text', + { + attrs: { 'data-icon': licon.Download, href: `/api/swiss/${ctrl.data.id}/results`, download: true }, + }, + 'Download results as NDJSON', + ), + h( + 'a.text', + { + attrs: { + 'data-icon': licon.Download, + href: `/api/swiss/${ctrl.data.id}/results?as=csv`, + download: true, + }, + }, + 'Download results as CSV', + ), + h('br'), + h( + 'a.text', + { attrs: { 'data-icon': licon.InfoCircle, href: 'https://lichess.org/api#tag/Swiss-tournaments' } }, + 'Swiss API documentation', + ), + ]), + ]); } diff --git a/ui/swiss/src/view/playerInfo.ts b/ui/swiss/src/view/playerInfo.ts index a0426052a28b8..b444f7d239d33 100644 --- a/ui/swiss/src/view/playerInfo.ts +++ b/ui/swiss/src/view/playerInfo.ts @@ -1,7 +1,7 @@ -import { h, VNode } from 'snabbdom'; +import { VNode } from 'snabbdom'; import * as licon from 'common/licon'; import { spinnerVdom as spinner } from 'common/spinner'; -import { bind, dataIcon } from 'common/snabbdom'; +import { bind, dataIcon, looseH as h } from 'common/snabbdom'; import { player as renderPlayer, numberRow } from './util'; import { Pairing } from '../interfaces'; import { isOutcome } from '../util'; @@ -20,83 +20,64 @@ export default function (ctrl: SwissCtrl): VNode | undefined { const avgOp: number | undefined = games ? Math.round(data.sheet.reduce((r, p) => r + ((p as any).rating || 1), 0) / games) : undefined; - return h( - tag, - { - hook: { - insert: setup, - postpatch(_, vnode) { - setup(vnode); - }, - }, - }, - [ - h('a.close', { - attrs: dataIcon(licon.X), - hook: bind('click', () => ctrl.showPlayerInfo(data), ctrl.redraw), - }), - h('div.stats', [ - h('h2', [h('span.rank', data.rank + '. '), renderPlayer(data, true, false)]), - h('table', [ - numberRow('Points', data.points, 'raw'), - numberRow('Tie break', data.tieBreak, 'raw'), - ...(games - ? [ - data.performance && ctrl.opts.showRatings - ? numberRow(noarg('performance'), data.performance + (games < 3 ? '?' : ''), 'raw') - : null, - numberRow(noarg('winRate'), [wins, games], 'percent'), - ctrl.opts.showRatings ? numberRow(noarg('averageOpponent'), avgOp, 'raw') : null, - ] - : []), - ]), + return h(tag, { hook: { insert: setup, postpatch: (_, vnode) => setup(vnode) } }, [ + h('a.close', { + attrs: dataIcon(licon.X), + hook: bind('click', () => ctrl.showPlayerInfo(data), ctrl.redraw), + }), + h('div.stats', [ + h('h2', [h('span.rank', data.rank + '. '), renderPlayer(data, true, false)]), + h('table', [ + numberRow('Points', data.points, 'raw'), + numberRow('Tie break', data.tieBreak, 'raw'), + ...(games + ? [ + data.performance && + ctrl.opts.showRatings && + numberRow(noarg('performance'), data.performance + (games < 3 ? '?' : ''), 'raw'), + numberRow(noarg('winRate'), [wins, games], 'percent'), + ctrl.opts.showRatings && numberRow(noarg('averageOpponent'), avgOp, 'raw'), + ] + : []), ]), - h('div', [ - h( - 'table.pairings.sublist', - { - hook: bind('click', e => { - const href = ((e.target as HTMLElement).parentNode as HTMLElement).getAttribute('data-href'); - if (href) window.open(href, '_blank', 'noopener'); - }), - }, - data.sheet.map((p, i) => { - const round = ctrl.data.round - i; - if (isOutcome(p)) - return h( - 'tr.' + p, - { - key: round, - }, - [ - h('th', '' + round), - h('td.outcome', { attrs: { colspan: 3 } }, p), - h('td', p == 'absent' ? '-' : p == 'bye' ? '1' : '½'), - ], - ); - const res = result(p); - return h( - 'tr.glpt.' + (res === '1' ? '.win' : res === '0' ? '.loss' : ''), - { - key: round, - attrs: { 'data-href': '/' + p.g + (p.c ? '' : '/black') }, - hook: { - destroy: vnode => $.powerTip.destroy(vnode.elm as HTMLElement), - }, - }, - [ - h('th', '' + round), - h('td', fullName(p.user)), - ctrl.opts.showRatings ? h('td', '' + p.rating) : null, - h('td.is.color-icon.' + (p.c ? 'white' : 'black')), - h('td.result', res), - ], - ); + ]), + h('div', [ + h( + 'table.pairings.sublist', + { + hook: bind('click', e => { + const href = ((e.target as HTMLElement).parentNode as HTMLElement).getAttribute('data-href'); + if (href) window.open(href, '_blank', 'noopener'); }), - ), - ]), - ], - ); + }, + data.sheet.map((p, i) => { + const round = ctrl.data.round - i; + if (isOutcome(p)) + return h('tr.' + p, { key: round }, [ + h('th', '' + round), + h('td.outcome', { attrs: { colspan: 3 } }, p), + h('td', p == 'absent' ? '-' : p == 'bye' ? '1' : '½'), + ]); + const res = result(p); + return h( + 'tr.glpt.' + (res === '1' ? '.win' : res === '0' ? '.loss' : ''), + { + key: round, + attrs: { 'data-href': '/' + p.g + (p.c ? '' : '/black') }, + hook: { destroy: vnode => $.powerTip.destroy(vnode.elm as HTMLElement) }, + }, + [ + h('th', '' + round), + h('td', fullName(p.user)), + ctrl.opts.showRatings && h('td', '' + p.rating), + h('td.is.color-icon.' + (p.c ? 'white' : 'black')), + h('td.result', res), + ], + ); + }), + ), + ]), + ]); } function result(p: Pairing): string { diff --git a/ui/swiss/src/view/podium.ts b/ui/swiss/src/view/podium.ts index ee701aa471b23..e2f57b61d00e0 100644 --- a/ui/swiss/src/view/podium.ts +++ b/ui/swiss/src/view/podium.ts @@ -13,17 +13,12 @@ const podiumStats = (p: PodiumPlayer, ctrl: SwissCtrl): VNode => ]); function podiumPosition(p: PodiumPlayer, pos: string, ctrl: SwissCtrl): VNode | undefined { - return p - ? h( - 'div.' + pos, - { - class: { - lame: !!p.lame, - }, - }, - [h('div.trophy'), userLink({ ...p.user, line: false }), podiumStats(p, ctrl)], - ) - : undefined; + if (!p) return undefined; + return h('div.' + pos, { class: { lame: !!p.lame } }, [ + h('div.trophy'), + userLink({ ...p.user, line: false }), + podiumStats(p, ctrl), + ]); } export default function podium(ctrl: SwissCtrl) { diff --git a/ui/swiss/src/view/standing.ts b/ui/swiss/src/view/standing.ts index 7a0c321198280..aff1597b5711b 100644 --- a/ui/swiss/src/view/standing.ts +++ b/ui/swiss/src/view/standing.ts @@ -11,22 +11,14 @@ function playerTr(ctrl: SwissCtrl, player: Player) { 'tr', { key: userId, - class: { - me: ctrl.data.me?.id == userId, - active: ctrl.playerInfoId === userId, - }, + class: { me: ctrl.data.me?.id == userId, active: ctrl.playerInfoId === userId }, hook: bind('click', _ => ctrl.showPlayerInfo(player), ctrl.redraw), }, [ h( 'td.rank', player.absent && ctrl.data.status != 'finished' - ? h('i', { - attrs: { - 'data-icon': licon.Pause, - title: 'Absent', - }, - }) + ? h('i', { attrs: { 'data-icon': licon.Pause, title: 'Absent' } }) : [player.rank], ), h('td.player', renderPlayer(player, false, ctrl.opts.showRatings)), @@ -44,13 +36,7 @@ function playerTr(ctrl: SwissCtrl, player: Player) { ? h(p, title('Late'), '½') : h( 'a.glpt.' + (p.o ? 'ongoing' : p.w === true ? 'win' : p.w === false ? 'loss' : 'draw'), - { - attrs: { - key: p.g, - href: `/${p.g}`, - }, - hook: onInsert(lichess.powertip.manualGame), - }, + { attrs: { key: p.g, href: `/${p.g}` }, hook: onInsert(lichess.powertip.manualGame) }, p.o ? '*' : p.w === true ? '1' : p.w === false ? '0' : '½', ), ) @@ -76,26 +62,11 @@ export default function standing(ctrl: SwissCtrl, pag: Pager, klass?: string): V if (pag.currentPageResults) lastBody = tableBody; return h( 'table.slist.swiss__standing' + (klass ? '.' + klass : ''), - { - class: { - loading: !pag.currentPageResults, - long: ctrl.data.round > 10, - xlong: ctrl.data.round > 20, - }, - }, - [ - h( - 'tbody', - { - hook: { - insert: preloadUserTips, - update(_, vnode) { - preloadUserTips(vnode); - }, - }, - }, - tableBody, - ), - ], + { class: { loading: !pag.currentPageResults, long: ctrl.data.round > 10, xlong: ctrl.data.round > 20 } }, + h( + 'tbody', + { hook: { insert: preloadUserTips, update: (_, vnode) => preloadUserTips(vnode) } }, + tableBody, + ), ); } diff --git a/ui/swiss/src/view/util.ts b/ui/swiss/src/view/util.ts index 9f0ded4880111..6d5bc034fdb73 100644 --- a/ui/swiss/src/view/util.ts +++ b/ui/swiss/src/view/util.ts @@ -8,9 +8,7 @@ export function player(p: BasePlayer, asLink: boolean, withRating: boolean) { 'a.ulpt.user-link' + (((p.user.title || '') + p.user.name).length > 15 ? '.long' : ''), { attrs: asLink ? { href: '/@/' + p.user.name } : { 'data-href': '/@/' + p.user.name }, - hook: { - destroy: vnode => $.powerTip.destroy(vnode.elm as HTMLElement), - }, + hook: { destroy: vnode => $.powerTip.destroy(vnode.elm as HTMLElement) }, }, [ h('span.name', fullName(p.user)), diff --git a/ui/tournament/css/_leaderboard.scss b/ui/tournament/css/_leaderboard.scss index 0d06905ef16cf..9c7d2a1c53acc 100644 --- a/ui/tournament/css/_leaderboard.scss +++ b/ui/tournament/css/_leaderboard.scss @@ -4,13 +4,12 @@ $user-list-width: 35ch; .tournament-shields { @extend %box-radius; - border: $border; display: grid; grid-template-columns: repeat(auto-fit, minmax($user-list-width, 1fr)); &__item { background: $c-bg-box; - border-#{$end-direction}: $border; + border: $border; } h2 { diff --git a/ui/tournament/css/_team-info.scss b/ui/tournament/css/_team-info.scss index e99cf2ce03c17..2719e0e5e3814 100644 --- a/ui/tournament/css/_team-info.scss +++ b/ui/tournament/css/_team-info.scss @@ -1,6 +1,10 @@ .tour__team-info { .players { width: 100%; + .name { + @extend %nowrap-ellipsis; + padding-#{$end-direction}: 5px; + } tr { cursor: pointer; diff --git a/ui/tournament/src/interfaces.ts b/ui/tournament/src/interfaces.ts index 1d6243630d088..94a42d903dde6 100644 --- a/ui/tournament/src/interfaces.ts +++ b/ui/tournament/src/interfaces.ts @@ -130,9 +130,13 @@ interface FeaturedPlayer extends SimplePlayer { berserk?: boolean; } +type TeamName = string; +type TeamFlair = string; +export type LightTeam = [TeamName, TeamFlair?]; + export interface TeamBattle { teams: { - [id: string]: string; + [id: string]: LightTeam; }; joinWith: string[]; hasMoreThanTenTeams?: boolean; diff --git a/ui/tournament/src/pagination.ts b/ui/tournament/src/pagination.ts index eaa5b0ce5742b..eb23a8d2d7061 100644 --- a/ui/tournament/src/pagination.ts +++ b/ui/tournament/src/pagination.ts @@ -15,11 +15,7 @@ function button( ctrl: TournamentController, ): VNode { return h('button.fbt.is', { - attrs: { - 'data-icon': icon, - disabled: !enable, - title: text, - }, + attrs: { 'data-icon': icon, disabled: !enable, title: text }, hook: bind('mousedown', click, ctrl.redraw), }); } @@ -27,10 +23,7 @@ function button( function scrollToMeButton(ctrl: TournamentController): VNode | undefined { if (ctrl.data.me) return h('button.fbt' + (ctrl.focusOnMe ? '.active' : ''), { - attrs: { - 'data-icon': licon.Target, - title: 'Scroll to your player', - }, + attrs: { 'data-icon': licon.Target, title: 'Scroll to your player' }, hook: bind('mousedown', ctrl.toggleFocusOnMe, ctrl.redraw), }); return undefined; diff --git a/ui/tournament/src/search.ts b/ui/tournament/src/search.ts index 5e97ca4c742c2..125900a146c53 100644 --- a/ui/tournament/src/search.ts +++ b/ui/tournament/src/search.ts @@ -6,10 +6,7 @@ import TournamentController from './ctrl'; export function button(ctrl: TournamentController): VNode { return h('button.fbt', { class: { active: ctrl.searching }, - attrs: { - 'data-icon': ctrl.searching ? licon.X : licon.Search, - title: 'Search tournament players', - }, + attrs: { 'data-icon': ctrl.searching ? licon.X : licon.Search, title: 'Search tournament players' }, hook: bind('click', ctrl.toggleSearch, ctrl.redraw), }); } @@ -18,11 +15,9 @@ export function input(ctrl: TournamentController): VNode { return h( 'div.search', h('input', { - attrs: { - spellcheck: 'false', - }, + attrs: { spellcheck: 'false' }, hook: onInsert((el: HTMLInputElement) => { - lichess + lichess.asset .userComplete({ input: el, tour: ctrl.data.id, diff --git a/ui/tournament/src/view/arena.ts b/ui/tournament/src/view/arena.ts index 0bf96ccdb3d23..174cf52c7d9bc 100644 --- a/ui/tournament/src/view/arena.ts +++ b/ui/tournament/src/view/arena.ts @@ -48,12 +48,7 @@ function playerTr(ctrl: TournamentController, player: StandingPlayer) { h( 'td.rank', player.withdraw - ? h('i', { - attrs: { - 'data-icon': licon.Pause, - title: ctrl.trans.noarg('pause'), - }, - }) + ? h('i', { attrs: { 'data-icon': licon.Pause, title: ctrl.trans.noarg('pause') } }) : player.rank, ), h('td.player', [ @@ -122,18 +117,14 @@ export function standing(ctrl: TournamentController, pag: Pagination, klass?: st if (pag.currentPageResults) lastBody = tableBody; return h( 'table.slist.tour__standing' + (klass ? '.' + klass : ''), - { - class: { loading: !pag.currentPageResults }, - }, + { class: { loading: !pag.currentPageResults } }, [ h( 'tbody', { hook: { insert: vnode => lichess.powertip.manualUserIn(vnode.elm as HTMLElement), - update(_, vnode) { - lichess.powertip.manualUserIn(vnode.elm as HTMLElement); - }, + update: (_, vnode) => lichess.powertip.manualUserIn(vnode.elm as HTMLElement), }, }, tableBody, diff --git a/ui/tournament/src/view/battle.ts b/ui/tournament/src/view/battle.ts index eea64431cbf5e..f7c1f99855a23 100644 --- a/ui/tournament/src/view/battle.ts +++ b/ui/tournament/src/view/battle.ts @@ -1,8 +1,8 @@ import TournamentController from '../ctrl'; import { bind, MaybeVNode } from 'common/snabbdom'; -import { fullName } from 'common/userLink'; +import { fullName, userFlair } from 'common/userLink'; import { h, VNode } from 'snabbdom'; -import { TeamBattle, RankedTeam } from '../interfaces'; +import { TeamBattle, RankedTeam, LightTeam } from '../interfaces'; import { snabDialog } from 'common/dialog'; export function joinWithTeamSelector(ctrl: TournamentController) { @@ -31,12 +31,8 @@ export function joinWithTeamSelector(ctrl: TournamentController) { ...tb.joinWith.map(id => h( 'button.button.team-picker__team', - { - attrs: { - 'data-id': id, - }, - }, - tb.teams[id], + { attrs: { 'data-id': id } }, + renderTeamArray(tb.teams[id]), ), ), ] @@ -44,17 +40,8 @@ export function joinWithTeamSelector(ctrl: TournamentController) { h('p', 'You must join one of these teams to participate!'), h( 'ul', - shuffleArray(Object.keys(tb.teams)).map((t: string) => - h( - 'li', - h( - 'a', - { - attrs: { href: '/team/' + t }, - }, - tb.teams[t], - ), - ), + shuffleArray(Object.keys(tb.teams)).map((id: string) => + h('li', h('a', { attrs: { href: '/team/' + id } }, renderTeamArray(tb.teams[id]))), ), ), ]), @@ -63,6 +50,8 @@ export function joinWithTeamSelector(ctrl: TournamentController) { }); } +const renderTeamArray = (team: LightTeam) => [team[0], userFlair({ flair: team[1] })]; + export function teamStanding(ctrl: TournamentController, klass?: string): VNode | null { const battle = ctrl.data.teamBattle, standing = ctrl.data.teamStanding, @@ -82,16 +71,10 @@ function extraTeams(ctrl: TournamentController): VNode { 'tr', h( 'td.more-teams', - { - attrs: { colspan: 4 }, - }, + { attrs: { colspan: 4 } }, h( 'a', - { - attrs: { - href: `/tournament/${ctrl.data.id}/teams`, - }, - }, + { attrs: { href: `/tournament/${ctrl.data.id}/teams` } }, ctrl.trans('viewAllXTeams', Object.keys(ctrl.data.teamBattle!.teams).length), ), ), @@ -106,7 +89,7 @@ function myTeam(ctrl: TournamentController, battle: TeamBattle): MaybeVNode { export function teamName(battle: TeamBattle, teamId: string): VNode { return h( battle.hasMoreThanTenTeams ? 'team' : 'team.ttc-' + Object.keys(battle.teams).indexOf(teamId), - battle.teams[teamId], + renderTeamArray(battle.teams[teamId]), ); } @@ -120,12 +103,8 @@ function teamTr(ctrl: TournamentController, battle: TeamBattle, team: RankedTeam { key: p.user.name, class: { top: i === 0 }, - attrs: { - 'data-href': '/@/' + p.user.name, - }, - hook: { - destroy: vnode => $.powerTip.destroy(vnode.elm as HTMLElement), - }, + attrs: { 'data-href': '/@/' + p.user.name }, + hook: { destroy: vnode => $.powerTip.destroy(vnode.elm as HTMLElement) }, }, [...(i === 0 ? [h('username', fullName(p.user)), ' '] : []), '' + p.score], ), @@ -135,9 +114,7 @@ function teamTr(ctrl: TournamentController, battle: TeamBattle, team: RankedTeam 'tr', { key: team.id, - class: { - active: ctrl.teamInfo.requested == team.id, - }, + class: { active: ctrl.teamInfo.requested == team.id }, hook: bind('click', _ => ctrl.showTeamInfo(team.id), ctrl.redraw), }, [ diff --git a/ui/tournament/src/view/button.ts b/ui/tournament/src/view/button.ts index d0fdb704e6ef5..c4a091be90d73 100644 --- a/ui/tournament/src/view/button.ts +++ b/ui/tournament/src/view/button.ts @@ -29,41 +29,32 @@ export function join(ctrl: TournamentController): VNode { const button = h( 'button.fbt.text' + (joinable ? '.highlight' : ''), { - attrs: { - disabled: !joinable, - 'data-icon': licon.PlayTriangle, - }, + attrs: { disabled: !joinable, 'data-icon': licon.PlayTriangle }, hook: bind('click', _ => ctrl.join(), ctrl.redraw), }, ctrl.trans.noarg('join'), ); return delay - ? h( - 'div.delay-wrap', - { - attrs: { title: 'Waiting to be able to re-join the tournament' }, - }, - [ - h( - 'div.delay', - { - hook: { - insert(vnode) { - const el = vnode.elm as HTMLElement; - el.style.animation = `tour-delay ${delay}s linear`; - setTimeout(() => { - if (delay === ctrl.data.me!.pauseDelay) { - ctrl.data.me!.pauseDelay = 0; - ctrl.redraw(); - } - }, delay * 1000); - }, + ? h('div.delay-wrap', { attrs: { title: 'Waiting to be able to re-join the tournament' } }, [ + h( + 'div.delay', + { + hook: { + insert(vnode) { + const el = vnode.elm as HTMLElement; + el.style.animation = `tour-delay ${delay}s linear`; + setTimeout(() => { + if (delay === ctrl.data.me!.pauseDelay) { + ctrl.data.me!.pauseDelay = 0; + ctrl.redraw(); + } + }, delay * 1000); }, }, - [button], - ), - ], - ) + }, + button, + ), + ]) : button; }); } @@ -72,12 +63,7 @@ export function joinWithdraw(ctrl: TournamentController): VNode | undefined { if (!ctrl.opts.userId) return h( 'a.fbt.text.highlight', - { - attrs: { - href: '/login?referrer=' + window.location.pathname, - 'data-icon': licon.PlayTriangle, - }, - }, + { attrs: { href: '/login?referrer=' + window.location.pathname, 'data-icon': licon.PlayTriangle } }, ctrl.trans('signIn'), ); if (!ctrl.data.isFinished) return ctrl.isIn() ? withdraw(ctrl) : join(ctrl); diff --git a/ui/tournament/src/view/created.ts b/ui/tournament/src/view/created.ts index 97f3d1052a96f..2200b57ed1d98 100644 --- a/ui/tournament/src/view/created.ts +++ b/ui/tournament/src/view/created.ts @@ -17,11 +17,7 @@ export function main(ctrl: TournamentController): MaybeVNodes { controls(ctrl, pag), standing(ctrl, pag, 'created'), h('blockquote.pull-quote', [h('p', ctrl.data.quote.text), h('footer', ctrl.data.quote.author)]), - ctrl.opts.$faq - ? h('div', { - hook: onInsert(el => $(el).replaceWith(ctrl.opts.$faq)), - }) - : null, + ctrl.opts.$faq ? h('div', { hook: onInsert(el => $(el).replaceWith(ctrl.opts.$faq)) }) : null, ]; } diff --git a/ui/tournament/src/view/finished.ts b/ui/tournament/src/view/finished.ts index aabd3a15f08aa..bedf9d9360093 100644 --- a/ui/tournament/src/view/finished.ts +++ b/ui/tournament/src/view/finished.ts @@ -14,9 +14,7 @@ import { MaybeVNodes } from 'common/snabbdom'; function confetti(data: TournamentData): VNode | undefined { if (data.me && data.isRecentlyFinished && lichess.once('tournament.end.canvas.' + data.id)) return h('canvas#confetti', { - hook: { - insert: _ => lichess.loadIife('javascripts/confetti.js'), - }, + hook: { insert: _ => lichess.asset.loadIife('javascripts/confetti.js') }, }); return undefined; } @@ -32,7 +30,7 @@ function stats(ctrl: TournamentController): VNode | undefined { numberRow(noarg('movesPlayed'), data.stats.moves), numberRow(noarg('whiteWins'), [data.stats.whiteWins, data.stats.games], 'percent'), numberRow(noarg('blackWins'), [data.stats.blackWins, data.stats.games], 'percent'), - numberRow(noarg('draws'), [data.stats.draws, data.stats.games], 'percent'), + numberRow(noarg('drawRate'), [data.stats.draws, data.stats.games], 'percent'), ]; if (data.berserkable) { @@ -47,11 +45,7 @@ function stats(ctrl: TournamentController): VNode | undefined { ? [ h( 'a', - { - attrs: { - href: `/tournament/${data.id}/teams`, - }, - }, + { attrs: { href: `/tournament/${data.id}/teams` } }, trans('viewAllXTeams', Object.keys(data.teamBattle.teams).length), ), h('br'), @@ -59,13 +53,7 @@ function stats(ctrl: TournamentController): VNode | undefined { : []), h( 'a.text', - { - attrs: { - 'data-icon': licon.Download, - href: `/api/tournament/${data.id}/games`, - download: true, - }, - }, + { attrs: { 'data-icon': licon.Download, href: `/api/tournament/${data.id}/games`, download: true } }, 'Download all games', ), data.me && @@ -83,11 +71,7 @@ function stats(ctrl: TournamentController): VNode | undefined { h( 'a.text', { - attrs: { - 'data-icon': licon.Download, - href: `/api/tournament/${data.id}/results`, - download: true, - }, + attrs: { 'data-icon': licon.Download, href: `/api/tournament/${data.id}/results`, download: true }, }, 'Download results as NDJSON', ), @@ -105,12 +89,7 @@ function stats(ctrl: TournamentController): VNode | undefined { h('br'), h( 'a.text', - { - attrs: { - 'data-icon': licon.InfoCircle, - href: 'https://lichess.org/api#tag/Arena-tournaments', - }, - }, + { attrs: { 'data-icon': licon.InfoCircle, href: 'https://lichess.org/api#tag/Arena-tournaments' } }, 'Arena API documentation', ), ]), diff --git a/ui/tournament/src/view/header.ts b/ui/tournament/src/view/header.ts index 28e465672290b..1ed6946b21059 100644 --- a/ui/tournament/src/view/header.ts +++ b/ui/tournament/src/view/header.ts @@ -15,12 +15,7 @@ const hasFreq = (freq: 'shield' | 'marathon', d: TournamentData) => d.schedule?. function clock(d: TournamentData): VNode | undefined { if (d.isFinished) return; - if (d.secondsToFinish) - return h('div.clock', [ - h('div.time', { - hook: startClock(d.secondsToFinish), - }), - ]); + if (d.secondsToFinish) return h('div.clock', [h('div.time', { hook: startClock(d.secondsToFinish) })]); if (d.secondsToStart) { if (d.secondsToStart > oneDayInSeconds) return h('div.clock', [ @@ -41,9 +36,7 @@ function clock(d: TournamentData): VNode | undefined { ]); return h('div.clock.clock-created', [ h('span.shy', 'Starting in'), - h('span.time.text', { - hook: startClock(d.secondsToStart), - }), + h('span.time.text', { hook: startClock(d.secondsToStart) }), ]); } return undefined; @@ -53,13 +46,8 @@ function image(d: TournamentData): VNode | undefined { if (d.isFinished) return; if (hasFreq('shield', d) || hasFreq('marathon', d)) return; const s = d.spotlight; - if (s && s.iconImg) - return h('img.img', { - attrs: { src: lichess.assetUrl('images/' + s.iconImg) }, - }); - return h('i.img', { - attrs: dataIcon(s?.iconFont || licon.Trophy), - }); + if (s && s.iconImg) return h('img.img', { attrs: { src: lichess.asset.url('images/' + s.iconImg) } }); + return h('i.img', { attrs: dataIcon(s?.iconFont || licon.Trophy) }); } function title(ctrl: TournamentController) { @@ -67,13 +55,7 @@ function title(ctrl: TournamentController) { if (hasFreq('marathon', d)) return h('h1', [h('i.fire-trophy', licon.Globe), d.fullName]); if (hasFreq('shield', d)) return h('h1', [ - h( - 'a.shield-trophy', - { - attrs: { href: '/tournament/shields' }, - }, - perfIcons[d.perf.key], - ), + h('a.shield-trophy', { attrs: { href: '/tournament/shields' } }, perfIcons[d.perf.key]), d.fullName, ]); return h( @@ -82,13 +64,7 @@ function title(ctrl: TournamentController) { ? [ h( 'a', - { - attrs: { - href: d.greatPlayer.url, - target: '_blank', - rel: 'noopener', - }, - }, + { attrs: { href: d.greatPlayer.url, target: '_blank', rel: 'noopener' } }, d.greatPlayer.name, ), ' Arena', diff --git a/ui/tournament/src/view/main.ts b/ui/tournament/src/view/main.ts index 958c64b5bd7c0..93a86d74eea13 100644 --- a/ui/tournament/src/view/main.ts +++ b/ui/tournament/src/view/main.ts @@ -24,26 +24,18 @@ export default function (ctrl: TournamentController) { }), }), h('div.tour__underchat', { - hook: onInsert(el => { - $(el).replaceWith($('.tour__underchat.none').removeClass('none')); - }), + hook: onInsert(el => $(el).replaceWith($('.tour__underchat.none').removeClass('none'))), }), handler.table(ctrl), h( 'div.tour__main', h( 'div.box.' + handler.name, - { - class: { 'tour__main-finished': ctrl.data.isFinished }, - }, + { class: { 'tour__main-finished': ctrl.data.isFinished } }, handler.main(ctrl), ), ), - ctrl.opts.chat - ? h('div.chat__members.none', { - hook: onInsert(lichess.watchers), - }) - : null, + ctrl.opts.chat ? h('div.chat__members.none', { hook: onInsert(lichess.watchers) }) : null, ctrl.joinWithTeamSelector ? joinWithTeamSelector(ctrl) : null, ]); } diff --git a/ui/tournament/src/view/playerInfo.ts b/ui/tournament/src/view/playerInfo.ts index 87c5ed69753a2..340826381e1c9 100644 --- a/ui/tournament/src/view/playerInfo.ts +++ b/ui/tournament/src/view/playerInfo.ts @@ -1,7 +1,7 @@ -import { h, VNode } from 'snabbdom'; +import { VNode } from 'snabbdom'; import * as licon from 'common/licon'; import { spinnerVdom as spinner } from 'common/spinner'; -import { bind, dataIcon } from 'common/snabbdom'; +import { bind, dataIcon, looseH as h } from 'common/snabbdom'; import { numberRow, player as renderPlayer } from './util'; import { fullName } from 'common/userLink'; import { teamName } from './battle'; @@ -41,81 +41,63 @@ export default function (ctrl: TournamentController): VNode { avgOp = pairingsLen ? Math.round(data.pairings.reduce((a, b) => a + b.op.rating, 0) / pairingsLen) : undefined; - return h( - tag, - { - hook: { - insert: setup, - postpatch(_, vnode) { - setup(vnode); - }, - }, - }, - [ - h('a.close', { - attrs: dataIcon(licon.X), - hook: bind('click', () => ctrl.showPlayerInfo(data.player), ctrl.redraw), - }), - h('div.stats', [ - playerTitle(data.player), - data.player.team - ? h( - 'team', - { - hook: bind('click', () => ctrl.showTeamInfo(data.player.team!), ctrl.redraw), - }, - [teamName(ctrl.data.teamBattle!, data.player.team)], - ) - : null, - h('table', [ - ctrl.opts.showRatings && data.player.performance - ? numberRow(noarg('performance'), data.player.performance + (nb.game < 3 ? '?' : ''), 'raw') - : null, - numberRow(noarg('gamesPlayed'), nb.game), - ...(nb.game - ? [ - numberRow(noarg('winRate'), [nb.win, nb.game], 'percent'), - numberRow(noarg('berserkRate'), [nb.berserk, nb.game], 'percent'), - ctrl.opts.showRatings ? numberRow(noarg('averageOpponent'), avgOp, 'raw') : null, - ] - : []), + return h(tag, { hook: { insert: setup, postpatch: (_, vnode) => setup(vnode) } }, [ + h('a.close', { + attrs: dataIcon(licon.X), + hook: bind('click', () => ctrl.showPlayerInfo(data.player), ctrl.redraw), + }), + h('div.stats', [ + playerTitle(data.player), + data.player.team && + h('team', { hook: bind('click', () => ctrl.showTeamInfo(data.player.team!), ctrl.redraw) }, [ + teamName(ctrl.data.teamBattle!, data.player.team), ]), + h('table', [ + ctrl.opts.showRatings && + data.player.performance && + numberRow(noarg('performance'), data.player.performance + (nb.game < 3 ? '?' : ''), 'raw'), + numberRow(noarg('gamesPlayed'), nb.game), + ...(nb.game + ? [ + numberRow(noarg('winRate'), [nb.win, nb.game], 'percent'), + numberRow(noarg('berserkRate'), [nb.berserk, nb.game], 'percent'), + ctrl.opts.showRatings && numberRow(noarg('averageOpponent'), avgOp, 'raw'), + ] + : []), ]), - h('div', [ - h( - 'table.pairings.sublist', - { - hook: bind('click', e => { - const href = ((e.target as HTMLElement).parentNode as HTMLElement).getAttribute('data-href'); - if (href) window.open(href, '_blank', 'noopener'); - }), - }, - data.pairings.map(function (p, i) { - const res = result(p.win, p.status); - return h( - 'tr.glpt.' + (res === '1' ? ' win' : res === '0' ? ' loss' : ''), - { - key: p.id, - attrs: { 'data-href': '/' + p.id + '/' + p.color }, - hook: { - destroy: vnode => $.powerTip.destroy(vnode.elm as HTMLElement), - }, - }, - [ - h('th', '' + (Math.max(nb.game, pairingsLen) - i)), - h('td', fullName(p.op)), - ctrl.opts.showRatings ? h('td', p.op.rating) : null, - berserkTd(!!p.op.berserk), - h('td.is.color-icon.' + p.color), - h('td.result', res), - berserkTd(p.berserk), - ], - ); + ]), + h('div', [ + h( + 'table.pairings.sublist', + { + hook: bind('click', e => { + const href = ((e.target as HTMLElement).parentNode as HTMLElement).getAttribute('data-href'); + if (href) window.open(href, '_blank', 'noopener'); }), - ), - ]), - ], - ); + }, + data.pairings.map(function (p, i) { + const res = result(p.win, p.status); + return h( + 'tr.glpt.' + (res === '1' ? ' win' : res === '0' ? ' loss' : ''), + { + key: p.id, + attrs: { 'data-href': '/' + p.id + '/' + p.color }, + hook: { destroy: vnode => $.powerTip.destroy(vnode.elm as HTMLElement) }, + }, + [ + h('th', '' + (Math.max(nb.game, pairingsLen) - i)), + h('td', fullName(p.op)), + ctrl.opts.showRatings ? h('td', `${p.op.rating}`) : null, + berserkTd(!!p.op.berserk), + h('td.is.color-icon.' + p.color), + h('td.result', res), + berserkTd(p.berserk), + ], + ); + }), + ), + ]), + ]); } const berserkTd = (b: boolean) => diff --git a/ui/tournament/src/view/started.ts b/ui/tournament/src/view/started.ts index b6826aea91a7f..9ebf72195d9b2 100644 --- a/ui/tournament/src/view/started.ts +++ b/ui/tournament/src/view/started.ts @@ -10,13 +10,11 @@ import TournamentController from '../ctrl'; import { MaybeVNodes } from 'common/snabbdom'; function joinTheGame(ctrl: TournamentController, gameId: string) { - return h( - 'a.tour__ur-playing.button.is.is-after', - { - attrs: { href: '/' + gameId }, - }, - [ctrl.trans('youArePlaying'), h('br'), ctrl.trans('joinTheGame')], - ); + return h('a.tour__ur-playing.button.is.is-after', { attrs: { href: '/' + gameId } }, [ + ctrl.trans('youArePlaying'), + h('br'), + ctrl.trans('joinTheGame'), + ]); } function notice(ctrl: TournamentController): VNode { diff --git a/ui/tournament/src/view/table.ts b/ui/tournament/src/view/table.ts index b61174221a14b..5844819526889 100644 --- a/ui/tournament/src/view/table.ts +++ b/ui/tournament/src/view/table.ts @@ -1,7 +1,7 @@ -import { h, VNode } from 'snabbdom'; +import { VNode } from 'snabbdom'; import { opposite } from 'chessground/util'; import * as licon from 'common/licon'; -import { bind, onInsert } from 'common/snabbdom'; +import { bind, onInsert, looseH as h } from 'common/snabbdom'; import { player as renderPlayer } from './util'; import { Duel, DuelPlayer, FeaturedGame, TournamentOpts } from '../interfaces'; import { teamName } from './battle'; @@ -13,23 +13,13 @@ function featuredPlayer(game: FeaturedGame, color: Color, opts: TournamentOpts) h('span.mini-game__user', [ h('strong', '#' + player.rank), renderPlayer(player, true, opts.showRatings, false), - player.berserk - ? h('i', { - attrs: { - 'data-icon': licon.Berserk, - title: 'Berserk', - }, - }) - : null, + player.berserk && h('i', { attrs: { 'data-icon': licon.Berserk, title: 'Berserk' } }), ]), game.c ? h(`span.mini-game__clock.mini-game__clock--${color}`, { - attrs: { - 'data-time': game.c[color], - 'data-managed': 1, - }, + attrs: { 'data-time': game.c[color], 'data-managed': 1 }, }) - : h('span.mini-game__result', game.winner ? (game.winner == color ? 1 : 0) : '½'), + : h('span.mini-game__result', game.winner ? (game.winner == color ? '1' : '0') : '½'), ]); } @@ -37,19 +27,12 @@ function featured(game: FeaturedGame, opts: TournamentOpts): VNode { return h( `div.tour__featured.mini-game.mini-game-${game.id}.mini-game--init.is2d`, { - attrs: { - 'data-state': `${game.fen},${game.orientation},${game.lastMove}`, - 'data-live': game.id, - }, + attrs: { 'data-state': `${game.fen},${game.orientation},${game.lastMove}`, 'data-live': game.id }, hook: onInsert(lichess.powertip.manualUserIn), }, [ featuredPlayer(game, opposite(game.orientation), opts), - h('a.cg-wrap', { - attrs: { - href: `/${game.id}/${game.orientation}`, - }, - }), + h('a.cg-wrap', { attrs: { href: `/${game.id}/${game.orientation}` } }), featuredPlayer(game, game.orientation, opts), ], ); @@ -57,55 +40,36 @@ function featured(game: FeaturedGame, opts: TournamentOpts): VNode { const duelPlayerMeta = (p: DuelPlayer, ctrl: TournamentController) => [ h('em.rank', '#' + p.k), - p.t ? h('em.utitle', p.t) : null, - ctrl.opts.showRatings ? h('em.rating', '' + p.r) : undefined, + p.t && h('em.utitle', p.t), + ctrl.opts.showRatings && h('em.rating', '' + p.r), ]; function renderDuel(ctrl: TournamentController) { const battle = ctrl.data.teamBattle, duelTeams = ctrl.data.duelTeams; return (d: Duel) => - h( - 'a.glpt.force-ltr', - { - key: d.id, - attrs: { href: '/' + d.id }, - }, - [ - battle && duelTeams - ? h( - 'line.t', - [0, 1].map(i => teamName(battle, duelTeams[d.p[i].n.toLowerCase()])), - ) - : undefined, - h('line.a', [h('strong', d.p[0].n), h('span', duelPlayerMeta(d.p[1], ctrl).reverse())]), - h('line.b', [h('span', duelPlayerMeta(d.p[0], ctrl)), h('strong', d.p[1].n)]), - ], - ); + h('a.glpt.force-ltr', { key: d.id, attrs: { href: '/' + d.id } }, [ + battle && + duelTeams && + h( + 'line.t', + [0, 1].map(i => teamName(battle, duelTeams[d.p[i].n.toLowerCase()])), + ), + h('line.a', [h('strong', d.p[0].n), h('span', duelPlayerMeta(d.p[1], ctrl).reverse())]), + h('line.b', [h('span', duelPlayerMeta(d.p[0], ctrl)), h('strong', d.p[1].n)]), + ]); } const initMiniGame = (node: VNode) => lichess.miniGame.initAll(node.elm as HTMLElement); export default function (ctrl: TournamentController): VNode { - return h( - 'div.tour__table', - { - hook: { - insert: initMiniGame, - postpatch: initMiniGame, - }, - }, - [ - ctrl.data.featured ? featured(ctrl.data.featured, ctrl.opts) : null, - ctrl.data.duels.length - ? h( - 'section.tour__duels', - { - hook: bind('click', _ => !ctrl.disableClicks), - }, - [h('h2', 'Top games')].concat(ctrl.data.duels.map(renderDuel(ctrl))), - ) - : null, - ], - ); + return h('div.tour__table', { hook: { insert: initMiniGame, postpatch: initMiniGame } }, [ + ctrl.data.featured && featured(ctrl.data.featured, ctrl.opts), + ctrl.data.duels.length > 0 && + h( + 'section.tour__duels', + { hook: bind('click', _ => !ctrl.disableClicks) }, + [h('h2', 'Top games')].concat(ctrl.data.duels.map(renderDuel(ctrl))), + ), + ]); } diff --git a/ui/tournament/src/view/teamInfo.ts b/ui/tournament/src/view/teamInfo.ts index e2de7b327f691..5361dd98bfde2 100644 --- a/ui/tournament/src/view/teamInfo.ts +++ b/ui/tournament/src/view/teamInfo.ts @@ -20,74 +20,44 @@ export default function (ctrl: TournamentController): VNode | undefined { const setup = (vnode: VNode) => { lichess.powertip.manualUserIn(vnode.elm as HTMLElement); }; - return h( - tag, - { - hook: { - insert: setup, - postpatch(_, vnode) { - setup(vnode); - }, - }, - }, - [ - h('a.close', { - attrs: dataIcon(licon.X), - hook: bind('click', () => ctrl.showTeamInfo(data.id), ctrl.redraw), - }), - h('div.stats', [ - h('h2', [teamTag]), - h('table', [ - numberRow('Players', data.nbPlayers), - ...(data.rating - ? [ - ctrl.opts.showRatings ? numberRow(noarg('averageElo'), data.rating, 'raw') : null, - ...(data.perf - ? [ - ctrl.opts.showRatings ? numberRow(noarg('averagePerformance'), data.perf, 'raw') : null, - numberRow(noarg('averageScore'), data.score, 'raw'), - ] - : []), - ] - : []), - h( - 'tr', - h( - 'th', - h( - 'a', - { - attrs: { href: '/team/' + data.id }, - }, - noarg('teamPage'), - ), - ), - ), - ]), + return h(tag, { hook: { insert: setup, postpatch: (_, vnode) => setup(vnode) } }, [ + h('a.close', { + attrs: dataIcon(licon.X), + hook: bind('click', () => ctrl.showTeamInfo(data.id), ctrl.redraw), + }), + h('div.stats', [ + h('h2', [teamTag]), + h('table', [ + numberRow('Players', data.nbPlayers), + ...(data.rating + ? [ + ctrl.opts.showRatings ? numberRow(noarg('averageElo'), data.rating, 'raw') : null, + ...(data.perf + ? [ + ctrl.opts.showRatings ? numberRow(noarg('averagePerformance'), data.perf, 'raw') : null, + numberRow(noarg('averageScore'), data.score, 'raw'), + ] + : []), + ] + : []), + h('tr', h('th', h('a', { attrs: { href: '/team/' + data.id } }, noarg('teamPage')))), ]), - h('div', [ - h( - 'table.players.sublist', - data.topPlayers.map((p, i) => - h( - 'tr', - { - key: p.name, - hook: bind('click', () => ctrl.jumpToPageOf(p.name)), - }, - [ - h('th', '' + (i + 1)), - h('td', renderPlayer(p, false, ctrl.opts.showRatings, false, i < nbLeaders)), - h('td.total', [ - p.fire && !ctrl.data.isFinished - ? h('strong.is-gold', { attrs: dataIcon(licon.Fire) }, '' + p.score) - : h('strong', '' + p.score), - ]), - ], - ), - ), + ]), + h('div', [ + h( + 'table.players.sublist', + data.topPlayers.map((p, i) => + h('tr', { key: p.name, hook: bind('click', () => ctrl.jumpToPageOf(p.name)) }, [ + h('th', '' + (i + 1)), + h('td', renderPlayer(p, false, ctrl.opts.showRatings, false, i < nbLeaders)), + h('td.total', [ + p.fire && !ctrl.data.isFinished + ? h('strong.is-gold', { attrs: dataIcon(licon.Fire) }, '' + p.score) + : h('strong', '' + p.score), + ]), + ]), ), - ]), - ], - ); + ), + ]), + ]); } diff --git a/ui/tournament/src/view/util.ts b/ui/tournament/src/view/util.ts index 09f922f5b18ba..a9a3709623bca 100644 --- a/ui/tournament/src/view/util.ts +++ b/ui/tournament/src/view/util.ts @@ -18,9 +18,7 @@ export const player = ( 'a.ulpt.user-link' + (((p.title || '') + p.name).length > 15 ? '.long' : ''), { attrs: asLink || 'ontouchstart' in window ? { href: '/@/' + p.name } : { 'data-href': '/@/' + p.name }, - hook: { - destroy: vnode => $.powerTip.destroy(vnode.elm as HTMLElement), - }, + hook: { destroy: vnode => $.powerTip.destroy(vnode.elm as HTMLElement) }, }, [ h( diff --git a/ui/tournamentCalendar/src/view.ts b/ui/tournamentCalendar/src/view.ts index 0dd00ef9e30e8..bda2da3367dce 100644 --- a/ui/tournamentCalendar/src/view.ts +++ b/ui/tournamentCalendar/src/view.ts @@ -41,16 +41,7 @@ function renderTournament(tour: Tournament, day: Date) { }, }, [ - h( - 'span.icon', - tour.perf - ? { - attrs: { - 'data-icon': iconOf(tour), - }, - } - : {}, - ), + h('span.icon', tour.perf ? { attrs: { 'data-icon': iconOf(tour) } } : {}), h('span.body', [tour.fullName]), ], ); @@ -84,15 +75,7 @@ function renderDay(ctrl: Ctrl) { const dayEnd = addDays(day, 1); const tours = ctrl.data.tournaments.filter(t => t.bounds.start < dayEnd && t.bounds.end > day); return h('day', [ - h( - 'date', - { - attrs: { - title: format(day, 'EEEE, dd/MM/yyyy'), - }, - }, - [format(day, 'dd/MM')], - ), + h('date', { attrs: { title: format(day, 'EEEE, dd/MM/yyyy') } }, [format(day, 'dd/MM')]), h( 'lanes', makeLanes(tours).map(l => renderLane(l, day)), @@ -115,9 +98,7 @@ function renderTimeline() { hours.map(hour => h( 'div.timeheader', - { - attrs: { style: startDirection() + ': ' + (hour / 24) * 100 + '%' }, - }, + { attrs: { style: startDirection() + ': ' + (hour / 24) * 100 + '%' } }, timeString(hour), ), ), diff --git a/ui/tournamentSchedule/src/view.ts b/ui/tournamentSchedule/src/view.ts index eade564a55786..650c79ae7981d 100644 --- a/ui/tournamentSchedule/src/view.ts +++ b/ui/tournamentSchedule/src/view.ts @@ -169,17 +169,7 @@ function renderTournament(ctrl: Ctrl, tour: Tournament) { }, }, [ - h( - 'span.icon', - tour.perf - ? { - attrs: { - 'data-icon': iconOf(tour), - title: tour.perf.name, - }, - } - : {}, - ), + h('span.icon', tour.perf ? { attrs: { 'data-icon': iconOf(tour), title: tour.perf.name } } : {}), h('span.body', [ h('span.name', i18nName(tour)), h('span.infos', [ @@ -190,13 +180,7 @@ function renderTournament(ctrl: Ctrl, tour: Tournament) { tour.rated ? ctrl.trans('ratedTournament') : ctrl.trans('casualTournament'), ]), tour.nbPlayers - ? h( - 'span.nb-players', - { - attrs: { 'data-icon': licon.User }, - }, - tour.nbPlayers, - ) + ? h('span.nb-players', { attrs: { 'data-icon': licon.User } }, tour.nbPlayers) : null, ]), ]), @@ -226,9 +210,7 @@ function renderTimeline() { time.setUTCMinutes(time.getUTCMinutes() + minutesBetween); } timeHeaders.push( - h('div.timeheader.now', { - attrs: { style: startDirection() + ': ' + leftPos(now) + 'px' }, - }), + h('div.timeheader.now', { attrs: { style: startDirection() + ': ' + leftPos(now) + 'px' } }), ); return h('div.timeline', timeHeaders); diff --git a/ui/tree/src/tree.ts b/ui/tree/src/tree.ts index 8ed3ca1a45f9b..52c4a86a6ba3e 100644 --- a/ui/tree/src/tree.ts +++ b/ui/tree/src/tree.ts @@ -29,7 +29,6 @@ export interface TreeWrapper { getCurrentNodesAfterPly(nodeList: Tree.Node[], mainline: Tree.Node[], ply: number): Tree.Node[]; merge(tree: Tree.Node): void; removeCeval(): void; - removeComputerVariations(): void; parentNode(path: Tree.Path): Tree.Node; getParentClock(node: Tree.Node, path: Tree.Path): Tree.Clock | undefined; } @@ -241,13 +240,6 @@ export function build(root: Tree.Node): TreeWrapper { delete n.threat; }); }, - removeComputerVariations() { - ops.mainlineNodeList(root).forEach(function (n) { - n.children = n.children.filter(function (c) { - return !c.comp; - }); - }); - }, parentNode, getParentClock, }; diff --git a/ui/voice/src/move/moveCtrl.ts b/ui/voice/src/move/moveCtrl.ts index 2791b3711ec64..6c50d4a69733b 100644 --- a/ui/voice/src/move/moveCtrl.ts +++ b/ui/voice/src/move/moveCtrl.ts @@ -16,7 +16,9 @@ export function load(ctrl: RootCtrl, initialFen: string): VoiceMove { let move: VoiceMove; const ui = makeCtrl({ redraw: ctrl.redraw, module: () => move, tpe: 'move' }); - lichess.loadEsm('voice.move', { init: { root: ctrl, ui, initialFen } }).then(x => (move = x)); + lichess.asset + .loadEsm('voice.move', { init: { root: ctrl, ui, initialFen } }) + .then(x => (move = x)); return { ui, initGrammar: () => move?.initGrammar(), @@ -94,7 +96,7 @@ export function initModule(opts: { root: RootCtrl; ui: VoiceCtrl; initialFen: st } async function initGrammar(): Promise { - const g = await xhr.jsonSimple(lichess.assetUrl(`compiled/grammar/move-${ui.lang()}.json`)); + const g = await xhr.jsonSimple(lichess.asset.url(`compiled/grammar/move-${ui.lang()}.json`)); byWord.clear(); byTok.clear(); byVal.clear(); diff --git a/ui/voice/src/move/view.ts b/ui/voice/src/move/view.ts index 77089443e2beb..f85821cf3aaba 100644 --- a/ui/voice/src/move/view.ts +++ b/ui/voice/src/move/view.ts @@ -35,12 +35,7 @@ export function claritySetting(clarity: Prop, redraw: () => void) { return h('div.voice-setting', [ h('label', { attrs: { for: 'voice-clarity' } }, 'Clarity'), h('input#voice-clarity', { - attrs: { - type: 'range', - min: 0, - max: 2, - step: 1, - }, + attrs: { type: 'range', min: 0, max: 2, step: 1 }, hook: rangeConfig(clarity, val => { clarity(val); redraw(); diff --git a/ui/voice/src/view.ts b/ui/voice/src/view.ts index 24a5a437a74d9..1ba806fac2df6 100644 --- a/ui/voice/src/view.ts +++ b/ui/voice/src/view.ts @@ -1,6 +1,5 @@ -import { h } from 'snabbdom'; import * as licon from 'common/licon'; -import { onInsert, bind } from 'common/snabbdom'; +import { onInsert, bind, looseH as h } from 'common/snabbdom'; import { snabDialog, type Dialog } from 'common/dialog'; import * as xhr from 'common/xhr'; import { onClickAway } from 'common'; @@ -26,15 +25,14 @@ export function renderVoiceBar(ctrl: VoiceCtrl, redraw: () => void, cls?: string hook: bind('click', () => ctrl.showPrefs.toggle(), redraw, false), }), ]), - ctrl.showPrefs() - ? h('div#voice-settings', { hook: onInsert(onClickAway(() => ctrl.showPrefs(false))) }, [ - deviceSelector(ctrl, redraw), - langSetting(ctrl), - ...(ctrl.module()?.prefNodes(redraw) ?? []), - pushTalkSetting(ctrl), - ]) - : null, - ctrl.showHelp() ? renderHelpModal(ctrl) : null, + ctrl.showPrefs() && + h('div#voice-settings', { hook: onInsert(onClickAway(() => ctrl.showPrefs(false))) }, [ + deviceSelector(ctrl, redraw), + langSetting(ctrl), + ...(ctrl.module()?.prefNodes(redraw) ?? []), + pushTalkSetting(ctrl), + ]), + ctrl.showHelp() && renderHelpModal(ctrl), ]); } @@ -67,29 +65,28 @@ function pushTalkSetting(ctrl: VoiceCtrl) { } function langSetting(ctrl: VoiceCtrl) { - return supportedLangs.length < 2 - ? null - : h('div.voice-setting', [ - h('label', { attrs: { for: 'voice-lang' } }, 'Language'), - h( - 'select#voice-lang', - { - attrs: { name: 'lang' }, - hook: bind('change', e => ctrl.lang((e.target as HTMLSelectElement).value)), - }, - [ - ...supportedLangs.map(l => - h( - 'option', - { - attrs: l[0] === ctrl.lang() ? { value: l[0], selected: '' } : { value: l[0] }, - }, - l[1], - ), + return ( + supportedLangs.length > 1 && + h('div.voice-setting', [ + h('label', { attrs: { for: 'voice-lang' } }, 'Language'), + h( + 'select#voice-lang', + { + attrs: { name: 'lang' }, + hook: bind('change', e => ctrl.lang((e.target as HTMLSelectElement).value)), + }, + [ + ...supportedLangs.map(l => + h( + 'option', + { attrs: l[0] === ctrl.lang() ? { value: l[0], selected: '' } : { value: l[0] } }, + l[1], ), - ], - ), - ]); + ), + ], + ), + ]) + ); } const nullMic: MediaDeviceInfo = { @@ -109,11 +106,10 @@ function deviceSelector(ctrl: VoiceCtrl, redraw: () => void) { { hook: onInsert((el: HTMLSelectElement) => { el.addEventListener('change', () => ctrl.micId(el.value)); - if (devices === undefined) - lichess.mic.getMics().then(ds => { - devices = ds.length ? ds : [nullMic]; - redraw(); - }); + lichess.mic.getMics().then(ds => { + devices = ds.length ? ds : [nullMic]; + redraw(); + }); }), }, devices.map(d => @@ -167,7 +163,7 @@ function renderHelpModal(ctrl: VoiceCtrl) { const grammar = ctrl.moduleId === 'coords' ? [] - : await xhr.jsonSimple(lichess.assetUrl(`compiled/grammar/${ctrl.moduleId}-${ctrl.lang()}.json`)); + : await xhr.jsonSimple(lichess.asset.url(`compiled/grammar/${ctrl.moduleId}-${ctrl.lang()}.json`)); const valToWord = (val: string, phonetic: boolean) => grammar.entries.find( From 29465d51512e0e7312592481ab37d24e0fef541f Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Sat, 30 Dec 2023 10:42:19 -0600 Subject: [PATCH 073/174] gah --- pnpm-lock.yaml | 28 +++++++++---------- ui/lobby/package.json | 2 +- ui/lobby/src/ctrl.ts | 4 +-- ui/lobby/tsconfig.json | 2 +- ui/{gameSetup => setup}/css/_bot-stuff.scss | 0 ui/{gameSetup => setup}/css/_game-setup.scss | 0 .../css/build/_game-setup.scss | 0 .../css/build/game-setup.ltr.dark.scss | 0 .../css/build/game-setup.ltr.light.scss | 0 .../css/build/game-setup.ltr.transp.scss | 0 .../css/build/game-setup.rtl.dark.scss | 0 .../css/build/game-setup.rtl.light.scss | 0 .../css/build/game-setup.rtl.transp.scss | 0 ui/{gameSetup => setup}/package.json | 4 +-- ui/{gameSetup => setup}/src/ctrl.ts | 0 ui/{gameSetup => setup}/src/interfaces.ts | 0 ui/{gameSetup => setup}/src/main.ts | 0 ui/{gameSetup => setup}/src/options.ts | 0 ui/{gameSetup => setup}/src/types.ts | 0 ui/{gameSetup => setup}/src/view/aiContent.ts | 0 .../src/view/components/colorButtons.ts | 0 .../src/view/components/fenInput.ts | 0 .../src/view/components/gameModeButtons.ts | 0 .../src/view/components/levelButtons.ts | 0 .../src/view/components/option.ts | 0 .../components/ratingDifferenceSliders.ts | 0 .../src/view/components/ratingView.ts | 0 .../view/components/timePickerAndSliders.ts | 0 .../src/view/components/variantPicker.ts | 0 .../src/view/friendContent.ts | 0 .../src/view/hookContent.ts | 0 .../src/view/localContent.ts | 0 ui/{gameSetup => setup}/src/view/modal.ts | 0 ui/{gameSetup => setup}/tsconfig.json | 0 ui/site/src/component/log.ts | 19 +++++++------ ui/site/src/component/socket.ts | 10 +++---- 36 files changed, 35 insertions(+), 34 deletions(-) rename ui/{gameSetup => setup}/css/_bot-stuff.scss (100%) rename ui/{gameSetup => setup}/css/_game-setup.scss (100%) rename ui/{gameSetup => setup}/css/build/_game-setup.scss (100%) rename ui/{gameSetup => setup}/css/build/game-setup.ltr.dark.scss (100%) rename ui/{gameSetup => setup}/css/build/game-setup.ltr.light.scss (100%) rename ui/{gameSetup => setup}/css/build/game-setup.ltr.transp.scss (100%) rename ui/{gameSetup => setup}/css/build/game-setup.rtl.dark.scss (100%) rename ui/{gameSetup => setup}/css/build/game-setup.rtl.light.scss (100%) rename ui/{gameSetup => setup}/css/build/game-setup.rtl.transp.scss (100%) rename ui/{gameSetup => setup}/package.json (87%) rename ui/{gameSetup => setup}/src/ctrl.ts (100%) rename ui/{gameSetup => setup}/src/interfaces.ts (100%) rename ui/{gameSetup => setup}/src/main.ts (100%) rename ui/{gameSetup => setup}/src/options.ts (100%) rename ui/{gameSetup => setup}/src/types.ts (100%) rename ui/{gameSetup => setup}/src/view/aiContent.ts (100%) rename ui/{gameSetup => setup}/src/view/components/colorButtons.ts (100%) rename ui/{gameSetup => setup}/src/view/components/fenInput.ts (100%) rename ui/{gameSetup => setup}/src/view/components/gameModeButtons.ts (100%) rename ui/{gameSetup => setup}/src/view/components/levelButtons.ts (100%) rename ui/{gameSetup => setup}/src/view/components/option.ts (100%) rename ui/{gameSetup => setup}/src/view/components/ratingDifferenceSliders.ts (100%) rename ui/{gameSetup => setup}/src/view/components/ratingView.ts (100%) rename ui/{gameSetup => setup}/src/view/components/timePickerAndSliders.ts (100%) rename ui/{gameSetup => setup}/src/view/components/variantPicker.ts (100%) rename ui/{gameSetup => setup}/src/view/friendContent.ts (100%) rename ui/{gameSetup => setup}/src/view/hookContent.ts (100%) rename ui/{gameSetup => setup}/src/view/localContent.ts (100%) rename ui/{gameSetup => setup}/src/view/modal.ts (100%) rename ui/{gameSetup => setup}/tsconfig.json (100%) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8101efad9d70b..fa5e31b8c9c43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -299,18 +299,6 @@ importers: specifier: ^3.5.1 version: 3.5.1 - ui/gameSetup: - dependencies: - common: - specifier: workspace:* - version: link:../common - libot: - specifier: workspace:* - version: link:../libot - snabbdom: - specifier: ^3.5.1 - version: 3.5.1 - ui/insight: dependencies: '@types/highcharts': @@ -400,9 +388,9 @@ importers: debounce-promise: specifier: ^3.1.2 version: 3.1.2 - gameSetup: + setup: specifier: workspace:* - version: link:../gameSetup + version: link:../setup snabbdom: specifier: ^3.5.1 version: 3.5.1 @@ -644,6 +632,18 @@ importers: specifier: ^0.0.1 version: 0.0.1 + ui/setup: + dependencies: + common: + specifier: workspace:* + version: link:../common + libot: + specifier: workspace:* + version: link:../libot + snabbdom: + specifier: ^3.5.1 + version: 3.5.1 + ui/simul: dependencies: chat: diff --git a/ui/lobby/package.json b/ui/lobby/package.json index 5bb9850281a2d..3712ff1e935c5 100644 --- a/ui/lobby/package.json +++ b/ui/lobby/package.json @@ -18,7 +18,7 @@ "common": "workspace:*", "dasher": "workspace:*", "debounce-promise": "^3.1.2", - "gameSetup": "workspace:*", + "setup": "workspace:*", "snabbdom": "^3.5.1" }, "scripts": { diff --git a/ui/lobby/src/ctrl.ts b/ui/lobby/src/ctrl.ts index edbc89186bdc8..0047fc1ba1119 100644 --- a/ui/lobby/src/ctrl.ts +++ b/ui/lobby/src/ctrl.ts @@ -6,7 +6,7 @@ import { make as makeStores, Stores } from './store'; import * as xhr from './xhr'; import * as poolRangeStorage from './poolRangeStorage'; import { LobbyOpts, LobbyData, Tab, Mode, Sort, Hook, Seek, Pool, PoolMember, LobbyMe } from './interfaces'; -import { ParentCtrl, SetupConstraints, GameType, GameSetup, SetupCtrl } from 'gameSetup'; +import { ParentCtrl, SetupConstraints, GameType, GameSetup, SetupCtrl } from 'setup'; import LobbySocket from './socket'; import Filter from './filter'; import disableDarkBoard from './disableDarkBoard'; @@ -277,7 +277,7 @@ export default class LobbyController implements ParentCtrl { hasPool = (id: string) => this.pools.some(p => p.id === id); showSetupModal = async (gameType: GameType, opts?: SetupConstraints, friendUser?: string) => { - if (!this.setupCtrl) this.setupCtrl = await lichess.asset.loadEsm('gameSetup', { init: this }); + if (!this.setupCtrl) this.setupCtrl = await lichess.asset.loadEsm('setup', { init: this }); this.leavePool(); this.setupCtrl.openModal(gameType, opts, friendUser); this.redraw(); diff --git a/ui/lobby/tsconfig.json b/ui/lobby/tsconfig.json index 47588ec98645e..7549bc959e1ad 100644 --- a/ui/lobby/tsconfig.json +++ b/ui/lobby/tsconfig.json @@ -8,6 +8,6 @@ "references": [ { "path": "../common/tsconfig.json" }, { "path": "../dasher/tsconfig.json" }, - { "path": "../gameSetup/tsconfig.json" } + { "path": "../setup/tsconfig.json" } ] } diff --git a/ui/gameSetup/css/_bot-stuff.scss b/ui/setup/css/_bot-stuff.scss similarity index 100% rename from ui/gameSetup/css/_bot-stuff.scss rename to ui/setup/css/_bot-stuff.scss diff --git a/ui/gameSetup/css/_game-setup.scss b/ui/setup/css/_game-setup.scss similarity index 100% rename from ui/gameSetup/css/_game-setup.scss rename to ui/setup/css/_game-setup.scss diff --git a/ui/gameSetup/css/build/_game-setup.scss b/ui/setup/css/build/_game-setup.scss similarity index 100% rename from ui/gameSetup/css/build/_game-setup.scss rename to ui/setup/css/build/_game-setup.scss diff --git a/ui/gameSetup/css/build/game-setup.ltr.dark.scss b/ui/setup/css/build/game-setup.ltr.dark.scss similarity index 100% rename from ui/gameSetup/css/build/game-setup.ltr.dark.scss rename to ui/setup/css/build/game-setup.ltr.dark.scss diff --git a/ui/gameSetup/css/build/game-setup.ltr.light.scss b/ui/setup/css/build/game-setup.ltr.light.scss similarity index 100% rename from ui/gameSetup/css/build/game-setup.ltr.light.scss rename to ui/setup/css/build/game-setup.ltr.light.scss diff --git a/ui/gameSetup/css/build/game-setup.ltr.transp.scss b/ui/setup/css/build/game-setup.ltr.transp.scss similarity index 100% rename from ui/gameSetup/css/build/game-setup.ltr.transp.scss rename to ui/setup/css/build/game-setup.ltr.transp.scss diff --git a/ui/gameSetup/css/build/game-setup.rtl.dark.scss b/ui/setup/css/build/game-setup.rtl.dark.scss similarity index 100% rename from ui/gameSetup/css/build/game-setup.rtl.dark.scss rename to ui/setup/css/build/game-setup.rtl.dark.scss diff --git a/ui/gameSetup/css/build/game-setup.rtl.light.scss b/ui/setup/css/build/game-setup.rtl.light.scss similarity index 100% rename from ui/gameSetup/css/build/game-setup.rtl.light.scss rename to ui/setup/css/build/game-setup.rtl.light.scss diff --git a/ui/gameSetup/css/build/game-setup.rtl.transp.scss b/ui/setup/css/build/game-setup.rtl.transp.scss similarity index 100% rename from ui/gameSetup/css/build/game-setup.rtl.transp.scss rename to ui/setup/css/build/game-setup.rtl.transp.scss diff --git a/ui/gameSetup/package.json b/ui/setup/package.json similarity index 87% rename from ui/gameSetup/package.json rename to ui/setup/package.json index ed0c4397cfce7..4d59dac6786a0 100644 --- a/ui/gameSetup/package.json +++ b/ui/setup/package.json @@ -1,5 +1,5 @@ { - "name": "gameSetup", + "name": "setup", "version": "2.0.0", "private": true, "author": "Thibault Duplessis", @@ -19,7 +19,7 @@ "lichess": { "modules": { "esm": { - "src/main.ts": "gameSetup" + "src/main.ts": "setup" } } } diff --git a/ui/gameSetup/src/ctrl.ts b/ui/setup/src/ctrl.ts similarity index 100% rename from ui/gameSetup/src/ctrl.ts rename to ui/setup/src/ctrl.ts diff --git a/ui/gameSetup/src/interfaces.ts b/ui/setup/src/interfaces.ts similarity index 100% rename from ui/gameSetup/src/interfaces.ts rename to ui/setup/src/interfaces.ts diff --git a/ui/gameSetup/src/main.ts b/ui/setup/src/main.ts similarity index 100% rename from ui/gameSetup/src/main.ts rename to ui/setup/src/main.ts diff --git a/ui/gameSetup/src/options.ts b/ui/setup/src/options.ts similarity index 100% rename from ui/gameSetup/src/options.ts rename to ui/setup/src/options.ts diff --git a/ui/gameSetup/src/types.ts b/ui/setup/src/types.ts similarity index 100% rename from ui/gameSetup/src/types.ts rename to ui/setup/src/types.ts diff --git a/ui/gameSetup/src/view/aiContent.ts b/ui/setup/src/view/aiContent.ts similarity index 100% rename from ui/gameSetup/src/view/aiContent.ts rename to ui/setup/src/view/aiContent.ts diff --git a/ui/gameSetup/src/view/components/colorButtons.ts b/ui/setup/src/view/components/colorButtons.ts similarity index 100% rename from ui/gameSetup/src/view/components/colorButtons.ts rename to ui/setup/src/view/components/colorButtons.ts diff --git a/ui/gameSetup/src/view/components/fenInput.ts b/ui/setup/src/view/components/fenInput.ts similarity index 100% rename from ui/gameSetup/src/view/components/fenInput.ts rename to ui/setup/src/view/components/fenInput.ts diff --git a/ui/gameSetup/src/view/components/gameModeButtons.ts b/ui/setup/src/view/components/gameModeButtons.ts similarity index 100% rename from ui/gameSetup/src/view/components/gameModeButtons.ts rename to ui/setup/src/view/components/gameModeButtons.ts diff --git a/ui/gameSetup/src/view/components/levelButtons.ts b/ui/setup/src/view/components/levelButtons.ts similarity index 100% rename from ui/gameSetup/src/view/components/levelButtons.ts rename to ui/setup/src/view/components/levelButtons.ts diff --git a/ui/gameSetup/src/view/components/option.ts b/ui/setup/src/view/components/option.ts similarity index 100% rename from ui/gameSetup/src/view/components/option.ts rename to ui/setup/src/view/components/option.ts diff --git a/ui/gameSetup/src/view/components/ratingDifferenceSliders.ts b/ui/setup/src/view/components/ratingDifferenceSliders.ts similarity index 100% rename from ui/gameSetup/src/view/components/ratingDifferenceSliders.ts rename to ui/setup/src/view/components/ratingDifferenceSliders.ts diff --git a/ui/gameSetup/src/view/components/ratingView.ts b/ui/setup/src/view/components/ratingView.ts similarity index 100% rename from ui/gameSetup/src/view/components/ratingView.ts rename to ui/setup/src/view/components/ratingView.ts diff --git a/ui/gameSetup/src/view/components/timePickerAndSliders.ts b/ui/setup/src/view/components/timePickerAndSliders.ts similarity index 100% rename from ui/gameSetup/src/view/components/timePickerAndSliders.ts rename to ui/setup/src/view/components/timePickerAndSliders.ts diff --git a/ui/gameSetup/src/view/components/variantPicker.ts b/ui/setup/src/view/components/variantPicker.ts similarity index 100% rename from ui/gameSetup/src/view/components/variantPicker.ts rename to ui/setup/src/view/components/variantPicker.ts diff --git a/ui/gameSetup/src/view/friendContent.ts b/ui/setup/src/view/friendContent.ts similarity index 100% rename from ui/gameSetup/src/view/friendContent.ts rename to ui/setup/src/view/friendContent.ts diff --git a/ui/gameSetup/src/view/hookContent.ts b/ui/setup/src/view/hookContent.ts similarity index 100% rename from ui/gameSetup/src/view/hookContent.ts rename to ui/setup/src/view/hookContent.ts diff --git a/ui/gameSetup/src/view/localContent.ts b/ui/setup/src/view/localContent.ts similarity index 100% rename from ui/gameSetup/src/view/localContent.ts rename to ui/setup/src/view/localContent.ts diff --git a/ui/gameSetup/src/view/modal.ts b/ui/setup/src/view/modal.ts similarity index 100% rename from ui/gameSetup/src/view/modal.ts rename to ui/setup/src/view/modal.ts diff --git a/ui/gameSetup/tsconfig.json b/ui/setup/tsconfig.json similarity index 100% rename from ui/gameSetup/tsconfig.json rename to ui/setup/tsconfig.json diff --git a/ui/site/src/component/log.ts b/ui/site/src/component/log.ts index 914d9aca81789..9adc96922bacf 100644 --- a/ui/site/src/component/log.ts +++ b/ui/site/src/component/log.ts @@ -29,8 +29,8 @@ export default function makeLog(): LichessLog { return !val || typeof val === 'string' ? String(val) : JSON.stringify(val); } - const log: any = async (...args: any[]) => { - const msg = `${lichess.info.commit.substr(0, 7)} - ${args.map(stringify).join(' ')}`; + const log: LichessLog = async (...args: any[]) => { + const msg = `#${lichess.info.commit.substr(0, 7)} - ${args.map(stringify).join(' ')}`; let nextKey = Date.now(); console.log(...args); if (nextKey === lastKey) { @@ -53,18 +53,19 @@ export default function makeLog(): LichessLog { log.get = async (): Promise => { await ready; const [keys, vals] = await Promise.all([store.list(), store.getMany()]); - return keys.map((k, i) => `${new Date(k).toISOString()} ${vals[i]}`).join('\n'); + return keys.map((k, i) => `${new Date(k).toISOString().replace(/[TZ]/g, ' ')}${vals[i]}`).join('\n'); }; + function terseHref(): string { + return window.location.href.replace(/^(https:\/\/)?lichess\.org\//, '/'); + } + window.addEventListener('error', async e => { - log( - `${window.location.href} - ${e.message} (${e.filename}:${e.lineno}:${e.colno})\n${ - e.error?.stack ?? '' - }`.trim(), - ); + const loc = e.filename ? ` - (${e.filename}:${e.lineno}:${e.colno})` : ''; + log(`${terseHref()} - ${e.message}${loc}\n${e.error?.stack ?? ''}`.trim()); }); window.addEventListener('unhandledrejection', async e => { - log(`${window.location.href} - ${e.reason}\n${e.reason.stack ?? ''}`.trim()); + log(`${terseHref()} - ${e.reason}`); }); return log; diff --git a/ui/site/src/component/socket.ts b/ui/site/src/component/socket.ts index 5e0b910fcfdab..46f1979194013 100644 --- a/ui/site/src/component/socket.ts +++ b/ui/site/src/component/socket.ts @@ -3,6 +3,7 @@ import idleTimer from './idle-timer'; import sri from './sri'; import { reload } from './reload'; import { storage as makeStorage } from './storage'; +import { storedIntProp } from 'common/storage'; import once from './once'; type Sri = string; @@ -66,7 +67,6 @@ export default class StrongSocket { private _sign?: string; private resendWhenOpen: [string, any, any][] = []; private baseUrls = document.body.dataset.socketDomains!.split(','); - static defaultOptions: Options = { idle: false, pingMaxLag: 9000, // time to wait for pong before resetting the connection @@ -97,9 +97,12 @@ export default class StrongSocket { ...(settings.params || {}), }, }; + const customPingDelay = storedIntProp('socket.ping.interval', 2500)(); + this.options = { ...StrongSocket.defaultOptions, ...(settings.options || {}), + pingDelay: customPingDelay > 400 ? customPingDelay : 2500, }; this.version = version; this.pubsub.on('socket.send', this.send); @@ -195,10 +198,7 @@ export default class StrongSocket { if (!this.tryOtherUrl) { // if this was set earlier, we've already logged the error this.tryOtherUrl = true; - const sri = this.settings.params?.sri; - lichess.log( - `socket.ts:${sri ? ' sri ' + sri : ''} timeout ${delay}ms, rotating to ${this.baseUrl()}`, - ); + lichess.log(`sri ${this.settings.params!.sri}timeout ${delay}ms, trying ${this.baseUrl()}`); } this.connect(); }, delay); From 92a28b1c7075b3d593cdad4a7d66451301d655e6 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble <101470903+schlawg@users.noreply.github.com> Date: Mon, 1 Jan 2024 09:39:13 -0600 Subject: [PATCH 074/174] gah --- pnpm-lock.yaml | 2 +- ui/libot/package.json | 2 +- ui/libot/src/ctrl.ts | 3 +-- ui/libot/src/zfbot.ts | 1 - ui/localPlay/src/ctrl.ts | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa5e31b8c9c43..00773e281c028 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -371,7 +371,7 @@ importers: specifier: workspace:* version: link:../tree zerofish: - specifier: link:/home/gamblej/ws/lichess/zerofish + specifier: link:/Users/gamblej/ws/lichess/zerofish version: link:../../../zerofish ui/lobby: diff --git a/ui/libot/package.json b/ui/libot/package.json index d95e211a98802..033fbdc0f2386 100644 --- a/ui/libot/package.json +++ b/ui/libot/package.json @@ -10,7 +10,7 @@ "chessops": "^0.12.8", "common": "workspace:*", "tree": "workspace:*", - "zerofish": "link:/home/gamblej/ws/lichess/zerofish" + "zerofish": "link:/Users/gamblej/ws/lichess/zerofish" }, "scripts": {}, "lichess": { diff --git a/ui/libot/src/ctrl.ts b/ui/libot/src/ctrl.ts index c845aaa66d22c..a16cddc99a2c6 100644 --- a/ui/libot/src/ctrl.ts +++ b/ui/libot/src/ctrl.ts @@ -18,8 +18,7 @@ export async function makeCtrl(libots: Libots, zf: Zerofish): Promise { bots: libots.bots, async setBot(id: string) { bot = libots.bots[id]; - if (!bot.netName) throw new Error(`unknown bot ${id} or no net`); - if (zf.netName !== bot.netName) { + if (bot.netName && zf.netName !== bot.netName) { if (!nets.has(bot.netName)) { nets.set(bot.netName, await fetchNet(bot.netName)); } diff --git a/ui/libot/src/zfbot.ts b/ui/libot/src/zfbot.ts index 34cc9f955e60e..a14623f26afe6 100644 --- a/ui/libot/src/zfbot.ts +++ b/ui/libot/src/zfbot.ts @@ -31,7 +31,6 @@ export class ZfBot implements Libot { const ctx = this.ctx; const chess = Chops.Chess.fromSetup(Chops.fen.parseFen(fen).unwrap()).unwrap(); const p = { ply: chess.halfmoves, material: Chops.Material.fromBoard(chess.board) }; - const zeroMove = this.netName ? this.zf.goZero(fen) : Promise.resolve(undefined); if (chance(ctx.zeroChance(p))) return (await zeroMove) ?? '0000'; const fishMove = this.zf.goFish(fen, { diff --git a/ui/localPlay/src/ctrl.ts b/ui/localPlay/src/ctrl.ts index 4d0fbd49e3e5e..eed5853a7c5bf 100644 --- a/ui/localPlay/src/ctrl.ts +++ b/ui/localPlay/src/ctrl.ts @@ -20,7 +20,7 @@ export class Ctrl { ) { this.loaded = lichess.asset.loadEsm('libot').then(libot => { this.libot = libot; - this.libot.setBot('coral'); + this.libot.setBot('babyhoward'); }); } From a6f899bd75df136e23b4e0f4284b7af93a2ef18a Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Tue, 2 Jan 2024 12:18:29 -0600 Subject: [PATCH 075/174] fix logging --- ui/site/src/diagnostic.ts | 77 +++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/ui/site/src/diagnostic.ts b/ui/site/src/diagnostic.ts index 1f81c45c2e1d4..a56cf08e74857 100644 --- a/ui/site/src/diagnostic.ts +++ b/ui/site/src/diagnostic.ts @@ -45,45 +45,60 @@ export default async function initModule() { dlg.showModal(); } -function processQueryParams() { - let changed = 0; - for (const p of location.hash.split('?')[1]?.split('&') ?? []) { - const op = p.indexOf('=') > -1 ? p.slice(0, p.indexOf('=')) : p; - if (op in operations) changed += operations[op](p.slice(op.length + 1)); - else console.error('Invalid query op', op); - } - return changed; -} +const storageProxy: { [key: string]: (val?: string) => string } = { + wsPing: (val?: string) => { + const storageKey = 'socket.ping.interval'; + if (val === undefined) return storageKey; + if (parseInt(val) > 249) { + lichess.storage.set(storageKey, val); + return val; + } + return ''; + }, + wsHost: (val?: string) => { + const storageKey = 'socket.host'; + if (val === undefined) return storageKey; + lichess.storage.set(storageKey, val); + return val; + }, + forceLSFW: (val?: string) => { + const storageKey = 'ceval.lsfw.forceEnable'; + if (val === undefined) return storageKey; + lichess.storage.set(storageKey, val); + return val; + }, +}; -const operations: { [op: string]: (val?: string) => number } = { +const ops: { [op: string]: (val?: string) => number } = { reset: (val: string) => { if (!val) { - lichess.storage.remove('socket.ping.interval'); - lichess.storage.remove('socket.host'); - return 2; - } else if (val === 'wsPing') lichess.storage.remove('socket.ping.interval'); - else if (val === 'wsHost') lichess.storage.remove('socket.host'); - else return 0; - return 1; + for (const key in storageProxy) lichess.storage.remove(storageProxy[key]?.()); + return Object.keys(storageProxy).length; + } else if (val in storageProxy) { + lichess.storage.remove(storageProxy[val]?.()); + return 1; + } + return 0; }, set: (data: string) => { + let changed = 0; try { const kv = atob(data).split('='); - if (kv[0] === 'wsPing') { - const interval = parseInt(kv[1]); - if (interval > 249) { - lichess.storage.set('socket.ping.interval', `${interval}`); - return 1; - } - } else if (kv[0] === 'wsHost' && kv[1]?.length > 4) { - lichess.storage.set('socket.host', kv[1]); - return 1; - } - console.error('Invalid query set payload', kv); + changed = storageProxy[kv[0]]?.(kv[1] ?? '') ? 1 : 0; + if (!changed) console.warn(`Invalid set payload '${data}'`, kv); } catch (_) { - // + console.warn('Invalid base64', data); } - console.error('Invalid base64', data); - return 0; + return changed; }, }; + +function processQueryParams() { + let changed = 0; + for (const p of location.hash.split('?')[1]?.split('&') ?? []) { + const op = p.indexOf('=') > -1 ? p.slice(0, p.indexOf('=')) : p; + if (op in ops) changed += ops[op](p.slice(op.length + 1)); + else console.warn('Invalid query op', op); + } + return changed; +} From 076344c9850908bfcfc0488a3682c48bec4ed7f7 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Sat, 16 Mar 2024 09:18:48 -0500 Subject: [PATCH 076/174] wtf, builds are 3x slower this branch --- package.json | 15 ++-- pnpm-lock.yaml | 185 ++++++++++++++++---------------------- ui/.build/package.json | 6 +- ui/.build/src/copies.ts | 1 + ui/.build/src/esbuild.ts | 3 +- ui/analyse/package.json | 2 +- ui/ceval/package.json | 2 +- ui/dgt/package.json | 2 +- ui/editor/package.json | 2 +- ui/libot/.build/index.mjs | 15 ++++ ui/libot/.build/make.mjs | 45 ++++++++++ ui/libot/package.json | 3 +- ui/libot/tsconfig.json | 3 +- ui/localPlay/package.json | 7 +- ui/nvui/package.json | 2 +- ui/puz/package.json | 2 +- ui/puzzle/package.json | 2 +- ui/racer/package.json | 2 +- ui/site/src/dialog.ts | 4 +- ui/storm/package.json | 2 +- 20 files changed, 164 insertions(+), 141 deletions(-) create mode 100644 ui/libot/.build/index.mjs create mode 100644 ui/libot/.build/make.mjs diff --git a/package.json b/package.json index a24828869dd10..b250e5114523b 100644 --- a/package.json +++ b/package.json @@ -26,17 +26,16 @@ }, "dependencies": { "@types/lichess": "workspace:*", - "@types/node": "^20.9.1", - "@types/web": "^0.0.119", - "@typescript-eslint/eslint-plugin": "^6.11.0", - "@typescript-eslint/parser": "^6.11.0", + "@types/node": "^20.11.28", + "@types/web": "^0.0.142", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", "ab": "github:lichess-org/ab-stub", "chessground": "^9.0.4", - "eslint": "^8.54.0", - "lint-staged": "^15.1.0", - "onchange": "^7.1.0", + "eslint": "^8.57.0", + "lint-staged": "^15.2.2", "prettier": "3.0.2", - "typescript": "^5.2.2" + "typescript": "^5.4.2" }, "scripts": { "format": "prettier --write --log-level warn .", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab6235be29d38..0811d2246307a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,17 +12,17 @@ importers: specifier: workspace:* version: link:ui/@types/lichess '@types/node': - specifier: ^20.9.1 + specifier: ^20.11.28 version: 20.11.28 '@types/web': - specifier: ^0.0.119 - version: 0.0.119 + specifier: ^0.0.142 + version: 0.0.142 '@typescript-eslint/eslint-plugin': - specifier: ^6.11.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.2) + specifier: ^7.2.0 + version: 7.2.0(@typescript-eslint/parser@7.2.0)(eslint@8.57.0)(typescript@5.4.2) '@typescript-eslint/parser': - specifier: ^6.11.0 - version: 6.21.0(eslint@8.57.0)(typescript@5.4.2) + specifier: ^7.2.0 + version: 7.2.0(eslint@8.57.0)(typescript@5.4.2) ab: specifier: github:lichess-org/ab-stub version: github.com/lichess-org/ab-stub/94236bf34dbc9c05daf50f4c9842d859b9142be0 @@ -30,19 +30,16 @@ importers: specifier: ^9.0.4 version: 9.0.4 eslint: - specifier: ^8.54.0 + specifier: ^8.57.0 version: 8.57.0 lint-staged: - specifier: ^15.1.0 + specifier: ^15.2.2 version: 15.2.2 - onchange: - specifier: ^7.1.0 - version: 7.1.0 prettier: specifier: 3.0.2 version: 3.0.2 typescript: - specifier: ^5.2.2 + specifier: ^5.4.2 version: 5.4.2 bin: @@ -95,8 +92,8 @@ importers: specifier: workspace:* version: link:../chess chessops: - specifier: ^0.13.0 - version: 0.13.0 + specifier: ^0.14.0 + version: 0.14.0 common: specifier: workspace:* version: link:../common @@ -137,8 +134,8 @@ importers: specifier: ^0.2.13 version: 0.2.13 chessops: - specifier: ^0.13.0 - version: 0.13.0 + specifier: ^0.14.0 + version: 0.14.0 common: specifier: workspace:* version: link:../common @@ -275,14 +272,14 @@ importers: ui/dgt: dependencies: chessops: - specifier: ^0.13.0 - version: 0.13.0 + specifier: ^0.14.0 + version: 0.14.0 ui/editor: dependencies: chessops: - specifier: ^0.13.0 - version: 0.13.0 + specifier: ^0.14.0 + version: 0.14.0 common: specifier: workspace:* version: link:../common @@ -359,8 +356,8 @@ importers: specifier: workspace:* version: link:../chess chessops: - specifier: ^0.12.8 - version: 0.12.8 + specifier: ^0.14.0 + version: 0.14.0 common: specifier: workspace:* version: link:../common @@ -404,8 +401,8 @@ importers: specifier: workspace:* version: link:../chess chessops: - specifier: ^0.12.7 - version: 0.12.8 + specifier: ^0.14.0 + version: 0.14.0 common: specifier: workspace:* version: link:../common @@ -482,8 +479,8 @@ importers: specifier: workspace:* version: link:../chess chessops: - specifier: ^0.13.0 - version: 0.13.0 + specifier: ^0.14.0 + version: 0.14.0 snabbdom: specifier: ^3.5.1 version: 3.6.2 @@ -587,8 +584,8 @@ importers: ui/puz: dependencies: chessops: - specifier: ^0.13.0 - version: 0.13.0 + specifier: ^0.14.0 + version: 0.14.0 common: specifier: workspace:* version: link:../common @@ -611,8 +608,8 @@ importers: specifier: workspace:* version: link:../chess chessops: - specifier: ^0.13.0 - version: 0.13.0 + specifier: ^0.14.0 + version: 0.14.0 common: specifier: workspace:* version: link:../common @@ -638,8 +635,8 @@ importers: specifier: workspace:* version: link:../chess chessops: - specifier: ^0.13.0 - version: 0.13.0 + specifier: ^0.14.0 + version: 0.14.0 common: specifier: workspace:* version: link:../common @@ -743,8 +740,8 @@ importers: specifier: workspace:* version: link:../chess chessops: - specifier: ^0.13.0 - version: 0.13.0 + specifier: ^0.14.0 + version: 0.14.0 common: specifier: workspace:* version: link:../common @@ -1206,14 +1203,6 @@ packages: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: false - /@blakeembrey/deque@1.0.5: - resolution: {integrity: sha512-6xnwtvp9DY1EINIKdTfvfeAtCYw4OqBZJhtiqkT3ivjnEfa25VQ3TsKvaFfKm8MyGIEfE95qLe+bNEt3nB0Ylg==} - dev: false - - /@blakeembrey/template@1.1.0: - resolution: {integrity: sha512-iZf+UWfL+DogJVpd/xMQyP6X6McYd6ArdYoPMiv/zlOTzeXXfQbYxBNJJBF6tThvsjLMbA8tLjkCdm9RWMFCCw==} - dev: false - /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1747,6 +1736,10 @@ packages: resolution: {integrity: sha512-CQVOcEWrxr0MXbQbR3rrw6GHo2mcr8WlhLHQkOKDhhySTjz15/35jk0Zm2FbHRyCvSEjr/J7A2iDD5GRrGxE2A==} dev: false + /@types/web@0.0.142: + resolution: {integrity: sha512-QWDvMW+P3sdq8rhiw3dNyBR64O5adSY80gBjRd2xI3BADGsAFpAglblWucsGUjKVKyPm4EbYZkvFs7BRxPsBOg==} + dev: false + /@types/webrtc@0.0.33: resolution: {integrity: sha512-xjN6BelzkY3lzXjIjXGqJVDS6XDleEsvp1bVIyNccXCcMoTH3wvUXFew4/qflwJdNqjmq98Zc5VcALV+XBKBvg==} dev: false @@ -1771,23 +1764,23 @@ packages: resolution: {integrity: sha512-Tuk4q7q0DnpzyJDI4aMeghGuFu2iS1QAdKpabn8JfbtfGmVDUgvZv1I7mEjP61Bvnp3ljKCC8BE6YYSTNxmvRQ==} dev: false - /@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.2): - resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} + /@typescript-eslint/eslint-plugin@7.2.0(@typescript-eslint/parser@7.2.0)(eslint@8.57.0)(typescript@5.4.2): + resolution: {integrity: sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: - '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha - eslint: ^7.0.0 || ^8.0.0 + '@typescript-eslint/parser': ^7.0.0 + eslint: ^8.56.0 typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.2) - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.0)(typescript@5.4.2) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.4.2) - '@typescript-eslint/visitor-keys': 6.21.0 + '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/scope-manager': 7.2.0 + '@typescript-eslint/type-utils': 7.2.0(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/utils': 7.2.0(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/visitor-keys': 7.2.0 debug: 4.3.4 eslint: 8.57.0 graphemer: 1.4.0 @@ -1800,20 +1793,20 @@ packages: - supports-color dev: false - /@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2): - resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} + /@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.2): + resolution: {integrity: sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 + eslint: ^8.56.0 typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.2) - '@typescript-eslint/visitor-keys': 6.21.0 + '@typescript-eslint/scope-manager': 7.2.0 + '@typescript-eslint/types': 7.2.0 + '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.4.2) + '@typescript-eslint/visitor-keys': 7.2.0 debug: 4.3.4 eslint: 8.57.0 typescript: 5.4.2 @@ -1821,26 +1814,26 @@ packages: - supports-color dev: false - /@typescript-eslint/scope-manager@6.21.0: - resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} + /@typescript-eslint/scope-manager@7.2.0: + resolution: {integrity: sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/visitor-keys': 6.21.0 + '@typescript-eslint/types': 7.2.0 + '@typescript-eslint/visitor-keys': 7.2.0 dev: false - /@typescript-eslint/type-utils@6.21.0(eslint@8.57.0)(typescript@5.4.2): - resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} + /@typescript-eslint/type-utils@7.2.0(eslint@8.57.0)(typescript@5.4.2): + resolution: {integrity: sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 + eslint: ^8.56.0 typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.2) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.4.2) + '@typescript-eslint/utils': 7.2.0(eslint@8.57.0)(typescript@5.4.2) debug: 4.3.4 eslint: 8.57.0 ts-api-utils: 1.3.0(typescript@5.4.2) @@ -1849,13 +1842,13 @@ packages: - supports-color dev: false - /@typescript-eslint/types@6.21.0: - resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} + /@typescript-eslint/types@7.2.0: + resolution: {integrity: sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==} engines: {node: ^16.0.0 || >=18.0.0} dev: false - /@typescript-eslint/typescript-estree@6.21.0(typescript@5.4.2): - resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} + /@typescript-eslint/typescript-estree@7.2.0(typescript@5.4.2): + resolution: {integrity: sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: typescript: '*' @@ -1863,8 +1856,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/visitor-keys': 6.21.0 + '@typescript-eslint/types': 7.2.0 + '@typescript-eslint/visitor-keys': 7.2.0 debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 @@ -1876,18 +1869,18 @@ packages: - supports-color dev: false - /@typescript-eslint/utils@6.21.0(eslint@8.57.0)(typescript@5.4.2): - resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} + /@typescript-eslint/utils@7.2.0(eslint@8.57.0)(typescript@5.4.2): + resolution: {integrity: sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 + eslint: ^8.56.0 dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@types/json-schema': 7.0.15 '@types/semver': 7.5.8 - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.2) + '@typescript-eslint/scope-manager': 7.2.0 + '@typescript-eslint/types': 7.2.0 + '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.4.2) eslint: 8.57.0 semver: 7.6.0 transitivePeerDependencies: @@ -1895,11 +1888,11 @@ packages: - typescript dev: false - /@typescript-eslint/visitor-keys@6.21.0: - resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} + /@typescript-eslint/visitor-keys@7.2.0: + resolution: {integrity: sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/types': 7.2.0 eslint-visitor-keys: 3.4.3 dev: false @@ -2037,10 +2030,6 @@ packages: svg.select.js: 3.0.1 dev: false - /arg@4.1.3: - resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} - dev: false - /argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: @@ -2289,8 +2278,8 @@ packages: '@badrap/result': 0.2.13 dev: false - /chessops@0.13.0: - resolution: {integrity: sha512-1hEY22Ajp3nxMItLhUSgPWYWdTIAeVurCrN6EJMgYjqyV9jWK2Q8u2B28beDMbcB4ORWRCVpoxds5cFSp5BJQA==} + /chessops@0.14.0: + resolution: {integrity: sha512-vg+jHoLJ1RPPVos/Uu44Z0jnVrzi7tkVtu5xwdIVjQ+QKw34it3gKUcW3l/artZlV7L8v4zLD98DJUXKGiUyzA==} dependencies: '@badrap/result': 0.2.13 dev: false @@ -3991,19 +3980,6 @@ packages: wrappy: 1.0.2 dev: false - /onchange@7.1.0: - resolution: {integrity: sha512-ZJcqsPiWUAUpvmnJri5TPBooqJOPmC0ttN65juhN15Q8xA+Nbg3BaxBHXQ45EistKKlKElb0edmbPWnKSBkvMg==} - hasBin: true - dependencies: - '@blakeembrey/deque': 1.0.5 - '@blakeembrey/template': 1.1.0 - arg: 4.1.3 - chokidar: 3.6.0 - cross-spawn: 7.0.3 - ignore: 5.3.1 - tree-kill: 1.2.2 - dev: false - /onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -4701,11 +4677,6 @@ packages: punycode: 2.3.1 dev: false - /tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true - dev: false - /ts-api-utils@1.3.0(typescript@5.4.2): resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} diff --git a/ui/.build/package.json b/ui/.build/package.json index 2b8f38f9bf0bc..24a656a1beb35 100644 --- a/ui/.build/package.json +++ b/ui/.build/package.json @@ -6,9 +6,9 @@ "license": "AGPL-3.0-or-later", "type": "commonjs", "dependencies": { - "@types/node": "^20.9.1", - "typescript": "^5.2.2", - "esbuild": "^0.19.5", + "@types/node": "^20.11.28", + "typescript": "^5.4.2", + "esbuild": "^0.20.2", "fast-glob": "^3.3.2" }, "scripts": { diff --git a/ui/.build/src/copies.ts b/ui/.build/src/copies.ts index 33201029b9669..c0ff27b7dfd93 100644 --- a/ui/.build/src/copies.ts +++ b/ui/.build/src/copies.ts @@ -61,6 +61,7 @@ async function globCopy(cp: Copy): Promise> { watchDirs.add(path.join(cp.mod.root, globRoot)); env.log(`[${c.grey(cp.mod.name)}] - Sync '${c.cyan(cp.src)}' to '${c.cyan(cp.dest)}'`); const fileCopies = []; + for (const src of srcs) { const srcPath = path.join(cp.mod.root, src); watchDirs.add(path.dirname(srcPath)); diff --git a/ui/.build/src/esbuild.ts b/ui/.build/src/esbuild.ts index 5812c3fe57e02..5b6bedbc6a60d 100644 --- a/ui/.build/src/esbuild.ts +++ b/ui/.build/src/esbuild.ts @@ -1,7 +1,7 @@ import * as cps from 'node:child_process'; import * as path from 'node:path'; import * as es from 'esbuild'; -import { buildModules } from './build'; +import { preModule, buildModules } from './build'; import { env, errorMark, colors as c } from './main'; const typeBundles = new Map>(); @@ -26,6 +26,7 @@ export async function esbuild(): Promise { __debug__: String(env.debug), }; for (const mod of buildModules) { + preModule(mod); for (const tpe in mod.bundles) { if (!typeBundles.has(tpe)) typeBundles.set(tpe, new Map()); for (const r of mod.bundles[tpe]) { diff --git a/ui/analyse/package.json b/ui/analyse/package.json index d012ae065df18..9867313dc675b 100644 --- a/ui/analyse/package.json +++ b/ui/analyse/package.json @@ -22,7 +22,7 @@ "chart": "workspace:*", "chat": "workspace:*", "chess": "workspace:*", - "chessops": "^0.13.0", + "chessops": "^0.14.0", "common": "workspace:*", "debounce-promise": "^3.1.2", "game": "workspace:*", diff --git a/ui/ceval/package.json b/ui/ceval/package.json index b75dcf326a8fd..6e91333b3140e 100644 --- a/ui/ceval/package.json +++ b/ui/ceval/package.json @@ -16,7 +16,7 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@badrap/result": "^0.2.13", - "chessops": "^0.13.0", + "chessops": "^0.14.0", "common": "workspace:*", "idb-keyval": "^6.2.1", "snabbdom": "^3.5.1", diff --git a/ui/dgt/package.json b/ui/dgt/package.json index 487f2bfa32dea..d752ec1be8b89 100644 --- a/ui/dgt/package.json +++ b/ui/dgt/package.json @@ -6,7 +6,7 @@ "author": "Thibault Duplessis", "license": "AGPL-3.0-or-later", "dependencies": { - "chessops": "^0.13.0" + "chessops": "^0.14.0" }, "lichess": { "modules": { diff --git a/ui/editor/package.json b/ui/editor/package.json index 382dc100c2b6e..4ff9bb5c704a3 100644 --- a/ui/editor/package.json +++ b/ui/editor/package.json @@ -12,7 +12,7 @@ "author": "Thibault Duplessis", "license": "AGPL-3.0-or-later", "dependencies": { - "chessops": "^0.13.0", + "chessops": "^0.14.0", "common": "workspace:*", "snabbdom": "^3.5.1" }, diff --git a/ui/libot/.build/index.mjs b/ui/libot/.build/index.mjs new file mode 100644 index 0000000000000..75f5f140adb57 --- /dev/null +++ b/ui/libot/.build/index.mjs @@ -0,0 +1,15 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const srcDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '../src'); +const prelude = + '// Generated by .build/index.mjs to export the bots directory from src/index.ts\n//\n' + + '// Relaunch ui/build to regenerate (the bots directory is not watched).\n\n'; +const barrel = fs + .readdirSync(path.join(srcDir, 'bots')) + .filter(file => file.endsWith('.ts')) + .map(file => `export * from './bots/${path.basename(file, '.ts')}';`) + .join('\n'); + +fs.writeFileSync(path.join(srcDir, 'index.gen.ts'), prelude + barrel); diff --git a/ui/libot/.build/make.mjs b/ui/libot/.build/make.mjs new file mode 100644 index 0000000000000..aa1e40500ce77 --- /dev/null +++ b/ui/libot/.build/make.mjs @@ -0,0 +1,45 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const buildDir = path.join(path.dirname(fileURLToPath(import.meta.url))); +const srcDir = path.join(buildDir, '../src/bots'); + +const localBots = JSON.parse(fs.readFileSync(path.join(buildDir, 'bots.json'), 'utf8')); +let ordinal = 0; + +for (const k in localBots) { + const bot = localBots[k]; + const ext = makeBot(k, bot, ordinal++); + fs.writeFileSync(path.join(srcDir, `${k}.ts`), ext); +} + +function makeBot(k, bot, ordinal) { + const clz = bot.name.replace(/ /g, ''); + return `import { type Zerofish } from 'zerofish'; +import { Libot } from '../interfaces'; +import { registry } from '../ctrl'; + +export class ${clz} implements Libot { + name = '${bot.name}'; + uid = '#${k}'; + ordinal = ${ordinal}; + description = '${bot.name} is a bot that plays random moves.'; + imageUrl = site.assetUrl('lifat/bots/images/${bot.image}', { noVersion: true }); + netName = 'maia-1100'; + ratings = new Map(); + zf: Zerofish; + + constructor(zf: Zerofish, opts?: any) { + opts; + this.zf = zf; + } + + async move(fen: string) { + return await this.zf.goZero(fen); + } +} + +registry.${k} = ${clz}; +`; +} diff --git a/ui/libot/package.json b/ui/libot/package.json index f9cde8da18bbd..989a8b567bb5a 100644 --- a/ui/libot/package.json +++ b/ui/libot/package.json @@ -7,12 +7,11 @@ "typings": "dist/main.d.ts", "dependencies": { "chess": "workspace:*", - "chessops": "^0.12.8", + "chessops": "^0.14.0", "common": "workspace:*", "tree": "workspace:*", "zerofish": "^0.0.18" }, - "scripts": {}, "lichess": { "modules": { "esm": { diff --git a/ui/libot/tsconfig.json b/ui/libot/tsconfig.json index 7797a9cc46781..04cc8ccc745f4 100644 --- a/ui/libot/tsconfig.json +++ b/ui/libot/tsconfig.json @@ -6,8 +6,7 @@ "allowUnreachableCode": true, "outDir": "dist", "rootDir": "src", - "composite": true, - "isolatedModules": true + "composite": true }, "references": [ { "path": "../chess/tsconfig.json" }, diff --git a/ui/localPlay/package.json b/ui/localPlay/package.json index 39da9aa967b03..99249999dbb49 100644 --- a/ui/localPlay/package.json +++ b/ui/localPlay/package.json @@ -7,7 +7,7 @@ "@types/lichess": "workspace:*", "ceval": "workspace:*", "chess": "workspace:*", - "chessops": "^0.12.7", + "chessops": "^0.14.0", "common": "workspace:*", "game": "workspace:*", "libot": "workspace:*", @@ -17,11 +17,6 @@ "snabbdom": "^3.5.1", "tree": "workspace:*" }, - "scripts": { - "compile": "tsc", - "dev": "tsc", - "prod": "tsc" - }, "lichess": { "modules": { "esm": { diff --git a/ui/nvui/package.json b/ui/nvui/package.json index 72eed1a69d3ea..1efdbcdd7e65d 100644 --- a/ui/nvui/package.json +++ b/ui/nvui/package.json @@ -24,7 +24,7 @@ "license": "AGPL-3.0-or-later", "dependencies": { "chess": "workspace:*", - "chessops": "^0.13.0", + "chessops": "^0.14.0", "snabbdom": "^3.5.1" } } diff --git a/ui/puz/package.json b/ui/puz/package.json index 2742bf5add654..cdcb2b35fe864 100644 --- a/ui/puz/package.json +++ b/ui/puz/package.json @@ -20,7 +20,7 @@ "author": "Thibault Duplessis", "license": "AGPL-3.0-or-later", "dependencies": { - "chessops": "^0.13.0", + "chessops": "^0.14.0", "common": "workspace:*", "snabbdom": "^3.5.1" } diff --git a/ui/puzzle/package.json b/ui/puzzle/package.json index dc53e00523ea6..a943db2048a61 100644 --- a/ui/puzzle/package.json +++ b/ui/puzzle/package.json @@ -16,7 +16,7 @@ "ceval": "workspace:*", "chart.js": "=4.4.0", "chess": "workspace:*", - "chessops": "^0.13.0", + "chessops": "^0.14.0", "common": "workspace:*", "keyboardMove": "workspace:*", "nvui": "workspace:*", diff --git a/ui/racer/package.json b/ui/racer/package.json index 3a41675a69a2a..ff82428fcbbc3 100644 --- a/ui/racer/package.json +++ b/ui/racer/package.json @@ -13,7 +13,7 @@ "license": "AGPL-3.0-or-later", "dependencies": { "chess": "workspace:*", - "chessops": "^0.13.0", + "chessops": "^0.14.0", "common": "workspace:*", "puz": "workspace:*", "snabbdom": "^3.5.1" diff --git a/ui/site/src/dialog.ts b/ui/site/src/dialog.ts index ea0236162735b..6aeca022993b1 100644 --- a/ui/site/src/dialog.ts +++ b/ui/site/src/dialog.ts @@ -112,7 +112,7 @@ class DialogWrapper implements Dialog { readonly view: HTMLElement, readonly o: DialogOpts, ) { - registerPolyfill?.(dialog); // ios < 15.4 + if (dialogPolyfill) dialogPolyfill.registerDialog(dialog); // ios < 15.4 const justThen = Date.now(); const cancelOnInterval = () => Date.now() - justThen > 200 && this.close('cancel'); @@ -227,5 +227,3 @@ const focusQuery = ['button', 'input', 'select', 'textarea'] .map(sel => `${sel}:not(:disabled)`) .concat(['[href]', '[tabindex="0"]', '[role="tab"]']) .join(','); - -let registerPolyfill: (dialog: HTMLDialogElement) => void; diff --git a/ui/storm/package.json b/ui/storm/package.json index 7816c2f98a39b..3410a1247f173 100644 --- a/ui/storm/package.json +++ b/ui/storm/package.json @@ -13,7 +13,7 @@ "license": "AGPL-3.0-or-later", "dependencies": { "chess": "workspace:*", - "chessops": "^0.13.0", + "chessops": "^0.14.0", "common": "workspace:*", "puz": "workspace:*", "snabbdom": "^3.5.1" From 5779776017cf68d2be0cf5800c8810cae6e7a3c1 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Sat, 16 Mar 2024 10:03:56 -0500 Subject: [PATCH 077/174] running out of ideas i never had --- package.json | 1 + pnpm-lock.yaml | 101 +- .../vendor/highcharts-4.2.5/highcharts-3d.js | 50 - .../highcharts-4.2.5/highcharts-3d.src.js | 1817 -- .../highcharts-4.2.5/highcharts-more.js | 56 - .../highcharts-4.2.5/highcharts-more.src.js | 2692 -- public/vendor/highcharts-4.2.5/highcharts.js | 343 - .../vendor/highcharts-4.2.5/highcharts.src.js | 19664 ------------ public/vendor/highcharts-4.2.5/highmaps.js | 370 - .../vendor/highcharts-4.2.5/highmaps.src.js | 20563 ------------- public/vendor/highcharts-4.2.5/highstock.js | 434 - .../vendor/highcharts-4.2.5/highstock.src.js | 24597 ---------------- public/vendor/stockfish-mv.wasm/stockfish.js | 104 - .../vendor/stockfish-mv.wasm/stockfish.wasm | Bin 552336 -> 0 bytes .../stockfish-mv.wasm/stockfish.worker.js | 1 - .../stockfish-no-embedded-nnue.wasm | Bin 781165 -> 0 bytes .../vendor/stockfish-nnue.wasm/stockfish.js | 161 - .../vendor/stockfish-nnue.wasm/stockfish.wasm | Bin 12819467 -> 0 bytes .../stockfish-nnue.wasm/stockfish.worker.js | 15 - public/vendor/stockfish-nnue.wasm/uci.js | 41 - public/vendor/stockfish.js/stockfish.js | 48 - public/vendor/stockfish.js/stockfish.wasm | Bin 558861 -> 0 bytes public/vendor/stockfish.js/stockfish.wasm.js | 16 - public/vendor/stockfish.wasm/stockfish.js | 102 - public/vendor/stockfish.wasm/stockfish.wasm | Bin 347350 -> 0 bytes .../vendor/stockfish.wasm/stockfish.worker.js | 1 - ui/libot/package.json | 3 - ui/libot/tsconfig.json | 6 +- ui/localPlay/package.json | 5 +- ui/localPlay/tsconfig.json | 9 +- ui/setup/package.json | 5 - 31 files changed, 61 insertions(+), 71144 deletions(-) delete mode 100644 public/vendor/highcharts-4.2.5/highcharts-3d.js delete mode 100644 public/vendor/highcharts-4.2.5/highcharts-3d.src.js delete mode 100644 public/vendor/highcharts-4.2.5/highcharts-more.js delete mode 100644 public/vendor/highcharts-4.2.5/highcharts-more.src.js delete mode 100644 public/vendor/highcharts-4.2.5/highcharts.js delete mode 100644 public/vendor/highcharts-4.2.5/highcharts.src.js delete mode 100644 public/vendor/highcharts-4.2.5/highmaps.js delete mode 100644 public/vendor/highcharts-4.2.5/highmaps.src.js delete mode 100644 public/vendor/highcharts-4.2.5/highstock.js delete mode 100644 public/vendor/highcharts-4.2.5/highstock.src.js delete mode 100644 public/vendor/stockfish-mv.wasm/stockfish.js delete mode 100755 public/vendor/stockfish-mv.wasm/stockfish.wasm delete mode 100644 public/vendor/stockfish-mv.wasm/stockfish.worker.js delete mode 100755 public/vendor/stockfish-nnue.wasm/stockfish-no-embedded-nnue.wasm delete mode 100644 public/vendor/stockfish-nnue.wasm/stockfish.js delete mode 100755 public/vendor/stockfish-nnue.wasm/stockfish.wasm delete mode 100644 public/vendor/stockfish-nnue.wasm/stockfish.worker.js delete mode 100644 public/vendor/stockfish-nnue.wasm/uci.js delete mode 100644 public/vendor/stockfish.js/stockfish.js delete mode 100644 public/vendor/stockfish.js/stockfish.wasm delete mode 100644 public/vendor/stockfish.js/stockfish.wasm.js delete mode 100644 public/vendor/stockfish.wasm/stockfish.js delete mode 100644 public/vendor/stockfish.wasm/stockfish.wasm delete mode 100644 public/vendor/stockfish.wasm/stockfish.worker.js diff --git a/package.json b/package.json index b250e5114523b..0547c130bda50 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "chessground": "^9.0.4", "eslint": "^8.57.0", "lint-staged": "^15.2.2", + "onchange": "^7.1.0", "prettier": "3.0.2", "typescript": "^5.4.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0811d2246307a..e62e6573a7ca3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: lint-staged: specifier: ^15.2.2 version: 15.2.2 + onchange: + specifier: ^7.1.0 + version: 7.1.0 prettier: specifier: 3.0.2 version: 3.0.2 @@ -92,8 +95,8 @@ importers: specifier: workspace:* version: link:../chess chessops: - specifier: ^0.14.0 - version: 0.14.0 + specifier: ^0.12.8 + version: 0.12.8 common: specifier: workspace:* version: link:../common @@ -134,8 +137,8 @@ importers: specifier: ^0.2.13 version: 0.2.13 chessops: - specifier: ^0.14.0 - version: 0.14.0 + specifier: ^0.12.8 + version: 0.12.8 common: specifier: workspace:* version: link:../common @@ -272,14 +275,14 @@ importers: ui/dgt: dependencies: chessops: - specifier: ^0.14.0 - version: 0.14.0 + specifier: ^0.12.8 + version: 0.12.8 ui/editor: dependencies: chessops: - specifier: ^0.14.0 - version: 0.14.0 + specifier: ^0.12.8 + version: 0.12.8 common: specifier: workspace:* version: link:../common @@ -352,18 +355,9 @@ importers: ui/libot: dependencies: - chess: - specifier: workspace:* - version: link:../chess chessops: - specifier: ^0.14.0 - version: 0.14.0 - common: - specifier: workspace:* - version: link:../common - tree: - specifier: workspace:* - version: link:../tree + specifier: ^0.12.8 + version: 0.12.8 zerofish: specifier: ^0.0.18 version: 0.0.18 @@ -401,8 +395,8 @@ importers: specifier: workspace:* version: link:../chess chessops: - specifier: ^0.14.0 - version: 0.14.0 + specifier: ^0.12.8 + version: 0.12.8 common: specifier: workspace:* version: link:../common @@ -412,21 +406,12 @@ importers: libot: specifier: workspace:* version: link:../libot - nvui: - specifier: workspace:* - version: link:../nvui - puz: - specifier: workspace:* - version: link:../puz round: specifier: workspace:* version: link:../round snabbdom: specifier: ^3.5.1 version: 3.6.2 - tree: - specifier: workspace:* - version: link:../tree ui/mod: dependencies: @@ -479,8 +464,8 @@ importers: specifier: workspace:* version: link:../chess chessops: - specifier: ^0.14.0 - version: 0.14.0 + specifier: ^0.12.8 + version: 0.12.8 snabbdom: specifier: ^3.5.1 version: 3.6.2 @@ -584,8 +569,8 @@ importers: ui/puz: dependencies: chessops: - specifier: ^0.14.0 - version: 0.14.0 + specifier: ^0.12.8 + version: 0.12.8 common: specifier: workspace:* version: link:../common @@ -608,8 +593,8 @@ importers: specifier: workspace:* version: link:../chess chessops: - specifier: ^0.14.0 - version: 0.14.0 + specifier: ^0.12.8 + version: 0.12.8 common: specifier: workspace:* version: link:../common @@ -635,8 +620,8 @@ importers: specifier: workspace:* version: link:../chess chessops: - specifier: ^0.14.0 - version: 0.14.0 + specifier: ^0.12.8 + version: 0.12.8 common: specifier: workspace:* version: link:../common @@ -740,8 +725,8 @@ importers: specifier: workspace:* version: link:../chess chessops: - specifier: ^0.14.0 - version: 0.14.0 + specifier: ^0.12.8 + version: 0.12.8 common: specifier: workspace:* version: link:../common @@ -1203,6 +1188,14 @@ packages: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: false + /@blakeembrey/deque@1.0.5: + resolution: {integrity: sha512-6xnwtvp9DY1EINIKdTfvfeAtCYw4OqBZJhtiqkT3ivjnEfa25VQ3TsKvaFfKm8MyGIEfE95qLe+bNEt3nB0Ylg==} + dev: false + + /@blakeembrey/template@1.1.0: + resolution: {integrity: sha512-iZf+UWfL+DogJVpd/xMQyP6X6McYd6ArdYoPMiv/zlOTzeXXfQbYxBNJJBF6tThvsjLMbA8tLjkCdm9RWMFCCw==} + dev: false + /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2030,6 +2023,10 @@ packages: svg.select.js: 3.0.1 dev: false + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + dev: false + /argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: @@ -2278,12 +2275,6 @@ packages: '@badrap/result': 0.2.13 dev: false - /chessops@0.14.0: - resolution: {integrity: sha512-vg+jHoLJ1RPPVos/Uu44Z0jnVrzi7tkVtu5xwdIVjQ+QKw34it3gKUcW3l/artZlV7L8v4zLD98DJUXKGiUyzA==} - dependencies: - '@badrap/result': 0.2.13 - dev: false - /chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -3980,6 +3971,19 @@ packages: wrappy: 1.0.2 dev: false + /onchange@7.1.0: + resolution: {integrity: sha512-ZJcqsPiWUAUpvmnJri5TPBooqJOPmC0ttN65juhN15Q8xA+Nbg3BaxBHXQ45EistKKlKElb0edmbPWnKSBkvMg==} + hasBin: true + dependencies: + '@blakeembrey/deque': 1.0.5 + '@blakeembrey/template': 1.1.0 + arg: 4.1.3 + chokidar: 3.6.0 + cross-spawn: 7.0.3 + ignore: 5.3.1 + tree-kill: 1.2.2 + dev: false + /onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -4677,6 +4681,11 @@ packages: punycode: 2.3.1 dev: false + /tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: false + /ts-api-utils@1.3.0(typescript@5.4.2): resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} diff --git a/public/vendor/highcharts-4.2.5/highcharts-3d.js b/public/vendor/highcharts-4.2.5/highcharts-3d.js deleted file mode 100644 index bec09a2902889..0000000000000 --- a/public/vendor/highcharts-4.2.5/highcharts-3d.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - Highcharts JS v4.2.5 (2016-05-06) - - 3D features for Highcharts JS - - @license: www.highcharts.com/license -*/ -(function(d){typeof module==="object"&&module.exports?module.exports=d:d(Highcharts)})(function(d){function o(c,b,a){var e,f,g=b.options.chart.options3d,d=!1,j=b.scale3d||1;a?(d=b.inverted,a=b.plotWidth/2,b=b.plotHeight/2,e=g.depth/2,f=s(g.depth,1)*s(g.viewDistance,0)):(a=b.plotLeft+b.plotWidth/2,b=b.plotTop+b.plotHeight/2,e=g.depth/2,f=s(g.depth,1)*s(g.viewDistance,0));var k=[],i=a,m=b,x=e,y=f,a=B*(d?g.beta:-g.beta),g=B*(d?-g.alpha:g.alpha),q=r(a),p=l(a),n=r(g),u=l(g),t,z,v,w,C,o;A(c,function(a){t= -(d?a.y:a.x)-i;z=(d?a.x:a.y)-m;v=(a.z||0)-x;w=p*t-q*v;C=-q*n*t+u*z-p*n*v;o=q*u*t+n*z+p*u*v;y>0&&yf&&g-f>n/2+ -1.0E-4?(k=k.concat(u(c,b,a,e,f,f+n/2,d,j)),k=k.concat(u(c,b,a,e,f+n/2,g,d,j))):gn/2+1.0E-4?(k=k.concat(u(c,b,a,e,f,f-n/2,d,j)),k=k.concat(u(c,b,a,e,f-n/2,g,d,j))):(k=g-f,k=["C",c+a*l(f)-a*F*k*r(f)+d,b+e*r(f)+e*F*k*l(f)+j,c+a*l(g)+a*F*k*r(g)+d,b+e*r(g)-e*F*k*l(g)+j,c+a*l(g)+d,b+e*r(g)+j]);return k}function J(c){if(this.chart.is3d()){var b=this.chart.options.plotOptions.column.grouping;if(b!==void 0&&!b&&this.group.zIndex!==void 0&&!this.zIndexSet)this.group.attr({zIndex:this.group.zIndex*10}), -this.zIndexSet=!0;var a=this.options,e=this.options.states;this.borderWidth=a.borderWidth=D(a.edgeWidth)?a.edgeWidth:1;d.each(this.data,function(b){if(b.y!==null)b=b.pointAttr,this.borderColor=d.pick(a.edgeColor,b[""].fill),b[""].stroke=this.borderColor,b.hover.stroke=d.pick(e.hover.edgeColor,this.borderColor),b.select.stroke=d.pick(e.select.edgeColor,this.borderColor)})}c.apply(this,[].slice.call(arguments,1))}var M=d.animObject,A=d.each,N=d.extend,O=d.inArray,G=d.merge,s=d.pick,K=d.wrap,n=Math.PI, -B=n/180,r=Math.sin,l=Math.cos,L=Math.round;d.perspective=o;var F=4*(Math.sqrt(2)-1)/3/(n/2);d.SVGRenderer.prototype.toLinePath=function(c,b){var a=[];d.each(c,function(b){a.push("L",b.x,b.y)});c.length&&(a[0]="M",b&&a.push("Z"));return a};d.SVGRenderer.prototype.cuboid=function(c){var b=this.g(),c=this.cuboidPath(c);b.front=this.path(c[0]).attr({zIndex:c[3],"stroke-linejoin":"round"}).add(b);b.top=this.path(c[1]).attr({zIndex:c[4],"stroke-linejoin":"round"}).add(b);b.side=this.path(c[2]).attr({zIndex:c[5], -"stroke-linejoin":"round"}).add(b);b.fillSetter=function(a){var b=d.Color(a).brighten(0.1).get(),c=d.Color(a).brighten(-0.1).get();this.front.attr({fill:a});this.top.attr({fill:b});this.side.attr({fill:c});this.color=a;return this};b.opacitySetter=function(a){this.front.attr({opacity:a});this.top.attr({opacity:a});this.side.attr({opacity:a});return this};b.attr=function(a){if(a.shapeArgs||D(a.x))a=this.renderer.cuboidPath(a.shapeArgs||a),this.front.attr({d:a[0],zIndex:a[3]}),this.top.attr({d:a[1], -zIndex:a[4]}),this.side.attr({d:a[2],zIndex:a[5]});else return d.SVGElement.prototype.attr.call(this,a);return this};b.animate=function(a,b,c){D(a.x)&&D(a.y)?(a=this.renderer.cuboidPath(a),this.front.attr({zIndex:a[3]}).animate({d:a[0]},b,c),this.top.attr({zIndex:a[4]}).animate({d:a[1]},b,c),this.side.attr({zIndex:a[5]}).animate({d:a[2]},b,c),this.attr({zIndex:-a[6]})):a.opacity?(this.front.animate(a,b,c),this.top.animate(a,b,c),this.side.animate(a,b,c)):d.SVGElement.prototype.animate.call(this,a, -b,c);return this};b.destroy=function(){this.front.destroy();this.top.destroy();this.side.destroy();return null};b.attr({zIndex:-c[6]});return b};d.SVGRenderer.prototype.cuboidPath=function(c){function b(a){return i[a]}var a=c.x,e=c.y,f=c.z,g=c.height,h=c.width,j=c.depth,k=d.map,i=[{x:a,y:e,z:f},{x:a+h,y:e,z:f},{x:a+h,y:e+g,z:f},{x:a,y:e+g,z:f},{x:a,y:e+g,z:f+j},{x:a+h,y:e+g,z:f+j},{x:a+h,y:e,z:f+j},{x:a,y:e,z:f+j}],i=o(i,d.charts[this.chartIndex],c.insidePlotArea),f=function(a,c){var e=[],a=k(a,b), -c=k(c,b);I(a)<0?e=a:I(c)<0&&(e=c);return e},c=f([3,2,1,0],[7,6,5,4]),a=[4,5,2,3],e=f([1,6,7,0],a),f=f([1,2,5,6],[0,7,4,3]);return[this.toLinePath(c,!0),this.toLinePath(e,!0),this.toLinePath(f,!0),E(c),E(e),E(f),E(k(a,b))*9E9]};d.SVGRenderer.prototype.arc3d=function(c){function b(a){var b=!1,c={},e;for(e in a)O(e,f)!==-1&&(c[e]=a[e],delete a[e],b=!0);return b?c:!1}var a=this.g(),e=a.renderer,f="x,y,r,innerR,start,end".split(","),c=G(c);c.alpha*=B;c.beta*=B;a.top=e.path();a.side1=e.path();a.side2=e.path(); -a.inn=e.path();a.out=e.path();a.onAdd=function(){var b=a.parentGroup;a.top.add(a);a.out.add(b);a.inn.add(b);a.side1.add(b);a.side2.add(b)};a.setPaths=function(b){var c=a.renderer.arc3dPath(b),e=c.zTop*100;a.attribs=b;a.top.attr({d:c.top,zIndex:c.zTop});a.inn.attr({d:c.inn,zIndex:c.zInn});a.out.attr({d:c.out,zIndex:c.zOut});a.side1.attr({d:c.side1,zIndex:c.zSide1});a.side2.attr({d:c.side2,zIndex:c.zSide2});a.zIndex=e;a.attr({zIndex:e});b.center&&(a.top.setRadialReference(b.center),delete b.center)}; -a.setPaths(c);a.fillSetter=function(a){var b=d.Color(a).brighten(-0.1).get();this.fill=a;this.side1.attr({fill:b});this.side2.attr({fill:b});this.inn.attr({fill:b});this.out.attr({fill:b});this.top.attr({fill:a});return this};A(["opacity","translateX","translateY","visibility"],function(b){a[b+"Setter"]=function(b,c){a[c]=b;A(["out","inn","side1","side2","top"],function(e){a[e].attr(c,b)})}});K(a,"attr",function(c,e,d){var f;if(typeof e==="object"&&(f=b(e)))N(a.attribs,f),a.setPaths(a.attribs);return c.call(this, -e,d)});K(a,"animate",function(a,c,e,d){var f,m=this.attribs,l;delete c.center;delete c.z;delete c.depth;delete c.alpha;delete c.beta;e=M(s(e,this.renderer.globalAnimation));if(e.duration&&(c=G(c),f=b(c)))l=f,e.step=function(a,b){function c(a){return m[a]+(s(l[a],m[a])-m[a])*b.pos}b.elem.setPaths(G(m,{x:c("x"),y:c("y"),r:c("r"),innerR:c("innerR"),start:c("start"),end:c("end")}))};return a.call(this,c,e,d)});a.destroy=function(){this.top.destroy();this.out.destroy();this.inn.destroy();this.side1.destroy(); -this.side2.destroy();d.SVGElement.prototype.destroy.call(this)};a.hide=function(){this.top.hide();this.out.hide();this.inn.hide();this.side1.hide();this.side2.hide()};a.show=function(){this.top.show();this.out.show();this.inn.show();this.side1.show();this.side2.show()};return a};d.SVGRenderer.prototype.arc3dPath=function(c){function b(a){a%=2*n;a>n&&(a=2*n-a);return a}var a=c.x,e=c.y,d=c.start,g=c.end-1.0E-5,h=c.r,j=c.innerR,k=c.depth,i=c.alpha,m=c.beta,x=l(d),y=r(d),c=l(g),q=r(g),p=h*l(m);h*=l(i); -var o=j*l(m),s=j*l(i),j=k*r(m),t=k*r(i),k=["M",a+p*x,e+h*y],k=k.concat(u(a,e,p,h,d,g,0,0)),k=k.concat(["L",a+o*c,e+s*q]),k=k.concat(u(a,e,o,s,g,d,0,0)),k=k.concat(["Z"]),z=m>0?n/2:0,m=i>0?0:n/2,z=d>-z?d:g>-z?-z:d,v=gw&&dn-m&&da&&(n=Math.min(n,1-Math.abs((h+m)/(a+m))%1));jd&&(n=d<0?Math.min(n,(k+l)/(-d+k+l)):Math.min(n,1-(k+l)/(d+l)%1));i0?4:-1}).css({stroke:g.color}).add()):(d={x:m+(b.yAxis[0].opposite?0:-f.size),y:l+(b.xAxis[0].opposite?-g.size:0),z:j,width:i+f.size,height:k+g.size,depth:h.size,insidePlotArea:!1},this.backFrame?this.backFrame.animate(d):this.backFrame=a.cuboid(d).attr({fill:h.color,zIndex:-3}).css({stroke:h.color}).add(),b={x:m+(b.yAxis[0].opposite?i:-f.size),y:l+(b.xAxis[0].opposite?-g.size:0),z:0,width:f.size,height:k+g.size,depth:j,insidePlotArea:!1},this.sideFrame?this.sideFrame.animate(b): -this.sideFrame=a.cuboid(b).attr({fill:f.color,zIndex:-2}).css({stroke:f.color}).add())}});d.wrap(d.Axis.prototype,"getPlotLinePath",function(c){var b=c.apply(this,[].slice.call(arguments,1));if(!this.chart.is3d())return b;if(b===null)return b;var a=this.chart,d=a.options.chart.options3d,a=this.isZAxis?a.plotWidth:d.depth,d=this.opposite;this.horiz&&(d=!d);b=[this.swapZ({x:b[1],y:b[2],z:d?a:0}),this.swapZ({x:b[1],y:b[2],z:a}),this.swapZ({x:b[4],y:b[5],z:a}),this.swapZ({x:b[4],y:b[5],z:d?0:a})];b=o(b, -this.chart,!1);return b=this.chart.renderer.toLinePath(b,!1)});d.wrap(d.Axis.prototype,"getLinePath",function(c){return this.chart.is3d()?[]:c.apply(this,[].slice.call(arguments,1))});d.wrap(d.Axis.prototype,"getPlotBandPath",function(c){if(!this.chart.is3d())return c.apply(this,[].slice.call(arguments,1));var b=arguments,a=b[1],b=this.getPlotLinePath(b[2]);(a=this.getPlotLinePath(a))&&b?a.push("L",b[10],b[11],"L",b[7],b[8],"L",b[4],b[5],"L",b[1],b[2]):a=null;return a});d.wrap(d.Tick.prototype,"getMarkPath", -function(c){var b=c.apply(this,[].slice.call(arguments,1));if(!this.axis.chart.is3d())return b;b=[this.axis.swapZ({x:b[1],y:b[2],z:0}),this.axis.swapZ({x:b[4],y:b[5],z:0})];b=o(b,this.axis.chart,!1);return b=["M",b[0].x,b[0].y,"L",b[1].x,b[1].y]});d.wrap(d.Tick.prototype,"getLabelPosition",function(c){var b=c.apply(this,[].slice.call(arguments,1));if(!this.axis.chart.is3d())return b;var a=o([this.axis.swapZ({x:b.x,y:b.y,z:0})],this.axis.chart,!1)[0];a.x-=!this.axis.horiz&&this.axis.opposite?this.axis.transA: -0;a.old=b;return a});d.wrap(d.Tick.prototype,"handleOverflow",function(c,b){if(this.axis.chart.is3d())b=b.old;return c.call(this,b)});d.wrap(d.Axis.prototype,"getTitlePosition",function(c){var b=this.chart.is3d(),a,d;if(b)d=this.axisTitleMargin,this.axisTitleMargin=0;a=c.apply(this,[].slice.call(arguments,1));if(b)a=o([this.swapZ({x:a.x,y:a.y,z:0})],this.chart,!1)[0],a[this.horiz?"y":"x"]+=(this.horiz?1:-1)*(this.opposite?-1:1)*d,this.axisTitleMargin=d;return a});d.wrap(d.Axis.prototype,"drawCrosshair", -function(c){var b=arguments;this.chart.is3d()&&b[2]&&(b[2]={plotX:b[2].plotXold||b[2].plotX,plotY:b[2].plotYold||b[2].plotY});c.apply(this,[].slice.call(b,1))});d.Axis.prototype.swapZ=function(c,b){if(this.isZAxis){var a=b?0:this.chart.plotLeft,d=this.chart;return{x:a+(d.yAxis[0].opposite?c.z:d.xAxis[0].width-c.z),y:c.y,z:c.x-a}}return c};var H=d.ZAxis=function(){this.isZAxis=!0;this.init.apply(this,arguments)};d.extend(H.prototype,d.Axis.prototype);d.extend(H.prototype,{setOptions:function(c){c= -d.merge({offset:0,lineWidth:0},c);d.Axis.prototype.setOptions.call(this,c);this.coll="zAxis"},setAxisSize:function(){d.Axis.prototype.setAxisSize.call(this);this.width=this.len=this.chart.options.chart.options3d.depth;this.right=this.chart.chartWidth-this.width-this.left},getSeriesExtremes:function(){var c=this,b=c.chart;c.hasVisibleSeries=!1;c.dataMin=c.dataMax=c.ignoreMinPadding=c.ignoreMaxPadding=null;c.buildStacks&&c.buildStacks();d.each(c.series,function(a){if(a.visible||!b.options.chart.ignoreHiddenSeries)if(c.hasVisibleSeries= -!0,a=a.zData,a.length)c.dataMin=Math.min(s(c.dataMin,a[0]),Math.min.apply(null,a)),c.dataMax=Math.max(s(c.dataMax,a[0]),Math.max.apply(null,a))})}});d.wrap(d.Chart.prototype,"getAxes",function(c){var b=this,a=this.options,a=a.zAxis=d.splat(a.zAxis||{});c.call(this);if(b.is3d())this.zAxis=[],d.each(a,function(a,c){a.index=c;a.isX=!0;(new H(b,a)).setScale()})});d.wrap(d.seriesTypes.column.prototype,"translate",function(c){c.apply(this,[].slice.call(arguments,1));if(this.chart.is3d()){var b=this.chart, -a=this.options,e=a.depth||25,f=(a.stacking?a.stack||0:this._i)*(e+(a.groupZPadding||1));a.grouping!==!1&&(f=0);f+=a.groupZPadding||1;d.each(this.data,function(a){if(a.y!==null){var c=a.shapeArgs,d=a.tooltipPos;a.shapeType="cuboid";c.z=f;c.depth=e;c.insidePlotArea=!0;d=o([{x:d[0],y:d[1],z:f}],b,!0)[0];a.tooltipPos=[d.x,d.y]}});this.z=f}});d.wrap(d.seriesTypes.column.prototype,"animate",function(c){if(this.chart.is3d()){var b=arguments[1],a=this.yAxis,e=this,f=this.yAxis.reversed;if(d.svg)b?d.each(e.data, -function(b){if(b.y!==null&&(b.height=b.shapeArgs.height,b.shapey=b.shapeArgs.y,b.shapeArgs.height=1,!f))b.shapeArgs.y=b.stackY?b.plotY+a.translate(b.stackY):b.plotY+(b.negative?-b.height:b.height)}):(d.each(e.data,function(a){if(a.y!==null)a.shapeArgs.height=a.height,a.shapeArgs.y=a.shapey,a.graphic&&a.graphic.animate(a.shapeArgs,e.options.animation)}),this.drawDataLabels(),e.animate=null)}else c.apply(this,[].slice.call(arguments,1))});d.wrap(d.seriesTypes.column.prototype,"init",function(c){c.apply(this, -[].slice.call(arguments,1));if(this.chart.is3d()){var b=this.options,a=b.grouping,d=b.stacking,f=s(this.yAxis.options.reversedStacks,!0),g=0;if(a===void 0||a){a=this.chart.retrieveStacks(d);g=b.stack||0;for(d=0;d=a.min&&g<=a.max:!1,e.push({x:f.plotX,y:f.plotY,z:f.plotZ});b=o(e,b,!0);for(h=0;h{point.x}

y: {point.y}
z: {point.z}
":"x: {point.x}
y: {point.y}
z: {point.z}
";return c});if(d.VMLRenderer)d.setOptions({animate:!1}),d.VMLRenderer.prototype.cuboid=d.SVGRenderer.prototype.cuboid, -d.VMLRenderer.prototype.cuboidPath=d.SVGRenderer.prototype.cuboidPath,d.VMLRenderer.prototype.toLinePath=d.SVGRenderer.prototype.toLinePath,d.VMLRenderer.prototype.createElement3D=d.SVGRenderer.prototype.createElement3D,d.VMLRenderer.prototype.arc3d=function(c){c=d.SVGRenderer.prototype.arc3d.call(this,c);c.css({zIndex:c.zIndex});return c},d.VMLRenderer.prototype.arc3dPath=d.SVGRenderer.prototype.arc3dPath,d.wrap(d.Axis.prototype,"render",function(c){c.apply(this,[].slice.call(arguments,1));this.sideFrame&& -(this.sideFrame.css({zIndex:0}),this.sideFrame.front.attr({fill:this.sideFrame.color}));this.bottomFrame&&(this.bottomFrame.css({zIndex:1}),this.bottomFrame.front.attr({fill:this.bottomFrame.color}));this.backFrame&&(this.backFrame.css({zIndex:0}),this.backFrame.front.attr({fill:this.backFrame.color}))})}); diff --git a/public/vendor/highcharts-4.2.5/highcharts-3d.src.js b/public/vendor/highcharts-4.2.5/highcharts-3d.src.js deleted file mode 100644 index f507ba8405aba..0000000000000 --- a/public/vendor/highcharts-4.2.5/highcharts-3d.src.js +++ /dev/null @@ -1,1817 +0,0 @@ -// ==ClosureCompiler== -// @compilation_level SIMPLE_OPTIMIZATIONS - -/** - * @license Highcharts JS v4.2.5 (2016-05-06) - * - * 3D features for Highcharts JS - * - * @license: www.highcharts.com/license - */ - -(function (factory) { - if (typeof module === 'object' && module.exports) { - module.exports = factory; - } else { - factory(Highcharts); - } -}(function (Highcharts) { -/** - Shorthands for often used function - */ - var animObject = Highcharts.animObject, - each = Highcharts.each, - extend = Highcharts.extend, - inArray = Highcharts.inArray, - merge = Highcharts.merge, - pick = Highcharts.pick, - wrap = Highcharts.wrap; - /** - * Mathematical Functionility - */ - var PI = Math.PI, - deg2rad = (PI / 180), // degrees to radians - sin = Math.sin, - cos = Math.cos, - round = Math.round; - - /** - * Transforms a given array of points according to the angles in chart.options. - * Parameters: - * - points: the array of points - * - chart: the chart - * - insidePlotArea: wether to verifiy the points are inside the plotArea - * Returns: - * - an array of transformed points - */ - function perspective(points, chart, insidePlotArea) { - var options3d = chart.options.chart.options3d, - inverted = false, - origin, - scale = chart.scale3d || 1; - - if (insidePlotArea) { - inverted = chart.inverted; - origin = { - x: chart.plotWidth / 2, - y: chart.plotHeight / 2, - z: options3d.depth / 2, - vd: pick(options3d.depth, 1) * pick(options3d.viewDistance, 0) - }; - } else { - origin = { - x: chart.plotLeft + (chart.plotWidth / 2), - y: chart.plotTop + (chart.plotHeight / 2), - z: options3d.depth / 2, - vd: pick(options3d.depth, 1) * pick(options3d.viewDistance, 0) - }; - } - - var result = [], - xe = origin.x, - ye = origin.y, - ze = origin.z, - vd = origin.vd, - angle1 = deg2rad * (inverted ? options3d.beta : -options3d.beta), - angle2 = deg2rad * (inverted ? -options3d.alpha : options3d.alpha), - s1 = sin(angle1), - c1 = cos(angle1), - s2 = sin(angle2), - c2 = cos(angle2); - - var x, y, z, px, py, pz; - - // Transform each point - each(points, function (point) { - x = (inverted ? point.y : point.x) - xe; - y = (inverted ? point.x : point.y) - ye; - z = (point.z || 0) - ze; - - // Apply 3-D rotation - // Euler Angles (XYZ): cosA = cos(Alfa|Roll), cosB = cos(Beta|Pitch), cosG = cos(Gamma|Yaw) - // - // Composite rotation: - // | cosB * cosG | cosB * sinG | -sinB | - // | sinA * sinB * cosG - cosA * sinG | sinA * sinB * sinG + cosA * cosG | sinA * cosB | - // | cosA * sinB * cosG + sinA * sinG | cosA * sinB * sinG - sinA * cosG | cosA * cosB | - // - // Now, Gamma/Yaw is not used (angle=0), so we assume cosG = 1 and sinG = 0, so we get: - // | cosB | 0 | - sinB | - // | sinA * sinB | cosA | sinA * cosB | - // | cosA * sinB | - sinA | cosA * cosB | - // - // But in browsers, y is reversed, so we get sinA => -sinA. The general result is: - // | cosB | 0 | - sinB | | x | | px | - // | - sinA * sinB | cosA | - sinA * cosB | x | y | = | py | - // | cosA * sinB | sinA | cosA * cosB | | z | | pz | - // - // Result: - px = c1 * x - s1 * z; - py = -s1 * s2 * x + c2 * y - c1 * s2 * z; - pz = s1 * c2 * x + s2 * y + c1 * c2 * z; - - - // Apply perspective - if ((vd > 0) && (vd < Number.POSITIVE_INFINITY)) { - px = px * (vd / (pz + ze + vd)); - py = py * (vd / (pz + ze + vd)); - } - - - //Apply translation - px = px * scale + xe; - py = py * scale + ye; - pz = pz * scale + ze; - - - result.push({ - x: (inverted ? py : px), - y: (inverted ? px : py), - z: pz - }); - }); - return result; - } - // Make function acessible to plugins - Highcharts.perspective = perspective; - /*** - EXTENSION TO THE SVG-RENDERER TO ENABLE 3D SHAPES - ***/ - ////// HELPER METHODS ////// - - var dFactor = (4 * (Math.sqrt(2) - 1) / 3) / (PI / 2); - - function defined(obj) { - return obj !== undefined && obj !== null; - } - - //Shoelace algorithm -- http://en.wikipedia.org/wiki/Shoelace_formula - function shapeArea(vertexes) { - var area = 0, - i, - j; - for (i = 0; i < vertexes.length; i++) { - j = (i + 1) % vertexes.length; - area += vertexes[i].x * vertexes[j].y - vertexes[j].x * vertexes[i].y; - } - return area / 2; - } - - function averageZ(vertexes) { - var z = 0, - i; - for (i = 0; i < vertexes.length; i++) { - z += vertexes[i].z; - } - return vertexes.length ? z / vertexes.length : 0; - } - - /** Method to construct a curved path - * Can 'wrap' around more then 180 degrees - */ - function curveTo(cx, cy, rx, ry, start, end, dx, dy) { - var result = []; - if ((end > start) && (end - start > PI / 2 + 0.0001)) { - result = result.concat(curveTo(cx, cy, rx, ry, start, start + (PI / 2), dx, dy)); - result = result.concat(curveTo(cx, cy, rx, ry, start + (PI / 2), end, dx, dy)); - } else if ((end < start) && (start - end > PI / 2 + 0.0001)) { - result = result.concat(curveTo(cx, cy, rx, ry, start, start - (PI / 2), dx, dy)); - result = result.concat(curveTo(cx, cy, rx, ry, start - (PI / 2), end, dx, dy)); - } else { - var arcAngle = end - start; - result = [ - 'C', - cx + (rx * cos(start)) - ((rx * dFactor * arcAngle) * sin(start)) + dx, - cy + (ry * sin(start)) + ((ry * dFactor * arcAngle) * cos(start)) + dy, - cx + (rx * cos(end)) + ((rx * dFactor * arcAngle) * sin(end)) + dx, - cy + (ry * sin(end)) - ((ry * dFactor * arcAngle) * cos(end)) + dy, - - cx + (rx * cos(end)) + dx, - cy + (ry * sin(end)) + dy - ]; - } - return result; - } - - Highcharts.SVGRenderer.prototype.toLinePath = function (points, closed) { - var result = []; - - // Put "L x y" for each point - Highcharts.each(points, function (point) { - result.push('L', point.x, point.y); - }); - - if (points.length) { - // Set the first element to M - result[0] = 'M'; - - // If it is a closed line, add Z - if (closed) { - result.push('Z'); - } - } - - return result; - }; - - ////// CUBOIDS ////// - Highcharts.SVGRenderer.prototype.cuboid = function (shapeArgs) { - - var result = this.g(), - paths = this.cuboidPath(shapeArgs); - - // create the 3 sides - result.front = this.path(paths[0]).attr({ zIndex: paths[3], 'stroke-linejoin': 'round' }).add(result); - result.top = this.path(paths[1]).attr({ zIndex: paths[4], 'stroke-linejoin': 'round' }).add(result); - result.side = this.path(paths[2]).attr({ zIndex: paths[5], 'stroke-linejoin': 'round' }).add(result); - - // apply the fill everywhere, the top a bit brighter, the side a bit darker - result.fillSetter = function (color) { - var c0 = color, - c1 = Highcharts.Color(color).brighten(0.1).get(), - c2 = Highcharts.Color(color).brighten(-0.1).get(); - - this.front.attr({ fill: c0 }); - this.top.attr({ fill: c1 }); - this.side.attr({ fill: c2 }); - - this.color = color; - return this; - }; - - // apply opacaity everywhere - result.opacitySetter = function (opacity) { - this.front.attr({ opacity: opacity }); - this.top.attr({ opacity: opacity }); - this.side.attr({ opacity: opacity }); - return this; - }; - - result.attr = function (args) { - if (args.shapeArgs || defined(args.x)) { - var shapeArgs = args.shapeArgs || args; - var paths = this.renderer.cuboidPath(shapeArgs); - this.front.attr({ d: paths[0], zIndex: paths[3] }); - this.top.attr({ d: paths[1], zIndex: paths[4] }); - this.side.attr({ d: paths[2], zIndex: paths[5] }); - } else { - return Highcharts.SVGElement.prototype.attr.call(this, args); // getter returns value - } - - return this; - }; - - result.animate = function (args, duration, complete) { - if (defined(args.x) && defined(args.y)) { - var paths = this.renderer.cuboidPath(args); - this.front.attr({ zIndex: paths[3] }).animate({ d: paths[0] }, duration, complete); - this.top.attr({ zIndex: paths[4] }).animate({ d: paths[1] }, duration, complete); - this.side.attr({ zIndex: paths[5] }).animate({ d: paths[2] }, duration, complete); - this.attr({ - zIndex: -paths[6] // #4774 - }); - } else if (args.opacity) { - this.front.animate(args, duration, complete); - this.top.animate(args, duration, complete); - this.side.animate(args, duration, complete); - } else { - Highcharts.SVGElement.prototype.animate.call(this, args, duration, complete); - } - return this; - }; - - // destroy all children - result.destroy = function () { - this.front.destroy(); - this.top.destroy(); - this.side.destroy(); - - return null; - }; - - // Apply the Z index to the cuboid group - result.attr({ zIndex: -paths[6] }); - - return result; - }; - - /** - * Generates a cuboid - */ - Highcharts.SVGRenderer.prototype.cuboidPath = function (shapeArgs) { - var x = shapeArgs.x, - y = shapeArgs.y, - z = shapeArgs.z, - h = shapeArgs.height, - w = shapeArgs.width, - d = shapeArgs.depth, - chart = Highcharts.charts[this.chartIndex], - map = Highcharts.map; - - // The 8 corners of the cube - var pArr = [ - { x: x, y: y, z: z }, - { x: x + w, y: y, z: z }, - { x: x + w, y: y + h, z: z }, - { x: x, y: y + h, z: z }, - { x: x, y: y + h, z: z + d }, - { x: x + w, y: y + h, z: z + d }, - { x: x + w, y: y, z: z + d }, - { x: x, y: y, z: z + d } - ]; - - // apply perspective - pArr = perspective(pArr, chart, shapeArgs.insidePlotArea); - - // helper method to decide which side is visible - function mapPath(i) { - return pArr[i]; - } - var pickShape = function (path1, path2) { - var ret = []; - path1 = map(path1, mapPath); - path2 = map(path2, mapPath); - if (shapeArea(path1) < 0) { - ret = path1; - } else if (shapeArea(path2) < 0) { - ret = path2; - } - return ret; - }; - - // front or back - var front = [3, 2, 1, 0]; - var back = [7, 6, 5, 4]; - var path1 = pickShape(front, back); - - // top or bottom - var top = [1, 6, 7, 0]; - var bottom = [4, 5, 2, 3]; - var path2 = pickShape(top, bottom); - - // side - var right = [1, 2, 5, 6]; - var left = [0, 7, 4, 3]; - var path3 = pickShape(right, left); - - return [this.toLinePath(path1, true), this.toLinePath(path2, true), this.toLinePath(path3, true), averageZ(path1), averageZ(path2), averageZ(path3), averageZ(map(bottom, mapPath)) * 9e9]; // #4774 - }; - - ////// SECTORS ////// - Highcharts.SVGRenderer.prototype.arc3d = function (attribs) { - - var wrapper = this.g(), - renderer = wrapper.renderer, - customAttribs = ['x', 'y', 'r', 'innerR', 'start', 'end']; - - /** - * Get custom attributes. Mutate the original object and return an object with only custom attr. - */ - function suckOutCustom(params) { - var hasCA = false, - ca = {}; - for (var key in params) { - if (inArray(key, customAttribs) !== -1) { - ca[key] = params[key]; - delete params[key]; - hasCA = true; - } - } - return hasCA ? ca : false; - } - - attribs = merge(attribs); - - attribs.alpha *= deg2rad; - attribs.beta *= deg2rad; - - // Create the different sub sections of the shape - wrapper.top = renderer.path(); - wrapper.side1 = renderer.path(); - wrapper.side2 = renderer.path(); - wrapper.inn = renderer.path(); - wrapper.out = renderer.path(); - - /** - * Add all faces - */ - wrapper.onAdd = function () { - var parent = wrapper.parentGroup; - wrapper.top.add(wrapper); - wrapper.out.add(parent); - wrapper.inn.add(parent); - wrapper.side1.add(parent); - wrapper.side2.add(parent); - }; - - /** - * Compute the transformed paths and set them to the composite shapes - */ - wrapper.setPaths = function (attribs) { - - var paths = wrapper.renderer.arc3dPath(attribs), - zIndex = paths.zTop * 100; - - wrapper.attribs = attribs; - - wrapper.top.attr({ d: paths.top, zIndex: paths.zTop }); - wrapper.inn.attr({ d: paths.inn, zIndex: paths.zInn }); - wrapper.out.attr({ d: paths.out, zIndex: paths.zOut }); - wrapper.side1.attr({ d: paths.side1, zIndex: paths.zSide1 }); - wrapper.side2.attr({ d: paths.side2, zIndex: paths.zSide2 }); - - - // show all children - wrapper.zIndex = zIndex; - wrapper.attr({ zIndex: zIndex }); - - // Set the radial gradient center the first time - if (attribs.center) { - wrapper.top.setRadialReference(attribs.center); - delete attribs.center; - } - }; - wrapper.setPaths(attribs); - - // Apply the fill to the top and a darker shade to the sides - wrapper.fillSetter = function (value) { - var darker = Highcharts.Color(value).brighten(-0.1).get(); - - this.fill = value; - - this.side1.attr({ fill: darker }); - this.side2.attr({ fill: darker }); - this.inn.attr({ fill: darker }); - this.out.attr({ fill: darker }); - this.top.attr({ fill: value }); - return this; - }; - - // Apply the same value to all. These properties cascade down to the children - // when set to the composite arc3d. - each(['opacity', 'translateX', 'translateY', 'visibility'], function (setter) { - wrapper[setter + 'Setter'] = function (value, key) { - wrapper[key] = value; - each(['out', 'inn', 'side1', 'side2', 'top'], function (el) { - wrapper[el].attr(key, value); - }); - }; - }); - - /** - * Override attr to remove shape attributes and use those to set child paths - */ - wrap(wrapper, 'attr', function (proceed, params, val) { - var ca; - if (typeof params === 'object') { - ca = suckOutCustom(params); - if (ca) { - extend(wrapper.attribs, ca); - wrapper.setPaths(wrapper.attribs); - } - } - return proceed.call(this, params, val); - }); - - /** - * Override the animate function by sucking out custom parameters related to the shapes directly, - * and update the shapes from the animation step. - */ - wrap(wrapper, 'animate', function (proceed, params, animation, complete) { - var ca, - from = this.attribs, - to; - - // Attribute-line properties connected to 3D. These shouldn't have been in the - // attribs collection in the first place. - delete params.center; - delete params.z; - delete params.depth; - delete params.alpha; - delete params.beta; - - animation = animObject(pick(animation, this.renderer.globalAnimation)); - - if (animation.duration) { - params = merge(params); // Don't mutate the original object - ca = suckOutCustom(params); - - if (ca) { - to = ca; - animation.step = function (a, fx) { - function interpolate(key) { - return from[key] + (pick(to[key], from[key]) - from[key]) * fx.pos; - } - fx.elem.setPaths(merge(from, { - x: interpolate('x'), - y: interpolate('y'), - r: interpolate('r'), - innerR: interpolate('innerR'), - start: interpolate('start'), - end: interpolate('end') - })); - }; - } - } - return proceed.call(this, params, animation, complete); - }); - - // destroy all children - wrapper.destroy = function () { - this.top.destroy(); - this.out.destroy(); - this.inn.destroy(); - this.side1.destroy(); - this.side2.destroy(); - - Highcharts.SVGElement.prototype.destroy.call(this); - }; - // hide all children - wrapper.hide = function () { - this.top.hide(); - this.out.hide(); - this.inn.hide(); - this.side1.hide(); - this.side2.hide(); - }; - wrapper.show = function () { - this.top.show(); - this.out.show(); - this.inn.show(); - this.side1.show(); - this.side2.show(); - }; - return wrapper; - }; - - /** - * Generate the paths required to draw a 3D arc - */ - Highcharts.SVGRenderer.prototype.arc3dPath = function (shapeArgs) { - var cx = shapeArgs.x, // x coordinate of the center - cy = shapeArgs.y, // y coordinate of the center - start = shapeArgs.start, // start angle - end = shapeArgs.end - 0.00001, // end angle - r = shapeArgs.r, // radius - ir = shapeArgs.innerR, // inner radius - d = shapeArgs.depth, // depth - alpha = shapeArgs.alpha, // alpha rotation of the chart - beta = shapeArgs.beta; // beta rotation of the chart - - // Derived Variables - var cs = cos(start), // cosinus of the start angle - ss = sin(start), // sinus of the start angle - ce = cos(end), // cosinus of the end angle - se = sin(end), // sinus of the end angle - rx = r * cos(beta), // x-radius - ry = r * cos(alpha), // y-radius - irx = ir * cos(beta), // x-radius (inner) - iry = ir * cos(alpha), // y-radius (inner) - dx = d * sin(beta), // distance between top and bottom in x - dy = d * sin(alpha); // distance between top and bottom in y - - // TOP - var top = ['M', cx + (rx * cs), cy + (ry * ss)]; - top = top.concat(curveTo(cx, cy, rx, ry, start, end, 0, 0)); - top = top.concat([ - 'L', cx + (irx * ce), cy + (iry * se) - ]); - top = top.concat(curveTo(cx, cy, irx, iry, end, start, 0, 0)); - top = top.concat(['Z']); - // OUTSIDE - var b = (beta > 0 ? PI / 2 : 0), - a = (alpha > 0 ? 0 : PI / 2); - - var start2 = start > -b ? start : (end > -b ? -b : start), - end2 = end < PI - a ? end : (start < PI - a ? PI - a : end), - midEnd = 2 * PI - a; - - // When slice goes over bottom middle, need to add both, left and right outer side. - // Additionally, when we cross right hand edge, create sharp edge. Outer shape/wall: - // - // ------- - // / ^ \ - // 4) / / \ \ 1) - // / / \ \ - // / / \ \ - // (c)=> ==== ==== <=(d) - // \ \ / / - // \ \<=(a)/ / - // \ \ / / <=(b) - // 3) \ v / 2) - // ------- - // - // (a) - inner side - // (b) - outer side - // (c) - left edge (sharp) - // (d) - right edge (sharp) - // 1..n - rendering order for startAngle = 0, when set to e.g 90, order changes clockwise (1->2, 2->3, n->1) and counterclockwise for negative startAngle - - var out = ['M', cx + (rx * cos(start2)), cy + (ry * sin(start2))]; - out = out.concat(curveTo(cx, cy, rx, ry, start2, end2, 0, 0)); - - if (end > midEnd && start < midEnd) { // When shape is wide, it can cross both, (c) and (d) edges, when using startAngle - // Go to outer side - out = out.concat([ - 'L', cx + (rx * cos(end2)) + dx, cy + (ry * sin(end2)) + dy - ]); - // Curve to the right edge of the slice (d) - out = out.concat(curveTo(cx, cy, rx, ry, end2, midEnd, dx, dy)); - // Go to the inner side - out = out.concat([ - 'L', cx + (rx * cos(midEnd)), cy + (ry * sin(midEnd)) - ]); - // Curve to the true end of the slice - out = out.concat(curveTo(cx, cy, rx, ry, midEnd, end, 0, 0)); - // Go to the outer side - out = out.concat([ - 'L', cx + (rx * cos(end)) + dx, cy + (ry * sin(end)) + dy - ]); - // Go back to middle (d) - out = out.concat(curveTo(cx, cy, rx, ry, end, midEnd, dx, dy)); - out = out.concat([ - 'L', cx + (rx * cos(midEnd)), cy + (ry * sin(midEnd)) - ]); - // Go back to the left edge - out = out.concat(curveTo(cx, cy, rx, ry, midEnd, end2, 0, 0)); - } else if (end > PI - a && start < PI - a) { // But shape can cross also only (c) edge: - // Go to outer side - out = out.concat([ - 'L', cx + (rx * cos(end2)) + dx, cy + (ry * sin(end2)) + dy - ]); - // Curve to the true end of the slice - out = out.concat(curveTo(cx, cy, rx, ry, end2, end, dx, dy)); - // Go to the inner side - out = out.concat([ - 'L', cx + (rx * cos(end)), cy + (ry * sin(end)) - ]); - // Go back to the artifical end2 - out = out.concat(curveTo(cx, cy, rx, ry, end, end2, 0, 0)); - } - - out = out.concat([ - 'L', cx + (rx * cos(end2)) + dx, cy + (ry * sin(end2)) + dy - ]); - out = out.concat(curveTo(cx, cy, rx, ry, end2, start2, dx, dy)); - out = out.concat(['Z']); - - // INSIDE - var inn = ['M', cx + (irx * cs), cy + (iry * ss)]; - inn = inn.concat(curveTo(cx, cy, irx, iry, start, end, 0, 0)); - inn = inn.concat([ - 'L', cx + (irx * cos(end)) + dx, cy + (iry * sin(end)) + dy - ]); - inn = inn.concat(curveTo(cx, cy, irx, iry, end, start, dx, dy)); - inn = inn.concat(['Z']); - - // SIDES - var side1 = [ - 'M', cx + (rx * cs), cy + (ry * ss), - 'L', cx + (rx * cs) + dx, cy + (ry * ss) + dy, - 'L', cx + (irx * cs) + dx, cy + (iry * ss) + dy, - 'L', cx + (irx * cs), cy + (iry * ss), - 'Z' - ]; - var side2 = [ - 'M', cx + (rx * ce), cy + (ry * se), - 'L', cx + (rx * ce) + dx, cy + (ry * se) + dy, - 'L', cx + (irx * ce) + dx, cy + (iry * se) + dy, - 'L', cx + (irx * ce), cy + (iry * se), - 'Z' - ]; - - // correction for changed position of vanishing point caused by alpha and beta rotations - var angleCorr = Math.atan2(dy, -dx), - angleEnd = Math.abs(end + angleCorr), - angleStart = Math.abs(start + angleCorr), - angleMid = Math.abs((start + end) / 2 + angleCorr); - - // set to 0-PI range - function toZeroPIRange(angle) { - angle = angle % (2 * PI); - if (angle > PI) { - angle = 2 * PI - angle; - } - return angle; - } - angleEnd = toZeroPIRange(angleEnd); - angleStart = toZeroPIRange(angleStart); - angleMid = toZeroPIRange(angleMid); - - // *1e5 is to compensate pInt in zIndexSetter - var incPrecision = 1e5, - a1 = angleMid * incPrecision, - a2 = angleStart * incPrecision, - a3 = angleEnd * incPrecision; - - return { - top: top, - zTop: PI * incPrecision + 1, // max angle is PI, so this is allways higher - out: out, - zOut: Math.max(a1, a2, a3), - inn: inn, - zInn: Math.max(a1, a2, a3), - side1: side1, - zSide1: a3 * 0.99, // to keep below zOut and zInn in case of same values - side2: side2, - zSide2: a2 * 0.99 - }; - }; - /*** - EXTENSION FOR 3D CHARTS - ***/ - // Shorthand to check the is3d flag - Highcharts.Chart.prototype.is3d = function () { - return this.options.chart.options3d && this.options.chart.options3d.enabled; // #4280 - }; - - /** - * Extend the getMargins method to calculate scale of the 3D view. That is required to - * fit chart's 3D projection into the actual plotting area. Reported as #4933. - */ - Highcharts.wrap(Highcharts.Chart.prototype, 'getMargins', function (proceed) { - var chart = this, - options3d = chart.options.chart.options3d, - bbox3d = { - minX: Number.MAX_VALUE, - maxX: -Number.MAX_VALUE, - minY: Number.MAX_VALUE, - maxY: -Number.MAX_VALUE - }, - plotLeft = chart.plotLeft, - plotRight = chart.plotWidth + plotLeft, - plotTop = chart.plotTop, - plotBottom = chart.plotHeight + plotTop, - originX = plotLeft + chart.plotWidth / 2, - originY = plotTop + chart.plotHeight / 2, - scale = 1, - corners = [], - i; - - proceed.apply(this, [].slice.call(arguments, 1)); - - if (this.is3d()) { - if (options3d.fitToPlot === true) { - // Clear previous scale in case of updates: - chart.scale3d = 1; - - // Top left corners: - corners = [{ - x: plotLeft, - y: plotTop, - z: 0 - }, { - x: plotLeft, - y: plotTop, - z: options3d.depth - }]; - - // Top right corners: - for (i = 0; i < 2; i++) { - corners.push({ - x: plotRight, - y: corners[i].y, - z: corners[i].z - }); - } - - // All bottom corners: - for (i = 0; i < 4; i++) { - corners.push({ - x: corners[i].x, - y: plotBottom, - z: corners[i].z - }); - } - - // Calculate 3D corners: - corners = perspective(corners, chart, false); - - // Get bounding box of 3D element: - each(corners, function (corner) { - bbox3d.minX = Math.min(bbox3d.minX, corner.x); - bbox3d.maxX = Math.max(bbox3d.maxX, corner.x); - bbox3d.minY = Math.min(bbox3d.minY, corner.y); - bbox3d.maxY = Math.max(bbox3d.maxY, corner.y); - }); - - // Left edge: - if (plotLeft > bbox3d.minX) { - scale = Math.min(scale, 1 - Math.abs((plotLeft + originX) / (bbox3d.minX + originX)) % 1); - } - - // Right edge: - if (plotRight < bbox3d.maxX) { - scale = Math.min(scale, (plotRight - originX) / (bbox3d.maxX - originX)); - } - - // Top edge: - if (plotTop > bbox3d.minY) { - if (bbox3d.minY < 0) { - scale = Math.min(scale, (plotTop + originY) / (-bbox3d.minY + plotTop + originY)); - } else { - scale = Math.min(scale, 1 - (plotTop + originY) / (bbox3d.minY + originY) % 1); - } - } - - // Bottom edge: - if (plotBottom < bbox3d.maxY) { - scale = Math.min(scale, Math.abs((plotBottom - originY) / (bbox3d.maxY - originY))); - } - - // Set scale, used later in perspective method(): - chart.scale3d = scale; - } - } - }); - - Highcharts.wrap(Highcharts.Chart.prototype, 'isInsidePlot', function (proceed) { - return this.is3d() || proceed.apply(this, [].slice.call(arguments, 1)); - }); - - var defaultChartOptions = Highcharts.getOptions(); - defaultChartOptions.chart.options3d = { - enabled: false, - alpha: 0, - beta: 0, - depth: 100, - fitToPlot: true, - viewDistance: 25, - frame: { - bottom: { size: 1, color: 'rgba(255,255,255,0)' }, - side: { size: 1, color: 'rgba(255,255,255,0)' }, - back: { size: 1, color: 'rgba(255,255,255,0)' } - } - }; - - Highcharts.wrap(Highcharts.Chart.prototype, 'init', function (proceed) { - var args = [].slice.call(arguments, 1), - plotOptions, - pieOptions; - - if (args[0].chart && args[0].chart.options3d && args[0].chart.options3d.enabled) { - // Normalize alpha and beta to (-360, 360) range - args[0].chart.options3d.alpha = (args[0].chart.options3d.alpha || 0) % 360; - args[0].chart.options3d.beta = (args[0].chart.options3d.beta || 0) % 360; - - plotOptions = args[0].plotOptions || {}; - pieOptions = plotOptions.pie || {}; - - pieOptions.borderColor = Highcharts.pick(pieOptions.borderColor, undefined); - } - proceed.apply(this, args); - }); - - Highcharts.wrap(Highcharts.Chart.prototype, 'setChartSize', function (proceed) { - proceed.apply(this, [].slice.call(arguments, 1)); - - if (this.is3d()) { - var inverted = this.inverted, - clipBox = this.clipBox, - margin = this.margin, - x = inverted ? 'y' : 'x', - y = inverted ? 'x' : 'y', - w = inverted ? 'height' : 'width', - h = inverted ? 'width' : 'height'; - - clipBox[x] = -(margin[3] || 0); - clipBox[y] = -(margin[0] || 0); - clipBox[w] = this.chartWidth + (margin[3] || 0) + (margin[1] || 0); - clipBox[h] = this.chartHeight + (margin[0] || 0) + (margin[2] || 0); - } - }); - - Highcharts.wrap(Highcharts.Chart.prototype, 'redraw', function (proceed) { - if (this.is3d()) { - // Set to force a redraw of all elements - this.isDirtyBox = true; - } - proceed.apply(this, [].slice.call(arguments, 1)); - }); - - // Draw the series in the reverse order (#3803, #3917) - Highcharts.wrap(Highcharts.Chart.prototype, 'renderSeries', function (proceed) { - var series, - i = this.series.length; - - if (this.is3d()) { - while (i--) { - series = this.series[i]; - series.translate(); - series.render(); - } - } else { - proceed.call(this); - } - }); - - Highcharts.Chart.prototype.retrieveStacks = function (stacking) { - var series = this.series, - stacks = {}, - stackNumber, - i = 1; - - Highcharts.each(this.series, function (s) { - stackNumber = pick(s.options.stack, (stacking ? 0 : series.length - 1 - s.index)); // #3841, #4532 - if (!stacks[stackNumber]) { - stacks[stackNumber] = { series: [s], position: i }; - i++; - } else { - stacks[stackNumber].series.push(s); - } - }); - - stacks.totalStacks = i + 1; - return stacks; - }; - - /*** - EXTENSION TO THE AXIS - ***/ - Highcharts.wrap(Highcharts.Axis.prototype, 'setOptions', function (proceed, userOptions) { - var options; - proceed.call(this, userOptions); - if (this.chart.is3d()) { - options = this.options; - options.tickWidth = Highcharts.pick(options.tickWidth, 0); - options.gridLineWidth = Highcharts.pick(options.gridLineWidth, 1); - } - }); - - Highcharts.wrap(Highcharts.Axis.prototype, 'render', function (proceed) { - proceed.apply(this, [].slice.call(arguments, 1)); - - // Do not do this if the chart is not 3D - if (!this.chart.is3d()) { - return; - } - - var chart = this.chart, - renderer = chart.renderer, - options3d = chart.options.chart.options3d, - frame = options3d.frame, - fbottom = frame.bottom, - fback = frame.back, - fside = frame.side, - depth = options3d.depth, - height = this.height, - width = this.width, - left = this.left, - top = this.top; - - if (this.isZAxis) { - return; - } - if (this.horiz) { - var bottomShape = { - x: left, - y: top + (chart.xAxis[0].opposite ? -fbottom.size : height), - z: 0, - width: width, - height: fbottom.size, - depth: depth, - insidePlotArea: false - }; - if (!this.bottomFrame) { - this.bottomFrame = renderer.cuboid(bottomShape).attr({ - fill: fbottom.color, - zIndex: (chart.yAxis[0].reversed && options3d.alpha > 0 ? 4 : -1) - }) - .css({ - stroke: fbottom.color - }).add(); - } else { - this.bottomFrame.animate(bottomShape); - } - } else { - // BACK - var backShape = { - x: left + (chart.yAxis[0].opposite ? 0 : -fside.size), - y: top + (chart.xAxis[0].opposite ? -fbottom.size : 0), - z: depth, - width: width + fside.size, - height: height + fbottom.size, - depth: fback.size, - insidePlotArea: false - }; - if (!this.backFrame) { - this.backFrame = renderer.cuboid(backShape).attr({ - fill: fback.color, - zIndex: -3 - }).css({ - stroke: fback.color - }).add(); - } else { - this.backFrame.animate(backShape); - } - var sideShape = { - x: left + (chart.yAxis[0].opposite ? width : -fside.size), - y: top + (chart.xAxis[0].opposite ? -fbottom.size : 0), - z: 0, - width: fside.size, - height: height + fbottom.size, - depth: depth, - insidePlotArea: false - }; - if (!this.sideFrame) { - this.sideFrame = renderer.cuboid(sideShape).attr({ - fill: fside.color, - zIndex: -2 - }).css({ - stroke: fside.color - }).add(); - } else { - this.sideFrame.animate(sideShape); - } - } - }); - - Highcharts.wrap(Highcharts.Axis.prototype, 'getPlotLinePath', function (proceed) { - var path = proceed.apply(this, [].slice.call(arguments, 1)); - - // Do not do this if the chart is not 3D - if (!this.chart.is3d()) { - return path; - } - - if (path === null) { - return path; - } - - var chart = this.chart, - options3d = chart.options.chart.options3d, - d = this.isZAxis ? chart.plotWidth : options3d.depth, - opposite = this.opposite; - if (this.horiz) { - opposite = !opposite; - } - var pArr = [ - this.swapZ({ x: path[1], y: path[2], z: (opposite ? d : 0) }), - this.swapZ({ x: path[1], y: path[2], z: d }), - this.swapZ({ x: path[4], y: path[5], z: d }), - this.swapZ({ x: path[4], y: path[5], z: (opposite ? 0 : d) }) - ]; - - pArr = perspective(pArr, this.chart, false); - path = this.chart.renderer.toLinePath(pArr, false); - - return path; - }); - - // Do not draw axislines in 3D - Highcharts.wrap(Highcharts.Axis.prototype, 'getLinePath', function (proceed) { - return this.chart.is3d() ? [] : proceed.apply(this, [].slice.call(arguments, 1)); - }); - - Highcharts.wrap(Highcharts.Axis.prototype, 'getPlotBandPath', function (proceed) { - // Do not do this if the chart is not 3D - if (!this.chart.is3d()) { - return proceed.apply(this, [].slice.call(arguments, 1)); - } - - var args = arguments, - from = args[1], - to = args[2], - toPath = this.getPlotLinePath(to), - path = this.getPlotLinePath(from); - - if (path && toPath) { - path.push( - 'L', - toPath[10], // These two do not exist in the regular getPlotLine - toPath[11], // ---- # 3005 - 'L', - toPath[7], - toPath[8], - 'L', - toPath[4], - toPath[5], - 'L', - toPath[1], - toPath[2] - ); - } else { // outside the axis area - path = null; - } - - return path; - }); - - /*** - EXTENSION TO THE TICKS - ***/ - - Highcharts.wrap(Highcharts.Tick.prototype, 'getMarkPath', function (proceed) { - var path = proceed.apply(this, [].slice.call(arguments, 1)); - - // Do not do this if the chart is not 3D - if (!this.axis.chart.is3d()) { - return path; - } - - var pArr = [ - this.axis.swapZ({ x: path[1], y: path[2], z: 0 }), - this.axis.swapZ({ x: path[4], y: path[5], z: 0 }) - ]; - - pArr = perspective(pArr, this.axis.chart, false); - path = [ - 'M', pArr[0].x, pArr[0].y, - 'L', pArr[1].x, pArr[1].y - ]; - return path; - }); - - Highcharts.wrap(Highcharts.Tick.prototype, 'getLabelPosition', function (proceed) { - var pos = proceed.apply(this, [].slice.call(arguments, 1)); - - // Do not do this if the chart is not 3D - if (!this.axis.chart.is3d()) { - return pos; - } - - var newPos = perspective([this.axis.swapZ({ x: pos.x, y: pos.y, z: 0 })], this.axis.chart, false)[0]; - newPos.x = newPos.x - (!this.axis.horiz && this.axis.opposite ? this.axis.transA : 0); //#3788 - newPos.old = pos; - return newPos; - }); - - Highcharts.wrap(Highcharts.Tick.prototype, 'handleOverflow', function (proceed, xy) { - if (this.axis.chart.is3d()) { - xy = xy.old; - } - return proceed.call(this, xy); - }); - - Highcharts.wrap(Highcharts.Axis.prototype, 'getTitlePosition', function (proceed) { - var is3d = this.chart.is3d(), - pos, - axisTitleMargin; - - // Pull out the axis title margin, that is not subject to the perspective - if (is3d) { - axisTitleMargin = this.axisTitleMargin; - this.axisTitleMargin = 0; - } - - pos = proceed.apply(this, [].slice.call(arguments, 1)); - - if (is3d) { - pos = perspective([this.swapZ({ x: pos.x, y: pos.y, z: 0 })], this.chart, false)[0]; - - // Re-apply the axis title margin outside the perspective - pos[this.horiz ? 'y' : 'x'] += (this.horiz ? 1 : -1) * // horizontal axis reverses the margin ... - (this.opposite ? -1 : 1) * // ... so does opposite axes - axisTitleMargin; - this.axisTitleMargin = axisTitleMargin; - } - return pos; - }); - - Highcharts.wrap(Highcharts.Axis.prototype, 'drawCrosshair', function (proceed) { - var args = arguments; - if (this.chart.is3d()) { - if (args[2]) { - args[2] = { - plotX: args[2].plotXold || args[2].plotX, - plotY: args[2].plotYold || args[2].plotY - }; - } - } - proceed.apply(this, [].slice.call(args, 1)); - }); - - /*** - Z-AXIS - ***/ - - Highcharts.Axis.prototype.swapZ = function (p, insidePlotArea) { - if (this.isZAxis) { - var plotLeft = insidePlotArea ? 0 : this.chart.plotLeft; - var chart = this.chart; - return { - x: plotLeft + (chart.yAxis[0].opposite ? p.z : chart.xAxis[0].width - p.z), - y: p.y, - z: p.x - plotLeft - }; - } - return p; - }; - - var ZAxis = Highcharts.ZAxis = function () { - this.isZAxis = true; - this.init.apply(this, arguments); - }; - Highcharts.extend(ZAxis.prototype, Highcharts.Axis.prototype); - Highcharts.extend(ZAxis.prototype, { - setOptions: function (userOptions) { - userOptions = Highcharts.merge({ - offset: 0, - lineWidth: 0 - }, userOptions); - Highcharts.Axis.prototype.setOptions.call(this, userOptions); - this.coll = 'zAxis'; - }, - setAxisSize: function () { - Highcharts.Axis.prototype.setAxisSize.call(this); - this.width = this.len = this.chart.options.chart.options3d.depth; - this.right = this.chart.chartWidth - this.width - this.left; - }, - getSeriesExtremes: function () { - var axis = this, - chart = axis.chart; - - axis.hasVisibleSeries = false; - - // Reset properties in case we're redrawing (#3353) - axis.dataMin = axis.dataMax = axis.ignoreMinPadding = axis.ignoreMaxPadding = null; - - if (axis.buildStacks) { - axis.buildStacks(); - } - - // loop through this axis' series - Highcharts.each(axis.series, function (series) { - - if (series.visible || !chart.options.chart.ignoreHiddenSeries) { - - var seriesOptions = series.options, - zData, - threshold = seriesOptions.threshold; - - axis.hasVisibleSeries = true; - - // Validate threshold in logarithmic axes - if (axis.isLog && threshold <= 0) { - threshold = null; - } - - zData = series.zData; - if (zData.length) { - axis.dataMin = Math.min(pick(axis.dataMin, zData[0]), Math.min.apply(null, zData)); - axis.dataMax = Math.max(pick(axis.dataMax, zData[0]), Math.max.apply(null, zData)); - } - } - }); - } - }); - - - /** - * Extend the chart getAxes method to also get the color axis - */ - Highcharts.wrap(Highcharts.Chart.prototype, 'getAxes', function (proceed) { - var chart = this, - options = this.options, - zAxisOptions = options.zAxis = Highcharts.splat(options.zAxis || {}); - - proceed.call(this); - - if (!chart.is3d()) { - return; - } - this.zAxis = []; - Highcharts.each(zAxisOptions, function (axisOptions, i) { - axisOptions.index = i; - axisOptions.isX = true; //Z-Axis is shown horizontally, so it's kind of a X-Axis - var zAxis = new ZAxis(chart, axisOptions); - zAxis.setScale(); - }); - }); - /*** - EXTENSION FOR 3D COLUMNS - ***/ - Highcharts.wrap(Highcharts.seriesTypes.column.prototype, 'translate', function (proceed) { - proceed.apply(this, [].slice.call(arguments, 1)); - - // Do not do this if the chart is not 3D - if (!this.chart.is3d()) { - return; - } - - var series = this, - chart = series.chart, - seriesOptions = series.options, - depth = seriesOptions.depth || 25; - - var stack = seriesOptions.stacking ? (seriesOptions.stack || 0) : series._i; - var z = stack * (depth + (seriesOptions.groupZPadding || 1)); - - if (seriesOptions.grouping !== false) { - z = 0; - } - - z += (seriesOptions.groupZPadding || 1); - - Highcharts.each(series.data, function (point) { - if (point.y !== null) { - var shapeArgs = point.shapeArgs, - tooltipPos = point.tooltipPos; - - point.shapeType = 'cuboid'; - shapeArgs.z = z; - shapeArgs.depth = depth; - shapeArgs.insidePlotArea = true; - - // Translate the tooltip position in 3d space - tooltipPos = perspective([{ x: tooltipPos[0], y: tooltipPos[1], z: z }], chart, true)[0]; - point.tooltipPos = [tooltipPos.x, tooltipPos.y]; - } - }); - // store for later use #4067 - series.z = z; - }); - - Highcharts.wrap(Highcharts.seriesTypes.column.prototype, 'animate', function (proceed) { - if (!this.chart.is3d()) { - proceed.apply(this, [].slice.call(arguments, 1)); - } else { - var args = arguments, - init = args[1], - yAxis = this.yAxis, - series = this, - reversed = this.yAxis.reversed; - - if (Highcharts.svg) { // VML is too slow anyway - if (init) { - Highcharts.each(series.data, function (point) { - if (point.y !== null) { - point.height = point.shapeArgs.height; - point.shapey = point.shapeArgs.y; //#2968 - point.shapeArgs.height = 1; - if (!reversed) { - if (point.stackY) { - point.shapeArgs.y = point.plotY + yAxis.translate(point.stackY); - } else { - point.shapeArgs.y = point.plotY + (point.negative ? -point.height : point.height); - } - } - } - }); - - } else { // run the animation - Highcharts.each(series.data, function (point) { - if (point.y !== null) { - point.shapeArgs.height = point.height; - point.shapeArgs.y = point.shapey; //#2968 - // null value do not have a graphic - if (point.graphic) { - point.graphic.animate(point.shapeArgs, series.options.animation); - } - } - }); - - // redraw datalabels to the correct position - this.drawDataLabels(); - - // delete this function to allow it only once - series.animate = null; - } - } - } - }); - - Highcharts.wrap(Highcharts.seriesTypes.column.prototype, 'init', function (proceed) { - proceed.apply(this, [].slice.call(arguments, 1)); - - if (this.chart.is3d()) { - var seriesOptions = this.options, - grouping = seriesOptions.grouping, - stacking = seriesOptions.stacking, - reversedStacks = pick(this.yAxis.options.reversedStacks, true), - z = 0; - - if (!(grouping !== undefined && !grouping)) { - var stacks = this.chart.retrieveStacks(stacking), - stack = seriesOptions.stack || 0, - i; // position within the stack - for (i = 0; i < stacks[stack].series.length; i++) { - if (stacks[stack].series[i] === this) { - break; - } - } - z = (10 * (stacks.totalStacks - stacks[stack].position)) + (reversedStacks ? i : -i); // #4369 - - // In case when axis is reversed, columns are also reversed inside the group (#3737) - if (!this.xAxis.reversed) { - z = (stacks.totalStacks * 10) - z; - } - } - - seriesOptions.zIndex = z; - } - }); - function draw3DPoints(proceed) { - // Do not do this if the chart is not 3D - if (this.chart.is3d()) { - var grouping = this.chart.options.plotOptions.column.grouping; - if (grouping !== undefined && !grouping && this.group.zIndex !== undefined && !this.zIndexSet) { - this.group.attr({ zIndex: this.group.zIndex * 10 }); - this.zIndexSet = true; // #4062 set zindex only once - } - - var options = this.options, - states = this.options.states; - - this.borderWidth = options.borderWidth = defined(options.edgeWidth) ? options.edgeWidth : 1; //#4055 - - Highcharts.each(this.data, function (point) { - if (point.y !== null) { - var pointAttr = point.pointAttr; - - // Set the border color to the fill color to provide a smooth edge - this.borderColor = Highcharts.pick(options.edgeColor, pointAttr[''].fill); - - pointAttr[''].stroke = this.borderColor; - pointAttr.hover.stroke = Highcharts.pick(states.hover.edgeColor, this.borderColor); - pointAttr.select.stroke = Highcharts.pick(states.select.edgeColor, this.borderColor); - } - }); - } - - proceed.apply(this, [].slice.call(arguments, 1)); - } - - Highcharts.wrap(Highcharts.Series.prototype, 'alignDataLabel', function (proceed) { - - // Only do this for 3D columns and columnranges - if (this.chart.is3d() && (this.type === 'column' || this.type === 'columnrange')) { - var series = this, - chart = series.chart; - - var args = arguments, - alignTo = args[4]; - - var pos = ({ x: alignTo.x, y: alignTo.y, z: series.z }); - pos = perspective([pos], chart, true)[0]; - alignTo.x = pos.x; - alignTo.y = pos.y; - } - - proceed.apply(this, [].slice.call(arguments, 1)); - }); - - if (Highcharts.seriesTypes.columnrange) { - Highcharts.wrap(Highcharts.seriesTypes.columnrange.prototype, 'drawPoints', draw3DPoints); - } - - Highcharts.wrap(Highcharts.seriesTypes.column.prototype, 'drawPoints', draw3DPoints); - - /*** - EXTENSION FOR 3D CYLINDRICAL COLUMNS - Not supported - ***/ - /* - var defaultOptions = Highcharts.getOptions(); - defaultOptions.plotOptions.cylinder = Highcharts.merge(defaultOptions.plotOptions.column); - var CylinderSeries = Highcharts.extendClass(Highcharts.seriesTypes.column, { - type: 'cylinder' - }); - Highcharts.seriesTypes.cylinder = CylinderSeries; - - Highcharts.wrap(Highcharts.seriesTypes.cylinder.prototype, 'translate', function (proceed) { - proceed.apply(this, [].slice.call(arguments, 1)); - - // Do not do this if the chart is not 3D - if (!this.chart.is3d()) { - return; - } - - var series = this, - chart = series.chart, - options = chart.options, - cylOptions = options.plotOptions.cylinder, - options3d = options.chart.options3d, - depth = cylOptions.depth || 0, - alpha = options3d.alpha; - - var z = cylOptions.stacking ? (this.options.stack || 0) * depth : series._i * depth; - z += depth / 2; - - if (cylOptions.grouping !== false) { z = 0; } - - Highcharts.each(series.data, function (point) { - var shapeArgs = point.shapeArgs; - point.shapeType = 'arc3d'; - shapeArgs.x += depth / 2; - shapeArgs.z = z; - shapeArgs.start = 0; - shapeArgs.end = 2 * PI; - shapeArgs.r = depth * 0.95; - shapeArgs.innerR = 0; - shapeArgs.depth = shapeArgs.height * (1 / sin((90 - alpha) * deg2rad)) - z; - shapeArgs.alpha = 90 - alpha; - shapeArgs.beta = 0; - }); - }); - */ - /*** - EXTENSION FOR 3D PIES - ***/ - - Highcharts.wrap(Highcharts.seriesTypes.pie.prototype, 'translate', function (proceed) { - proceed.apply(this, [].slice.call(arguments, 1)); - - // Do not do this if the chart is not 3D - if (!this.chart.is3d()) { - return; - } - - var series = this, - chart = series.chart, - options = chart.options, - seriesOptions = series.options, - depth = seriesOptions.depth || 0, - options3d = options.chart.options3d, - alpha = options3d.alpha, - beta = options3d.beta, - z = seriesOptions.stacking ? (seriesOptions.stack || 0) * depth : series._i * depth; - - z += depth / 2; - - if (seriesOptions.grouping !== false) { - z = 0; - } - - each(series.data, function (point) { - - var shapeArgs = point.shapeArgs, - angle; - - point.shapeType = 'arc3d'; - - shapeArgs.z = z; - shapeArgs.depth = depth * 0.75; - shapeArgs.alpha = alpha; - shapeArgs.beta = beta; - shapeArgs.center = series.center; - - angle = (shapeArgs.end + shapeArgs.start) / 2; - - point.slicedTranslation = { - translateX: round(cos(angle) * seriesOptions.slicedOffset * cos(alpha * deg2rad)), - translateY: round(sin(angle) * seriesOptions.slicedOffset * cos(alpha * deg2rad)) - }; - }); - }); - - Highcharts.wrap(Highcharts.seriesTypes.pie.prototype.pointClass.prototype, 'haloPath', function (proceed) { - var args = arguments; - return this.series.chart.is3d() ? [] : proceed.call(this, args[1]); - }); - - Highcharts.wrap(Highcharts.seriesTypes.pie.prototype, 'drawPoints', function (proceed) { - - var options = this.options, - states = options.states; - - // Do not do this if the chart is not 3D - if (this.chart.is3d()) { - // Set the border color to the fill color to provide a smooth edge - this.borderWidth = options.borderWidth = options.edgeWidth || 1; - this.borderColor = options.edgeColor = Highcharts.pick(options.edgeColor, options.borderColor, undefined); - - states.hover.borderColor = Highcharts.pick(states.hover.edgeColor, this.borderColor); - states.hover.borderWidth = Highcharts.pick(states.hover.edgeWidth, this.borderWidth); - states.select.borderColor = Highcharts.pick(states.select.edgeColor, this.borderColor); - states.select.borderWidth = Highcharts.pick(states.select.edgeWidth, this.borderWidth); - - each(this.data, function (point) { - var pointAttr = point.pointAttr; - pointAttr[''].stroke = point.series.borderColor || point.color; - pointAttr['']['stroke-width'] = point.series.borderWidth; - pointAttr.hover.stroke = states.hover.borderColor; - pointAttr.hover['stroke-width'] = states.hover.borderWidth; - pointAttr.select.stroke = states.select.borderColor; - pointAttr.select['stroke-width'] = states.select.borderWidth; - }); - } - - proceed.apply(this, [].slice.call(arguments, 1)); - - if (this.chart.is3d()) { - each(this.points, function (point) { - var graphic = point.graphic; - - // #4584 Check if has graphic - null points don't have it - if (graphic) { - // Hide null or 0 points (#3006, 3650) - graphic[point.y && point.visible ? 'show' : 'hide'](); - } - }); - } - }); - - Highcharts.wrap(Highcharts.seriesTypes.pie.prototype, 'drawDataLabels', function (proceed) { - if (this.chart.is3d()) { - var series = this, - chart = series.chart, - options3d = chart.options.chart.options3d; - each(series.data, function (point) { - var shapeArgs = point.shapeArgs, - r = shapeArgs.r, - a1 = (shapeArgs.alpha || options3d.alpha) * deg2rad, //#3240 issue with datalabels for 0 and null values - b1 = (shapeArgs.beta || options3d.beta) * deg2rad, - a2 = (shapeArgs.start + shapeArgs.end) / 2, - labelPos = point.labelPos, - labelIndexes = [0, 2, 4], // [x1, y1, x2, y2, x3, y3] - yOffset = (-r * (1 - cos(a1)) * sin(a2)), // + (sin(a2) > 0 ? sin(a1) * d : 0) - xOffset = r * (cos(b1) - 1) * cos(a2); - - // Apply perspective on label positions - each(labelIndexes, function (index) { - labelPos[index] += xOffset; - labelPos[index + 1] += yOffset; - }); - }); - } - - proceed.apply(this, [].slice.call(arguments, 1)); - }); - - Highcharts.wrap(Highcharts.seriesTypes.pie.prototype, 'addPoint', function (proceed) { - proceed.apply(this, [].slice.call(arguments, 1)); - if (this.chart.is3d()) { - // destroy (and rebuild) everything!!! - this.update(this.userOptions, true); // #3845 pass the old options - } - }); - - Highcharts.wrap(Highcharts.seriesTypes.pie.prototype, 'animate', function (proceed) { - if (!this.chart.is3d()) { - proceed.apply(this, [].slice.call(arguments, 1)); - } else { - var args = arguments, - init = args[1], - animation = this.options.animation, - attribs, - center = this.center, - group = this.group, - markerGroup = this.markerGroup; - - if (Highcharts.svg) { // VML is too slow anyway - - if (animation === true) { - animation = {}; - } - // Initialize the animation - if (init) { - - // Scale down the group and place it in the center - group.oldtranslateX = group.translateX; - group.oldtranslateY = group.translateY; - attribs = { - translateX: center[0], - translateY: center[1], - scaleX: 0.001, // #1499 - scaleY: 0.001 - }; - - group.attr(attribs); - if (markerGroup) { - markerGroup.attrSetters = group.attrSetters; - markerGroup.attr(attribs); - } - - // Run the animation - } else { - attribs = { - translateX: group.oldtranslateX, - translateY: group.oldtranslateY, - scaleX: 1, - scaleY: 1 - }; - group.animate(attribs, animation); - - if (markerGroup) { - markerGroup.animate(attribs, animation); - } - - // Delete this function to allow it only once - this.animate = null; - } - - } - } - }); - /*** - EXTENSION FOR 3D SCATTER CHART - ***/ - - Highcharts.wrap(Highcharts.seriesTypes.scatter.prototype, 'translate', function (proceed) { - //function translate3d(proceed) { - proceed.apply(this, [].slice.call(arguments, 1)); - - if (!this.chart.is3d()) { - return; - } - - var series = this, - chart = series.chart, - zAxis = Highcharts.pick(series.zAxis, chart.options.zAxis[0]), - rawPoints = [], - rawPoint, - projectedPoints, - projectedPoint, - zValue, - i; - - for (i = 0; i < series.data.length; i++) { - rawPoint = series.data[i]; - zValue = zAxis.isLog && zAxis.val2lin ? zAxis.val2lin(rawPoint.z) : rawPoint.z; // #4562 - rawPoint.plotZ = zAxis.translate(zValue); - - rawPoint.isInside = rawPoint.isInside ? (zValue >= zAxis.min && zValue <= zAxis.max) : false; - - rawPoints.push({ - x: rawPoint.plotX, - y: rawPoint.plotY, - z: rawPoint.plotZ - }); - } - - projectedPoints = perspective(rawPoints, chart, true); - - for (i = 0; i < series.data.length; i++) { - rawPoint = series.data[i]; - projectedPoint = projectedPoints[i]; - - rawPoint.plotXold = rawPoint.plotX; - rawPoint.plotYold = rawPoint.plotY; - - rawPoint.plotX = projectedPoint.x; - rawPoint.plotY = projectedPoint.y; - rawPoint.plotZ = projectedPoint.z; - - - } - - }); - - Highcharts.wrap(Highcharts.seriesTypes.scatter.prototype, 'init', function (proceed, chart, options) { - if (chart.is3d()) { - // add a third coordinate - this.axisTypes = ['xAxis', 'yAxis', 'zAxis']; - this.pointArrayMap = ['x', 'y', 'z']; - this.parallelArrays = ['x', 'y', 'z']; - - // Require direct touch rather than using the k-d-tree, because the k-d-tree currently doesn't - // take the xyz coordinate system into account (#4552) - this.directTouch = true; - } - - var result = proceed.apply(this, [chart, options]); - - if (this.chart.is3d()) { - // Set a new default tooltip formatter - var default3dScatterTooltip = 'x: {point.x}
y: {point.y}
z: {point.z}
'; - if (this.userOptions.tooltip) { - this.tooltipOptions.pointFormat = this.userOptions.tooltip.pointFormat || default3dScatterTooltip; - } else { - this.tooltipOptions.pointFormat = default3dScatterTooltip; - } - } - return result; - }); - /** - * Extension to the VML Renderer - */ - if (Highcharts.VMLRenderer) { - - Highcharts.setOptions({ animate: false }); - - Highcharts.VMLRenderer.prototype.cuboid = Highcharts.SVGRenderer.prototype.cuboid; - Highcharts.VMLRenderer.prototype.cuboidPath = Highcharts.SVGRenderer.prototype.cuboidPath; - - Highcharts.VMLRenderer.prototype.toLinePath = Highcharts.SVGRenderer.prototype.toLinePath; - - Highcharts.VMLRenderer.prototype.createElement3D = Highcharts.SVGRenderer.prototype.createElement3D; - - Highcharts.VMLRenderer.prototype.arc3d = function (shapeArgs) { - var result = Highcharts.SVGRenderer.prototype.arc3d.call(this, shapeArgs); - result.css({ zIndex: result.zIndex }); - return result; - }; - - Highcharts.VMLRenderer.prototype.arc3dPath = Highcharts.SVGRenderer.prototype.arc3dPath; - - Highcharts.wrap(Highcharts.Axis.prototype, 'render', function (proceed) { - proceed.apply(this, [].slice.call(arguments, 1)); - // VML doesn't support a negative z-index - if (this.sideFrame) { - this.sideFrame.css({ zIndex: 0 }); - this.sideFrame.front.attr({ fill: this.sideFrame.color }); - } - if (this.bottomFrame) { - this.bottomFrame.css({ zIndex: 1 }); - this.bottomFrame.front.attr({ fill: this.bottomFrame.color }); - } - if (this.backFrame) { - this.backFrame.css({ zIndex: 0 }); - this.backFrame.front.attr({ fill: this.backFrame.color }); - } - }); - - } - -})); diff --git a/public/vendor/highcharts-4.2.5/highcharts-more.js b/public/vendor/highcharts-4.2.5/highcharts-more.js deleted file mode 100644 index fcd9cc6b778e1..0000000000000 --- a/public/vendor/highcharts-4.2.5/highcharts-more.js +++ /dev/null @@ -1,56 +0,0 @@ -/* - Highcharts JS v4.2.5 (2016-05-06) - - (c) 2009-2016 Torstein Honsi - - License: www.highcharts.com/license -*/ -(function(m){typeof module==="object"&&module.exports?module.exports=m:m(Highcharts)})(function(m){function M(a,b,c){this.init(a,b,c)}var R=m.arrayMin,S=m.arrayMax,t=m.each,H=m.extend,I=m.isNumber,u=m.merge,T=m.map,o=m.pick,B=m.pInt,G=m.correctFloat,p=m.getOptions().plotOptions,i=m.seriesTypes,v=m.extendClass,N=m.splat,w=m.wrap,O=m.Axis,z=m.Tick,J=m.Point,U=m.Pointer,V=m.CenteredSeriesMixin,C=m.TrackerMixin,x=m.Series,y=Math,F=y.round,D=y.floor,P=y.max,W=m.Color,r=function(){};H(M.prototype,{init:function(a, -b,c){var d=this,g=d.defaultOptions;d.chart=b;d.options=a=u(g,b.angular?{background:{}}:void 0,a);(a=a.background)&&t([].concat(N(a)).reverse(),function(a){var b=a.backgroundColor,g=c.userOptions,a=u(d.defaultBackgroundOptions,a);if(b)a.backgroundColor=b;a.color=a.backgroundColor;c.options.plotBands.unshift(a);g.plotBands=g.plotBands||[];g.plotBands!==c.options.plotBands&&g.plotBands.unshift(a)})},defaultOptions:{center:["50%","50%"],size:"85%",startAngle:0},defaultBackgroundOptions:{shape:"circle", -borderWidth:1,borderColor:"silver",backgroundColor:{linearGradient:{x1:0,y1:0,x2:0,y2:1},stops:[[0,"#FFF"],[1,"#DDD"]]},from:-Number.MAX_VALUE,innerRadius:0,to:Number.MAX_VALUE,outerRadius:"105%"}});var A=O.prototype,z=z.prototype,X={getOffset:r,redraw:function(){this.isDirty=!1},render:function(){this.isDirty=!1},setScale:r,setCategories:r,setTitle:r},Q={isRadial:!0,defaultRadialGaugeOptions:{labels:{align:"center",x:0,y:null},minorGridLineWidth:0,minorTickInterval:"auto",minorTickLength:10,minorTickPosition:"inside", -minorTickWidth:1,tickLength:10,tickPosition:"inside",tickWidth:2,title:{rotation:0},zIndex:2},defaultRadialXOptions:{gridLineWidth:1,labels:{align:null,distance:15,x:0,y:null},maxPadding:0,minPadding:0,showLastLabel:!1,tickLength:0},defaultRadialYOptions:{gridLineInterpolation:"circle",labels:{align:"right",x:-3,y:-2},showLastLabel:!1,title:{x:4,text:null,rotation:90}},setOptions:function(a){a=this.options=u(this.defaultOptions,this.defaultRadialOptions,a);if(!a.plotBands)a.plotBands=[]},getOffset:function(){A.getOffset.call(this); -this.chart.axisOffset[this.side]=0;this.center=this.pane.center=V.getCenter.call(this.pane)},getLinePath:function(a,b){var c=this.center,b=o(b,c[2]/2-this.offset);return this.chart.renderer.symbols.arc(this.left+c[0],this.top+c[1],b,b,{start:this.startAngleRad,end:this.endAngleRad,open:!0,innerR:0})},setAxisTranslation:function(){A.setAxisTranslation.call(this);if(this.center)this.transA=this.isCircular?(this.endAngleRad-this.startAngleRad)/(this.max-this.min||1):this.center[2]/2/(this.max-this.min|| -1),this.minPixelPadding=this.isXAxis?this.transA*this.minPointOffset:0},beforeSetTickPositions:function(){this.autoConnect&&(this.max+=this.categories&&1||this.pointRange||this.closestPointRange||0)},setAxisSize:function(){A.setAxisSize.call(this);if(this.isRadial){this.center=this.pane.center=m.CenteredSeriesMixin.getCenter.call(this.pane);if(this.isCircular)this.sector=this.endAngleRad-this.startAngleRad;this.len=this.width=this.height=this.center[2]*o(this.sector,1)/2}},getPosition:function(a, -b){return this.postTranslate(this.isCircular?this.translate(a):0,o(this.isCircular?b:this.translate(a),this.center[2]/2)-this.offset)},postTranslate:function(a,b){var c=this.chart,d=this.center,a=this.startAngleRad+a;return{x:c.plotLeft+d[0]+Math.cos(a)*b,y:c.plotTop+d[1]+Math.sin(a)*b}},getPlotBandPath:function(a,b,c){var d=this.center,g=this.startAngleRad,e=d[2]/2,j=[o(c.outerRadius,"100%"),c.innerRadius,o(c.thickness,10)],l=/%$/,h,f=this.isCircular;this.options.gridLineInterpolation==="polygon"? -d=this.getPlotLinePath(a).concat(this.getPlotLinePath(b,!0)):(a=Math.max(a,this.min),b=Math.min(b,this.max),f||(j[0]=this.translate(a),j[1]=this.translate(b)),j=T(j,function(a){l.test(a)&&(a=B(a,10)*e/100);return a}),c.shape==="circle"||!f?(a=-Math.PI/2,b=Math.PI*1.5,h=!0):(a=g+this.translate(a),b=g+this.translate(b)),d=this.chart.renderer.symbols.arc(this.left+d[0],this.top+d[1],j[0],j[0],{start:Math.min(a,b),end:Math.max(a,b),innerR:o(j[1],j[0]-j[2]),open:h}));return d},getPlotLinePath:function(a, -b){var c=this,d=c.center,g=c.chart,e=c.getPosition(a),j,l,h;c.isCircular?h=["M",d[0]+g.plotLeft,d[1]+g.plotTop,"L",e.x,e.y]:c.options.gridLineInterpolation==="circle"?(a=c.translate(a))&&(h=c.getLinePath(0,a)):(t(g.xAxis,function(a){a.pane===c.pane&&(j=a)}),h=[],a=c.translate(a),d=j.tickPositions,j.autoConnect&&(d=d.concat([d[0]])),b&&(d=[].concat(d).reverse()),t(d,function(e,b){l=j.getPosition(e,a);h.push(b?"L":"M",l.x,l.y)}));return h},getTitlePosition:function(){var a=this.center,b=this.chart, -c=this.options.title;return{x:b.plotLeft+a[0]+(c.x||0),y:b.plotTop+a[1]-{high:0.5,middle:0.25,low:0}[c.align]*a[2]+(c.y||0)}}};w(A,"init",function(a,b,c){var k;var d=b.angular,g=b.polar,e=c.isX,j=d&&e,l,h;h=b.options;var f=c.pane||0;if(d){if(H(this,j?X:Q),l=!e)this.defaultRadialOptions=this.defaultRadialGaugeOptions}else if(g)H(this,Q),this.defaultRadialOptions=(l=e)?this.defaultRadialXOptions:u(this.defaultYAxisOptions,this.defaultRadialYOptions);if(d||g)b.inverted=!1,h.chart.zoomType=null;a.call(this, -b,c);if(!j&&(d||g)){a=this.options;if(!b.panes)b.panes=[];this.pane=(k=b.panes[f]=b.panes[f]||new M(N(h.pane)[f],b,this),b=k);h=b.options;this.startAngleRad=b=(h.startAngle-90)*Math.PI/180;this.endAngleRad=h=(o(h.endAngle,h.startAngle+360)-90)*Math.PI/180;this.offset=a.offset||0;if((this.isCircular=l)&&c.max===void 0&&h-b===2*Math.PI)this.autoConnect=!0}});w(A,"autoLabelAlign",function(a){if(!this.isRadial)return a.apply(this,[].slice.call(arguments,1))});w(z,"getPosition",function(a,b,c,d,g){var e= -this.axis;return e.getPosition?e.getPosition(c):a.call(this,b,c,d,g)});w(z,"getLabelPosition",function(a,b,c,d,g,e,j,l,h){var f=this.axis,k=e.y,n=20,s=e.align,i=(f.translate(this.pos)+f.startAngleRad+Math.PI/2)/Math.PI*180%360;f.isRadial?(a=f.getPosition(this.pos,f.center[2]/2+o(e.distance,-25)),e.rotation==="auto"?d.attr({rotation:i}):k===null&&(k=f.chart.renderer.fontMetrics(d.styles.fontSize).b-d.getBBox().height/2),s===null&&(f.isCircular?(this.label.getBBox().width>f.len*f.tickInterval/(f.max- -f.min)&&(n=0),s=i>n&&i<180-n?"left":i>180+n&&i<360-n?"right":"center"):s="center",d.attr({align:s})),a.x+=e.x,a.y+=k):a=a.call(this,b,c,d,g,e,j,l,h);return a});w(z,"getMarkPath",function(a,b,c,d,g,e,j){var l=this.axis;l.isRadial?(a=l.getPosition(this.pos,l.center[2]/2+d),b=["M",b,c,"L",a.x,a.y]):b=a.call(this,b,c,d,g,e,j);return b});p.arearange=u(p.area,{lineWidth:1,marker:null,threshold:null,tooltip:{pointFormat:'\u25cf {series.name}: {point.low} - {point.high}
'}, -trackByArea:!0,dataLabels:{align:null,verticalAlign:null,xLow:0,xHigh:0,yLow:0,yHigh:0},states:{hover:{halo:!1}}});i.arearange=v(i.area,{type:"arearange",pointArrayMap:["low","high"],dataLabelCollections:["dataLabel","dataLabelUpper"],toYData:function(a){return[a.low,a.high]},pointValKey:"low",deferTranslatePolar:!0,highToXY:function(a){var b=this.chart,c=this.xAxis.postTranslate(a.rectPlotX,this.yAxis.len-a.plotHigh);a.plotHighX=c.x-b.plotLeft;a.plotHigh=c.y-b.plotTop},translate:function(){var a= -this,b=a.yAxis;i.area.prototype.translate.apply(a);t(a.points,function(a){var d=a.low,g=a.high,e=a.plotY;g===null||d===null?a.isNull=!0:(a.plotLow=e,a.plotHigh=b.translate(g,0,1,0,1))});this.chart.polar&&t(this.points,function(b){a.highToXY(b)})},getGraphPath:function(){var a=this.points,b=[],c=[],d=a.length,g=x.prototype.getGraphPath,e,j,l;l=this.options;for(var h=l.step,d=a.length;d--;)e=a[d],!e.isNull&&(!a[d+1]||a[d+1].isNull)&&c.push({plotX:e.plotX,plotY:e.plotLow}),j={plotX:e.plotX,plotY:e.plotHigh, -isNull:e.isNull},c.push(j),b.push(j),!e.isNull&&(!a[d-1]||a[d-1].isNull)&&c.push({plotX:e.plotX,plotY:e.plotLow});a=g.call(this,a);if(h)h===!0&&(h="left"),l.step={left:"right",center:"center",right:"left"}[h];b=g.call(this,b);c=g.call(this,c);l.step=h;l=[].concat(a,b);!this.chart.polar&&c[0]==="M"&&(c[0]="L");this.areaPath=this.areaPath.concat(a,c);return l},drawDataLabels:function(){var a=this.data,b=a.length,c,d=[],g=x.prototype,e=this.options.dataLabels,j=e.align,l=e.verticalAlign,h=e.inside,f, -k,n=this.chart.inverted;if(e.enabled||this._hasPointLabels){for(c=b;c--;)if(f=a[c]){k=h?f.plotHighf.plotLow;f.y=f.high;f._plotY=f.plotY;f.plotY=f.plotHigh;d[c]=f.dataLabel;f.dataLabel=f.dataLabelUpper;f.below=k;if(n){if(!j)e.align=k?"right":"left"}else if(!l)e.verticalAlign=k?"top":"bottom";e.x=e.xHigh;e.y=e.yHigh}g.drawDataLabels&&g.drawDataLabels.apply(this,arguments);for(c=b;c--;)if(f=a[c]){k=h?f.plotHighf.plotLow;f.dataLabelUpper=f.dataLabel;f.dataLabel= -d[c];f.y=f.low;f.plotY=f._plotY;f.below=!k;if(n){if(!j)e.align=k?"left":"right"}else if(!l)e.verticalAlign=k?"bottom":"top";e.x=e.xLow;e.y=e.yLow}g.drawDataLabels&&g.drawDataLabels.apply(this,arguments)}e.align=j;e.verticalAlign=l},alignDataLabel:function(){i.column.prototype.alignDataLabel.apply(this,arguments)},setStackedPoints:r,getSymbol:r,drawPoints:r});p.areasplinerange=u(p.arearange);i.areasplinerange=v(i.arearange,{type:"areasplinerange",getPointSpline:i.spline.prototype.getPointSpline}); -(function(){var a=i.column.prototype;p.columnrange=u(p.column,p.arearange,{lineWidth:1,pointRange:null});i.columnrange=v(i.arearange,{type:"columnrange",translate:function(){var b=this,c=b.yAxis,d=b.xAxis,g=d.startAngleRad,e,j=b.chart,l=b.xAxis.isRadial,h;a.translate.apply(b);t(b.points,function(a){var k=a.shapeArgs,n=b.options.minPointLength,s,i;a.plotHigh=h=c.translate(a.high,0,1,0,1);a.plotLow=a.plotY;i=h;s=o(a.rectPlotY,a.plotY)-h;Math.abs(s)\u25cf {series.name}
Maximum: {point.high}
Upper quartile: {point.q3}
Median: {point.median}
Lower quartile: {point.q1}
Minimum: {point.low}
'}, -whiskerLength:"50%",whiskerWidth:2});i.boxplot=v(i.column,{type:"boxplot",pointArrayMap:["low","q1","median","q3","high"],toYData:function(a){return[a.low,a.q1,a.median,a.q3,a.high]},pointValKey:"high",pointAttrToOptions:{fill:"fillColor",stroke:"color","stroke-width":"lineWidth"},drawDataLabels:r,translate:function(){var a=this.yAxis,b=this.pointArrayMap;i.column.prototype.translate.apply(this);t(this.points,function(c){t(b,function(b){c[b]!==null&&(c[b+"Plot"]=a.translate(c[b],0,1,0,1))})})},drawPoints:function(){var a= -this,b=a.options,c=a.chart.renderer,d,g,e,j,l,h,f,k,n,i,m,K,L,p,u,r,w,v,x,y,C,B,z=a.doQuartiles!==!1,A,E=a.options.whiskerLength;t(a.points,function(q){n=q.graphic;C=q.shapeArgs;m={};p={};r={};B=q.color||a.color;if(q.plotY!==void 0)if(d=q.pointAttr[q.selected?"selected":""],w=C.width,v=D(C.x),x=v+w,y=F(w/2),g=D(z?q.q1Plot:q.lowPlot),e=D(z?q.q3Plot:q.lowPlot),j=D(q.highPlot),l=D(q.lowPlot),m.stroke=q.stemColor||b.stemColor||B,m["stroke-width"]=o(q.stemWidth,b.stemWidth,b.lineWidth),m.dashstyle=q.stemDashStyle|| -b.stemDashStyle,p.stroke=q.whiskerColor||b.whiskerColor||B,p["stroke-width"]=o(q.whiskerWidth,b.whiskerWidth,b.lineWidth),r.stroke=q.medianColor||b.medianColor||B,r["stroke-width"]=o(q.medianWidth,b.medianWidth,b.lineWidth),f=m["stroke-width"]%2/2,k=v+y+f,i=["M",k,e,"L",k,j,"M",k,g,"L",k,l],z&&(f=d["stroke-width"]%2/2,k=D(k)+f,g=D(g)+f,e=D(e)+f,v+=f,x+=f,K=["M",v,e,"L",v,g,"L",x,g,"L",x,e,"L",v,e,"z"]),E&&(f=p["stroke-width"]%2/2,j+=f,l+=f,A=/%$/.test(E)?y*parseFloat(E)/100:E/2,L=["M",k-A,j,"L",k+ -A,j,"M",k-A,l,"L",k+A,l]),f=r["stroke-width"]%2/2,h=F(q.medianPlot)+f,u=["M",v,h,"L",x,h],n)q.stem.animate({d:i}),E&&q.whiskers.animate({d:L}),z&&q.box.animate({d:K}),q.medianShape.animate({d:u});else{q.graphic=n=c.g().add(a.group);q.stem=c.path(i).attr(m).add(n);if(E)q.whiskers=c.path(L).attr(p).add(n);if(z)q.box=c.path(K).attr(d).add(n);q.medianShape=c.path(u).attr(r).add(n)}})},setStackedPoints:r});p.errorbar=u(p.boxplot,{color:"#000000",grouping:!1,linkedTo:":previous",tooltip:{pointFormat:'\u25cf {series.name}: {point.low} - {point.high}
'}, -whiskerWidth:null});i.errorbar=v(i.boxplot,{type:"errorbar",pointArrayMap:["low","high"],toYData:function(a){return[a.low,a.high]},pointValKey:"high",doQuartiles:!1,drawDataLabels:i.arearange?i.arearange.prototype.drawDataLabels:r,getColumnMetrics:function(){return this.linkedParent&&this.linkedParent.columnMetrics||i.column.prototype.getColumnMetrics.call(this)}});p.waterfall=u(p.column,{lineWidth:1,lineColor:"#333",dashStyle:"dot",borderColor:"#333",dataLabels:{inside:!0},states:{hover:{lineWidthPlus:0}}}); -i.waterfall=v(i.column,{type:"waterfall",upColorProp:"fill",pointValKey:"y",translate:function(){var a=this.options,b=this.yAxis,c,d,g,e,j,l,h,f,k,n=o(a.minPointLength,5),s=a.threshold,m=a.stacking;i.column.prototype.translate.apply(this);this.minPointLengthOffset=0;h=f=s;d=this.points;for(c=0,a=d.length;c0?b.translate(h,0,1)-e.y:b.translate(h,0,1)-b.translate(h-l,0,1);h+=l}e.height<0&&(e.y+=e.height,e.height*=-1);g.plotY=e.y=F(e.y)-this.borderWidth%2/2;e.height=P(F(e.height),0.001); -g.yBottom=e.y+e.height;if(e.height<=n)e.height=n,this.minPointLengthOffset+=n;e.y-=this.minPointLengthOffset;e=g.plotY+(g.negative?e.height:0)-this.minPointLengthOffset;this.chart.inverted?g.tooltipPos[0]=b.len-e:g.tooltipPos[1]=e}},processData:function(a){var b=this.yData,c=this.options.data,d,g=b.length,e,j,l,h,f,k;j=e=l=h=this.options.threshold||0;for(k=0;k -0?(e.pointAttr=g,e.color=d):e.pointAttr=a.pointAttr})},getGraphPath:function(){var a=this.data,b=a.length,c=F(this.options.lineWidth+this.borderWidth)%2/2,d=[],g,e,j;for(j=1;j0?(j-a)/i:0.5,k&&j>=0&&(j=Math.sqrt(j)),j=y.ceil(c+j*(d-c))/2),h.push(j);this.radii=h},animate:function(a){var b=this.options.animation;if(!a)t(this.points, -function(a){var d=a.graphic,a=a.shapeArgs;d&&a&&(d.attr("r",1),d.animate({r:a.r},b))}),this.animate=null},translate:function(){var a,b=this.data,c,d,g=this.radii;i.scatter.prototype.translate.call(this);for(a=b.length;a--;)c=b[a],d=g?g[a]:0,I(d)&&d>=this.minPxSize/2?(c.shapeType="circle",c.shapeArgs={x:c.plotX,y:c.plotY,r:d},c.dlBox={x:c.plotX-d,y:c.plotY-d,width:2*d,height:2*d}):c.shapeArgs=c.plotY=c.dlBox=void 0},drawLegendSymbol:function(a,b){var c=this.chart.renderer,d=c.fontMetrics(a.itemStyle.fontSize).f/ -2;b.legendSymbol=c.circle(d,a.baseline-d,d).attr({zIndex:3}).add(b.legendGroup);b.legendSymbol.isMarker=!0},drawPoints:i.column.prototype.drawPoints,alignDataLabel:i.column.prototype.alignDataLabel,buildKDTree:r,applyZones:r});O.prototype.beforePadding=function(){var a=this,b=this.len,c=this.chart,d=0,g=b,e=this.isXAxis,j=e?"xData":"yData",l=this.min,h={},f=y.min(c.plotWidth,c.plotHeight),k=Number.MAX_VALUE,n=-Number.MAX_VALUE,i=this.max-l,m=b/i,p=[];t(this.series,function(b){var g=b.options;if(b.bubblePadding&& -(b.visible||!c.options.chart.ignoreHiddenSeries))if(a.allowZoomOutside=!0,p.push(b),e)t(["minSize","maxSize"],function(a){var b=g[a],e=/%$/.test(b),b=B(b);h[a]=e?f*b/100:b}),b.minPxSize=h.minSize,b.maxPxSize=h.maxSize,b=b.zData,b.length&&(k=o(g.zMin,y.min(k,y.max(R(b),g.displayNegative===!1?g.zThreshold:-Number.MAX_VALUE))),n=o(g.zMax,y.max(n,S(b))))});t(p,function(b){var c=b[j],f=c.length,h;e&&b.getRadii(k,n,b.minPxSize,b.maxPxSize);if(i>0)for(;f--;)I(c[f])&&a.dataMin<=c[f]&&c[f]<=a.dataMax&&(h= -b.radii[f],d=Math.min((c[f]-l)*m-h,d),g=Math.max((c[f]-l)*m+h,g))});p.length&&i>0&&!this.isLog&&(g-=b,m*=(b+d-g)/b,t([["min","userMin",d],["max","userMax",g]],function(b){o(a.options[b[0]],a[b[1]])===void 0&&(a[b[0]]+=b[2]/m)}))};(function(){function a(a,b){var c=this.chart,d=this.options.animation,h=this.group,f=this.markerGroup,k=this.xAxis.center,i=c.plotLeft,m=c.plotTop;if(c.polar){if(c.renderer.isSVG)d===!0&&(d={}),b?(c={translateX:k[0]+i,translateY:k[1]+m,scaleX:0.001,scaleY:0.001},h.attr(c), -f&&f.attr(c)):(c={translateX:i,translateY:m,scaleX:1,scaleY:1},h.animate(c,d),f&&f.animate(c,d),this.animate=null)}else a.call(this,b)}var b=x.prototype,c=U.prototype,d;b.searchPointByAngle=function(a){var b=this.chart,c=this.xAxis.pane.center;return this.searchKDTree({clientX:180+Math.atan2(a.chartX-c[0]-b.plotLeft,a.chartY-c[1]-b.plotTop)*(-180/Math.PI)})};w(b,"buildKDTree",function(a){if(this.chart.polar)this.kdByAngle?this.searchPoint=this.searchPointByAngle:this.kdDimensions=2;a.apply(this)}); -b.toXY=function(a){var b,c=this.chart,d=a.plotX;b=a.plotY;a.rectPlotX=d;a.rectPlotY=b;b=this.xAxis.postTranslate(a.plotX,this.yAxis.len-b);a.plotX=a.polarPlotX=b.x-c.plotLeft;a.plotY=a.polarPlotY=b.y-c.plotTop;this.kdByAngle?(c=(d/Math.PI*180+this.xAxis.pane.options.startAngle)%360,c<0&&(c+=360),a.clientX=c):a.clientX=a.plotX};i.spline&&w(i.spline.prototype,"getPointSpline",function(a,b,c,d){var h,f,k,i,m,p,o;if(this.chart.polar){h=c.plotX;f=c.plotY;a=b[d-1];k=b[d+1];this.connectEnds&&(a||(a=b[b.length- -2]),k||(k=b[1]));if(a&&k)i=a.plotX,m=a.plotY,b=k.plotX,p=k.plotY,i=(1.5*h+i)/2.5,m=(1.5*f+m)/2.5,k=(1.5*h+b)/2.5,o=(1.5*f+p)/2.5,b=Math.sqrt(Math.pow(i-h,2)+Math.pow(m-f,2)),p=Math.sqrt(Math.pow(k-h,2)+Math.pow(o-f,2)),i=Math.atan2(m-f,i-h),m=Math.atan2(o-f,k-h),o=Math.PI/2+(i+m)/2,Math.abs(i-o)>Math.PI/2&&(o-=Math.PI),i=h+Math.cos(o)*b,m=f+Math.sin(o)*b,k=h+Math.cos(Math.PI+o)*p,o=f+Math.sin(Math.PI+o)*p,c.rightContX=k,c.rightContY=o;d?(c=["C",a.rightContX||a.plotX,a.rightContY||a.plotY,i||h,m|| -f,h,f],a.rightContX=a.rightContY=null):c=["M",h,f]}else c=a.call(this,b,c,d);return c});w(b,"translate",function(a){var b=this.chart;a.call(this);if(b.polar&&(this.kdByAngle=b.tooltip&&b.tooltip.shared,!this.preventPostTranslate)){a=this.points;for(b=a.length;b--;)this.toXY(a[b])}});w(b,"getGraphPath",function(a,b){var c=this;if(this.chart.polar){b=b||this.points;if(this.options.connectEnds!==!1&&b[0]&&b[0].y!==null)this.connectEnds=!0,b.splice(b.length,0,b[0]);t(b,function(a){a.polarPlotY===void 0&& -c.toXY(a)})}return a.apply(this,[].slice.call(arguments,1))});w(b,"animate",a);if(i.column)d=i.column.prototype,d.polarArc=function(a,b,c,d){var h=this.xAxis.center,f=this.yAxis.len;return this.chart.renderer.symbols.arc(h[0],h[1],f-b,null,{start:c,end:d,innerR:f-o(a,f)})},w(d,"animate",a),w(d,"translate",function(a){var b=this.xAxis,c=b.startAngleRad,d,h,f;this.preventPostTranslate=!0;a.call(this);if(b.isRadial){d=this.points;for(f=d.length;f--;)h=d[f],a=h.barX+c,h.shapeType="path",h.shapeArgs={d:this.polarArc(h.yBottom, -h.plotY,a,a+h.pointWidth)},this.toXY(h),h.tooltipPos=[h.plotX,h.plotY],h.ttBelow=h.plotY>b.center[1]}}),w(d,"alignDataLabel",function(a,c,d,i,h,f){if(this.chart.polar){a=c.rectPlotX/Math.PI*180;if(i.align===null)i.align=a>20&&a<160?"left":a>200&&a<340?"right":"center";if(i.verticalAlign===null)i.verticalAlign=a<45||a>315?"bottom":a>135&&a<225?"top":"middle";b.alignDataLabel.call(this,c,d,i,h,f)}else a.call(this,c,d,i,h,f)});w(c,"getCoordinates",function(a,b){var c=this.chart,d={xAxis:[],yAxis:[]}; -c.polar?t(c.axes,function(a){var f=a.isXAxis,g=a.center,i=b.chartX-g[0]-c.plotLeft,g=b.chartY-g[1]-c.plotTop;d[f?"xAxis":"yAxis"].push({axis:a,value:a.translate(f?Math.PI-Math.atan2(i,g):Math.sqrt(Math.pow(i,2)+Math.pow(g,2)),!0)})}):d=a.call(this,b);return d})})()}); diff --git a/public/vendor/highcharts-4.2.5/highcharts-more.src.js b/public/vendor/highcharts-4.2.5/highcharts-more.src.js deleted file mode 100644 index ddda4d43affca..0000000000000 --- a/public/vendor/highcharts-4.2.5/highcharts-more.src.js +++ /dev/null @@ -1,2692 +0,0 @@ -// ==ClosureCompiler== -// @compilation_level SIMPLE_OPTIMIZATIONS - -/** - * @license Highcharts JS v4.2.5 (2016-05-06) - * - * (c) 2009-2016 Torstein Honsi - * - * License: www.highcharts.com/license - */ - -(function (factory) { - if (typeof module === 'object' && module.exports) { - module.exports = factory; - } else { - factory(Highcharts); - } -}(function (Highcharts) { -var arrayMin = Highcharts.arrayMin, - arrayMax = Highcharts.arrayMax, - each = Highcharts.each, - extend = Highcharts.extend, - isNumber = Highcharts.isNumber, - merge = Highcharts.merge, - map = Highcharts.map, - pick = Highcharts.pick, - pInt = Highcharts.pInt, - correctFloat = Highcharts.correctFloat, - defaultPlotOptions = Highcharts.getOptions().plotOptions, - seriesTypes = Highcharts.seriesTypes, - extendClass = Highcharts.extendClass, - splat = Highcharts.splat, - wrap = Highcharts.wrap, - Axis = Highcharts.Axis, - Tick = Highcharts.Tick, - Point = Highcharts.Point, - Pointer = Highcharts.Pointer, - CenteredSeriesMixin = Highcharts.CenteredSeriesMixin, - TrackerMixin = Highcharts.TrackerMixin, - Series = Highcharts.Series, - math = Math, - mathRound = math.round, - mathFloor = math.floor, - mathMax = math.max, - Color = Highcharts.Color, - noop = function () {}, - UNDEFINED;/** - * The Pane object allows options that are common to a set of X and Y axes. - * - * In the future, this can be extended to basic Highcharts and Highstock. - */ - function Pane(options, chart, firstAxis) { - this.init(options, chart, firstAxis); - } - - // Extend the Pane prototype - extend(Pane.prototype, { - - /** - * Initiate the Pane object - */ - init: function (options, chart, firstAxis) { - var pane = this, - backgroundOption, - defaultOptions = pane.defaultOptions; - - pane.chart = chart; - - // Set options. Angular charts have a default background (#3318) - pane.options = options = merge(defaultOptions, chart.angular ? { background: {} } : undefined, options); - - backgroundOption = options.background; - - // To avoid having weighty logic to place, update and remove the backgrounds, - // push them to the first axis' plot bands and borrow the existing logic there. - if (backgroundOption) { - each([].concat(splat(backgroundOption)).reverse(), function (config) { - var backgroundColor = config.backgroundColor, // if defined, replace the old one (specific for gradients) - axisUserOptions = firstAxis.userOptions; - config = merge(pane.defaultBackgroundOptions, config); - if (backgroundColor) { - config.backgroundColor = backgroundColor; - } - config.color = config.backgroundColor; // due to naming in plotBands - firstAxis.options.plotBands.unshift(config); - axisUserOptions.plotBands = axisUserOptions.plotBands || []; // #3176 - if (axisUserOptions.plotBands !== firstAxis.options.plotBands) { - axisUserOptions.plotBands.unshift(config); - } - }); - } - }, - - /** - * The default options object - */ - defaultOptions: { - // background: {conditional}, - center: ['50%', '50%'], - size: '85%', - startAngle: 0 - //endAngle: startAngle + 360 - }, - - /** - * The default background options - */ - defaultBackgroundOptions: { - shape: 'circle', - borderWidth: 1, - borderColor: 'silver', - backgroundColor: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0, '#FFF'], - [1, '#DDD'] - ] - }, - from: -Number.MAX_VALUE, // corrected to axis min - innerRadius: 0, - to: Number.MAX_VALUE, // corrected to axis max - outerRadius: '105%' - } - }); - - var axisProto = Axis.prototype, - tickProto = Tick.prototype; - - /** - * Augmented methods for the x axis in order to hide it completely, used for the X axis in gauges - */ - var hiddenAxisMixin = { - getOffset: noop, - redraw: function () { - this.isDirty = false; // prevent setting Y axis dirty - }, - render: function () { - this.isDirty = false; // prevent setting Y axis dirty - }, - setScale: noop, - setCategories: noop, - setTitle: noop - }; - - /** - * Augmented methods for the value axis - */ - var radialAxisMixin = { - isRadial: true, - - /** - * The default options extend defaultYAxisOptions - */ - defaultRadialGaugeOptions: { - labels: { - align: 'center', - x: 0, - y: null // auto - }, - minorGridLineWidth: 0, - minorTickInterval: 'auto', - minorTickLength: 10, - minorTickPosition: 'inside', - minorTickWidth: 1, - tickLength: 10, - tickPosition: 'inside', - tickWidth: 2, - title: { - rotation: 0 - }, - zIndex: 2 // behind dials, points in the series group - }, - - // Circular axis around the perimeter of a polar chart - defaultRadialXOptions: { - gridLineWidth: 1, // spokes - labels: { - align: null, // auto - distance: 15, - x: 0, - y: null // auto - }, - maxPadding: 0, - minPadding: 0, - showLastLabel: false, - tickLength: 0 - }, - - // Radial axis, like a spoke in a polar chart - defaultRadialYOptions: { - gridLineInterpolation: 'circle', - labels: { - align: 'right', - x: -3, - y: -2 - }, - showLastLabel: false, - title: { - x: 4, - text: null, - rotation: 90 - } - }, - - /** - * Merge and set options - */ - setOptions: function (userOptions) { - - var options = this.options = merge( - this.defaultOptions, - this.defaultRadialOptions, - userOptions - ); - - // Make sure the plotBands array is instanciated for each Axis (#2649) - if (!options.plotBands) { - options.plotBands = []; - } - - }, - - /** - * Wrap the getOffset method to return zero offset for title or labels in a radial - * axis - */ - getOffset: function () { - // Call the Axis prototype method (the method we're in now is on the instance) - axisProto.getOffset.call(this); - - // Title or label offsets are not counted - this.chart.axisOffset[this.side] = 0; - - // Set the center array - this.center = this.pane.center = CenteredSeriesMixin.getCenter.call(this.pane); - }, - - - /** - * Get the path for the axis line. This method is also referenced in the getPlotLinePath - * method. - */ - getLinePath: function (lineWidth, radius) { - var center = this.center; - radius = pick(radius, center[2] / 2 - this.offset); - - return this.chart.renderer.symbols.arc( - this.left + center[0], - this.top + center[1], - radius, - radius, - { - start: this.startAngleRad, - end: this.endAngleRad, - open: true, - innerR: 0 - } - ); - }, - - /** - * Override setAxisTranslation by setting the translation to the difference - * in rotation. This allows the translate method to return angle for - * any given value. - */ - setAxisTranslation: function () { - - // Call uber method - axisProto.setAxisTranslation.call(this); - - // Set transA and minPixelPadding - if (this.center) { // it's not defined the first time - if (this.isCircular) { - - this.transA = (this.endAngleRad - this.startAngleRad) / - ((this.max - this.min) || 1); - - - } else { - this.transA = (this.center[2] / 2) / ((this.max - this.min) || 1); - } - - if (this.isXAxis) { - this.minPixelPadding = this.transA * this.minPointOffset; - } else { - // This is a workaround for regression #2593, but categories still don't position correctly. - this.minPixelPadding = 0; - } - } - }, - - /** - * In case of auto connect, add one closestPointRange to the max value right before - * tickPositions are computed, so that ticks will extend passed the real max. - */ - beforeSetTickPositions: function () { - if (this.autoConnect) { - this.max += (this.categories && 1) || this.pointRange || this.closestPointRange || 0; // #1197, #2260 - } - }, - - /** - * Override the setAxisSize method to use the arc's circumference as length. This - * allows tickPixelInterval to apply to pixel lengths along the perimeter - */ - setAxisSize: function () { - - axisProto.setAxisSize.call(this); - - if (this.isRadial) { - - // Set the center array - this.center = this.pane.center = Highcharts.CenteredSeriesMixin.getCenter.call(this.pane); - - // The sector is used in Axis.translate to compute the translation of reversed axis points (#2570) - if (this.isCircular) { - this.sector = this.endAngleRad - this.startAngleRad; - } - - // Axis len is used to lay out the ticks - this.len = this.width = this.height = this.center[2] * pick(this.sector, 1) / 2; - - - } - }, - - /** - * Returns the x, y coordinate of a point given by a value and a pixel distance - * from center - */ - getPosition: function (value, length) { - return this.postTranslate( - this.isCircular ? this.translate(value) : 0, // #2848 - pick(this.isCircular ? length : this.translate(value), this.center[2] / 2) - this.offset - ); - }, - - /** - * Translate from intermediate plotX (angle), plotY (axis.len - radius) to final chart coordinates. - */ - postTranslate: function (angle, radius) { - - var chart = this.chart, - center = this.center; - - angle = this.startAngleRad + angle; - - return { - x: chart.plotLeft + center[0] + Math.cos(angle) * radius, - y: chart.plotTop + center[1] + Math.sin(angle) * radius - }; - - }, - - /** - * Find the path for plot bands along the radial axis - */ - getPlotBandPath: function (from, to, options) { - var center = this.center, - startAngleRad = this.startAngleRad, - fullRadius = center[2] / 2, - radii = [ - pick(options.outerRadius, '100%'), - options.innerRadius, - pick(options.thickness, 10) - ], - percentRegex = /%$/, - start, - end, - open, - isCircular = this.isCircular, // X axis in a polar chart - ret; - - // Polygonal plot bands - if (this.options.gridLineInterpolation === 'polygon') { - ret = this.getPlotLinePath(from).concat(this.getPlotLinePath(to, true)); - - // Circular grid bands - } else { - - // Keep within bounds - from = Math.max(from, this.min); - to = Math.min(to, this.max); - - // Plot bands on Y axis (radial axis) - inner and outer radius depend on to and from - if (!isCircular) { - radii[0] = this.translate(from); - radii[1] = this.translate(to); - } - - // Convert percentages to pixel values - radii = map(radii, function (radius) { - if (percentRegex.test(radius)) { - radius = (pInt(radius, 10) * fullRadius) / 100; - } - return radius; - }); - - // Handle full circle - if (options.shape === 'circle' || !isCircular) { - start = -Math.PI / 2; - end = Math.PI * 1.5; - open = true; - } else { - start = startAngleRad + this.translate(from); - end = startAngleRad + this.translate(to); - } - - - ret = this.chart.renderer.symbols.arc( - this.left + center[0], - this.top + center[1], - radii[0], - radii[0], - { - start: Math.min(start, end), // Math is for reversed yAxis (#3606) - end: Math.max(start, end), - innerR: pick(radii[1], radii[0] - radii[2]), - open: open - } - ); - } - - return ret; - }, - - /** - * Find the path for plot lines perpendicular to the radial axis. - */ - getPlotLinePath: function (value, reverse) { - var axis = this, - center = axis.center, - chart = axis.chart, - end = axis.getPosition(value), - xAxis, - xy, - tickPositions, - ret; - - // Spokes - if (axis.isCircular) { - ret = ['M', center[0] + chart.plotLeft, center[1] + chart.plotTop, 'L', end.x, end.y]; - - // Concentric circles - } else if (axis.options.gridLineInterpolation === 'circle') { - value = axis.translate(value); - if (value) { // a value of 0 is in the center - ret = axis.getLinePath(0, value); - } - // Concentric polygons - } else { - // Find the X axis in the same pane - each(chart.xAxis, function (a) { - if (a.pane === axis.pane) { - xAxis = a; - } - }); - ret = []; - value = axis.translate(value); - tickPositions = xAxis.tickPositions; - if (xAxis.autoConnect) { - tickPositions = tickPositions.concat([tickPositions[0]]); - } - // Reverse the positions for concatenation of polygonal plot bands - if (reverse) { - tickPositions = [].concat(tickPositions).reverse(); - } - - each(tickPositions, function (pos, i) { - xy = xAxis.getPosition(pos, value); - ret.push(i ? 'L' : 'M', xy.x, xy.y); - }); - - } - return ret; - }, - - /** - * Find the position for the axis title, by default inside the gauge - */ - getTitlePosition: function () { - var center = this.center, - chart = this.chart, - titleOptions = this.options.title; - - return { - x: chart.plotLeft + center[0] + (titleOptions.x || 0), - y: chart.plotTop + center[1] - ({ high: 0.5, middle: 0.25, low: 0 }[titleOptions.align] * - center[2]) + (titleOptions.y || 0) - }; - } - - }; - - /** - * Override axisProto.init to mix in special axis instance functions and function overrides - */ - wrap(axisProto, 'init', function (proceed, chart, userOptions) { - var axis = this, - angular = chart.angular, - polar = chart.polar, - isX = userOptions.isX, - isHidden = angular && isX, - isCircular, - startAngleRad, - endAngleRad, - options, - chartOptions = chart.options, - paneIndex = userOptions.pane || 0, - pane, - paneOptions; - - // Before prototype.init - if (angular) { - extend(this, isHidden ? hiddenAxisMixin : radialAxisMixin); - isCircular = !isX; - if (isCircular) { - this.defaultRadialOptions = this.defaultRadialGaugeOptions; - } - - } else if (polar) { - //extend(this, userOptions.isX ? radialAxisMixin : radialAxisMixin); - extend(this, radialAxisMixin); - isCircular = isX; - this.defaultRadialOptions = isX ? this.defaultRadialXOptions : merge(this.defaultYAxisOptions, this.defaultRadialYOptions); - - } - - // Disable certain features on angular and polar axes - if (angular || polar) { - chart.inverted = false; - chartOptions.chart.zoomType = null; - } - - // Run prototype.init - proceed.call(this, chart, userOptions); - - if (!isHidden && (angular || polar)) { - options = this.options; - - // Create the pane and set the pane options. - if (!chart.panes) { - chart.panes = []; - } - this.pane = pane = chart.panes[paneIndex] = chart.panes[paneIndex] || new Pane( - splat(chartOptions.pane)[paneIndex], - chart, - axis - ); - paneOptions = pane.options; - - // Start and end angle options are - // given in degrees relative to top, while internal computations are - // in radians relative to right (like SVG). - this.startAngleRad = startAngleRad = (paneOptions.startAngle - 90) * Math.PI / 180; - this.endAngleRad = endAngleRad = (pick(paneOptions.endAngle, paneOptions.startAngle + 360) - 90) * Math.PI / 180; - this.offset = options.offset || 0; - - this.isCircular = isCircular; - - // Automatically connect grid lines? - if (isCircular && userOptions.max === UNDEFINED && endAngleRad - startAngleRad === 2 * Math.PI) { - this.autoConnect = true; - } - } - - }); - - /** - * Wrap auto label align to avoid setting axis-wide rotation on radial axes (#4920) - * @param {Function} proceed - * @returns {String} Alignment - */ - wrap(axisProto, 'autoLabelAlign', function (proceed) { - if (!this.isRadial) { - return proceed.apply(this, [].slice.call(arguments, 1)); - } // else return undefined - }); - - /** - * Add special cases within the Tick class' methods for radial axes. - */ - wrap(tickProto, 'getPosition', function (proceed, horiz, pos, tickmarkOffset, old) { - var axis = this.axis; - - return axis.getPosition ? - axis.getPosition(pos) : - proceed.call(this, horiz, pos, tickmarkOffset, old); - }); - - /** - * Wrap the getLabelPosition function to find the center position of the label - * based on the distance option - */ - wrap(tickProto, 'getLabelPosition', function (proceed, x, y, label, horiz, labelOptions, tickmarkOffset, index, step) { - var axis = this.axis, - optionsY = labelOptions.y, - ret, - centerSlot = 20, // 20 degrees to each side at the top and bottom - align = labelOptions.align, - angle = ((axis.translate(this.pos) + axis.startAngleRad + Math.PI / 2) / Math.PI * 180) % 360; - - if (axis.isRadial) { - ret = axis.getPosition(this.pos, (axis.center[2] / 2) + pick(labelOptions.distance, -25)); - - // Automatically rotated - if (labelOptions.rotation === 'auto') { - label.attr({ - rotation: angle - }); - - // Vertically centered - } else if (optionsY === null) { - optionsY = axis.chart.renderer.fontMetrics(label.styles.fontSize).b - label.getBBox().height / 2; - } - - // Automatic alignment - if (align === null) { - if (axis.isCircular) { - if (this.label.getBBox().width > axis.len * axis.tickInterval / (axis.max - axis.min)) { // #3506 - centerSlot = 0; - } - if (angle > centerSlot && angle < 180 - centerSlot) { - align = 'left'; // right hemisphere - } else if (angle > 180 + centerSlot && angle < 360 - centerSlot) { - align = 'right'; // left hemisphere - } else { - align = 'center'; // top or bottom - } - } else { - align = 'center'; - } - label.attr({ - align: align - }); - } - - ret.x += labelOptions.x; - ret.y += optionsY; - - } else { - ret = proceed.call(this, x, y, label, horiz, labelOptions, tickmarkOffset, index, step); - } - return ret; - }); - - /** - * Wrap the getMarkPath function to return the path of the radial marker - */ - wrap(tickProto, 'getMarkPath', function (proceed, x, y, tickLength, tickWidth, horiz, renderer) { - var axis = this.axis, - endPoint, - ret; - - if (axis.isRadial) { - endPoint = axis.getPosition(this.pos, axis.center[2] / 2 + tickLength); - ret = [ - 'M', - x, - y, - 'L', - endPoint.x, - endPoint.y - ]; - } else { - ret = proceed.call(this, x, y, tickLength, tickWidth, horiz, renderer); - } - return ret; - });/* - * The AreaRangeSeries class - * - */ - - /** - * Extend the default options with map options - */ - defaultPlotOptions.arearange = merge(defaultPlotOptions.area, { - lineWidth: 1, - marker: null, - threshold: null, - tooltip: { - pointFormat: '\u25CF {series.name}: {point.low} - {point.high}
' - }, - trackByArea: true, - dataLabels: { - align: null, - verticalAlign: null, - xLow: 0, - xHigh: 0, - yLow: 0, - yHigh: 0 - }, - states: { - hover: { - halo: false - } - } - }); - - /** - * Add the series type - */ - seriesTypes.arearange = extendClass(seriesTypes.area, { - type: 'arearange', - pointArrayMap: ['low', 'high'], - dataLabelCollections: ['dataLabel', 'dataLabelUpper'], - toYData: function (point) { - return [point.low, point.high]; - }, - pointValKey: 'low', - deferTranslatePolar: true, - - /** - * Translate a point's plotHigh from the internal angle and radius measures to - * true plotHigh coordinates. This is an addition of the toXY method found in - * Polar.js, because it runs too early for arearanges to be considered (#3419). - */ - highToXY: function (point) { - // Find the polar plotX and plotY - var chart = this.chart, - xy = this.xAxis.postTranslate(point.rectPlotX, this.yAxis.len - point.plotHigh); - point.plotHighX = xy.x - chart.plotLeft; - point.plotHigh = xy.y - chart.plotTop; - }, - - /** - * Translate data points from raw values x and y to plotX and plotY - */ - translate: function () { - var series = this, - yAxis = series.yAxis; - - seriesTypes.area.prototype.translate.apply(series); - - // Set plotLow and plotHigh - each(series.points, function (point) { - - var low = point.low, - high = point.high, - plotY = point.plotY; - - if (high === null || low === null) { - point.isNull = true; - } else { - point.plotLow = plotY; - point.plotHigh = yAxis.translate(high, 0, 1, 0, 1); - } - }); - - // Postprocess plotHigh - if (this.chart.polar) { - each(this.points, function (point) { - series.highToXY(point); - }); - } - }, - - /** - * Extend the line series' getSegmentPath method by applying the segment - * path to both lower and higher values of the range - */ - getGraphPath: function () { - - var points = this.points, - highPoints = [], - highAreaPoints = [], - i = points.length, - getGraphPath = Series.prototype.getGraphPath, - point, - pointShim, - linePath, - lowerPath, - options = this.options, - step = options.step, - higherPath, - higherAreaPath; - - // Create the top line and the top part of the area fill. The area fill compensates for - // null points by drawing down to the lower graph, moving across the null gap and - // starting again at the lower graph. - i = points.length; - while (i--) { - point = points[i]; - - if (!point.isNull && (!points[i + 1] || points[i + 1].isNull)) { - highAreaPoints.push({ - plotX: point.plotX, - plotY: point.plotLow - }); - } - pointShim = { - plotX: point.plotX, - plotY: point.plotHigh, - isNull: point.isNull - }; - highAreaPoints.push(pointShim); - highPoints.push(pointShim); - if (!point.isNull && (!points[i - 1] || points[i - 1].isNull)) { - highAreaPoints.push({ - plotX: point.plotX, - plotY: point.plotLow - }); - } - } - - // Get the paths - lowerPath = getGraphPath.call(this, points); - if (step) { - if (step === true) { - step = 'left'; - } - options.step = { left: 'right', center: 'center', right: 'left' }[step]; // swap for reading in getGraphPath - } - higherPath = getGraphPath.call(this, highPoints); - higherAreaPath = getGraphPath.call(this, highAreaPoints); - options.step = step; - - // Create a line on both top and bottom of the range - linePath = [].concat(lowerPath, higherPath); - - // For the area path, we need to change the 'move' statement into 'lineTo' or 'curveTo' - if (!this.chart.polar && higherAreaPath[0] === 'M') { - higherAreaPath[0] = 'L'; // this probably doesn't work for spline - } - this.areaPath = this.areaPath.concat(lowerPath, higherAreaPath); - return linePath; - }, - - /** - * Extend the basic drawDataLabels method by running it for both lower and higher - * values. - */ - drawDataLabels: function () { - - var data = this.data, - length = data.length, - i, - originalDataLabels = [], - seriesProto = Series.prototype, - dataLabelOptions = this.options.dataLabels, - align = dataLabelOptions.align, - verticalAlign = dataLabelOptions.verticalAlign, - inside = dataLabelOptions.inside, - point, - up, - inverted = this.chart.inverted; - - if (dataLabelOptions.enabled || this._hasPointLabels) { - - // Step 1: set preliminary values for plotY and dataLabel and draw the upper labels - i = length; - while (i--) { - point = data[i]; - if (point) { - up = inside ? point.plotHigh < point.plotLow : point.plotHigh > point.plotLow; - - // Set preliminary values - point.y = point.high; - point._plotY = point.plotY; - point.plotY = point.plotHigh; - - // Store original data labels and set preliminary label objects to be picked up - // in the uber method - originalDataLabels[i] = point.dataLabel; - point.dataLabel = point.dataLabelUpper; - - // Set the default offset - point.below = up; - if (inverted) { - if (!align) { - dataLabelOptions.align = up ? 'right' : 'left'; - } - } else { - if (!verticalAlign) { - dataLabelOptions.verticalAlign = up ? 'top' : 'bottom'; - } - } - - dataLabelOptions.x = dataLabelOptions.xHigh; - dataLabelOptions.y = dataLabelOptions.yHigh; - } - } - - if (seriesProto.drawDataLabels) { - seriesProto.drawDataLabels.apply(this, arguments); // #1209 - } - - // Step 2: reorganize and handle data labels for the lower values - i = length; - while (i--) { - point = data[i]; - if (point) { - up = inside ? point.plotHigh < point.plotLow : point.plotHigh > point.plotLow; - - // Move the generated labels from step 1, and reassign the original data labels - point.dataLabelUpper = point.dataLabel; - point.dataLabel = originalDataLabels[i]; - - // Reset values - point.y = point.low; - point.plotY = point._plotY; - - // Set the default offset - point.below = !up; - if (inverted) { - if (!align) { - dataLabelOptions.align = up ? 'left' : 'right'; - } - } else { - if (!verticalAlign) { - dataLabelOptions.verticalAlign = up ? 'bottom' : 'top'; - } - - } - - dataLabelOptions.x = dataLabelOptions.xLow; - dataLabelOptions.y = dataLabelOptions.yLow; - } - } - if (seriesProto.drawDataLabels) { - seriesProto.drawDataLabels.apply(this, arguments); - } - } - - dataLabelOptions.align = align; - dataLabelOptions.verticalAlign = verticalAlign; - }, - - alignDataLabel: function () { - seriesTypes.column.prototype.alignDataLabel.apply(this, arguments); - }, - - setStackedPoints: noop, - - getSymbol: noop, - - drawPoints: noop - }); - /** - * The AreaSplineRangeSeries class - */ - - defaultPlotOptions.areasplinerange = merge(defaultPlotOptions.arearange); - - /** - * AreaSplineRangeSeries object - */ - seriesTypes.areasplinerange = extendClass(seriesTypes.arearange, { - type: 'areasplinerange', - getPointSpline: seriesTypes.spline.prototype.getPointSpline - }); - - (function () { - - var colProto = seriesTypes.column.prototype; - - /** - * The ColumnRangeSeries class - */ - defaultPlotOptions.columnrange = merge(defaultPlotOptions.column, defaultPlotOptions.arearange, { - lineWidth: 1, - pointRange: null - }); - - /** - * ColumnRangeSeries object - */ - seriesTypes.columnrange = extendClass(seriesTypes.arearange, { - type: 'columnrange', - /** - * Translate data points from raw values x and y to plotX and plotY - */ - translate: function () { - var series = this, - yAxis = series.yAxis, - xAxis = series.xAxis, - startAngleRad = xAxis.startAngleRad, - start, - chart = series.chart, - isRadial = series.xAxis.isRadial, - plotHigh; - - colProto.translate.apply(series); - - // Set plotLow and plotHigh - each(series.points, function (point) { - var shapeArgs = point.shapeArgs, - minPointLength = series.options.minPointLength, - heightDifference, - height, - y; - - point.plotHigh = plotHigh = yAxis.translate(point.high, 0, 1, 0, 1); - point.plotLow = point.plotY; - - // adjust shape - y = plotHigh; - height = pick(point.rectPlotY, point.plotY) - plotHigh; - - // Adjust for minPointLength - if (Math.abs(height) < minPointLength) { - heightDifference = (minPointLength - height); - height += heightDifference; - y -= heightDifference / 2; - - // Adjust for negative ranges or reversed Y axis (#1457) - } else if (height < 0) { - height *= -1; - y -= height; - } - - if (isRadial) { - - start = point.barX + startAngleRad; - point.shapeType = 'path'; - point.shapeArgs = { - d: series.polarArc(y + height, y, start, start + point.pointWidth) - }; - } else { - shapeArgs.height = height; - shapeArgs.y = y; - - point.tooltipPos = chart.inverted ? - [ - yAxis.len + yAxis.pos - chart.plotLeft - y - height / 2, - xAxis.len + xAxis.pos - chart.plotTop - shapeArgs.x - shapeArgs.width / 2, - height - ] : [ - xAxis.left - chart.plotLeft + shapeArgs.x + shapeArgs.width / 2, - yAxis.pos - chart.plotTop + y + height / 2, - height - ]; // don't inherit from column tooltip position - #3372 - } - }); - }, - directTouch: true, - trackerGroups: ['group', 'dataLabelsGroup'], - drawGraph: noop, - crispCol: colProto.crispCol, - pointAttrToOptions: colProto.pointAttrToOptions, - drawPoints: colProto.drawPoints, - drawTracker: colProto.drawTracker, - getColumnMetrics: colProto.getColumnMetrics, - animate: function () { - return colProto.animate.apply(this, arguments); - }, - polarArc: function () { - return colProto.polarArc.apply(this, arguments); - } - }); - }()); - - /* - * The GaugeSeries class - */ - - - - /** - * Extend the default options - */ - defaultPlotOptions.gauge = merge(defaultPlotOptions.line, { - dataLabels: { - enabled: true, - defer: false, - y: 15, - borderWidth: 1, - borderColor: 'silver', - borderRadius: 3, - crop: false, - verticalAlign: 'top', - zIndex: 2 - }, - dial: { - // radius: '80%', - // backgroundColor: 'black', - // borderColor: 'silver', - // borderWidth: 0, - // baseWidth: 3, - // topWidth: 1, - // baseLength: '70%' // of radius - // rearLength: '10%' - }, - pivot: { - //radius: 5, - //borderWidth: 0 - //borderColor: 'silver', - //backgroundColor: 'black' - }, - tooltip: { - headerFormat: '' - }, - showInLegend: false - }); - - /** - * Extend the point object - */ - var GaugePoint = extendClass(Point, { - /** - * Don't do any hover colors or anything - */ - setState: function (state) { - this.state = state; - } - }); - - - /** - * Add the series type - */ - var GaugeSeries = { - type: 'gauge', - pointClass: GaugePoint, - - // chart.angular will be set to true when a gauge series is present, and this will - // be used on the axes - angular: true, - directTouch: true, // #5063 - drawGraph: noop, - fixedBox: true, - forceDL: true, - trackerGroups: ['group', 'dataLabelsGroup'], - - /** - * Calculate paths etc - */ - translate: function () { - - var series = this, - yAxis = series.yAxis, - options = series.options, - center = yAxis.center; - - series.generatePoints(); - - each(series.points, function (point) { - - var dialOptions = merge(options.dial, point.dial), - radius = (pInt(pick(dialOptions.radius, 80)) * center[2]) / 200, - baseLength = (pInt(pick(dialOptions.baseLength, 70)) * radius) / 100, - rearLength = (pInt(pick(dialOptions.rearLength, 10)) * radius) / 100, - baseWidth = dialOptions.baseWidth || 3, - topWidth = dialOptions.topWidth || 1, - overshoot = options.overshoot, - rotation = yAxis.startAngleRad + yAxis.translate(point.y, null, null, null, true); - - // Handle the wrap and overshoot options - if (isNumber(overshoot)) { - overshoot = overshoot / 180 * Math.PI; - rotation = Math.max(yAxis.startAngleRad - overshoot, Math.min(yAxis.endAngleRad + overshoot, rotation)); - - } else if (options.wrap === false) { - rotation = Math.max(yAxis.startAngleRad, Math.min(yAxis.endAngleRad, rotation)); - } - - rotation = rotation * 180 / Math.PI; - - point.shapeType = 'path'; - point.shapeArgs = { - d: dialOptions.path || [ - 'M', - -rearLength, -baseWidth / 2, - 'L', - baseLength, -baseWidth / 2, - radius, -topWidth / 2, - radius, topWidth / 2, - baseLength, baseWidth / 2, - -rearLength, baseWidth / 2, - 'z' - ], - translateX: center[0], - translateY: center[1], - rotation: rotation - }; - - // Positions for data label - point.plotX = center[0]; - point.plotY = center[1]; - }); - }, - - /** - * Draw the points where each point is one needle - */ - drawPoints: function () { - - var series = this, - center = series.yAxis.center, - pivot = series.pivot, - options = series.options, - pivotOptions = options.pivot, - renderer = series.chart.renderer; - - each(series.points, function (point) { - - var graphic = point.graphic, - shapeArgs = point.shapeArgs, - d = shapeArgs.d, - dialOptions = merge(options.dial, point.dial); // #1233 - - if (graphic) { - graphic.animate(shapeArgs); - shapeArgs.d = d; // animate alters it - } else { - point.graphic = renderer[point.shapeType](shapeArgs) - .attr({ - stroke: dialOptions.borderColor || 'none', - 'stroke-width': dialOptions.borderWidth || 0, - fill: dialOptions.backgroundColor || 'black', - rotation: shapeArgs.rotation, // required by VML when animation is false - zIndex: 1 - }) - .add(series.group); - } - }); - - // Add or move the pivot - if (pivot) { - pivot.animate({ // #1235 - translateX: center[0], - translateY: center[1] - }); - } else { - series.pivot = renderer.circle(0, 0, pick(pivotOptions.radius, 5)) - .attr({ - 'stroke-width': pivotOptions.borderWidth || 0, - stroke: pivotOptions.borderColor || 'silver', - fill: pivotOptions.backgroundColor || 'black', - zIndex: 2 - }) - .translate(center[0], center[1]) - .add(series.group); - } - }, - - /** - * Animate the arrow up from startAngle - */ - animate: function (init) { - var series = this; - - if (!init) { - each(series.points, function (point) { - var graphic = point.graphic; - - if (graphic) { - // start value - graphic.attr({ - rotation: series.yAxis.startAngleRad * 180 / Math.PI - }); - - // animate - graphic.animate({ - rotation: point.shapeArgs.rotation - }, series.options.animation); - } - }); - - // delete this function to allow it only once - series.animate = null; - } - }, - - render: function () { - this.group = this.plotGroup( - 'group', - 'series', - this.visible ? 'visible' : 'hidden', - this.options.zIndex, - this.chart.seriesGroup - ); - Series.prototype.render.call(this); - this.group.clip(this.chart.clipRect); - }, - - /** - * Extend the basic setData method by running processData and generatePoints immediately, - * in order to access the points from the legend. - */ - setData: function (data, redraw) { - Series.prototype.setData.call(this, data, false); - this.processData(); - this.generatePoints(); - if (pick(redraw, true)) { - this.chart.redraw(); - } - }, - - /** - * If the tracking module is loaded, add the point tracker - */ - drawTracker: TrackerMixin && TrackerMixin.drawTrackerPoint - }; - seriesTypes.gauge = extendClass(seriesTypes.line, GaugeSeries); - - /* **************************************************************************** - * Start Box plot series code * - *****************************************************************************/ - - // Set default options - defaultPlotOptions.boxplot = merge(defaultPlotOptions.column, { - fillColor: '#FFFFFF', - lineWidth: 1, - //medianColor: null, - medianWidth: 2, - states: { - hover: { - brightness: -0.3 - } - }, - //stemColor: null, - //stemDashStyle: 'solid' - //stemWidth: null, - threshold: null, - tooltip: { - pointFormat: '\u25CF {series.name}
' + - 'Maximum: {point.high}
' + - 'Upper quartile: {point.q3}
' + - 'Median: {point.median}
' + - 'Lower quartile: {point.q1}
' + - 'Minimum: {point.low}
' - - }, - //whiskerColor: null, - whiskerLength: '50%', - whiskerWidth: 2 - }); - - // Create the series object - seriesTypes.boxplot = extendClass(seriesTypes.column, { - type: 'boxplot', - pointArrayMap: ['low', 'q1', 'median', 'q3', 'high'], // array point configs are mapped to this - toYData: function (point) { // return a plain array for speedy calculation - return [point.low, point.q1, point.median, point.q3, point.high]; - }, - pointValKey: 'high', // defines the top of the tracker - - /** - * One-to-one mapping from options to SVG attributes - */ - pointAttrToOptions: { // mapping between SVG attributes and the corresponding options - fill: 'fillColor', - stroke: 'color', - 'stroke-width': 'lineWidth' - }, - - /** - * Disable data labels for box plot - */ - drawDataLabels: noop, - - /** - * Translate data points from raw values x and y to plotX and plotY - */ - translate: function () { - var series = this, - yAxis = series.yAxis, - pointArrayMap = series.pointArrayMap; - - seriesTypes.column.prototype.translate.apply(series); - - // do the translation on each point dimension - each(series.points, function (point) { - each(pointArrayMap, function (key) { - if (point[key] !== null) { - point[key + 'Plot'] = yAxis.translate(point[key], 0, 1, 0, 1); - } - }); - }); - }, - - /** - * Draw the data points - */ - drawPoints: function () { - var series = this, //state = series.state, - points = series.points, - options = series.options, - chart = series.chart, - renderer = chart.renderer, - pointAttr, - q1Plot, - q3Plot, - highPlot, - lowPlot, - medianPlot, - crispCorr, - crispX, - graphic, - stemPath, - stemAttr, - boxPath, - whiskersPath, - whiskersAttr, - medianPath, - medianAttr, - width, - left, - right, - halfWidth, - shapeArgs, - color, - doQuartiles = series.doQuartiles !== false, // error bar inherits this series type but doesn't do quartiles - pointWiskerLength, - whiskerLength = series.options.whiskerLength; - - - each(points, function (point) { - - graphic = point.graphic; - shapeArgs = point.shapeArgs; // the box - stemAttr = {}; - whiskersAttr = {}; - medianAttr = {}; - color = point.color || series.color; - - if (point.plotY !== UNDEFINED) { - - pointAttr = point.pointAttr[point.selected ? 'selected' : '']; - - // crisp vector coordinates - width = shapeArgs.width; - left = mathFloor(shapeArgs.x); - right = left + width; - halfWidth = mathRound(width / 2); - //crispX = mathRound(left + halfWidth) + crispCorr; - q1Plot = mathFloor(doQuartiles ? point.q1Plot : point.lowPlot);// + crispCorr; - q3Plot = mathFloor(doQuartiles ? point.q3Plot : point.lowPlot);// + crispCorr; - highPlot = mathFloor(point.highPlot);// + crispCorr; - lowPlot = mathFloor(point.lowPlot);// + crispCorr; - - // Stem attributes - stemAttr.stroke = point.stemColor || options.stemColor || color; - stemAttr['stroke-width'] = pick(point.stemWidth, options.stemWidth, options.lineWidth); - stemAttr.dashstyle = point.stemDashStyle || options.stemDashStyle; - - // Whiskers attributes - whiskersAttr.stroke = point.whiskerColor || options.whiskerColor || color; - whiskersAttr['stroke-width'] = pick(point.whiskerWidth, options.whiskerWidth, options.lineWidth); - - // Median attributes - medianAttr.stroke = point.medianColor || options.medianColor || color; - medianAttr['stroke-width'] = pick(point.medianWidth, options.medianWidth, options.lineWidth); - - // The stem - crispCorr = (stemAttr['stroke-width'] % 2) / 2; - crispX = left + halfWidth + crispCorr; - stemPath = [ - // stem up - 'M', - crispX, q3Plot, - 'L', - crispX, highPlot, - - // stem down - 'M', - crispX, q1Plot, - 'L', - crispX, lowPlot - ]; - - // The box - if (doQuartiles) { - crispCorr = (pointAttr['stroke-width'] % 2) / 2; - crispX = mathFloor(crispX) + crispCorr; - q1Plot = mathFloor(q1Plot) + crispCorr; - q3Plot = mathFloor(q3Plot) + crispCorr; - left += crispCorr; - right += crispCorr; - boxPath = [ - 'M', - left, q3Plot, - 'L', - left, q1Plot, - 'L', - right, q1Plot, - 'L', - right, q3Plot, - 'L', - left, q3Plot, - 'z' - ]; - } - - // The whiskers - if (whiskerLength) { - crispCorr = (whiskersAttr['stroke-width'] % 2) / 2; - highPlot = highPlot + crispCorr; - lowPlot = lowPlot + crispCorr; - pointWiskerLength = (/%$/).test(whiskerLength) ? halfWidth * parseFloat(whiskerLength) / 100 : whiskerLength / 2; - whiskersPath = [ - // High whisker - 'M', - crispX - pointWiskerLength, - highPlot, - 'L', - crispX + pointWiskerLength, - highPlot, - - // Low whisker - 'M', - crispX - pointWiskerLength, - lowPlot, - 'L', - crispX + pointWiskerLength, - lowPlot - ]; - } - - // The median - crispCorr = (medianAttr['stroke-width'] % 2) / 2; - medianPlot = mathRound(point.medianPlot) + crispCorr; - medianPath = [ - 'M', - left, - medianPlot, - 'L', - right, - medianPlot - ]; - - // Create or update the graphics - if (graphic) { // update - - point.stem.animate({ d: stemPath }); - if (whiskerLength) { - point.whiskers.animate({ d: whiskersPath }); - } - if (doQuartiles) { - point.box.animate({ d: boxPath }); - } - point.medianShape.animate({ d: medianPath }); - - } else { // create new - point.graphic = graphic = renderer.g() - .add(series.group); - - point.stem = renderer.path(stemPath) - .attr(stemAttr) - .add(graphic); - - if (whiskerLength) { - point.whiskers = renderer.path(whiskersPath) - .attr(whiskersAttr) - .add(graphic); - } - if (doQuartiles) { - point.box = renderer.path(boxPath) - .attr(pointAttr) - .add(graphic); - } - point.medianShape = renderer.path(medianPath) - .attr(medianAttr) - .add(graphic); - } - } - }); - - }, - setStackedPoints: noop // #3890 - - - }); - - /* **************************************************************************** - * End Box plot series code * - *****************************************************************************/ - /* **************************************************************************** - * Start error bar series code * - *****************************************************************************/ - - // 1 - set default options - defaultPlotOptions.errorbar = merge(defaultPlotOptions.boxplot, { - color: '#000000', - grouping: false, - linkedTo: ':previous', - tooltip: { - pointFormat: '\u25CF {series.name}: {point.low} - {point.high}
' - }, - whiskerWidth: null - }); - - // 2 - Create the series object - seriesTypes.errorbar = extendClass(seriesTypes.boxplot, { - type: 'errorbar', - pointArrayMap: ['low', 'high'], // array point configs are mapped to this - toYData: function (point) { // return a plain array for speedy calculation - return [point.low, point.high]; - }, - pointValKey: 'high', // defines the top of the tracker - doQuartiles: false, - drawDataLabels: seriesTypes.arearange ? seriesTypes.arearange.prototype.drawDataLabels : noop, - - /** - * Get the width and X offset, either on top of the linked series column - * or standalone - */ - getColumnMetrics: function () { - return (this.linkedParent && this.linkedParent.columnMetrics) || - seriesTypes.column.prototype.getColumnMetrics.call(this); - } - }); - - /* **************************************************************************** - * End error bar series code * - *****************************************************************************/ - /* **************************************************************************** - * Start Waterfall series code * - *****************************************************************************/ - - // 1 - set default options - defaultPlotOptions.waterfall = merge(defaultPlotOptions.column, { - lineWidth: 1, - lineColor: '#333', - dashStyle: 'dot', - borderColor: '#333', - dataLabels: { - inside: true - }, - states: { - hover: { - lineWidthPlus: 0 // #3126 - } - } - }); - - - // 2 - Create the series object - seriesTypes.waterfall = extendClass(seriesTypes.column, { - type: 'waterfall', - - upColorProp: 'fill', - - pointValKey: 'y', - - /** - * Translate data points from raw values - */ - translate: function () { - var series = this, - options = series.options, - yAxis = series.yAxis, - len, - i, - points, - point, - shapeArgs, - stack, - y, - yValue, - previousY, - previousIntermediate, - range, - minPointLength = pick(options.minPointLength, 5), - threshold = options.threshold, - stacking = options.stacking, - tooltipY; - - // run column series translate - seriesTypes.column.prototype.translate.apply(this); - series.minPointLengthOffset = 0; - - previousY = previousIntermediate = threshold; - points = series.points; - - for (i = 0, len = points.length; i < len; i++) { - // cache current point object - point = points[i]; - yValue = this.processedYData[i]; - shapeArgs = point.shapeArgs; - - // get current stack - stack = stacking && yAxis.stacks[(series.negStacks && yValue < threshold ? '-' : '') + series.stackKey]; - range = stack ? - stack[point.x].points[series.index + ',' + i] : - [0, yValue]; - - // override point value for sums - // #3710 Update point does not propagate to sum - if (point.isSum) { - point.y = correctFloat(yValue); - } else if (point.isIntermediateSum) { - point.y = correctFloat(yValue - previousIntermediate); // #3840 - } - // up points - y = mathMax(previousY, previousY + point.y) + range[0]; - shapeArgs.y = yAxis.translate(y, 0, 1); - - - // sum points - if (point.isSum) { - shapeArgs.y = yAxis.translate(range[1], 0, 1); - shapeArgs.height = Math.min(yAxis.translate(range[0], 0, 1), yAxis.len) - shapeArgs.y + series.minPointLengthOffset; // #4256 - - } else if (point.isIntermediateSum) { - shapeArgs.y = yAxis.translate(range[1], 0, 1); - shapeArgs.height = Math.min(yAxis.translate(previousIntermediate, 0, 1), yAxis.len) - shapeArgs.y + series.minPointLengthOffset; - previousIntermediate = range[1]; - - // If it's not the sum point, update previous stack end position and get - // shape height (#3886) - } else { - if (previousY !== 0) { // Not the first point - shapeArgs.height = yValue > 0 ? - yAxis.translate(previousY, 0, 1) - shapeArgs.y : - yAxis.translate(previousY, 0, 1) - yAxis.translate(previousY - yValue, 0, 1); - } - previousY += yValue; - } - // #3952 Negative sum or intermediate sum not rendered correctly - if (shapeArgs.height < 0) { - shapeArgs.y += shapeArgs.height; - shapeArgs.height *= -1; - } - - point.plotY = shapeArgs.y = mathRound(shapeArgs.y) - (series.borderWidth % 2) / 2; - shapeArgs.height = mathMax(mathRound(shapeArgs.height), 0.001); // #3151 - point.yBottom = shapeArgs.y + shapeArgs.height; - - if (shapeArgs.height <= minPointLength) { - shapeArgs.height = minPointLength; - series.minPointLengthOffset += minPointLength; - } - - shapeArgs.y -= series.minPointLengthOffset; - - // Correct tooltip placement (#3014) - tooltipY = point.plotY + (point.negative ? shapeArgs.height : 0) - series.minPointLengthOffset; - if (series.chart.inverted) { - point.tooltipPos[0] = yAxis.len - tooltipY; - } else { - point.tooltipPos[1] = tooltipY; - } - - } - }, - - /** - * Call default processData then override yData to reflect waterfall's extremes on yAxis - */ - processData: function (force) { - var series = this, - options = series.options, - yData = series.yData, - points = series.options.data, // #3710 Update point does not propagate to sum - point, - dataLength = yData.length, - threshold = options.threshold || 0, - subSum, - sum, - dataMin, - dataMax, - y, - i; - - sum = subSum = dataMin = dataMax = threshold; - - for (i = 0; i < dataLength; i++) { - y = yData[i]; - point = points && points[i] ? points[i] : {}; - - if (y === 'sum' || point.isSum) { - yData[i] = correctFloat(sum); - } else if (y === 'intermediateSum' || point.isIntermediateSum) { - yData[i] = correctFloat(subSum); - } else { - sum += y; - subSum += y; - } - dataMin = Math.min(sum, dataMin); - dataMax = Math.max(sum, dataMax); - } - - Series.prototype.processData.call(this, force); - - // Record extremes - series.dataMin = dataMin; - series.dataMax = dataMax; - }, - - /** - * Return y value or string if point is sum - */ - toYData: function (pt) { - if (pt.isSum) { - return (pt.x === 0 ? null : 'sum'); //#3245 Error when first element is Sum or Intermediate Sum - } - if (pt.isIntermediateSum) { - return (pt.x === 0 ? null : 'intermediateSum'); //#3245 - } - return pt.y; - }, - - /** - * Postprocess mapping between options and SVG attributes - */ - getAttribs: function () { - seriesTypes.column.prototype.getAttribs.apply(this, arguments); - - var series = this, - options = series.options, - stateOptions = options.states, - upColor = options.upColor || series.color, - hoverColor = Highcharts.Color(upColor).brighten(0.1).get(), - seriesDownPointAttr = merge(series.pointAttr), - upColorProp = series.upColorProp; - - seriesDownPointAttr[''][upColorProp] = upColor; - seriesDownPointAttr.hover[upColorProp] = stateOptions.hover.upColor || hoverColor; - seriesDownPointAttr.select[upColorProp] = stateOptions.select.upColor || upColor; - - each(series.points, function (point) { - if (!point.options.color) { - // Up color - if (point.y > 0) { - point.pointAttr = seriesDownPointAttr; - point.color = upColor; - - // Down color (#3710, update to negative) - } else { - point.pointAttr = series.pointAttr; - } - } - }); - }, - - /** - * Draw columns' connector lines - */ - getGraphPath: function () { - - var data = this.data, - length = data.length, - lineWidth = this.options.lineWidth + this.borderWidth, - normalizer = mathRound(lineWidth) % 2 / 2, - path = [], - M = 'M', - L = 'L', - prevArgs, - pointArgs, - i, - d; - - for (i = 1; i < length; i++) { - pointArgs = data[i].shapeArgs; - prevArgs = data[i - 1].shapeArgs; - - d = [ - M, - prevArgs.x + prevArgs.width, prevArgs.y + normalizer, - L, - pointArgs.x, prevArgs.y + normalizer - ]; - - if (data[i - 1].y < 0) { - d[2] += prevArgs.height; - d[5] += prevArgs.height; - } - - path = path.concat(d); - } - - return path; - }, - - /** - * Extremes are recorded in processData - */ - getExtremes: noop, - - drawGraph: Series.prototype.drawGraph - }); - - /* **************************************************************************** - * End Waterfall series code * - *****************************************************************************/ - /** - * Set the default options for polygon - */ - defaultPlotOptions.polygon = merge(defaultPlotOptions.scatter, { - marker: { - enabled: false - } - }); - - /** - * The polygon series class - */ - seriesTypes.polygon = extendClass(seriesTypes.scatter, { - type: 'polygon', - fillGraph: true, - // Close all segments - getSegmentPath: function (segment) { - return Series.prototype.getSegmentPath.call(this, segment).concat('z'); - }, - drawGraph: Series.prototype.drawGraph, - drawLegendSymbol: Highcharts.LegendSymbolMixin.drawRectangle - }); - /* **************************************************************************** - * Start Bubble series code * - *****************************************************************************/ - - // 1 - set default options - defaultPlotOptions.bubble = merge(defaultPlotOptions.scatter, { - dataLabels: { - formatter: function () { // #2945 - return this.point.z; - }, - inside: true, - verticalAlign: 'middle' - }, - // displayNegative: true, - marker: { - // fillOpacity: 0.5, - lineColor: null, // inherit from series.color - lineWidth: 1 - }, - minSize: 8, - maxSize: '20%', - // negativeColor: null, - // sizeBy: 'area' - softThreshold: false, - states: { - hover: { - halo: { - size: 5 - } - } - }, - tooltip: { - pointFormat: '({point.x}, {point.y}), Size: {point.z}' - }, - turboThreshold: 0, - zThreshold: 0, - zoneAxis: 'z' - }); - - var BubblePoint = extendClass(Point, { - haloPath: function () { - return Point.prototype.haloPath.call(this, this.shapeArgs.r + this.series.options.states.hover.halo.size); - }, - ttBelow: false - }); - - // 2 - Create the series object - seriesTypes.bubble = extendClass(seriesTypes.scatter, { - type: 'bubble', - pointClass: BubblePoint, - pointArrayMap: ['y', 'z'], - parallelArrays: ['x', 'y', 'z'], - trackerGroups: ['group', 'dataLabelsGroup'], - bubblePadding: true, - zoneAxis: 'z', - - /** - * Mapping between SVG attributes and the corresponding options - */ - pointAttrToOptions: { - stroke: 'lineColor', - 'stroke-width': 'lineWidth', - fill: 'fillColor' - }, - - /** - * Apply the fillOpacity to all fill positions - */ - applyOpacity: function (fill) { - var markerOptions = this.options.marker, - fillOpacity = pick(markerOptions.fillOpacity, 0.5); - - // When called from Legend.colorizeItem, the fill isn't predefined - fill = fill || markerOptions.fillColor || this.color; - - if (fillOpacity !== 1) { - fill = Color(fill).setOpacity(fillOpacity).get('rgba'); - } - return fill; - }, - - /** - * Extend the convertAttribs method by applying opacity to the fill - */ - convertAttribs: function () { - var obj = Series.prototype.convertAttribs.apply(this, arguments); - - obj.fill = this.applyOpacity(obj.fill); - - return obj; - }, - - /** - * Get the radius for each point based on the minSize, maxSize and each point's Z value. This - * must be done prior to Series.translate because the axis needs to add padding in - * accordance with the point sizes. - */ - getRadii: function (zMin, zMax, minSize, maxSize) { - var len, - i, - pos, - zData = this.zData, - radii = [], - options = this.options, - sizeByArea = options.sizeBy !== 'width', - zThreshold = options.zThreshold, - zRange = zMax - zMin, - value, - radius; - - // Set the shape type and arguments to be picked up in drawPoints - for (i = 0, len = zData.length; i < len; i++) { - - value = zData[i]; - - // When sizing by threshold, the absolute value of z determines the size - // of the bubble. - if (options.sizeByAbsoluteValue && value !== null) { - value = Math.abs(value - zThreshold); - zMax = Math.max(zMax - zThreshold, Math.abs(zMin - zThreshold)); - zMin = 0; - } - - if (value === null) { - radius = null; - // Issue #4419 - if value is less than zMin, push a radius that's always smaller than the minimum size - } else if (value < zMin) { - radius = minSize / 2 - 1; - } else { - // Relative size, a number between 0 and 1 - pos = zRange > 0 ? (value - zMin) / zRange : 0.5; - - if (sizeByArea && pos >= 0) { - pos = Math.sqrt(pos); - } - radius = math.ceil(minSize + pos * (maxSize - minSize)) / 2; - } - radii.push(radius); - } - this.radii = radii; - }, - - /** - * Perform animation on the bubbles - */ - animate: function (init) { - var animation = this.options.animation; - - if (!init) { // run the animation - each(this.points, function (point) { - var graphic = point.graphic, - shapeArgs = point.shapeArgs; - - if (graphic && shapeArgs) { - // start values - graphic.attr('r', 1); - - // animate - graphic.animate({ - r: shapeArgs.r - }, animation); - } - }); - - // delete this function to allow it only once - this.animate = null; - } - }, - - /** - * Extend the base translate method to handle bubble size - */ - translate: function () { - - var i, - data = this.data, - point, - radius, - radii = this.radii; - - // Run the parent method - seriesTypes.scatter.prototype.translate.call(this); - - // Set the shape type and arguments to be picked up in drawPoints - i = data.length; - - while (i--) { - point = data[i]; - radius = radii ? radii[i] : 0; // #1737 - - if (isNumber(radius) && radius >= this.minPxSize / 2) { - // Shape arguments - point.shapeType = 'circle'; - point.shapeArgs = { - x: point.plotX, - y: point.plotY, - r: radius - }; - - // Alignment box for the data label - point.dlBox = { - x: point.plotX - radius, - y: point.plotY - radius, - width: 2 * radius, - height: 2 * radius - }; - } else { // below zThreshold or z = null - point.shapeArgs = point.plotY = point.dlBox = UNDEFINED; // #1691 - } - } - }, - - /** - * Get the series' symbol in the legend - * - * @param {Object} legend The legend object - * @param {Object} item The series (this) or point - */ - drawLegendSymbol: function (legend, item) { - var renderer = this.chart.renderer, - radius = renderer.fontMetrics(legend.itemStyle.fontSize).f / 2; - - item.legendSymbol = renderer.circle( - radius, - legend.baseline - radius, - radius - ).attr({ - zIndex: 3 - }).add(item.legendGroup); - item.legendSymbol.isMarker = true; - - }, - - drawPoints: seriesTypes.column.prototype.drawPoints, - alignDataLabel: seriesTypes.column.prototype.alignDataLabel, - buildKDTree: noop, - applyZones: noop - }); - - /** - * Add logic to pad each axis with the amount of pixels - * necessary to avoid the bubbles to overflow. - */ - Axis.prototype.beforePadding = function () { - var axis = this, - axisLength = this.len, - chart = this.chart, - pxMin = 0, - pxMax = axisLength, - isXAxis = this.isXAxis, - dataKey = isXAxis ? 'xData' : 'yData', - min = this.min, - extremes = {}, - smallestSize = math.min(chart.plotWidth, chart.plotHeight), - zMin = Number.MAX_VALUE, - zMax = -Number.MAX_VALUE, - range = this.max - min, - transA = axisLength / range, - activeSeries = []; - - // Handle padding on the second pass, or on redraw - each(this.series, function (series) { - - var seriesOptions = series.options, - zData; - - if (series.bubblePadding && (series.visible || !chart.options.chart.ignoreHiddenSeries)) { - - // Correction for #1673 - axis.allowZoomOutside = true; - - // Cache it - activeSeries.push(series); - - if (isXAxis) { // because X axis is evaluated first - - // For each series, translate the size extremes to pixel values - each(['minSize', 'maxSize'], function (prop) { - var length = seriesOptions[prop], - isPercent = /%$/.test(length); - - length = pInt(length); - extremes[prop] = isPercent ? - smallestSize * length / 100 : - length; - - }); - series.minPxSize = extremes.minSize; - series.maxPxSize = extremes.maxSize; - - // Find the min and max Z - zData = series.zData; - if (zData.length) { // #1735 - zMin = pick(seriesOptions.zMin, math.min( - zMin, - math.max( - arrayMin(zData), - seriesOptions.displayNegative === false ? seriesOptions.zThreshold : -Number.MAX_VALUE - ) - )); - zMax = pick(seriesOptions.zMax, math.max(zMax, arrayMax(zData))); - } - } - } - }); - - each(activeSeries, function (series) { - - var data = series[dataKey], - i = data.length, - radius; - - if (isXAxis) { - series.getRadii(zMin, zMax, series.minPxSize, series.maxPxSize); - } - - if (range > 0) { - while (i--) { - if (isNumber(data[i]) && axis.dataMin <= data[i] && data[i] <= axis.dataMax) { - radius = series.radii[i]; - pxMin = Math.min(((data[i] - min) * transA) - radius, pxMin); - pxMax = Math.max(((data[i] - min) * transA) + radius, pxMax); - } - } - } - }); - - - if (activeSeries.length && range > 0 && !this.isLog) { - pxMax -= axisLength; - transA *= (axisLength + pxMin - pxMax) / axisLength; - each([['min', 'userMin', pxMin], ['max', 'userMax', pxMax]], function (keys) { - if (pick(axis.options[keys[0]], axis[keys[1]]) === UNDEFINED) { - axis[keys[0]] += keys[2] / transA; - } - }); - } - }; - - /* **************************************************************************** - * End Bubble series code * - *****************************************************************************/ - - (function () { - - /** - * Extensions for polar charts. Additionally, much of the geometry required for polar charts is - * gathered in RadialAxes.js. - * - */ - - var seriesProto = Series.prototype, - pointerProto = Pointer.prototype, - colProto; - - /** - * Search a k-d tree by the point angle, used for shared tooltips in polar charts - */ - seriesProto.searchPointByAngle = function (e) { - var series = this, - chart = series.chart, - xAxis = series.xAxis, - center = xAxis.pane.center, - plotX = e.chartX - center[0] - chart.plotLeft, - plotY = e.chartY - center[1] - chart.plotTop; - - return this.searchKDTree({ - clientX: 180 + (Math.atan2(plotX, plotY) * (-180 / Math.PI)) - }); - - }; - - /** - * Wrap the buildKDTree function so that it searches by angle (clientX) in case of shared tooltip, - * and by two dimensional distance in case of non-shared. - */ - wrap(seriesProto, 'buildKDTree', function (proceed) { - if (this.chart.polar) { - if (this.kdByAngle) { - this.searchPoint = this.searchPointByAngle; - } else { - this.kdDimensions = 2; - } - } - proceed.apply(this); - }); - - /** - * Translate a point's plotX and plotY from the internal angle and radius measures to - * true plotX, plotY coordinates - */ - seriesProto.toXY = function (point) { - var xy, - chart = this.chart, - plotX = point.plotX, - plotY = point.plotY, - clientX; - - // Save rectangular plotX, plotY for later computation - point.rectPlotX = plotX; - point.rectPlotY = plotY; - - // Find the polar plotX and plotY - xy = this.xAxis.postTranslate(point.plotX, this.yAxis.len - plotY); - point.plotX = point.polarPlotX = xy.x - chart.plotLeft; - point.plotY = point.polarPlotY = xy.y - chart.plotTop; - - // If shared tooltip, record the angle in degrees in order to align X points. Otherwise, - // use a standard k-d tree to get the nearest point in two dimensions. - if (this.kdByAngle) { - clientX = ((plotX / Math.PI * 180) + this.xAxis.pane.options.startAngle) % 360; - if (clientX < 0) { // #2665 - clientX += 360; - } - point.clientX = clientX; - } else { - point.clientX = point.plotX; - } - }; - - if (seriesTypes.spline) { - /** - * Overridden method for calculating a spline from one point to the next - */ - wrap(seriesTypes.spline.prototype, 'getPointSpline', function (proceed, segment, point, i) { - - var ret, - smoothing = 1.5, // 1 means control points midway between points, 2 means 1/3 from the point, 3 is 1/4 etc; - denom = smoothing + 1, - plotX, - plotY, - lastPoint, - nextPoint, - lastX, - lastY, - nextX, - nextY, - leftContX, - leftContY, - rightContX, - rightContY, - distanceLeftControlPoint, - distanceRightControlPoint, - leftContAngle, - rightContAngle, - jointAngle; - - - if (this.chart.polar) { - - plotX = point.plotX; - plotY = point.plotY; - lastPoint = segment[i - 1]; - nextPoint = segment[i + 1]; - - // Connect ends - if (this.connectEnds) { - if (!lastPoint) { - lastPoint = segment[segment.length - 2]; // not the last but the second last, because the segment is already connected - } - if (!nextPoint) { - nextPoint = segment[1]; - } - } - - // find control points - if (lastPoint && nextPoint) { - - lastX = lastPoint.plotX; - lastY = lastPoint.plotY; - nextX = nextPoint.plotX; - nextY = nextPoint.plotY; - leftContX = (smoothing * plotX + lastX) / denom; - leftContY = (smoothing * plotY + lastY) / denom; - rightContX = (smoothing * plotX + nextX) / denom; - rightContY = (smoothing * plotY + nextY) / denom; - distanceLeftControlPoint = Math.sqrt(Math.pow(leftContX - plotX, 2) + Math.pow(leftContY - plotY, 2)); - distanceRightControlPoint = Math.sqrt(Math.pow(rightContX - plotX, 2) + Math.pow(rightContY - plotY, 2)); - leftContAngle = Math.atan2(leftContY - plotY, leftContX - plotX); - rightContAngle = Math.atan2(rightContY - plotY, rightContX - plotX); - jointAngle = (Math.PI / 2) + ((leftContAngle + rightContAngle) / 2); - - - // Ensure the right direction, jointAngle should be in the same quadrant as leftContAngle - if (Math.abs(leftContAngle - jointAngle) > Math.PI / 2) { - jointAngle -= Math.PI; - } - - // Find the corrected control points for a spline straight through the point - leftContX = plotX + Math.cos(jointAngle) * distanceLeftControlPoint; - leftContY = plotY + Math.sin(jointAngle) * distanceLeftControlPoint; - rightContX = plotX + Math.cos(Math.PI + jointAngle) * distanceRightControlPoint; - rightContY = plotY + Math.sin(Math.PI + jointAngle) * distanceRightControlPoint; - - // Record for drawing in next point - point.rightContX = rightContX; - point.rightContY = rightContY; - - } - - - // moveTo or lineTo - if (!i) { - ret = ['M', plotX, plotY]; - } else { // curve from last point to this - ret = [ - 'C', - lastPoint.rightContX || lastPoint.plotX, - lastPoint.rightContY || lastPoint.plotY, - leftContX || plotX, - leftContY || plotY, - plotX, - plotY - ]; - lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later - } - - - } else { - ret = proceed.call(this, segment, point, i); - } - return ret; - }); - } - - /** - * Extend translate. The plotX and plotY values are computed as if the polar chart were a - * cartesian plane, where plotX denotes the angle in radians and (yAxis.len - plotY) is the pixel distance from - * center. - */ - wrap(seriesProto, 'translate', function (proceed) { - var chart = this.chart, - points, - i; - - // Run uber method - proceed.call(this); - - // Postprocess plot coordinates - if (chart.polar) { - this.kdByAngle = chart.tooltip && chart.tooltip.shared; - - if (!this.preventPostTranslate) { - points = this.points; - i = points.length; - - while (i--) { - // Translate plotX, plotY from angle and radius to true plot coordinates - this.toXY(points[i]); - } - } - } - }); - - /** - * Extend getSegmentPath to allow connecting ends across 0 to provide a closed circle in - * line-like series. - */ - wrap(seriesProto, 'getGraphPath', function (proceed, points) { - var series = this; - - // Connect the path - if (this.chart.polar) { - points = points || this.points; - - if (this.options.connectEnds !== false && points[0] && points[0].y !== null) { - this.connectEnds = true; // re-used in splines - points.splice(points.length, 0, points[0]); - } - - // For area charts, pseudo points are added to the graph, now we need to translate these - each(points, function (point) { - if (point.polarPlotY === undefined) { - series.toXY(point); - } - }); - } - - // Run uber method - return proceed.apply(this, [].slice.call(arguments, 1)); - - }); - - - function polarAnimate(proceed, init) { - var chart = this.chart, - animation = this.options.animation, - group = this.group, - markerGroup = this.markerGroup, - center = this.xAxis.center, - plotLeft = chart.plotLeft, - plotTop = chart.plotTop, - attribs; - - // Specific animation for polar charts - if (chart.polar) { - - // Enable animation on polar charts only in SVG. In VML, the scaling is different, plus animation - // would be so slow it would't matter. - if (chart.renderer.isSVG) { - - if (animation === true) { - animation = {}; - } - - // Initialize the animation - if (init) { - - // Scale down the group and place it in the center - attribs = { - translateX: center[0] + plotLeft, - translateY: center[1] + plotTop, - scaleX: 0.001, // #1499 - scaleY: 0.001 - }; - - group.attr(attribs); - if (markerGroup) { - //markerGroup.attrSetters = group.attrSetters; - markerGroup.attr(attribs); - } - - // Run the animation - } else { - attribs = { - translateX: plotLeft, - translateY: plotTop, - scaleX: 1, - scaleY: 1 - }; - group.animate(attribs, animation); - if (markerGroup) { - markerGroup.animate(attribs, animation); - } - - // Delete this function to allow it only once - this.animate = null; - } - } - - // For non-polar charts, revert to the basic animation - } else { - proceed.call(this, init); - } - } - - // Define the animate method for regular series - wrap(seriesProto, 'animate', polarAnimate); - - - if (seriesTypes.column) { - - colProto = seriesTypes.column.prototype; - - colProto.polarArc = function (low, high, start, end) { - var center = this.xAxis.center, - len = this.yAxis.len; - - return this.chart.renderer.symbols.arc( - center[0], - center[1], - len - high, - null, - { - start: start, - end: end, - innerR: len - pick(low, len) - } - ); - }; - - /** - * Define the animate method for columnseries - */ - wrap(colProto, 'animate', polarAnimate); - - - /** - * Extend the column prototype's translate method - */ - wrap(colProto, 'translate', function (proceed) { - - var xAxis = this.xAxis, - startAngleRad = xAxis.startAngleRad, - start, - points, - point, - i; - - this.preventPostTranslate = true; - - // Run uber method - proceed.call(this); - - // Postprocess plot coordinates - if (xAxis.isRadial) { - points = this.points; - i = points.length; - while (i--) { - point = points[i]; - start = point.barX + startAngleRad; - point.shapeType = 'path'; - point.shapeArgs = { - d: this.polarArc(point.yBottom, point.plotY, start, start + point.pointWidth) - }; - // Provide correct plotX, plotY for tooltip - this.toXY(point); - point.tooltipPos = [point.plotX, point.plotY]; - point.ttBelow = point.plotY > xAxis.center[1]; - } - } - }); - - - /** - * Align column data labels outside the columns. #1199. - */ - wrap(colProto, 'alignDataLabel', function (proceed, point, dataLabel, options, alignTo, isNew) { - - if (this.chart.polar) { - var angle = point.rectPlotX / Math.PI * 180, - align, - verticalAlign; - - // Align nicely outside the perimeter of the columns - if (options.align === null) { - if (angle > 20 && angle < 160) { - align = 'left'; // right hemisphere - } else if (angle > 200 && angle < 340) { - align = 'right'; // left hemisphere - } else { - align = 'center'; // top or bottom - } - options.align = align; - } - if (options.verticalAlign === null) { - if (angle < 45 || angle > 315) { - verticalAlign = 'bottom'; // top part - } else if (angle > 135 && angle < 225) { - verticalAlign = 'top'; // bottom part - } else { - verticalAlign = 'middle'; // left or right - } - options.verticalAlign = verticalAlign; - } - - seriesProto.alignDataLabel.call(this, point, dataLabel, options, alignTo, isNew); - } else { - proceed.call(this, point, dataLabel, options, alignTo, isNew); - } - - }); - } - - /** - * Extend getCoordinates to prepare for polar axis values - */ - wrap(pointerProto, 'getCoordinates', function (proceed, e) { - var chart = this.chart, - ret = { - xAxis: [], - yAxis: [] - }; - - if (chart.polar) { - - each(chart.axes, function (axis) { - var isXAxis = axis.isXAxis, - center = axis.center, - x = e.chartX - center[0] - chart.plotLeft, - y = e.chartY - center[1] - chart.plotTop; - - ret[isXAxis ? 'xAxis' : 'yAxis'].push({ - axis: axis, - value: axis.translate( - isXAxis ? - Math.PI - Math.atan2(x, y) : // angle - Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)), // distance from center - true - ) - }); - }); - - } else { - ret = proceed.call(this, e); - } - - return ret; - }); - - }()); - -})); diff --git a/public/vendor/highcharts-4.2.5/highcharts.js b/public/vendor/highcharts-4.2.5/highcharts.js deleted file mode 100644 index 01498be8e17be..0000000000000 --- a/public/vendor/highcharts-4.2.5/highcharts.js +++ /dev/null @@ -1,343 +0,0 @@ -/* - Highcharts JS v4.2.5 (2016-05-06) - - (c) 2009-2016 Torstein Honsi - - License: www.highcharts.com/license -*/ -(function(D,aa){typeof module==="object"&&module.exports?module.exports=D.document?aa(D):aa:D.Highcharts=aa(D)})(typeof window!=="undefined"?window:this,function(D){function aa(a,b){var c="Highcharts error #"+a+": www.highcharts.com/errors/"+a;if(b)throw Error(c);D.console&&console.log(c)}function pb(a,b,c){this.options=b;this.elem=a;this.prop=c}function E(){var a,b=arguments,c,d={},e=function(a,b){var c,d;typeof a!=="object"&&(a={});for(d in b)b.hasOwnProperty(d)&&(c=b[d],a[d]=c&&typeof c==="object"&& -Object.prototype.toString.call(c)!=="[object Array]"&&d!=="renderTo"&&typeof c.nodeType!=="number"?e(a[d]||{},c):b[d]);return a};b[0]===!0&&(d=b[1],b=Array.prototype.slice.call(b,2));c=b.length;for(a=0;a-1?h.thousandsSep:""))):e=Qa(f,e)}k.push(e);a=a.slice(c+1);c=(d=!d)?"}":"{"}k.push(a);return k.join("")}function rb(a){return Y.pow(10,V(Y.log(a)/Y.LN10))}function sb(a,b,c,d,e){var f,g=a,c=o(c,1);f=a/c;b||(b=[1,2,2.5,5,10],d===!1&&(c===1?b=[1,2,5,10]:c<=0.1&&(b=[1/c])));for(d=0;d=a||!e&&f<=(b[d]+(b[d+1]||b[d]))/2)break;g*=c;return g}function hb(a,b){var c=a.length,d,e;for(e=0;ec&&(c=a[b]);return c}function Ra(a,b){for(var c in a)a[c]&&a[c]!==b&&a[c].destroy&&a[c].destroy(),delete a[c]}function Sa(a){ib||(ib=ba(Ma));a&&ib.appendChild(a);ib.innerHTML=""}function ca(a,b){return parseFloat(a.toPrecision(b||14))}function Ta(a,b){b.renderer.globalAnimation=o(a,b.animation)}function $a(a){return Z(a)? -E(a):{duration:a?500:0}}function Eb(){var a=U.global,b=a.useUTC,c=b?"getUTC":"get",d=b?"setUTC":"set";la=a.Date||D.Date;qb=b&&a.timezoneOffset;Za=b&&a.getTimezoneOffset;jb=function(a,c,d,h,i,k){var j;b?(j=la.UTC.apply(0,arguments),j+=Ya(j)):j=(new la(a,c,o(d,1),o(h,0),o(i,0),o(k,0))).getTime();return j};tb=c+"Minutes";ub=c+"Hours";vb=c+"Day";Ua=c+"Date";ab=c+"Month";bb=c+"FullYear";Fb=d+"Milliseconds";Gb=d+"Seconds";Hb=d+"Minutes";Ib=d+"Hours";kb=d+"Date";wb=d+"Month";xb=d+"FullYear"}function ma(a){if(!(this instanceof -ma))return new ma(a);this.init(a)}function O(){}function Va(a,b,c,d){this.axis=a;this.pos=b;this.type=c||"";this.isNew=!0;!c&&!d&&this.addLabel()}function Jb(a,b,c,d,e){var f=a.chart.inverted;this.axis=a;this.isNegative=c;this.options=b;this.x=d;this.total=null;this.points={};this.stack=e;this.rightCliff=this.leftCliff=0;this.alignOptions={align:b.align||(f?c?"left":"right":"center"),verticalAlign:b.verticalAlign||(f?"middle":c?"bottom":"top"),y:o(b.y,f?4:c?14:-6),x:o(b.x,f?c?-6:6:0)};this.textAlign= -b.textAlign||(f?c?"right":"left":"center")}var y,A=D.document,Y=Math,B=Y.round,V=Y.floor,ua=Y.ceil,t=Y.max,F=Y.min,Q=Y.abs,W=Y.cos,da=Y.sin,ra=Y.PI,ja=ra*2/360,za=D.navigator&&D.navigator.userAgent||"",Kb=D.opera,ya=/(msie|trident|edge)/i.test(za)&&!Kb,lb=A&&A.documentMode===8,mb=!ya&&/AppleWebKit/.test(za),Na=/Firefox/.test(za),Lb=/(Mobile|Android|Windows Phone)/.test(za),Ha="http://www.w3.org/2000/svg",fa=A&&A.createElementNS&&!!A.createElementNS(Ha,"svg").createSVGRect,Pb=Na&&parseInt(za.split("Firefox/")[1], -10)<4,ka=A&&!fa&&!ya&&!!A.createElement("canvas").getContext,cb,db,Mb={},yb=0,ib,U,Qa,H,Aa=function(){},T=[],eb=0,Ma="div",Qb=/^[0-9]+$/,nb=["plotTop","marginRight","marginBottom","plotLeft"],la,jb,qb,Za,tb,ub,vb,Ua,ab,bb,Fb,Gb,Hb,Ib,kb,wb,xb,L={},x;x=D.Highcharts?aa(16,!0):{win:D};x.seriesTypes=L;var Ia=[],na,sa,p,Ba,zb,Ca,N,X,I,Wa,Oa;pb.prototype={dSetter:function(){var a=this.paths[0],b=this.paths[1],c=[],d=this.now,e=a.length,f;if(d===1)c=this.toD;else if(e===b.length&&d<1)for(;e--;)f=parseFloat(a[e]), -c[e]=isNaN(f)?a[e]:d*parseFloat(b[e]-f)+f;else c=b;this.elem.attr("d",c)},update:function(){var a=this.elem,b=this.prop,c=this.now,d=this.options.step;if(this[b+"Setter"])this[b+"Setter"]();else a.attr?a.element&&a.attr(b,c):a.style[b]=c+this.unit;d&&d.call(a,c,this)},run:function(a,b,c){var d=this,e=function(a){return e.stopped?!1:d.step(a)},f;this.startTime=+new la;this.start=a;this.end=b;this.unit=c;this.now=this.start;this.pos=0;e.elem=this.elem;if(e()&&Ia.push(e)===1)e.timerId=setInterval(function(){for(f= -0;f=f+this.startTime){this.now=this.end;this.pos=1;this.update();a=g[this.prop]=!0;for(h in g)g[h]!==!0&&(a=!1);a&&e&&e.call(c);c=!1}else this.pos=d.easing((b-this.startTime)/f),this.now=this.start+(this.end-this.start)*this.pos,this.update(),c=!0;return c},initPath:function(a, -b,c){var b=b||"",d=a.shift,e=b.indexOf("C")>-1,f=e?7:3,g,b=b.split(" "),c=[].concat(c),h=a.isArea,i=h?2:1,k=function(a){for(g=a.length;g--;)(a[g]==="M"||a[g]==="L")&&a.splice(g+1,0,a[g+1],a[g+2],a[g+1],a[g+2])};e&&(k(b),k(c));if(d<=c.length/f&&b.length===c.length)for(;d--;)c=c.slice(0,f).concat(c),h&&(c=c.concat(c.slice(c.length-f)));a.shift=0;if(b.length)for(a=c.length;b.length3?g.length%3:0;c=o(c,e.decimalPoint);d=o(d,e.thousandsSep);a=a<0?"-":"";a+=h?g.substr(0,h)+d:"";a+=g.substr(h).replace(/(\d{3})(?=\d)/g,"$1"+d);b&&(d=Math.abs(i-g+Math.pow(10,-Math.max(b,f)-1)),a+=c+d.toFixed(b).slice(2)); -return a};Math.easeInOutSine=function(a){return-0.5*(Math.cos(Math.PI*a)-1)};na=function(a,b){var c;if(b==="width")return Math.min(a.offsetWidth,a.scrollWidth)-na(a,"padding-left")-na(a,"padding-right");else if(b==="height")return Math.min(a.offsetHeight,a.scrollHeight)-na(a,"padding-top")-na(a,"padding-bottom");return(c=D.getComputedStyle(a,void 0))&&C(c.getPropertyValue(b))};sa=function(a,b){return b.indexOf?b.indexOf(a):[].indexOf.call(b,a)};Ba=function(a,b){return[].filter.call(a,b)};Ca=function(a, -b){for(var c=[],d=0,e=a.length;d-1&&(f.splice(h,1),g[b]=f),d(b,c)):(e(),g[b]=[])):(e(),a.hcEvents={})};I=function(a,b,c,d){var e;e=a.hcEvents;var f,g,c=c||{};if(A.createEvent&&(a.dispatchEvent||a.fireEvent))e=A.createEvent("Events"),e.initEvent(b,!0,!0),e.target=a,u(e,c),a.dispatchEvent?a.dispatchEvent(e):a.fireEvent(b,e);else if(e){e=e[b]||[];f=e.length;if(!c.preventDefault)c.preventDefault=function(){c.defaultPrevented=!0};c.target=a;if(!c.type)c.type=b;for(b=0;b{point.key}
',pointFormat:'\u25cf {series.name}: {point.y}
', -shadow:!0,snap:Lb?25:10,style:{color:"#333333",cursor:"default",fontSize:"12px",padding:"8px",pointerEvents:"none",whiteSpace:"nowrap"}},credits:{enabled:!0,text:"Highcharts.com",href:"http://www.highcharts.com",position:{align:"right",x:-10,verticalAlign:"bottom",y:-5},style:{cursor:"pointer",color:"#909090",fontSize:"9px"}}};var ea=U.plotOptions,ga=ea.line;Eb();ma.prototype={parsers:[{regex:/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/,parse:function(a){return[C(a[1]), -C(a[2]),C(a[3]),parseFloat(a[4],10)]}},{regex:/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/,parse:function(a){return[C(a[1],16),C(a[2],16),C(a[3],16),1]}},{regex:/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/,parse:function(a){return[C(a[1]),C(a[2]),C(a[3]),1]}}],init:function(a){var b,c,d,e;if((this.input=a)&&a.stops)this.stops=Ca(a.stops,function(a){return new ma(a[1])});else for(d=this.parsers.length;d--&&!c;)e=this.parsers[d],(b=e.regex.exec(a))&&(c=e.parse(b));this.rgba= -c||[]},get:function(a){var b=this.input,c=this.rgba,d;this.stops?(d=E(b),d.stops=[].concat(d.stops),p(this.stops,function(b,c){d.stops[c]=[d.stops[c][0],b.get(a)]})):d=c&&J(c[0])?a==="rgb"||!a&&c[3]===1?"rgb("+c[0]+","+c[1]+","+c[2]+")":a==="a"?c[3]:"rgba("+c.join(",")+")":b;return d},brighten:function(a){var b,c=this.rgba;if(this.stops)p(this.stops,function(b){b.brighten(a)});else if(J(a)&&a!==0)for(b=0;b<3;b++)c[b]+=C(a*255),c[b]<0&&(c[b]=0),c[b]>255&&(c[b]=255);return this},setOpacity:function(a){this.rgba[3]= -a;return this}};O.prototype={opacity:1,textProps:"direction,fontSize,fontWeight,fontFamily,fontStyle,color,lineHeight,width,textDecoration,textOverflow,textShadow".split(","),init:function(a,b){this.element=b==="span"?ba(b):A.createElementNS(Ha,b);this.renderer=a},animate:function(a,b,c){b=o(b,this.renderer.globalAnimation,!0);Oa(this);if(b){if(c)b.complete=c;Wa(this,a,b)}else this.attr(a,null,c);return this},colorGradient:function(a,b,c){var d=this.renderer,e,f,g,h,i,k,j,l,m,n,q,z=[],s;a.linearGradient? -f="linearGradient":a.radialGradient&&(f="radialGradient");if(f){g=a[f];i=d.gradients;j=a.stops;n=c.radialReference;Ea(g)&&(a[f]=g={x1:g[0],y1:g[1],x2:g[2],y2:g[3],gradientUnits:"userSpaceOnUse"});f==="radialGradient"&&n&&!r(g.gradientUnits)&&(h=g,g=E(g,d.getRadialAttr(n,h),{gradientUnits:"userSpaceOnUse"}));for(q in g)q!=="id"&&z.push(q,g[q]);for(q in j)z.push(j[q]);z=z.join(",");i[z]?n=i[z].attr("id"):(g.id=n="highcharts-"+yb++,i[z]=k=d.createElement(f).attr(g).add(d.defs),k.radAttr=h,k.stops=[], -p(j,function(a){a[1].indexOf("rgba")===0?(e=ma(a[1]),l=e.get("rgb"),m=e.get("a")):(l=a[1],m=1);a=d.createElement("stop").attr({offset:a[0],"stop-color":l,"stop-opacity":m}).add(k);k.stops.push(a)}));s="url("+d.url+"#"+n+")";c.setAttribute(b,s);c.gradient=z;a.toString=function(){return s}}},applyTextShadow:function(a){var b=this.element,c,d=a.indexOf("contrast")!==-1,e={},f=this.renderer.forExport,g=f||b.style.textShadow!==y&&!ya;if(d)e.textShadow=a=a.replace(/contrast/g,this.renderer.getContrast(b.style.fill)); -if(mb||f)e.textRendering="geometricPrecision";g?this.css(e):(this.fakeTS=!0,this.ySetter=this.xSetter,c=[].slice.call(b.getElementsByTagName("tspan")),p(a.split(/\s?,\s?/g),function(a){var d=b.firstChild,e,f,a=a.split(" ");e=a[a.length-1];(f=a[a.length-2])&&p(c,function(a,c){var g;c===0&&(a.setAttribute("x",b.getAttribute("x")),c=b.getAttribute("y"),a.setAttribute("y",c||0),c===null&&b.setAttribute("y",0));g=a.cloneNode(1);P(g,{"class":"highcharts-text-shadow",fill:e,stroke:e,"stroke-opacity":1/t(C(f), -3),"stroke-width":f,"stroke-linejoin":"round"});b.insertBefore(g,d)})}))},attr:function(a,b,c){var d,e=this.element,f,g=this,h;typeof a==="string"&&b!==y&&(d=a,a={},a[d]=b);if(typeof a==="string")g=(this[a+"Getter"]||this._defaultGetter).call(this,a,e);else{for(d in a){b=a[d];h=!1;this.symbolName&&/^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)/.test(d)&&(f||(this.symbolAttr(a),f=!0),h=!0);if(this.rotation&&(d==="x"||d==="y"))this.doTransform=!0;h||(h=this[d+"Setter"]||this._defaultSetter, -h.call(this,b,d,e),this.shadows&&/^(width|height|visibility|x|y|d|transform|cx|cy|r)$/.test(d)&&this.updateShadows(d,b,h))}if(this.doTransform)this.updateTransform(),this.doTransform=!1}c&&c();return g},updateShadows:function(a,b,c){for(var d=this.shadows,e=d.length;e--;)c.call(d[e],a==="height"?Math.max(b-(d[e].cutHeight||0),0):a==="d"?this.d:b,a,d[e])},addClass:function(a){var b=this.element,c=P(b,"class")||"";c.indexOf(a)===-1&&P(b,"class",c+" "+a);return this},symbolAttr:function(a){var b=this; -p("x,y,r,start,end,width,height,innerR,anchorX,anchorY".split(","),function(c){b[c]=o(a[c],b[c])});b.attr({d:b.renderer.symbols[b.symbolName](b.x,b.y,b.width,b.height,b)})},clip:function(a){return this.attr("clip-path",a?"url("+this.renderer.url+"#"+a.id+")":"none")},crisp:function(a){var b,c={},d,e=this.strokeWidth||0;d=B(e)%2/2;a.x=V(a.x||this.x||0)+d;a.y=V(a.y||this.y||0)+d;a.width=V((a.width||this.width||0)-2*d);a.height=V((a.height||this.height||0)-2*d);a.strokeWidth=e;for(b in a)this[b]!==a[b]&& -(this[b]=c[b]=a[b]);return c},css:function(a){var b=this.styles,c={},d=this.element,e,f,g="";e=!b;if(a&&a.color)a.fill=a.color;if(b)for(f in a)a[f]!==b[f]&&(c[f]=a[f],e=!0);if(e){e=this.textWidth=a&&a.width&&d.nodeName.toLowerCase()==="text"&&C(a.width)||this.textWidth;b&&(a=u(b,c));this.styles=a;e&&(ka||!fa&&this.renderer.forExport)&&delete a.width;if(ya&&!fa)M(this.element,a);else{b=function(a,b){return"-"+b.toLowerCase()};for(f in a)g+=f.replace(/([A-Z])/g,b)+":"+a[f]+";";P(d,"style",g)}e&&this.added&& -this.renderer.buildText(this)}return this},on:function(a,b){var c=this,d=c.element;db&&a==="click"?(d.ontouchstart=function(a){c.touchEventFired=la.now();a.preventDefault();b.call(d,a)},d.onclick=function(a){(za.indexOf("Android")===-1||la.now()-(c.touchEventFired||0)>1100)&&b.call(d,a)}):d["on"+a]=b;return this},setRadialReference:function(a){var b=this.renderer.gradients[this.element.gradient];this.element.radialReference=a;b&&b.radAttr&&b.animate(this.renderer.getRadialAttr(a,b.radAttr));return this}, -translate:function(a,b){return this.attr({translateX:a,translateY:b})},invert:function(){this.inverted=!0;this.updateTransform();return this},updateTransform:function(){var a=this.translateX||0,b=this.translateY||0,c=this.scaleX,d=this.scaleY,e=this.inverted,f=this.rotation,g=this.element;e&&(a+=this.attr("width"),b+=this.attr("height"));a=["translate("+a+","+b+")"];e?a.push("rotate(90) scale(-1,1)"):f&&a.push("rotate("+f+" "+(g.getAttribute("x")||0)+" "+(g.getAttribute("y")||0)+")");(r(c)||r(d))&& -a.push("scale("+o(c,1)+" "+o(d,1)+")");a.length&&g.setAttribute("transform",a.join(" "))},toFront:function(){var a=this.element;a.parentNode.appendChild(a);return this},align:function(a,b,c){var d,e,f,g,h={};e=this.renderer;f=e.alignedObjects;if(a){if(this.alignOptions=a,this.alignByTranslate=b,!c||xa(c))this.alignTo=d=c||"renderer",pa(f,this),f.push(this),c=null}else a=this.alignOptions,b=this.alignByTranslate,d=this.alignTo;c=o(c,e[d],e);d=a.align;e=a.verticalAlign;f=(c.x||0)+(a.x||0);g=(c.y||0)+ -(a.y||0);if(d==="right"||d==="center")f+=(c.width-(a.width||0))/{right:1,center:2}[d];h[b?"translateX":"x"]=B(f);if(e==="bottom"||e==="middle")g+=(c.height-(a.height||0))/({bottom:1,middle:2}[e]||1);h[b?"translateY":"y"]=B(g);this[this.placed?"animate":"attr"](h);this.placed=!0;this.alignAttr=h;return this},getBBox:function(a,b){var c,d=this.renderer,e,f,g,h=this.element,i=this.styles;e=this.textStr;var k,j=h.style,l,m=d.cache,n=d.cacheKeys,q;f=o(b,this.rotation);g=f*ja;e!==y&&(q=["",f||0,i&&i.fontSize, -h.style.width].join(","),q=e===""||Qb.test(e)?"num:"+e.toString().length+q:e+q);q&&!a&&(c=m[q]);if(!c){if(h.namespaceURI===Ha||d.forExport){try{l=this.fakeTS&&function(a){p(h.querySelectorAll(".highcharts-text-shadow"),function(b){b.style.display=a})},Na&&j.textShadow?(k=j.textShadow,j.textShadow=""):l&&l("none"),c=h.getBBox?u({},h.getBBox()):{width:h.offsetWidth,height:h.offsetHeight},k?j.textShadow=k:l&&l("")}catch(z){}if(!c||c.width<0)c={width:0,height:0}}else c=this.htmlGetBBox();if(d.isSVG){d= -c.width;e=c.height;if(ya&&i&&i.fontSize==="11px"&&e.toPrecision(3)==="16.9")c.height=e=14;if(f)c.width=Q(e*da(g))+Q(d*W(g)),c.height=Q(e*W(g))+Q(d*da(g))}if(q){for(;n.length>250;)delete m[n.shift()];m[q]||n.push(q);m[q]=c}}return c},show:function(a){return this.attr({visibility:a?"inherit":"visible"})},hide:function(){return this.attr({visibility:"hidden"})},fadeOut:function(a){var b=this;b.animate({opacity:0},{duration:a||150,complete:function(){b.attr({y:-9999})}})},add:function(a){var b=this.renderer, -c=this.element,d;if(a)this.parentGroup=a;this.parentInverted=a&&a.inverted;this.textStr!==void 0&&b.buildText(this);this.added=!0;if(!a||a.handleZ||this.zIndex)d=this.zIndexSetter();d||(a?a.element:b.box).appendChild(c);if(this.onAdd)this.onAdd();return this},safeRemoveChild:function(a){var b=a.parentNode;b&&b.removeChild(a)},destroy:function(){var a=this,b=a.element||{},c=a.shadows,d=a.renderer.isSVG&&b.nodeName==="SPAN"&&a.parentGroup,e,f;b.onclick=b.onmouseout=b.onmouseover=b.onmousemove=b.point= -null;Oa(a);if(a.clipPath)a.clipPath=a.clipPath.destroy();if(a.stops){for(f=0;f]*>/g,"")))},textSetter:function(a){if(a!==this.textStr)delete this.bBox,this.textStr=a,this.added&&this.renderer.buildText(this)},fillSetter:function(a,b,c){typeof a==="string"?c.setAttribute(b,a):a&&this.colorGradient(a,b,c)},visibilitySetter:function(a,b,c){a==="inherit"?c.removeAttribute(b):c.setAttribute(b,a)},zIndexSetter:function(a,b){var c=this.renderer,d=this.parentGroup,c=(d||c).element||c.box,e,f,g= -this.element,h;e=this.added;var i;if(r(a))g.zIndex=a,a=+a,this[b]===a&&(e=!1),this[b]=a;if(e){if((a=this.zIndex)&&d)d.handleZ=!0;d=c.childNodes;for(i=0;ia||!r(a)&&r(f)))c.insertBefore(g,e),h=!0;h||c.appendChild(g)}return h},_defaultSetter:function(a,b,c){c.setAttribute(b,a)}};O.prototype.yGetter=O.prototype.xGetter;O.prototype.translateXSetter=O.prototype.translateYSetter=O.prototype.rotationSetter=O.prototype.verticalAlignSetter=O.prototype.scaleXSetter= -O.prototype.scaleYSetter=function(a,b){this[b]=a;this.doTransform=!0};O.prototype["stroke-widthSetter"]=O.prototype.strokeSetter=function(a,b,c){this[b]=a;if(this.stroke&&this["stroke-width"])this.strokeWidth=this["stroke-width"],O.prototype.fillSetter.call(this,this.stroke,"stroke",c),c.setAttribute("stroke-width",this["stroke-width"]),this.hasStroke=!0;else if(b==="stroke-width"&&a===0&&this.hasStroke)c.removeAttribute("stroke"),this.hasStroke=!1};var Da=function(){this.init.apply(this,arguments)}; -Da.prototype={Element:O,init:function(a,b,c,d,e,f){var g,d=this.createElement("svg").attr({version:"1.1"}).css(this.getStyle(d));g=d.element;a.appendChild(g);a.innerHTML.indexOf("xmlns")===-1&&P(g,"xmlns",Ha);this.isSVG=!0;this.box=g;this.boxWrapper=d;this.alignedObjects=[];this.url=(Na||mb)&&A.getElementsByTagName("base").length?D.location.href.replace(/#.*?$/,"").replace(/([\('\)])/g,"\\$1").replace(/ /g,"%20"):"";this.createElement("desc").add().element.appendChild(A.createTextNode("Created with Highcharts 4.2.5")); -this.defs=this.createElement("defs").add();this.allowHTML=f;this.forExport=e;this.gradients={};this.cache={};this.cacheKeys=[];this.imgCount=0;this.setSize(b,c,!1);var h;if(Na&&a.getBoundingClientRect)this.subPixelFix=b=function(){M(a,{left:0,top:0});h=a.getBoundingClientRect();M(a,{left:ua(h.left)-h.left+"px",top:ua(h.top)-h.top+"px"})},b(),N(D,"resize",b)},getStyle:function(a){return this.style=u({fontFamily:'"Lucida Grande", "Lucida Sans Unicode", Arial, Helvetica, sans-serif',fontSize:"12px"}, -a)},isHidden:function(){return!this.boxWrapper.getBBox().width},destroy:function(){var a=this.defs;this.box=null;this.boxWrapper=this.boxWrapper.destroy();Ra(this.gradients||{});this.gradients=null;if(a)this.defs=a.destroy();this.subPixelFix&&X(D,"resize",this.subPixelFix);return this.alignedObjects=null},createElement:function(a){var b=new this.Element;b.init(this,a);return b},draw:function(){},getRadialAttr:function(a,b){return{cx:a[0]-a[2]/2+b.cx*a[2],cy:a[1]-a[2]/2+b.cy*a[2],r:b.r*a[2]}},buildText:function(a){for(var b= -a.element,c=this,d=c.forExport,e=o(a.textStr,"").toString(),f=e.indexOf("<")!==-1,g=b.childNodes,h,i,k,j=P(b,"x"),l=a.styles,m=a.textWidth,n=l&&l.lineHeight,q=l&&l.textShadow,z=l&&l.textOverflow==="ellipsis",s=g.length,G=m&&!a.added&&this.box,w=function(a){return n?C(n):c.fontMetrics(/(px|em)$/.test(a&&a.style.fontSize)?a.style.fontSize:l&&l.fontSize||c.style.fontSize||12,a).h},v=function(a){return a.replace(/</g,"<").replace(/>/g,">")};s--;)b.removeChild(g[s]);!f&&!q&&!z&&e.indexOf(" ")=== --1?b.appendChild(A.createTextNode(v(e))):(h=/<.*style="([^"]+)".*>/,i=/<.*href="(http[^"]+)".*>/,G&&G.appendChild(b),e=f?e.replace(/<(b|strong)>/g,'').replace(/<(i|em)>/g,'').replace(/
/g,"").split(//g):[e],e=Ba(e,function(a){return a!==""}),p(e,function(e,f){var g,n=0,e=e.replace(/^\s+|\s+$/g,"").replace(//g,"|||");g=e.split("|||");p(g, -function(e){if(e!==""||g.length===1){var q={},s=A.createElementNS(Ha,"tspan"),o;h.test(e)&&(o=e.match(h)[1].replace(/(;| |^)color([ :])/,"$1fill$2"),P(s,"style",o));i.test(e)&&!d&&(P(s,"onclick",'location.href="'+e.match(i)[1]+'"'),M(s,{cursor:"pointer"}));e=v(e.replace(/<(.|\n)*?>/g,"")||" ");if(e!==" "){s.appendChild(A.createTextNode(e));if(n)q.dx=0;else if(f&&j!==null)q.x=j;P(s,q);b.appendChild(s);!n&&f&&(!fa&&d&&M(s,{display:"block"}),P(s,"dy",w(s)));if(m){for(var q=e.replace(/([^\^])-/g,"$1- ").split(" "), -p=g.length>1||f||q.length>1&&l.whiteSpace!=="nowrap",G,S,r=[],t=w(s),u=1,B=a.rotation,y=e,x=y.length;(p||z)&&(q.length||r.length);)a.rotation=0,G=a.getBBox(!0),S=G.width,!fa&&c.forExport&&(S=c.measureSpanWidth(s.firstChild.data,a.styles)),G=S>m,k===void 0&&(k=G),z&&k?(x/=2,y===""||!G&&x<0.5?q=[]:(y=e.substring(0,y.length+(G?-1:1)*ua(x)),q=[y+(m>3?"\u2026":"")],s.removeChild(s.firstChild))):!G||q.length===1?(q=r,r=[],q.length&&(u++,s=A.createElementNS(Ha,"tspan"),P(s,{dy:t,x:j}),o&&P(s,"style",o), -b.appendChild(s)),S>m&&(m=S)):(s.removeChild(s.firstChild),r.unshift(q.pop())),q.length&&s.appendChild(A.createTextNode(q.join(" ").replace(/- /g,"-")));a.rotation=B}n++}}})}),k&&a.attr("title",a.textStr),G&&G.removeChild(b),q&&a.applyTextShadow&&a.applyTextShadow(q))},getContrast:function(a){a=ma(a).rgba;return a[0]+a[1]+a[2]>384?"#000000":"#FFFFFF"},button:function(a,b,c,d,e,f,g,h,i){var k=this.label(a,b,c,i,null,null,null,null,"button"),j=0,l,m,n,q,z,s,a={x1:0,y1:0,x2:0,y2:1},e=E({"stroke-width":1, -stroke:"#CCCCCC",fill:{linearGradient:a,stops:[[0,"#FEFEFE"],[1,"#F6F6F6"]]},r:2,padding:5,style:{color:"black"}},e);n=e.style;delete e.style;f=E(e,{stroke:"#68A",fill:{linearGradient:a,stops:[[0,"#FFF"],[1,"#ACF"]]}},f);q=f.style;delete f.style;g=E(e,{stroke:"#68A",fill:{linearGradient:a,stops:[[0,"#9BD"],[1,"#CDF"]]}},g);z=g.style;delete g.style;h=E(e,{style:{color:"#CCC"}},h);s=h.style;delete h.style;N(k.element,ya?"mouseover":"mouseenter",function(){j!==3&&k.attr(f).css(q)});N(k.element,ya?"mouseout": -"mouseleave",function(){j!==3&&(l=[e,f,g][j],m=[n,q,z][j],k.attr(l).css(m))});k.setState=function(a){(k.state=j=a)?a===2?k.attr(g).css(z):a===3&&k.attr(h).css(s):k.attr(e).css(n)};return k.on("click",function(a){j!==3&&d.call(k,a)}).attr(e).css(u({cursor:"default"},n))},crispLine:function(a,b){a[1]===a[4]&&(a[1]=a[4]=B(a[1])-b%2/2);a[2]===a[5]&&(a[2]=a[5]=B(a[2])+b%2/2);return a},path:function(a){var b={fill:"none"};Ea(a)?b.d=a:Z(a)&&u(b,a);return this.createElement("path").attr(b)},circle:function(a, -b,c){a=Z(a)?a:{x:a,y:b,r:c};b=this.createElement("circle");b.xSetter=b.ySetter=function(a,b,c){c.setAttribute("c"+b,a)};return b.attr(a)},arc:function(a,b,c,d,e,f){if(Z(a))b=a.y,c=a.r,d=a.innerR,e=a.start,f=a.end,a=a.x;a=this.symbol("arc",a||0,b||0,c||0,c||0,{innerR:d||0,start:e||0,end:f||0});a.r=c;return a},rect:function(a,b,c,d,e,f){var e=Z(a)?a.r:e,g=this.createElement("rect"),a=Z(a)?a:a===y?{}:{x:a,y:b,width:t(c,0),height:t(d,0)};if(f!==y)g.strokeWidth=f,a=g.crisp(a);if(e)a.r=e;g.rSetter=function(a, -b,c){P(c,{rx:a,ry:a})};return g.attr(a)},setSize:function(a,b,c){var d=this.alignedObjects,e=d.length;this.width=a;this.height=b;for(this.boxWrapper[o(c,!0)?"animate":"attr"]({width:a,height:b});e--;)d[e].align()},g:function(a){var b=this.createElement("g");return r(a)?b.attr({"class":"highcharts-"+a}):b},image:function(a,b,c,d,e){var f={preserveAspectRatio:"none"};arguments.length>1&&u(f,{x:b,y:c,width:d,height:e});f=this.createElement("image").attr(f);f.element.setAttributeNS?f.element.setAttributeNS("http://www.w3.org/1999/xlink", -"href",a):f.element.setAttribute("hc-svg-href",a);return f},symbol:function(a,b,c,d,e,f){var g=this,h,i=this.symbols[a],i=i&&i(B(b),B(c),d,e,f),k=/^url\((.*?)\)$/,j,l;if(i)h=this.path(i),u(h,{symbolName:a,x:b,y:c,width:d,height:e}),f&&u(h,f);else if(k.test(a))l=function(a,b){a.element&&(a.attr({width:b[0],height:b[1]}),a.alignByTranslate||a.translate(B((d-b[0])/2),B((e-b[1])/2)))},j=a.match(k)[1],a=Mb[j]||f&&f.width&&f.height&&[f.width,f.height],h=this.image(j).attr({x:b,y:c}),h.isImg=!0,a?l(h,a): -(h.attr({width:0,height:0}),ba("img",{onload:function(){this.width===0&&(M(this,{position:"absolute",top:"-999em"}),A.body.appendChild(this));l(h,Mb[j]=[this.width,this.height]);this.parentNode&&this.parentNode.removeChild(this);g.imgCount--;if(!g.imgCount&&T[g.chartIndex].onload)T[g.chartIndex].onload()},src:j}),this.imgCount++);return h},symbols:{circle:function(a,b,c,d){var e=0.166*c;return["M",a+c/2,b,"C",a+c+e,b,a+c+e,b+d,a+c/2,b+d,"C",a-e,b+d,a-e,b,a+c/2,b,"Z"]},square:function(a,b,c,d){return["M", -a,b,"L",a+c,b,a+c,b+d,a,b+d,"Z"]},triangle:function(a,b,c,d){return["M",a+c/2,b,"L",a+c,b+d,a,b+d,"Z"]},"triangle-down":function(a,b,c,d){return["M",a,b,"L",a+c,b,a+c/2,b+d,"Z"]},diamond:function(a,b,c,d){return["M",a+c/2,b,"L",a+c,b+d/2,a+c/2,b+d,a,b+d/2,"Z"]},arc:function(a,b,c,d,e){var f=e.start,c=e.r||c||d,g=e.end-0.001,d=e.innerR,h=e.open,i=W(f),k=da(f),j=W(g),g=da(g),e=e.end-fc&&e>b+g&&eb+g&&ed&&h>a+g&&ha+g&&hj&&/[ \-]/.test(b.textContent||b.innerText))M(b,{width:j+"px",display:"block",whiteSpace:l||"normal"}),this.hasTextWidth=!0;else if(this.hasTextWidth)M(b,{width:"",display:"",whiteSpace:l||"nowrap"}),this.hasTextWidth=!1;this.getSpanCorrection(this.hasTextWidth?j:b.offsetWidth,k,h,i,g)}M(b,{left:e+(this.xCorr||0)+"px",top:f+(this.yCorr||0)+"px"});if(mb)k=b.offsetHeight; -this.cTT=m}}else this.alignOnAdd=!0},setSpanRotation:function(a,b,c){var d={},e=ya?"-ms-transform":mb?"-webkit-transform":Na?"MozTransform":Kb?"-o-transform":"";d[e]=d.transform="rotate("+a+"deg)";d[e+(Na?"Origin":"-origin")]=d.transformOrigin=b*100+"% "+c+"px";M(this.element,d)},getSpanCorrection:function(a,b,c){this.xCorr=-a*c;this.yCorr=-b}});u(Da.prototype,{html:function(a,b,c){var d=this.createElement("span"),e=d.element,f=d.renderer,g=f.isSVG,h=function(a,b){p(["opacity","visibility"],function(c){fb(a, -c+"Setter",function(a,c,d,e){a.call(this,c,d,e);b[d]=c})})};d.textSetter=function(a){a!==e.innerHTML&&delete this.bBox;e.innerHTML=this.textStr=a;d.htmlUpdateTransform()};g&&h(d,d.element.style);d.xSetter=d.ySetter=d.alignSetter=d.rotationSetter=function(a,b){b==="align"&&(b="textAlign");d[b]=a;d.htmlUpdateTransform()};d.attr({text:a,x:B(b),y:B(c)}).css({position:"absolute",fontFamily:this.style.fontFamily,fontSize:this.style.fontSize});e.style.whiteSpace="nowrap";d.css=d.htmlCss;if(g)d.add=function(a){var b, -c=f.box.parentNode,g=[];if(this.parentGroup=a){if(b=a.div,!b){for(;a;)g.push(a),a=a.parentGroup;p(g.reverse(),function(a){var d,e=P(a.element,"class");e&&(e={className:e});b=a.div=a.div||ba(Ma,e,{position:"absolute",left:(a.translateX||0)+"px",top:(a.translateY||0)+"px",opacity:a.opacity},b||c);d=b.style;u(a,{translateXSetter:function(b,c){d.left=b+"px";a[c]=b;a.doTransform=!0},translateYSetter:function(b,c){d.top=b+"px";a[c]=b;a.doTransform=!0}});h(a,d)})}}else b=c;b.appendChild(e);d.added=!0;d.alignOnAdd&& -d.htmlUpdateTransform();return d};return d}});var K;if(!fa&&!ka){K={init:function(a,b){var c=["<",b,' filled="f" stroked="f"'],d=["position: ","absolute",";"],e=b===Ma;(b==="shape"||e)&&d.push("left:0;top:0;width:1px;height:1px;");d.push("visibility: ",e?"hidden":"visible");c.push(' style="',d.join(""),'"/>');if(b)c=e||b==="span"||b==="img"?c.join(""):a.prepVML(c),this.element=ba(c);this.renderer=a},add:function(a){var b=this.renderer,c=this.element,d=b.box,e=a&&a.inverted,d=a?a.element||a:d;if(a)this.parentGroup= -a;e&&b.invertChild(c,d);d.appendChild(c);this.added=!0;this.alignOnAdd&&!this.deferUpdateTransform&&this.updateTransform();if(this.onAdd)this.onAdd();return this},updateTransform:O.prototype.htmlUpdateTransform,setSpanRotation:function(){var a=this.rotation,b=W(a*ja),c=da(a*ja);M(this.element,{filter:a?["progid:DXImageTransform.Microsoft.Matrix(M11=",b,", M12=",-c,", M21=",c,", M22=",b,", sizingMethod='auto expand')"].join(""):"none"})},getSpanCorrection:function(a,b,c,d,e){var f=d?W(d*ja):1,g=d? -da(d*ja):0,h=o(this.elemHeight,this.element.offsetHeight),i;this.xCorr=f<0&&-a;this.yCorr=g<0&&-h;i=f*g<0;this.xCorr+=g*b*(i?1-c:c);this.yCorr-=f*b*(d?i?c:1-c:1);e&&e!=="left"&&(this.xCorr-=a*c*(f<0?-1:1),d&&(this.yCorr-=h*c*(g<0?-1:1)),M(this.element,{textAlign:e}))},pathToVML:function(a){for(var b=a.length,c=[];b--;)if(J(a[b]))c[b]=B(a[b]*10)-5;else if(a[b]==="Z")c[b]="x";else if(c[b]=a[b],a.isArc&&(a[b]==="wa"||a[b]==="at"))c[b+5]===c[b+7]&&(c[b+7]+=a[b+7]>a[b+5]?1:-1),c[b+6]===c[b+8]&&(c[b+8]+= -a[b+8]>a[b+6]?1:-1);return c.join(" ")||"x"},clip:function(a){var b=this,c;a?(c=a.members,pa(c,b),c.push(b),b.destroyClip=function(){pa(c,b)},a=a.getCSS(b)):(b.destroyClip&&b.destroyClip(),a={clip:lb?"inherit":"rect(auto)"});return b.css(a)},css:O.prototype.htmlCss,safeRemoveChild:function(a){a.parentNode&&Sa(a)},destroy:function(){this.destroyClip&&this.destroyClip();return O.prototype.destroy.apply(this)},on:function(a,b){this.element["on"+a]=function(){var a=D.event;a.target=a.srcElement;b(a)}; -return this},cutOffPath:function(a,b){var c,a=a.split(/[ ,]/);c=a.length;if(c===9||c===11)a[c-4]=a[c-2]=C(a[c-2])-10*b;return a.join(" ")},shadow:function(a,b,c){var d=[],e,f=this.element,g=this.renderer,h,i=f.style,k,j=f.path,l,m,n,q;j&&typeof j.value!=="string"&&(j="x");m=j;if(a){n=o(a.width,3);q=(a.opacity||0.15)/n;for(e=1;e<=3;e++){l=n*2+1-2*e;c&&(m=this.cutOffPath(j.value,l+0.5));k=[''];h=ba(g.prepVML(k),null,{left:C(i.left)+o(a.offsetX,1),top:C(i.top)+o(a.offsetY,1)});if(c)h.cutOff=l+1;k=[''];ba(g.prepVML(k),null,null,h);b?b.element.appendChild(h):f.parentNode.insertBefore(h,f);d.push(h)}this.shadows=d}return this},updateShadows:Aa,setAttr:function(a,b){lb?this.element[a]=b:this.element.setAttribute(a,b)},classSetter:function(a){this.element.className=a},dashstyleSetter:function(a,b,c){(c.getElementsByTagName("stroke")[0]|| -ba(this.renderer.prepVML([""]),null,null,c))[b]=a||"solid";this[b]=a},dSetter:function(a,b,c){var d=this.shadows,a=a||[];this.d=a.join&&a.join(" ");c.path=a=this.pathToVML(a);if(d)for(c=d.length;c--;)d[c].path=d[c].cutOff?this.cutOffPath(a,d[c].cutOff):a;this.setAttr(b,a)},fillSetter:function(a,b,c){var d=c.nodeName;if(d==="SPAN")c.style.color=a;else if(d!=="IMG")c.filled=a!=="none",this.setAttr("fillcolor",this.renderer.color(a,c,b,this))},"fill-opacitySetter":function(a,b,c){ba(this.renderer.prepVML(["<", -b.split("-")[0],' opacity="',a,'"/>']),null,null,c)},opacitySetter:Aa,rotationSetter:function(a,b,c){c=c.style;this[b]=c[b]=a;c.left=-B(da(a*ja)+1)+"px";c.top=B(W(a*ja))+"px"},strokeSetter:function(a,b,c){this.setAttr("strokecolor",this.renderer.color(a,c,b,this))},"stroke-widthSetter":function(a,b,c){c.stroked=!!a;this[b]=a;J(a)&&(a+="px");this.setAttr("strokeweight",a)},titleSetter:function(a,b){this.setAttr(b,a)},visibilitySetter:function(a,b,c){a==="inherit"&&(a="visible");this.shadows&&p(this.shadows, -function(c){c.style[b]=a});c.nodeName==="DIV"&&(a=a==="hidden"?"-999em":0,lb||(c.style[b]=a?"visible":"hidden"),b="top");c.style[b]=a},xSetter:function(a,b,c){this[b]=a;b==="x"?b="left":b==="y"&&(b="top");this.updateClipping?(this[b]=a,this.updateClipping()):c.style[b]=a},zIndexSetter:function(a,b,c){c.style[b]=a}};K["stroke-opacitySetter"]=K["fill-opacitySetter"];x.VMLElement=K=qa(O,K);K.prototype.ySetter=K.prototype.widthSetter=K.prototype.heightSetter=K.prototype.xSetter;var Bb={Element:K,isIE8:za.indexOf("MSIE 8.0")> --1,init:function(a,b,c,d){var e;this.alignedObjects=[];d=this.createElement(Ma).css(u(this.getStyle(d),{position:"relative"}));e=d.element;a.appendChild(d.element);this.isVML=!0;this.box=e;this.boxWrapper=d;this.gradients={};this.cache={};this.cacheKeys=[];this.imgCount=0;this.setSize(b,c,!1);if(!A.namespaces.hcv){A.namespaces.add("hcv","urn:schemas-microsoft-com:vml");try{A.createStyleSheet().cssText="hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke{ behavior:url(#default#VML); display: inline-block; } "}catch(f){A.styleSheets[0].cssText+= -"hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke{ behavior:url(#default#VML); display: inline-block; } "}}},isHidden:function(){return!this.box.offsetWidth},clipRect:function(a,b,c,d){var e=this.createElement(),f=Z(a);return u(e,{members:[],count:0,left:(f?a.x:a)+1,top:(f?a.y:b)+1,width:(f?a.width:c)-1,height:(f?a.height:d)-1,getCSS:function(a){var b=a.element,c=b.nodeName,a=a.inverted,d=this.top-(c==="shape"?b.offsetTop:0),e=this.left,b=e+this.width,f=d+this.height,d={clip:"rect("+B(a?e:d)+"px,"+ -B(a?f:b)+"px,"+B(a?b:f)+"px,"+B(a?d:e)+"px)"};!a&&lb&&c==="DIV"&&u(d,{width:b+"px",height:f+"px"});return d},updateClipping:function(){p(e.members,function(a){a.element&&a.css(e.getCSS(a))})}})},color:function(a,b,c,d){var e=this,f,g=/^rgba/,h,i,k="none";a&&a.linearGradient?i="gradient":a&&a.radialGradient&&(i="pattern");if(i){var j,l,m=a.linearGradient||a.radialGradient,n,q,z,s,o,w="",a=a.stops,v,S=[],r=function(){h=[''];ba(e.prepVML(h),null,null,b)};n=a[0];v=a[a.length-1];n[0]>0&&a.unshift([0,n[1]]);v[0]<1&&a.push([1,v[1]]);p(a,function(a,b){g.test(a[1])?(f=ma(a[1]),j=f.get("rgb"),l=f.get("a")):(j=a[1],l=1);S.push(a[0]*100+"% "+j);b?(z=l,s=j):(q=l,o=j)});if(c==="fill")if(i==="gradient")c=m.x1||m[0]||0,a=m.y1||m[1]||0,n=m.x2||m[2]||0,m=m.y2||m[3]||0,w='angle="'+(90-Y.atan((m-a)/(n-c))*180/ra)+'"',r();else{var k=m.r,$=k*2,t=k*2,u=m.cx,y=m.cy,B=b.radialReference,x,k=function(){B&&(x= -d.getBBox(),u+=(B[0]-x.x)/x.width-0.5,y+=(B[1]-x.y)/x.height-0.5,$*=B[2]/x.width,t*=B[2]/x.height);w='src="'+U.global.VMLRadialGradientURL+'" size="'+$+","+t+'" origin="0.5,0.5" position="'+u+","+y+'" color2="'+o+'" ';r()};d.added?k():d.onAdd=k;k=s}else k=j}else if(g.test(a)&&b.tagName!=="IMG")f=ma(a),d[c+"-opacitySetter"](f.get("a"),c,b),k=f.get("rgb");else{k=b.getElementsByTagName(c);if(k.length)k[0].opacity=1,k[0].type="solid";k=a}return k},prepVML:function(a){var b=this.isIE8,a=a.join("");b?(a= -a.replace("/>",' xmlns="urn:schemas-microsoft-com:vml" />'),a=a.indexOf('style="')===-1?a.replace("/>",' style="display:inline-block;behavior:url(#default#VML);" />'):a.replace('style="','style="display:inline-block;behavior:url(#default#VML);')):a=a.replace("<","1&&f.attr({x:b,y:c,width:d,height:e});return f},createElement:function(a){return a==="rect"?this.symbol(a):Da.prototype.createElement.call(this,a)},invertChild:function(a,b){var c=this,d=b.style,e=a.tagName==="IMG"&&a.style;M(a,{flip:"x",left:C(d.width)-(e?C(e.top): -1),top:C(d.height)-(e?C(e.left):1),rotation:-90});p(a.childNodes,function(b){c.invertChild(b,a)})},symbols:{arc:function(a,b,c,d,e){var f=e.start,g=e.end,h=e.r||c||d,c=e.innerR,d=W(f),i=da(f),k=W(g),j=da(g);if(g-f===0)return["x"];f=["wa",a-h,b-h,a+h,b+h,a+h*d,b+h*i,a+h*k,b+h*j];e.open&&!c&&f.push("e","M",a,b);f.push("at",a-c,b-c,a+c,b+c,a+c*k,b+c*j,a+c*d,b+c*i,"x","e");f.isArc=!0;return f},circle:function(a,b,c,d,e){e&&(c=d=2*e.r);e&&e.isCircle&&(a-=c/2,b-=d/2);return["wa",a,b,a+c,b+d,a+c,b+d/2,a+ -c,b+d/2,"e"]},rect:function(a,b,c,d,e){return Da.prototype.symbols[!r(e)||!e.r?"square":"callout"].call(0,a,b,c,d,e)}}};x.VMLRenderer=K=function(){this.init.apply(this,arguments)};K.prototype=E(Da.prototype,Bb);cb=K}Da.prototype.measureSpanWidth=function(a,b){var c=A.createElement("span"),d;d=A.createTextNode(a);c.appendChild(d);M(c,b);this.box.appendChild(c);d=c.offsetWidth;Sa(c);return d};var Nb;if(ka)x.CanVGRenderer=K=function(){Ha="http://www.w3.org/1999/xhtml"},K.prototype.symbols={},Nb=function(){function a(){var a= -b.length,d;for(d=0;d0&&c+i*k>e&&(n=B((d-c)/W(h*ja)));else if(d=c+(1-i)*k,c-i*ke&&(l=e-a.x+l*i,m=-1),l=F(j,l),ll||b.autoRotation&&g.styles.width)n=l;if(n){q.width=n;if(!b.options.labels.style.textOverflow)q.textOverflow= -"ellipsis";g.css(q)}},getPosition:function(a,b,c,d){var e=this.axis,f=e.chart,g=d&&f.oldChartHeight||f.chartHeight;return{x:a?e.translate(b+c,null,null,d)+e.transB:e.left+e.offset+(e.opposite?(d&&f.oldChartWidth||f.chartWidth)-e.right-e.left:0),y:a?g-e.bottom+e.offset-(e.opposite?e.height:0):g-e.translate(b+c,null,null,d)-e.transB}},getLabelPosition:function(a,b,c,d,e,f,g,h){var i=this.axis,k=i.transA,j=i.reversed,l=i.staggerLines,m=i.tickRotCorr||{x:0,y:0},n=e.y;r(n)||(n=i.side===0?c.rotation?-8: --c.getBBox().height:i.side===2?m.y+8:W(c.rotation*ja)*(m.y-c.getBBox(!1,0).height/2));a=a+e.x+m.x-(f&&d?f*k*(j?-1:1):0);b=b+n-(f&&!d?f*k*(j?1:-1):0);l&&(c=g/(h||1)%l,i.opposite&&(c=l-c-1),b+=c*(i.labelOffset/l));return{x:a,y:B(b)}},getMarkPath:function(a,b,c,d,e,f){return f.crispLine(["M",a,b,"L",a+(e?0:-c),b+(e?c:0)],d)},render:function(a,b,c){var d=this.axis,e=d.options,f=d.chart.renderer,g=d.horiz,h=this.type,i=this.label,k=this.pos,j=e.labels,l=this.gridLine,m=h?h+"Grid":"grid",n=h?h+"Tick":"tick", -q=e[m+"LineWidth"],z=e[m+"LineColor"],s=e[m+"LineDashStyle"],m=d.tickSize(n),n=e[n+"Color"],p=this.mark,w=j.step,v=!0,S=d.tickmarkOffset,r=this.getPosition(g,k,S,b),$=r.x,r=r.y,t=g&&$===d.pos+d.len||!g&&r===d.pos?-1:1,c=o(c,1);this.isActive=!0;if(q){k=d.getPlotLinePath(k+S,q*t,b,!0);if(l===y){l={stroke:z,"stroke-width":q};if(s)l.dashstyle=s;if(!h)l.zIndex=1;if(b)l.opacity=0;this.gridLine=l=q?f.path(k).attr(l).add(d.gridGroup):null}if(!b&&l&&k)l[this.isNew?"attr":"animate"]({d:k,opacity:c})}if(m)d.opposite&& -(m[0]=-m[0]),h=this.getMarkPath($,r,m[0],m[1]*t,g,f),p?p.animate({d:h,opacity:c}):this.mark=f.path(h).attr({stroke:n,"stroke-width":m[1],opacity:c}).add(d.axisGroup);if(i&&J($))i.xy=r=this.getLabelPosition($,r,i,g,j,S,a,w),this.isFirst&&!this.isLast&&!o(e.showFirstLabel,1)||this.isLast&&!this.isFirst&&!o(e.showLastLabel,1)?v=!1:g&&!d.isRadial&&!j.step&&!j.rotation&&!b&&c!==0&&this.handleOverflow(r),w&&a%w&&(v=!1),v&&J(r.y)?(r.opacity=c,i[this.isNew?"attr":"animate"](r),this.isNew=!1):i.attr("y",-9999)}, -destroy:function(){Ra(this,this.axis)}};x.PlotLineOrBand=function(a,b){this.axis=a;if(b)this.options=b,this.id=b.id};x.PlotLineOrBand.prototype={render:function(){var a=this,b=a.axis,c=b.horiz,d=a.options,e=d.label,f=a.label,g=d.width,h=d.to,i=d.from,k=r(i)&&r(h),j=d.value,l=d.dashStyle,m=a.svgElem,n=[],q,z=d.color,s=o(d.zIndex,0),p=d.events,w={},v=b.chart.renderer,n=b.log2lin;b.isLog&&(i=n(i),h=n(h),j=n(j));if(g){if(n=b.getPlotLinePath(j,g),w={stroke:z,"stroke-width":g},l)w.dashstyle=l}else if(k){n= -b.getPlotBandPath(i,h,d);if(z)w.fill=z;if(d.borderWidth)w.stroke=d.borderColor,w["stroke-width"]=d.borderWidth}else return;w.zIndex=s;if(m)if(n)m.show(),m.animate({d:n});else{if(m.hide(),f)a.label=f=f.destroy()}else if(n&&n.length&&(a.svgElem=m=v.path(n).attr(w).add(),p))for(q in d=function(b){m.on(b,function(c){p[b].apply(a,[c])})},p)d(q);e&&r(e.text)&&n&&n.length&&b.width>0&&b.height>0&&!n.flat?(e=E({align:c&&k&&"center",x:c?!k&&4:10,verticalAlign:!c&&k&&"middle",y:c?k?16:10:k?6:-4,rotation:c&& -!k&&90},e),this.renderLabel(e,n,k,s)):f&&f.hide();return a},renderLabel:function(a,b,c,d){var e=this.label,f=this.axis.chart.renderer;if(!e)e={align:a.textAlign||a.align,rotation:a.rotation},e.zIndex=d,this.label=e=f.text(a.text,0,0,a.useHTML).attr(e).css(a.style).add();d=[b[1],b[4],c?b[6]:b[1]];b=[b[2],b[5],c?b[7]:b[2]];c=La(d);f=La(b);e.align(a,!1,{x:c,y:f,width:Ga(d)-c,height:Ga(b)-f});e.show()},destroy:function(){pa(this.axis.plotLinesAndBands,this);delete this.axis;Ra(this)}};var ha=x.Axis=function(){this.init.apply(this, -arguments)};ha.prototype={defaultOptions:{dateTimeLabelFormats:{millisecond:"%H:%M:%S.%L",second:"%H:%M:%S",minute:"%H:%M",hour:"%H:%M",day:"%e. %b",week:"%e. %b",month:"%b '%y",year:"%Y"},endOnTick:!1,gridLineColor:"#D8D8D8",labels:{enabled:!0,style:{color:"#606060",cursor:"default",fontSize:"11px"},x:0},lineColor:"#C0D0E0",lineWidth:1,minPadding:0.01,maxPadding:0.01,minorGridLineColor:"#E0E0E0",minorGridLineWidth:1,minorTickColor:"#A0A0A0",minorTickLength:2,minorTickPosition:"outside",startOfWeek:1, -startOnTick:!1,tickColor:"#C0D0E0",tickLength:10,tickmarkPlacement:"between",tickPixelInterval:100,tickPosition:"outside",title:{align:"middle",style:{color:"#707070"}},type:"linear"},defaultYAxisOptions:{endOnTick:!0,gridLineWidth:1,tickPixelInterval:72,showLastLabel:!0,labels:{x:-8},lineWidth:0,maxPadding:0.05,minPadding:0.05,startOnTick:!0,title:{rotation:270,text:"Values"},stackLabels:{enabled:!1,formatter:function(){return x.numberFormat(this.total,-1)},style:E(ea.line.dataLabels.style,{color:"#000000"})}}, -defaultLeftAxisOptions:{labels:{x:-15},title:{rotation:270}},defaultRightAxisOptions:{labels:{x:15},title:{rotation:90}},defaultBottomAxisOptions:{labels:{autoRotation:[-45],x:0},title:{rotation:0}},defaultTopAxisOptions:{labels:{autoRotation:[-45],x:0},title:{rotation:0}},init:function(a,b){var c=b.isX;this.chart=a;this.horiz=a.inverted?!c:c;this.coll=(this.isXAxis=c)?"xAxis":"yAxis";this.opposite=b.opposite;this.side=b.side||(this.horiz?this.opposite?0:2:this.opposite?1:3);this.setOptions(b);var d= -this.options,e=d.type;this.labelFormatter=d.labels.formatter||this.defaultLabelFormatter;this.userOptions=b;this.minPixelPadding=0;this.reversed=d.reversed;this.visible=d.visible!==!1;this.zoomEnabled=d.zoomEnabled!==!1;this.categories=d.categories||e==="category";this.names=this.names||[];this.isLog=e==="logarithmic";this.isDatetimeAxis=e==="datetime";this.isLinked=r(d.linkedTo);this.ticks={};this.labelEdge=[];this.minorTicks={};this.plotLinesAndBands=[];this.alternateBands={};this.len=0;this.minRange= -this.userMinRange=d.minRange||d.maxZoom;this.range=d.range;this.offset=d.offset||0;this.stacks={};this.oldStacks={};this.stacksTouched=0;this.min=this.max=null;this.crosshair=o(d.crosshair,ta(a.options.tooltip.crosshairs)[c?0:1],!1);var f,d=this.options.events;sa(this,a.axes)===-1&&(c&&!this.isColorAxis?a.axes.splice(a.xAxis.length,0,this):a.axes.push(this),a[this.coll].push(this));this.series=this.series||[];if(a.inverted&&c&&this.reversed===y)this.reversed=!0;this.removePlotLine=this.removePlotBand= -this.removePlotBandOrLine;for(f in d)N(this,f,d[f]);if(this.isLog)this.val2lin=this.log2lin,this.lin2val=this.lin2log},setOptions:function(a){this.options=E(this.defaultOptions,this.isXAxis?{}:this.defaultYAxisOptions,[this.defaultTopAxisOptions,this.defaultRightAxisOptions,this.defaultBottomAxisOptions,this.defaultLeftAxisOptions][this.side],E(U[this.coll],a))},defaultLabelFormatter:function(){var a=this.axis,b=this.value,c=a.categories,d=this.dateTimeLabelFormat,e=U.lang.numericSymbols,f=e&&e.length, -g,h=a.options.labels.format,a=a.isLog?b:a.tickInterval;if(h)g=Ka(h,this);else if(c)g=b;else if(d)g=Qa(d,b);else if(f&&a>=1E3)for(;f--&&g===y;)c=Math.pow(1E3,f+1),a>=c&&b*10%c===0&&e[f]!==null&&(g=x.numberFormat(b/c,-1)+e[f]);g===y&&(g=Q(b)>=1E4?x.numberFormat(b,-1):x.numberFormat(b,-1,y,""));return g},getSeriesExtremes:function(){var a=this,b=a.chart;a.hasVisibleSeries=!1;a.dataMin=a.dataMax=a.threshold=null;a.softThreshold=!a.isXAxis;a.buildStacks&&a.buildStacks();p(a.series,function(c){if(c.visible|| -!b.options.chart.ignoreHiddenSeries){var d=c.options,e=d.threshold,f;a.hasVisibleSeries=!0;a.isLog&&e<=0&&(e=null);if(a.isXAxis){if(d=c.xData,d.length)c=La(d),!J(c)&&!(c instanceof la)&&(d=Ba(d,function(a){return J(a)}),c=La(d)),a.dataMin=F(o(a.dataMin,d[0]),c),a.dataMax=t(o(a.dataMax,d[0]),Ga(d))}else{c.getExtremes();f=c.dataMax;c=c.dataMin;if(r(c)&&r(f))a.dataMin=F(o(a.dataMin,c),c),a.dataMax=t(o(a.dataMax,f),f);if(r(e))a.threshold=e;if(!d.softThreshold||a.isLog)a.softThreshold=!1}}})},translate:function(a, -b,c,d,e,f){var g=this.linkedParent||this,h=1,i=0,k=d?g.oldTransA:g.transA,d=d?g.oldMin:g.min,j=g.minPixelPadding,e=(g.isOrdinal||g.isBroken||g.isLog&&e)&&g.lin2val;if(!k)k=g.transA;if(c)h*=-1,i=g.len;g.reversed&&(h*=-1,i-=h*(g.sector||g.len));b?(a=a*h+i,a-=j,a=a/k+d,e&&(a=g.lin2val(a))):(e&&(a=g.val2lin(a)),f==="between"&&(f=0.5),a=h*(a-d)*k+i+h*j+(J(f)?k*f*g.pointRange:0));return a},toPixels:function(a,b){return this.translate(a,!1,!this.horiz,null,!0)+(b?0:this.pos)},toValue:function(a,b){return this.translate(a- -(b?0:this.pos),!0,!this.horiz,null,!0)},getPlotLinePath:function(a,b,c,d,e){var f=this.chart,g=this.left,h=this.top,i,k,j=c&&f.oldChartHeight||f.chartHeight,l=c&&f.oldChartWidth||f.chartWidth,m;i=this.transB;var n=function(a,b,c){if(ac)d?a=F(t(b,a),c):m=!0;return a},e=o(e,this.translate(a,null,null,c)),a=c=B(e+i);i=k=B(j-e-i);J(e)?this.horiz?(i=h,k=j-this.bottom,a=c=n(a,g,g+this.width)):(a=g,c=l-this.right,i=k=n(i,h,h+this.height)):m=!0;return m&&!d?null:f.renderer.crispLine(["M",a,i,"L",c, -k],b||1)},getLinearTickPositions:function(a,b,c){var d,e=ca(V(b/a)*a),f=ca(ua(c/a)*a),g=[];if(b===c&&J(b))return[b];for(b=e;b<=f;){g.push(b);b=ca(b+a);if(b===d)break;d=b}return g},getMinorTickPositions:function(){var a=this.options,b=this.tickPositions,c=this.minorTickInterval,d=[],e,f=this.pointRangePadding||0;e=this.min-f;var f=this.max+f,g=f-e;if(g&&g/c=this.minRange,f,g,h,i,k,j;if(this.isXAxis&&this.minRange===y&&!this.isLog)r(a.min)||r(a.max)?this.minRange=null:(p(this.series,function(a){i=a.xData;for(g=k=a.xIncrement?1:i.length-1;g>0;g--)if(h=i[g]- -i[g-1],f===y||h=q?(s=q,j=0):b.dataMax<=q&&(G=q,k=0)),b.min=o(w,s,b.dataMin),b.max=o(v,G,b.dataMax));if(e)!a&&F(b.min,o(b.dataMin,b.min))<=0&&aa(10,1),b.min=ca(f(b.min),15),b.max=ca(f(b.max),15);if(b.range&&r(b.max))b.userMin=b.min=w=t(b.min,b.minFromRange()),b.userMax=v=b.max,b.range=null;I(b,"foundExtremes");b.beforePadding&&b.beforePadding();b.adjustForMinRange();if(!n&&!b.axisPointRange&&!b.usePercentage&& -!i&&r(b.min)&&r(b.max)&&(f=b.max-b.min))!r(w)&&j&&(b.min-=f*j),!r(v)&&k&&(b.max+=f*k);if(J(d.floor))b.min=t(b.min,d.floor);if(J(d.ceiling))b.max=F(b.max,d.ceiling);if(z&&r(b.dataMin))if(q=q||0,!r(w)&&b.min=q)b.min=q;else if(!r(v)&&b.max>q&&b.dataMax<=q)b.max=q;b.tickInterval=b.min===b.max||b.min===void 0||b.max===void 0?1:i&&!l&&m===b.linkedParent.options.tickPixelInterval?l=b.linkedParent.tickInterval:o(l,this.tickAmount?(b.max-b.min)/t(this.tickAmount-1,1):void 0,n?1:(b.max-b.min)* -m/t(b.len,m));h&&!a&&p(b.series,function(a){a.processData(b.min!==b.oldMin||b.max!==b.oldMax)});b.setAxisTranslation(!0);b.beforeSetTickPositions&&b.beforeSetTickPositions();if(b.postProcessTickInterval)b.tickInterval=b.postProcessTickInterval(b.tickInterval);if(b.pointRange&&!l)b.tickInterval=t(b.pointRange,b.tickInterval);a=o(d.minTickInterval,b.isDatetimeAxis&&b.closestPointRange);if(!l&&b.tickInterval0.5&&b.tickInterval<5&&b.max>1E3&&b.max<9999)),!!this.tickAmount);if(!this.tickAmount&&this.len)b.tickInterval=b.unsquish();this.setTickPositions()},setTickPositions:function(){var a=this.options,b,c=a.tickPositions,d=a.tickPositioner,e=a.startOnTick,f=a.endOnTick,g;this.tickmarkOffset=this.categories&&a.tickmarkPlacement==="between"&&this.tickInterval===1?0.5:0;this.minorTickInterval=a.minorTickInterval==="auto"&&this.tickInterval?this.tickInterval/5:a.minorTickInterval;this.tickPositions= -b=c&&c.slice();if(!b&&(b=this.isDatetimeAxis?this.getTimeTicks(this.normalizeTimeTickInterval(this.tickInterval,a.units),this.min,this.max,a.startOfWeek,this.ordinalPositions,this.closestPointRange,!0):this.isLog?this.getLogTickPositions(this.tickInterval,this.min,this.max):this.getLinearTickPositions(this.tickInterval,this.min,this.max),b.length>this.len&&(b=[b[0],b.pop()]),this.tickPositions=b,d&&(d=d.apply(this,[this.min,this.max]))))this.tickPositions=b=d;if(!this.isLinked)this.trimTicks(b,e, -f),this.min===this.max&&r(this.min)&&!this.tickAmount&&(g=!0,this.min-=0.5,this.max+=0.5),this.single=g,!c&&!d&&this.adjustTickAmount()},trimTicks:function(a,b,c){var d=a[0],e=a[a.length-1],f=this.minPointOffset||0;if(b)this.min=d;else for(;this.min-f>a[0];)a.shift();if(c)this.max=e;else for(;this.max+fc&&(this.tickInterval*=2,this.setTickPositions());if(r(d)){for(a=c=b.length;a--;)(d===3&&a%2===1||d<=2&&a>0&&a=e&&(b=e));this.displayBtn=a!==y||b!==y;this.setExtremes(a,b,!1,y,{trigger:"zoom"});return!0},setAxisSize:function(){var a=this.chart,b=this.options,c=b.offsetLeft||0,d=this.horiz,e=o(b.width,a.plotWidth-c+(b.offsetRight||0)),f=o(b.height, -a.plotHeight),g=o(b.top,a.plotTop),b=o(b.left,a.plotLeft+c),c=/%$/;c.test(f)&&(f=Math.round(parseFloat(f)/100*a.plotHeight));c.test(g)&&(g=Math.round(parseFloat(g)/100*a.plotHeight+a.plotTop));this.left=b;this.top=g;this.width=e;this.height=f;this.bottom=a.chartHeight-f-g;this.right=a.chartWidth-e-b;this.len=t(d?e:f,0);this.pos=d?b:g},getExtremes:function(){var a=this.isLog,b=this.lin2log;return{min:a?ca(b(this.min)):this.min,max:a?ca(b(this.max)):this.max,dataMin:this.dataMin,dataMax:this.dataMax, -userMin:this.userMin,userMax:this.userMax}},getThreshold:function(a){var b=this.isLog,c=this.lin2log,d=b?c(this.min):this.min,b=b?c(this.max):this.max;a===null?a=b<0?b:d:d>a?a=d:b15&&a<165?"right":a>195&&a<345?"left":"center"},tickSize:function(a){var b=this.options,c=b[a+"Length"],d=o(b[a+"Width"],a==="tick"&&this.isXAxis?1:0);if(d&&c)return b[a+"Position"]==="inside"&&(c=-c),[c,d]},labelMetrics:function(){return this.chart.renderer.fontMetrics(this.options.labels.style.fontSize, -this.ticks[0]&&this.ticks[0].label)},unsquish:function(){var a=this.options.labels,b=this.horiz,c=this.tickInterval,d=c,e=this.len/(((this.categories?1:0)+this.max-this.min)/c),f,g=a.rotation,h=this.labelMetrics(),i,k=Number.MAX_VALUE,j,l=function(a){a/=e||1;a=a>1?ua(a):1;return a*c};b?(j=!a.staggerLines&&!a.step&&(r(g)?[g]:e=-90&&a<=90)i=l(Q(h.h/da(ja*a))),b=i+Q(a/360),bm)m=a.labelLength}),m>h&&m>k.h?i.rotation=this.labelRotation:this.labelRotation=0;else if(g&&(l={width:h+"px"},!j)){l.textOverflow="clip";for(n=c.length;!f&&n--;)if(q=c[n],h=d[q].label)if(h.styles.textOverflow==="ellipsis"?h.css({textOverflow:"clip"}):d[q].labelLength>g&&h.css({width:g+"px"}),h.getBBox().height>this.len/c.length-(k.h-k.f))h.specCss={textOverflow:"ellipsis"}}if(i.rotation&& -(l={width:(m>a.chartHeight*0.5?a.chartHeight*0.33:a.chartHeight)+"px"},!j))l.textOverflow="ellipsis";if(this.labelAlign=e.align||this.autoLabelAlign(this.labelRotation))i.align=this.labelAlign;p(c,function(a){var b=(a=d[a])&&a.label;if(b)b.attr(i),l&&b.css(E(l,b.specCss)),delete b.specCss,a.rotation=i.rotation});this.tickRotCorr=b.rotCorr(k.b,this.labelRotation||0,this.side!==0)},hasData:function(){return this.hasVisibleSeries||r(this.min)&&r(this.max)&&!!this.tickPositions},getOffset:function(){var a= -this,b=a.chart,c=b.renderer,d=a.options,e=a.tickPositions,f=a.ticks,g=a.horiz,h=a.side,i=b.inverted?[1,0,3,2][h]:h,k,j,l=0,m,n=0,q=d.title,z=d.labels,s=0,G=a.opposite,w=b.axisOffset,b=b.clipOffset,v=[-1,1,1,-1][h],S,u=a.axisParent,$=this.tickSize("tick");k=a.hasData();a.showAxis=j=k||o(d.showEmpty,!0);a.staggerLines=a.horiz&&z.staggerLines;if(!a.axisGroup)a.gridGroup=c.g("grid").attr({zIndex:d.gridZIndex||1}).add(u),a.axisGroup=c.g("axis").attr({zIndex:d.zIndex||2}).add(u),a.labelGroup=c.g("axis-labels").attr({zIndex:z.zIndex|| -7}).addClass("highcharts-"+a.coll.toLowerCase()+"-labels").add(u);if(k||a.isLinked){if(p(e,function(b){f[b]?f[b].addLabel():f[b]=new Va(a,b)}),a.renderUnsquish(),z.reserveSpace!==!1&&(h===0||h===2||{1:"left",3:"right"}[h]===a.labelAlign||a.labelAlign==="center")&&p(e,function(a){s=t(f[a].getLabelSize(),s)}),a.staggerLines)s*=a.staggerLines,a.labelOffset=s*(a.opposite?-1:1)}else for(S in f)f[S].destroy(),delete f[S];if(q&&q.text&&q.enabled!==!1){if(!a.axisTitle)(S=q.textAlign)||(S=(g?{low:"left",middle:"center", -high:"right"}:{low:G?"right":"left",middle:"center",high:G?"left":"right"})[q.align]),a.axisTitle=c.text(q.text,0,0,q.useHTML).attr({zIndex:7,rotation:q.rotation||0,align:S}).addClass("highcharts-"+this.coll.toLowerCase()+"-title").css(q.style).add(a.axisGroup),a.axisTitle.isNew=!0;if(j)l=a.axisTitle.getBBox()[g?"height":"width"],m=q.offset,n=r(m)?0:o(q.margin,g?5:10);a.axisTitle[j?"show":"hide"](!0)}a.offset=v*o(d.offset,w[h]);a.tickRotCorr=a.tickRotCorr||{x:0,y:0};c=h===0?-a.labelMetrics().h:h=== -2?a.tickRotCorr.y:0;n=Math.abs(s)+n;s&&(n-=c,n+=v*(g?o(z.y,a.tickRotCorr.y+v*8):z.x));a.axisTitleMargin=o(m,n);w[h]=t(w[h],a.axisTitleMargin+l+v*a.offset,n,k&&e.length&&$?$[0]:0);d=d.offset?0:V(d.lineWidth/2)*2;b[i]=t(b[i],d)},getLinePath:function(a){var b=this.chart,c=this.opposite,d=this.offset,e=this.horiz,f=this.left+(c?this.width:0)+d,d=b.chartHeight-this.bottom-(c?this.height:0)+d;c&&(a*=-1);return b.renderer.crispLine(["M",e?this.left:f,e?d:this.top,"L",e?b.chartWidth-this.right:f,e?d:b.chartHeight- -this.bottom],a)},getTitlePosition:function(){var a=this.horiz,b=this.left,c=this.top,d=this.len,e=this.options.title,f=a?b:c,g=this.opposite,h=this.offset,i=e.x||0,k=e.y||0,j=C(e.style.fontSize||12),d={low:f+(a?0:d),middle:f+d/2,high:f+(a?d:0)}[e.align],b=(a?c+this.height:b)+(a?1:-1)*(g?-1:1)*this.axisTitleMargin+(this.side===2?j:0);return{x:a?d+i:b+(g?this.width:0)+h+i,y:a?b+k-(g?this.height:0)+h:d+k}},render:function(){var a=this,b=a.chart,c=b.renderer,d=a.options,e=a.isLog,f=a.lin2log,g=a.isLinked, -h=a.tickPositions,i=a.axisTitle,k=a.ticks,j=a.minorTicks,l=a.alternateBands,m=d.stackLabels,n=d.alternateGridColor,q=a.tickmarkOffset,z=d.lineWidth,s,o=b.hasRendered&&J(a.oldMin),w=a.showAxis,v=$a(c.globalAnimation),r,t;a.labelEdge.length=0;a.overlap=!1;p([k,j,l],function(a){for(var b in a)a[b].isActive=!1});if(a.hasData()||g){a.minorTickInterval&&!a.categories&&p(a.getMinorTickPositions(),function(b){j[b]||(j[b]=new Va(a,b,"minor"));o&&j[b].isNew&&j[b].render(null,!0);j[b].render(null,!1,1)});if(h.length&& -(p(h,function(b,c){if(!g||b>=a.min&&b<=a.max)k[b]||(k[b]=new Va(a,b)),o&&k[b].isNew&&k[b].render(c,!0,0.1),k[b].render(c)}),q&&(a.min===0||a.single)))k[-1]||(k[-1]=new Va(a,-1,null,!0)),k[-1].render(-1);n&&p(h,function(c,d){t=h[d+1]!==y?h[d+1]+q:a.max-q;if(d%2===0&&c=H.second?0:j*V(i.getMilliseconds()/j));if(k>=H.second)i[Gb](k>=H.minute?0:j*V(i.getSeconds()/j));if(k>=H.minute)i[Hb](k>=H.hour?0:j*V(i[tb]()/j));if(k>=H.hour)i[Ib](k>=H.day?0:j*V(i[ub]()/j)); -if(k>=H.day)i[kb](k>=H.month?1:j*V(i[Ua]()/j));k>=H.month&&(i[wb](k>=H.year?0:j*V(i[ab]()/j)),h=i[bb]());k>=H.year&&(h-=h%j,i[xb](h));if(k===H.week)i[kb](i[Ua]()-i[vb]()+o(d,1));b=1;if(qb||Za)i=i.getTime(),i=new la(i+Ya(i));h=i[bb]();for(var d=i.getTime(),l=i[ab](),m=i[Ua](),n=!g||!!Za,q=(H.day+(g?Ya(i):i.getTimezoneOffset()*6E4))%H.day;d=0.5)a=B(a),i=this.getLinearTickPositions(a,b,c);else if(a>=0.08)for(var f=V(b),k,j,l,m,n,e=a>0.3?[1,2,4]:a>0.15?[1,2,4,6,8]:[1,2,3,4,5,6,7,8,9];fb&&(!d||m<=c)&&m!==y&&i.push(m),m> -c&&(n=!0),m=l}else if(b=g(b),c=g(c),a=e[d?"minorTickInterval":"tickInterval"],a=o(a==="auto"?null:a,this._minorAutoInterval,(c-b)*(e.tickPixelInterval/(d?5:1))/((d?f/this.tickPositions.length:f)||1)),a=sb(a,null,rb(a)),i=Ca(this.getLinearTickPositions(a,b,c),h),!d)this._minorAutoInterval=a/5;if(!d)this.tickInterval=a;return i};ha.prototype.log2lin=function(a){return Y.log(a)/Y.LN10};ha.prototype.lin2log=function(a){return Y.pow(10,a)};var Ob=x.Tooltip=function(){this.init.apply(this,arguments)};Ob.prototype= -{init:function(a,b){var c=b.borderWidth,d=b.style,e=C(d.padding);this.chart=a;this.options=b;this.crosshairs=[];this.now={x:0,y:0};this.isHidden=!0;this.label=a.renderer.label("",0,0,b.shape||"callout",null,null,b.useHTML,null,"tooltip").attr({padding:e,fill:b.backgroundColor,"stroke-width":c,r:b.borderRadius,zIndex:8}).css(d).css({padding:0}).add().attr({y:-9999});ka||this.label.shadow(b.shadow);this.shared=b.shared},destroy:function(){if(this.label)this.label=this.label.destroy();clearTimeout(this.hideTimer); -clearTimeout(this.tooltipTimeout)},move:function(a,b,c,d){var e=this,f=e.now,g=e.options.animation!==!1&&!e.isHidden&&(Q(a-f.x)>1||Q(b-f.y)>1),h=e.followPointer||e.len>1;u(f,{x:g?(2*f.x+a)/3:a,y:g?(f.y+b)/2:b,anchorX:h?y:g?(2*f.anchorX+c)/3:c,anchorY:h?y:g?(f.anchorY+d)/2:d});e.label.attr(f);if(g)clearTimeout(this.tooltipTimeout),this.tooltipTimeout=setTimeout(function(){e&&e.move(a,b,c,d)},32)},hide:function(a){var b=this;clearTimeout(this.hideTimer);a=o(a,this.options.hideDelay,500);if(!this.isHidden)this.hideTimer= -Pa(function(){b.label[a?"fadeOut":"hide"]();b.isHidden=!0},a)},getAnchor:function(a,b){var c,d=this.chart,e=d.inverted,f=d.plotTop,g=d.plotLeft,h=0,i=0,k,j,a=ta(a);c=a[0].tooltipPos;this.followPointer&&b&&(b.chartX===y&&(b=d.pointer.normalize(b)),c=[b.chartX-d.plotLeft,b.chartY-f]);c||(p(a,function(a){k=a.series.yAxis;j=a.series.xAxis;h+=a.plotX+(!e&&j?j.left-g:0);i+=(a.plotLow?(a.plotLow+a.plotHigh)/2:a.plotY)+(!e&&k?k.top-f:0)}),h/=a.length,i/=a.length,c=[e?d.plotWidth-i:h,this.shared&&!e&&a.length> -1&&b?b.chartY-f:e?d.plotHeight-h:i]);return Ca(c,B)},getPosition:function(a,b,c){var d=this.chart,e=this.distance,f={},g=c.h||0,h,i=["y",d.chartHeight,b,c.plotY+d.plotTop,d.plotTop,d.plotTop+d.plotHeight],k=["x",d.chartWidth,a,c.plotX+d.plotLeft,d.plotLeft,d.plotLeft+d.plotWidth],j=!this.followPointer&&o(c.ttBelow,!d.inverted===!!c.negative),l=function(a,b,c,d,h,i){var k=cb?d: -d+g);else return!1},m=function(a,b,c,d){var g;db-e?g=!1:f[a]=db-c/2?b-c-2:d-c/2;return g},n=function(a){var b=i;i=k;k=b;h=a},q=function(){l.apply(0,i)!==!1?m.apply(0,k)===!1&&!h&&(n(!0),q()):h?f.x=f.y=0:(n(!0),q())};(d.inverted||this.len>1)&&n();q();return f},defaultFormatter:function(a){var b=this.points||ta(this),c;c=[a.tooltipFooterHeaderFormatter(b[0])];c=c.concat(a.bodyFormatter(b));c.push(a.tooltipFooterHeaderFormatter(b[0],!0));return c.join("")},refresh:function(a,b){var c= -this.chart,d=this.label,e=this.options,f,g,h,i={},k,j=[];k=e.formatter||this.defaultFormatter;var i=c.hoverPoints,l,m=this.shared;clearTimeout(this.hideTimer);this.followPointer=ta(a)[0].series.tooltipOptions.followPointer;h=this.getAnchor(a,b);f=h[0];g=h[1];m&&(!a.series||!a.series.noSharedTooltip)?(c.hoverPoints=a,i&&p(i,function(a){a.setState()}),p(a,function(a){a.setState("hover");j.push(a.getLabelConfig())}),i={x:a[0].category,y:a[0].y},i.points=j,this.len=j.length,a=a[0]):i=a.getLabelConfig(); -k=k.call(i,this);i=a.series;this.distance=o(i.tooltipOptions.distance,16);k===!1?this.hide():(this.isHidden&&(Oa(d),d.attr("opacity",1).show()),d.attr({text:k}),l=e.borderColor||a.color||i.color||"#606060",d.attr({stroke:l}),this.updatePosition({plotX:f,plotY:g,negative:a.negative,ttBelow:a.ttBelow,h:h[2]||0}),this.isHidden=!1);I(c,"tooltipRefresh",{text:k,x:f+c.plotLeft,y:g+c.plotTop,borderColor:l})},updatePosition:function(a){var b=this.chart,c=this.label,c=(this.options.positioner||this.getPosition).call(this, -c.width,c.height,a);this.move(B(c.x),B(c.y||0),a.plotX+b.plotLeft,a.plotY+b.plotTop)},getXDateFormat:function(a,b,c){var d,b=b.dateTimeLabelFormats,e=c&&c.closestPointRange,f,g={millisecond:15,second:12,minute:9,hour:6,day:3},h,i="millisecond";if(e){h=Qa("%m-%d %H:%M:%S.%L",a.x);for(f in H){if(e===H.week&&+Qa("%w",a.x)===c.options.startOfWeek&&h.substr(6)==="00:00:00.000"){f="week";break}if(H[f]>e){f=i;break}if(g[f]&&h.substr(g[f])!=="01-01 00:00:00.000".substr(g[f]))break;f!=="week"&&(i=f)}f&&(d= -b[f])}else d=b.day;return d||b.year},tooltipFooterHeaderFormatter:function(a,b){var c=b?"footer":"header",d=a.series,e=d.tooltipOptions,f=e.xDateFormat,g=d.xAxis,h=g&&g.options.type==="datetime"&&J(a.key),c=e[c+"Format"];h&&!f&&(f=this.getXDateFormat(a,e,g));h&&f&&(c=c.replace("{point.key}","{point.key:"+f+"}"));return Ka(c,{point:a,series:d})},bodyFormatter:function(a){return Ca(a,function(a){var c=a.series.tooltipOptions;return(c.pointFormatter||a.point.tooltipFormatter).call(a.point,c.pointFormat)})}}; -var ia;db=A&&A.documentElement.ontouchstart!==y;var Xa=x.Pointer=function(a,b){this.init(a,b)};Xa.prototype={init:function(a,b){var c=b.chart,d=c.events,e=ka?"":c.zoomType,c=a.inverted,f;this.options=b;this.chart=a;this.zoomX=f=/x/.test(e);this.zoomY=e=/y/.test(e);this.zoomHor=f&&!c||e&&c;this.zoomVert=e&&!c||f&&c;this.hasZoom=f||e;this.runChartClick=d&&!!d.click;this.pinchDown=[];this.lastValidTouch={};if(x.Tooltip&&b.tooltip.enabled)a.tooltip=new Ob(a,b.tooltip),this.followTouchMove=o(b.tooltip.followTouchMove, -!0);this.setDOMEvents()},normalize:function(a,b){var c,d,a=a||D.event;if(!a.target)a.target=a.srcElement;d=a.touches?a.touches.length?a.touches.item(0):a.changedTouches[0]:a;if(!b)this.chartPosition=b=zb(this.chart.container);d.pageX===y?(c=t(a.x,a.clientX-b.left),d=a.y):(c=d.pageX-b.left,d=d.pageY-b.top);return u(a,{chartX:B(c),chartY:B(d)})},getCoordinates:function(a){var b={xAxis:[],yAxis:[]};p(this.chart.axes,function(c){b[c.isXAxis?"xAxis":"yAxis"].push({axis:c,value:c.toValue(a[c.horiz?"chartX": -"chartY"])})});return b},runPointActions:function(a){var b=this.chart,c=b.series,d=b.tooltip,e=d?d.shared:!1,f=b.hoverPoint,g=b.hoverSeries,h,i=[Number.MAX_VALUE,Number.MAX_VALUE],k,j,l=[],m=[],n;if(!e&&!g)for(h=0;h=m[c].series.group.zIndex;if(a[b]h+k&&(d=h+k),ei+j&&(e=i+j),this.hasDragged=Math.sqrt(Math.pow(n-d,2)+Math.pow(q-e,2)),this.hasDragged>10){l=b.isInsidePlot(n-h,q-i);if(b.hasCartesianSeries&&(this.zoomX||this.zoomY)&&l&&!o&&!m)this.selectionMarker=m=b.renderer.rect(h,i, -f?1:k,g?1:j,0).attr({fill:c.selectionMarkerFill||"rgba(69,114,167,0.25)",zIndex:7}).add();m&&f&&(d-=n,m.attr({width:Q(d),x:(d>0?0:d)+n}));m&&g&&(d=e-q,m.attr({height:Q(d),y:(d>0?0:d)+q}));l&&!m&&c.panning&&b.pan(a,c.panning)}},drop:function(a){var b=this,c=this.chart,d=this.hasPinched;if(this.selectionMarker){var e={originalEvent:a,xAxis:[],yAxis:[]},f=this.selectionMarker,g=f.attr?f.attr("x"):f.x,h=f.attr?f.attr("y"):f.y,i=f.attr?f.attr("width"):f.width,k=f.attr?f.attr("height"):f.height,j;if(this.hasDragged|| -d)p(c.axes,function(c){if(c.zoomEnabled&&r(c.min)&&(d||b[{xAxis:"zoomX",yAxis:"zoomY"}[c.coll]])){var f=c.horiz,n=a.type==="touchend"?c.minPixelPadding:0,q=c.toValue((f?g:h)+n),f=c.toValue((f?g+i:h+k)-n);e[c.coll].push({axis:c,min:F(q,f),max:t(q,f)});j=!0}}),j&&I(c,"selection",e,function(a){c.zoom(u(a,d?{animation:!1}:null))});this.selectionMarker=this.selectionMarker.destroy();d&&this.scaleGroups()}if(c)M(c.container,{cursor:c._cursor}),c.cancelClick=this.hasDragged>10,c.mouseIsDown=this.hasDragged= -this.hasPinched=!1,this.pinchDown=[]},onContainerMouseDown:function(a){a=this.normalize(a);a.preventDefault&&a.preventDefault();this.dragStart(a)},onDocumentMouseUp:function(a){T[ia]&&T[ia].pointer.drop(a)},onDocumentMouseMove:function(a){var b=this.chart,c=this.chartPosition,a=this.normalize(a,c);c&&!this.inClass(a.target,"highcharts-tracker")&&!b.isInsidePlot(a.chartX-b.plotLeft,a.chartY-b.plotTop)&&this.reset()},onContainerMouseLeave:function(a){var b=T[ia];if(b&&(a.relatedTarget||a.toElement))b.pointer.reset(), -b.pointer.chartPosition=null},onContainerMouseMove:function(a){var b=this.chart;if(!r(ia)||!T[ia]||!T[ia].mouseIsDown)ia=b.index;a=this.normalize(a);a.returnValue=!1;b.mouseIsDown==="mousedown"&&this.drag(a);(this.inClass(a.target,"highcharts-tracker")||b.isInsidePlot(a.chartX-b.plotLeft,a.chartY-b.plotTop))&&!b.openMenu&&this.runPointActions(a)},inClass:function(a,b){for(var c;a;){if(c=P(a,"class")){if(c.indexOf(b)!==-1)return!0;if(c.indexOf("highcharts-container")!==-1)return!1}a=a.parentNode}}, -onTrackerMouseOut:function(a){var b=this.chart.hoverSeries,a=a.relatedTarget||a.toElement;if(b&&a&&!b.options.stickyTracking&&!this.inClass(a,"highcharts-tooltip")&&!this.inClass(a,"highcharts-series-"+b.index))b.onMouseOut()},onContainerClick:function(a){var b=this.chart,c=b.hoverPoint,d=b.plotLeft,e=b.plotTop,a=this.normalize(a);b.cancelClick||(c&&this.inClass(a.target,"highcharts-tracker")?(I(c.series,"click",u(a,{point:c})),b.hoverPoint&&c.firePointEvent("click",a)):(u(a,this.getCoordinates(a)), -b.isInsidePlot(a.chartX-d,a.chartY-e)&&I(b,"click",a)))},setDOMEvents:function(){var a=this,b=a.chart.container;b.onmousedown=function(b){a.onContainerMouseDown(b)};b.onmousemove=function(b){a.onContainerMouseMove(b)};b.onclick=function(b){a.onContainerClick(b)};N(b,"mouseleave",a.onContainerMouseLeave);eb===1&&N(A,"mouseup",a.onDocumentMouseUp);if(db)b.ontouchstart=function(b){a.onContainerTouchStart(b)},b.ontouchmove=function(b){a.onContainerTouchMove(b)},eb===1&&N(A,"touchend",a.onDocumentTouchEnd)}, -destroy:function(){var a;X(this.chart.container,"mouseleave",this.onContainerMouseLeave);eb||(X(A,"mouseup",this.onDocumentMouseUp),X(A,"touchend",this.onDocumentTouchEnd));clearInterval(this.tooltipTimeout);for(a in this)this[a]=null}};u(x.Pointer.prototype,{pinchTranslate:function(a,b,c,d,e,f){(this.zoomHor||this.pinchHor)&&this.pinchTranslateDirection(!0,a,b,c,d,e,f);(this.zoomVert||this.pinchVert)&&this.pinchTranslateDirection(!1,a,b,c,d,e,f)},pinchTranslateDirection:function(a,b,c,d,e,f,g,h){var i= -this.chart,k=a?"x":"y",j=a?"X":"Y",l="chart"+j,m=a?"width":"height",n=i["plot"+(a?"Left":"Top")],q,o,s=h||1,p=i.inverted,w=i.bounds[a?"h":"v"],v=b.length===1,r=b[0][l],t=c[0][l],u=!v&&b[1][l],x=!v&&c[1][l],B,c=function(){!v&&Q(r-u)>20&&(s=h||Q(t-x)/Q(r-u));o=(n-t)/s+r;q=i["plot"+(a?"Width":"Height")]/s};c();b=o;bw.max&&(b=w.max-q,B=!0);B?(t-=0.8*(t-g[k][0]),v||(x-=0.8*(x-g[k][1])),c()):g[k]=[t,x];p||(f[k]=o-n,f[m]=q);f=p?1/s:s;e[m]=q;e[k]=b;d[p?a?"scaleY":"scaleX":"scale"+ -j]=s;d["translate"+j]=f*n+(t-f*r)},pinch:function(a){var b=this,c=b.chart,d=b.pinchDown,e=a.touches,f=e.length,g=b.lastValidTouch,h=b.hasZoom,i=b.selectionMarker,k={},j=f===1&&(b.inClass(a.target,"highcharts-tracker")&&c.runTrackerClick||b.runChartClick),l={};if(f>1)b.initiated=!0;h&&b.initiated&&!j&&a.preventDefault();Ca(e,function(a){return b.normalize(a)});if(a.type==="touchstart")p(e,function(a,b){d[b]={chartX:a.chartX,chartY:a.chartY}}),g.x=[d[0].chartX,d[1]&&d[1].chartX],g.y=[d[0].chartY,d[1]&& -d[1].chartY],p(c.axes,function(a){if(a.zoomEnabled){var b=c.bounds[a.horiz?"h":"v"],d=a.minPixelPadding,e=a.toPixels(o(a.options.min,a.dataMin)),f=a.toPixels(o(a.options.max,a.dataMax)),g=F(e,f),e=t(e,f);b.min=F(a.pos,g-d);b.max=t(a.pos+a.len,e+d)}}),b.res=!0;else if(d.length){if(!i)b.selectionMarker=i=u({destroy:Aa,touch:!0},c.plotBox);b.pinchTranslate(d,e,k,i,l,g);b.hasPinched=h;b.scaleGroups(k,l);if(!h&&b.followTouchMove&&f===1)this.runPointActions(b.normalize(a));else if(b.res)b.res=!1,this.reset(!1, -0)}},touch:function(a,b){var c=this.chart,d;ia=c.index;if(a.touches.length===1)if(a=this.normalize(a),c.isInsidePlot(a.chartX-c.plotLeft,a.chartY-c.plotTop)&&!c.openMenu){b&&this.runPointActions(a);if(a.type==="touchmove")c=this.pinchDown,d=c[0]?Math.sqrt(Math.pow(c[0].chartX-a.chartX,2)+Math.pow(c[0].chartY-a.chartY,2))>=4:!1;o(d,!0)&&this.pinch(a)}else b&&this.reset();else a.touches.length===2&&this.pinch(a)},onContainerTouchStart:function(a){this.touch(a,!0)},onContainerTouchMove:function(a){this.touch(a)}, -onDocumentTouchEnd:function(a){T[ia]&&T[ia].pointer.drop(a)}});if(D.PointerEvent||D.MSPointerEvent){var va={},Cb=!!D.PointerEvent,Rb=function(){var a,b=[];b.item=function(a){return this[a]};for(a in va)va.hasOwnProperty(a)&&b.push({pageX:va[a].pageX,pageY:va[a].pageY,target:va[a].target});return b},Db=function(a,b,c,d){if((a.pointerType==="touch"||a.pointerType===a.MSPOINTER_TYPE_TOUCH)&&T[ia])d(a),d=T[ia].pointer,d[b]({type:c,target:a.currentTarget,preventDefault:Aa,touches:Rb()})};u(Xa.prototype, -{onContainerPointerDown:function(a){Db(a,"onContainerTouchStart","touchstart",function(a){va[a.pointerId]={pageX:a.pageX,pageY:a.pageY,target:a.currentTarget}})},onContainerPointerMove:function(a){Db(a,"onContainerTouchMove","touchmove",function(a){va[a.pointerId]={pageX:a.pageX,pageY:a.pageY};if(!va[a.pointerId].target)va[a.pointerId].target=a.currentTarget})},onDocumentPointerUp:function(a){Db(a,"onDocumentTouchEnd","touchend",function(a){delete va[a.pointerId]})},batchMSEvents:function(a){a(this.chart.container, -Cb?"pointerdown":"MSPointerDown",this.onContainerPointerDown);a(this.chart.container,Cb?"pointermove":"MSPointerMove",this.onContainerPointerMove);a(A,Cb?"pointerup":"MSPointerUp",this.onDocumentPointerUp)}});fb(Xa.prototype,"init",function(a,b,c){a.call(this,b,c);this.hasZoom&&M(b.container,{"-ms-touch-action":"none","touch-action":"none"})});fb(Xa.prototype,"setDOMEvents",function(a){a.apply(this);(this.hasZoom||this.followTouchMove)&&this.batchMSEvents(N)});fb(Xa.prototype,"destroy",function(a){this.batchMSEvents(X); -a.call(this)})}var ob=x.Legend=function(a,b){this.init(a,b)};ob.prototype={init:function(a,b){var c=this,d=b.itemStyle,e=b.itemMarginTop||0;this.options=b;if(b.enabled)c.itemStyle=d,c.itemHiddenStyle=E(d,b.itemHiddenStyle),c.itemMarginTop=e,c.padding=d=o(b.padding,8),c.initialItemX=d,c.initialItemY=d-5,c.maxItemWidth=0,c.chart=a,c.itemHeight=0,c.symbolWidth=o(b.symbolWidth,16),c.pages=[],c.render(),N(c.chart,"endResize",function(){c.positionCheckboxes()})},colorizeItem:function(a,b){var c=this.options, -d=a.legendItem,e=a.legendLine,f=a.legendSymbol,g=this.itemHiddenStyle.color,c=b?c.itemStyle.color:g,h=b?a.legendColor||a.color||"#CCC":g,g=a.options&&a.options.marker,i={fill:h},k;d&&d.css({fill:c,color:c});e&&e.attr({stroke:h});if(f){if(g&&f.isMarker)for(k in i.stroke=h,g=a.convertAttribs(g),g)d=g[k],d!==y&&(i[k]=d);f.attr(i)}},positionItem:function(a){var b=this.options,c=b.symbolPadding,b=!b.rtl,d=a._legendItemPos,e=d[0],d=d[1],f=a.checkbox;(a=a.legendGroup)&&a.element&&a.translate(b?e:this.legendWidth- -e-2*c-4,d);if(f)f.x=e,f.y=d},destroyItem:function(a){var b=a.checkbox;p(["legendItem","legendLine","legendSymbol","legendGroup"],function(b){a[b]&&(a[b]=a[b].destroy())});b&&Sa(a.checkbox)},destroy:function(){var a=this.group,b=this.box;if(b)this.box=b.destroy();if(a)this.group=a.destroy()},positionCheckboxes:function(a){var b=this.group.alignAttr,c,d=this.clipHeight||this.legendHeight,e=this.titleHeight;if(b)c=b.translateY,p(this.allItems,function(f){var g=f.checkbox,h;g&&(h=c+e+g.y+(a||0)+3,M(g, -{left:b.translateX+f.checkboxOffset+g.x-20+"px",top:h+"px",display:h>c-6&&h(m||b.chartWidth-2*k-z-d.x))this.itemX=z,this.itemY+=q+this.lastLineHeight+n,this.lastLineHeight=0;this.maxItemWidth=t(this.maxItemWidth,f);this.lastItemY=q+this.itemY+n;this.lastLineHeight=t(g,this.lastLineHeight);a._legendItemPos=[this.itemX,this.itemY];e?this.itemX+=f:(this.itemY+=q+g+n,this.lastLineHeight=g);this.offsetWidth=m||t((e?this.itemX-z-j:f)+k,this.offsetWidth)}, -getAllItems:function(){var a=[];p(this.chart.series,function(b){var c=b.options;if(o(c.showInLegend,!r(c.linkedTo)?y:!1,!0))a=a.concat(b.legendItems||(c.legendType==="point"?b.data:b))});return a},adjustMargins:function(a,b){var c=this.chart,d=this.options,e=d.align.charAt(0)+d.verticalAlign.charAt(0)+d.layout.charAt(0);this.display&&!d.floating&&p([/(lth|ct|rth)/,/(rtv|rm|rbv)/,/(rbh|cb|lbh)/,/(lbv|lm|ltv)/],function(f,g){f.test(e)&&!r(a[g])&&(c[nb[g]]=t(c[nb[g]],c.legend[(g+1)%2?"legendHeight": -"legendWidth"]+[1,-1,-1,1][g]*d[g%2?"x":"y"]+o(d.margin,12)+b[g]))})},render:function(){var a=this,b=a.chart,c=b.renderer,d=a.group,e,f,g,h,i=a.box,k=a.options,j=a.padding,l=k.borderWidth,m=k.backgroundColor;a.itemX=a.initialItemX;a.itemY=a.initialItemY;a.offsetWidth=0;a.lastItemY=0;if(!d)a.group=d=c.g("legend").attr({zIndex:7}).add(),a.contentGroup=c.g().attr({zIndex:1}).add(d),a.scrollGroup=c.g().add(a.contentGroup);a.renderTitle();e=a.getAllItems();hb(e,function(a,b){return(a.options&&a.options.legendIndex|| -0)-(b.options&&b.options.legendIndex||0)});k.reversed&&e.reverse();a.allItems=e;a.display=f=!!e.length;a.lastLineHeight=0;p(e,function(b){a.renderItem(b)});g=(k.width||a.offsetWidth)+j;h=a.lastItemY+a.lastLineHeight+a.titleHeight;h=a.handleOverflow(h);h+=j;if(l||m){if(i){if(g>0&&h>0)i[i.isNew?"attr":"animate"](i.crisp({width:g,height:h})),i.isNew=!1}else a.box=i=c.rect(0,0,g,h,k.borderRadius,l||0).attr({stroke:k.borderColor,"stroke-width":l||0,fill:m||"none"}).add(d).shadow(k.shadow),i.isNew=!0;i[f? -"show":"hide"]()}a.legendWidth=g;a.legendHeight=h;p(e,function(b){a.positionItem(b)});f&&d.align(u({width:g,height:h},k),!0,"spacingBox");b.isResizing||this.positionCheckboxes()},handleOverflow:function(a){var b=this,c=this.chart,d=c.renderer,e=this.options,f=e.y,f=c.spacingBox.height+(e.verticalAlign==="top"?-f:f)-this.padding,g=e.maxHeight,h,i=this.clipRect,k=e.navigation,j=o(k.animation,!0),l=k.arrowSize||12,m=this.nav,n=this.pages,q=this.padding,z,s=this.allItems,r=function(a){i.attr({height:a}); -if(b.contentGroup.div)b.contentGroup.div.style.clip="rect("+q+"px,9999px,"+(q+a)+"px,0)"};e.layout==="horizontal"&&(f/=2);g&&(f=F(f,g));n.length=0;if(a>f&&k.enabled!==!1){this.clipHeight=h=t(f-20-this.titleHeight-q,0);this.currentPage=o(this.currentPage,1);this.fullHeight=a;p(s,function(a,b){var c=a._legendItemPos[1],d=B(a.legendItem.getBBox().height),e=n.length;if(!e||c-n[e-1]>h&&(z||c)!==n[e-1])n.push(z||c),e++;b===s.length-1&&c+d-n[e-1]>h&&n.push(c);c!==z&&(z=c)});if(!i)i=b.clipRect=d.clipRect(0, -q,9999,0),b.contentGroup.clip(i);r(h);if(!m)this.nav=m=d.g().attr({zIndex:1}).add(this.group),this.up=d.symbol("triangle",0,0,l,l).on("click",function(){b.scroll(-1,j)}).add(m),this.pager=d.text("",15,10).css(k.style).add(m),this.down=d.symbol("triangle-down",0,0,l,l).on("click",function(){b.scroll(1,j)}).add(m);b.scroll(0);a=f}else if(m)r(c.chartHeight),m.hide(),this.scrollGroup.attr({translateY:1}),this.clipHeight=0;return a},scroll:function(a,b){var c=this.pages,d=c.length,e=this.currentPage+a, -f=this.clipHeight,g=this.options.navigation,h=g.activeColor,g=g.inactiveColor,i=this.pager,k=this.padding;e>d&&(e=d);if(e>0)b!==y&&Ta(b,this.chart),this.nav.attr({translateX:k,translateY:f+this.padding+7+this.titleHeight,visibility:"visible"}),this.up.attr({fill:e===1?g:h}).css({cursor:e===1?"default":"pointer"}),i.attr({text:e+"/"+d}),this.down.attr({x:18+this.pager.getBBox().width,fill:e===d?g:h}).css({cursor:e===d?"default":"pointer"}),c=-c[e-1]+this.initialItemY,this.scrollGroup.animate({translateY:c}), -this.currentPage=e,this.positionCheckboxes(c)}};K=x.LegendSymbolMixin={drawRectangle:function(a,b){var c=a.options.symbolHeight||a.fontMetrics.f;b.legendSymbol=this.chart.renderer.rect(0,a.baseline-c+1,a.symbolWidth,c,a.options.symbolRadius||0).attr({zIndex:3}).add(b.legendGroup)},drawLineMarker:function(a){var b=this.options,c=b.marker,d=a.symbolWidth,e=this.chart.renderer,f=this.legendGroup,a=a.baseline-B(a.fontMetrics.b*0.3),g;if(b.lineWidth){g={"stroke-width":b.lineWidth};if(b.dashStyle)g.dashstyle= -b.dashStyle;this.legendLine=e.path(["M",0,a,"L",d,a]).attr(g).add(f)}if(c&&c.enabled!==!1)b=c.radius,this.legendSymbol=c=e.symbol(this.symbol,d/2-b,a-b,2*b,2*b,c).add(f),c.isMarker=!0}};(/Trident\/7\.0/.test(za)||Na)&&fb(ob.prototype,"positionItem",function(a,b){var c=this,d=function(){b._legendItemPos&&a.call(c,b)};d();setTimeout(d)});var gb=x.Chart=function(){this.getArgs.apply(this,arguments)};x.chart=function(a,b,c){return new gb(a,b,c)};gb.prototype={callbacks:[],getArgs:function(){var a=[].slice.call(arguments); -if(xa(a[0])||a[0].nodeName)this.renderTo=a.shift();this.init(a[0],a[1])},init:function(a,b){var c,d=a.series;a.series=null;c=E(U,a);c.series=a.series=d;this.userOptions=a;d=c.chart;this.margin=this.splashArray("margin",d);this.spacing=this.splashArray("spacing",d);var e=d.events;this.bounds={h:{},v:{}};this.callback=b;this.isResizing=0;this.options=c;this.axes=[];this.series=[];this.hasCartesianSeries=d.showAxes;var f=this,g;f.index=T.length;T.push(f);eb++;d.reflow!==!1&&N(f,"load",function(){f.initReflow()}); -if(e)for(g in e)N(f,g,e[g]);f.xAxis=[];f.yAxis=[];f.animation=ka?!1:o(d.animation,!0);f.pointCount=f.colorCounter=f.symbolCounter=0;f.firstRender()},initSeries:function(a){var b=this.options.chart;(b=L[a.type||b.type||b.defaultSeriesType])||aa(17,!0);b=new b;b.init(this,a);return b},isInsidePlot:function(a,b,c){var d=c?b:a,a=c?a:b;return d>=0&&d<=this.plotWidth&&a>=0&&a<=this.plotHeight},redraw:function(a){var b=this.axes,c=this.series,d=this.pointer,e=this.legend,f=this.isDirtyLegend,g,h,i=this.hasCartesianSeries, -k=this.isDirtyBox,j=c.length,l=j,m=this.renderer,n=m.isHidden(),q=[];Ta(a,this);n&&this.cloneRenderTo();for(this.layOutTitles();l--;)if(a=c[l],a.options.stacking&&(g=!0,a.isDirty)){h=!0;break}if(h)for(l=j;l--;)if(a=c[l],a.options.stacking)a.isDirty=!0;p(c,function(a){a.isDirty&&a.options.legendType==="point"&&(a.updateTotals&&a.updateTotals(),f=!0);a.isDirtyData&&I(a,"updatedData")});if(f&&e.options.enabled)e.render(),this.isDirtyLegend=!1;g&&this.getStacks();if(i&&!this.isResizing)this.maxTicks= -null,p(b,function(a){a.setScale()});this.getMargins();i&&(p(b,function(a){a.isDirty&&(k=!0)}),p(b,function(a){var b=a.min+","+a.max;if(a.extKey!==b)a.extKey=b,q.push(function(){I(a,"afterSetExtremes",u(a.eventArgs,a.getExtremes()));delete a.eventArgs});(k||g)&&a.redraw()}));k&&this.drawChartBox();p(c,function(a){a.isDirty&&a.visible&&(!a.isCartesian||a.xAxis)&&a.redraw()});d&&d.reset(!0);m.draw();I(this,"redraw");n&&this.cloneRenderTo(!0);p(q,function(a){a.call()})},get:function(a){var b=this.axes, -c=this.series,d,e;for(d=0;d19?this.containerHeight:400))},cloneRenderTo:function(a){var b= -this.renderToClone,c=this.container;a?b&&(this.renderTo.appendChild(c),Sa(b),delete this.renderToClone):(c&&c.parentNode===this.renderTo&&this.renderTo.removeChild(c),this.renderToClone=b=this.renderTo.cloneNode(0),M(b,{position:"absolute",top:"-9999px",display:"block"}),b.style.setProperty&&b.style.setProperty("display","block","important"),A.body.appendChild(b),c&&b.appendChild(c))},getContainer:function(){var a,b=this.options,c=b.chart,d,e;a=this.renderTo;var f="highcharts-"+yb++;if(!a)this.renderTo= -a=c.renderTo;if(xa(a))this.renderTo=a=A.getElementById(a);a||aa(13,!0);d=C(P(a,"data-highcharts-chart"));J(d)&&T[d]&&T[d].hasRendered&&T[d].destroy();P(a,"data-highcharts-chart",this.index);a.innerHTML="";!c.skipClone&&!a.offsetWidth&&this.cloneRenderTo();this.getChartSize();d=this.chartWidth;e=this.chartHeight;this.container=a=ba(Ma,{className:"highcharts-container"+(c.className?" "+c.className:""),id:f},u({position:"relative",overflow:"hidden",width:d+"px",height:e+"px",textAlign:"left",lineHeight:"normal", -zIndex:0,"-webkit-tap-highlight-color":"rgba(0,0,0,0)"},c.style),this.renderToClone||a);this._cursor=a.style.cursor;this.renderer=new (x[c.renderer]||cb)(a,d,e,c.style,c.forExport,b.exporting&&b.exporting.allowHTML);ka&&this.renderer.create(this,a,d,e);this.renderer.chartIndex=this.index},getMargins:function(a){var b=this.spacing,c=this.margin,d=this.titleOffset;this.resetMargins();if(d&&!r(c[0]))this.plotTop=t(this.plotTop,d+this.options.title.margin+b[0]);this.legend.adjustMargins(c,b);this.extraBottomMargin&& -(this.marginBottom+=this.extraBottomMargin);this.extraTopMargin&&(this.plotTop+=this.extraTopMargin);a||this.getAxisMargins()},getAxisMargins:function(){var a=this,b=a.axisOffset=[0,0,0,0],c=a.margin;a.hasCartesianSeries&&p(a.axes,function(a){a.visible&&a.getOffset()});p(nb,function(d,e){r(c[e])||(a[d]+=b[e])});a.setChartSize()},reflow:function(a){var b=this,c=b.options.chart,d=b.renderTo,e=c.width||na(d,"width"),f=c.height||na(d,"height"),c=a?a.target:D;if(!b.hasUserSize&&!b.isPrinting&&e&&f&&(c=== -D||c===A)){if(e!==b.containerWidth||f!==b.containerHeight)clearTimeout(b.reflowTimeout),b.reflowTimeout=Pa(function(){if(b.container)b.setSize(e,f,!1),b.hasUserSize=null},a?100:0);b.containerWidth=e;b.containerHeight=f}},initReflow:function(){var a=this,b=function(b){a.reflow(b)};N(D,"resize",b);N(a,"destroy",function(){X(D,"resize",b)})},setSize:function(a,b,c){var d=this,e,f,g=d.renderer;d.isResizing+=1;Ta(c,d);d.oldChartHeight=d.chartHeight;d.oldChartWidth=d.chartWidth;if(r(a))d.chartWidth=e=t(0, -B(a)),d.hasUserSize=!!e;if(r(b))d.chartHeight=f=t(0,B(b));a=g.globalAnimation;(a?Wa:M)(d.container,{width:e+"px",height:f+"px"},a);d.setChartSize(!0);g.setSize(e,f,c);d.maxTicks=null;p(d.axes,function(a){a.isDirty=!0;a.setScale()});p(d.series,function(a){a.isDirty=!0});d.isDirtyLegend=!0;d.isDirtyBox=!0;d.layOutTitles();d.getMargins();d.redraw(c);d.oldChartHeight=null;I(d,"resize");Pa(function(){d&&I(d,"endResize",null,function(){d.isResizing-=1})},$a(a).duration)},setChartSize:function(a){var b= -this.inverted,c=this.renderer,d=this.chartWidth,e=this.chartHeight,f=this.options.chart,g=this.spacing,h=this.clipOffset,i,k,j,l;this.plotLeft=i=B(this.plotLeft);this.plotTop=k=B(this.plotTop);this.plotWidth=j=t(0,B(d-i-this.marginRight));this.plotHeight=l=t(0,B(e-k-this.marginBottom));this.plotSizeX=b?l:j;this.plotSizeY=b?j:l;this.plotBorderWidth=f.plotBorderWidth||0;this.spacingBox=c.spacingBox={x:g[3],y:g[0],width:d-g[3]-g[1],height:e-g[0]-g[2]};this.plotBox=c.plotBox={x:i,y:k,width:j,height:l}; -d=2*V(this.plotBorderWidth/2);b=ua(t(d,h[3])/2);c=ua(t(d,h[0])/2);this.clipBox={x:b,y:c,width:V(this.plotSizeX-t(d,h[1])/2-b),height:t(0,V(this.plotSizeY-t(d,h[2])/2-c))};a||p(this.axes,function(a){a.setAxisSize();a.setAxisTranslation()})},resetMargins:function(){var a=this;p(nb,function(b,c){a[b]=o(a.margin[c],a.spacing[c])});a.axisOffset=[0,0,0,0];a.clipOffset=[0,0,0,0]},drawChartBox:function(){var a=this.options.chart,b=this.renderer,c=this.chartWidth,d=this.chartHeight,e=this.chartBackground, -f=this.plotBackground,g=this.plotBorder,h=this.plotBGImage,i=a.borderWidth||0,k=a.backgroundColor,j=a.plotBackgroundColor,l=a.plotBackgroundImage,m=a.plotBorderWidth||0,n,q=this.plotLeft,o=this.plotTop,p=this.plotWidth,r=this.plotHeight,w=this.plotBox,v=this.clipRect,t=this.clipBox;n=i+(a.shadow?8:0);if(i||k)if(e)e.animate(e.crisp({width:c-n,height:d-n}));else{e={fill:k||"none"};if(i)e.stroke=a.borderColor,e["stroke-width"]=i;this.chartBackground=b.rect(n/2,n/2,c-n,d-n,a.borderRadius,i).attr(e).addClass("highcharts-background").add().shadow(a.shadow)}if(j)f? -f.animate(w):this.plotBackground=b.rect(q,o,p,r,0).attr({fill:j}).add().shadow(a.plotShadow);if(l)h?h.animate(w):this.plotBGImage=b.image(l,q,o,p,r).add();v?v.animate({width:t.width,height:t.height}):this.clipRect=b.clipRect(t);if(m)g?(g.strokeWidth=-m,g.animate(g.crisp({x:q,y:o,width:p,height:r}))):this.plotBorder=b.rect(q,o,p,r,0,-m).attr({stroke:a.plotBorderColor,"stroke-width":m,fill:"none",zIndex:1}).add();this.isDirtyBox=!1},propFromSeries:function(){var a=this,b=a.options.chart,c,d=a.options.series, -e,f;p(["inverted","angular","polar"],function(g){c=L[b.type||b.defaultSeriesType];f=a[g]||b[g]||c&&c.prototype[g];for(e=d&&d.length;!f&&e--;)(c=L[d[e].type])&&c.prototype[g]&&(f=!0);a[g]=f})},linkSeries:function(){var a=this,b=a.series;p(b,function(a){a.linkedSeries.length=0});p(b,function(b){var d=b.options.linkedTo;if(xa(d)&&(d=d===":previous"?a.series[b.index-1]:a.get(d)))d.linkedSeries.push(b),b.linkedParent=d,b.visible=o(b.options.visible,d.options.visible,b.visible)})},renderSeries:function(){p(this.series, -function(a){a.translate();a.render()})},renderLabels:function(){var a=this,b=a.options.labels;b.items&&p(b.items,function(c){var d=u(b.style,c.style),e=C(d.left)+a.plotLeft,f=C(d.top)+a.plotTop+12;delete d.left;delete d.top;a.renderer.text(c.html,e,f).attr({zIndex:2}).css(d).add()})},render:function(){var a=this.axes,b=this.renderer,c=this.options,d,e,f,g;this.setTitle();this.legend=new ob(this,c.legend);this.getStacks&&this.getStacks();this.getMargins(!0);this.setChartSize();d=this.plotWidth;e=this.plotHeight-= -21;p(a,function(a){a.setScale()});this.getAxisMargins();f=d/this.plotWidth>1.1;g=e/this.plotHeight>1.05;if(f||g)this.maxTicks=null,p(a,function(a){(a.horiz&&f||!a.horiz&&g)&&a.setTickInterval(!0)}),this.getMargins();this.drawChartBox();this.hasCartesianSeries&&p(a,function(a){a.visible&&a.render()});if(!this.seriesGroup)this.seriesGroup=b.g("series-group").attr({zIndex:3}).add();this.renderSeries();this.renderLabels();this.showCredits(c.credits);this.hasRendered=!0},showCredits:function(a){if(a.enabled&& -!this.credits)this.credits=this.renderer.text(a.text,0,0).on("click",function(){if(a.href)D.location.href=a.href}).attr({align:a.position.align,zIndex:8}).css(a.style).add().align(a.position)},destroy:function(){var a=this,b=a.axes,c=a.series,d=a.container,e,f=d&&d.parentNode;I(a,"destroy");T[a.index]=y;eb--;a.renderTo.removeAttribute("data-highcharts-chart");X(a);for(e=b.length;e--;)b[e]=b[e].destroy();for(e=c.length;e--;)c[e]=c[e].destroy();p("title,subtitle,chartBackground,plotBackground,plotBGImage,plotBorder,seriesGroup,clipRect,credits,pointer,scroller,rangeSelector,legend,resetZoomButton,tooltip,renderer".split(","), -function(b){var c=a[b];c&&c.destroy&&(a[b]=c.destroy())});if(d)d.innerHTML="",X(d),f&&Sa(d);for(e in a)delete a[e]},isReadyToRender:function(){var a=this;return!fa&&D==D.top&&A.readyState!=="complete"||ka&&!D.canvg?(ka?Nb.push(function(){a.firstRender()},a.options.global.canvasToolsURL):A.attachEvent("onreadystatechange",function(){A.detachEvent("onreadystatechange",a.firstRender);A.readyState==="complete"&&a.firstRender()}),!1):!0},firstRender:function(){var a=this,b=a.options;if(a.isReadyToRender()){a.getContainer(); -I(a,"init");a.resetMargins();a.setChartSize();a.propFromSeries();a.getAxes();p(b.series||[],function(b){a.initSeries(b)});a.linkSeries();I(a,"beforeRender");if(x.Pointer)a.pointer=new Xa(a,b);a.render();a.renderer.draw();if(!a.renderer.imgCount&&a.onload)a.onload();a.cloneRenderTo(!0)}},onload:function(){var a=this;p([this.callback].concat(this.callbacks),function(b){b&&a.index!==void 0&&b.apply(a,[a])});I(a,"load");this.onload=null},splashArray:function(a,b){var c=b[a],c=Z(c)?c:[c,c,c,c];return[o(b[a+ -"Top"],c[0]),o(b[a+"Right"],c[1]),o(b[a+"Bottom"],c[2]),o(b[a+"Left"],c[3])]}};var Bb=x.CenteredSeriesMixin={getCenter:function(){var a=this.options,b=this.chart,c=2*(a.slicedOffset||0),d=b.plotWidth-2*c,b=b.plotHeight-2*c,e=a.center,e=[o(e[0],"50%"),o(e[1],"50%"),a.size||"100%",a.innerSize||0],f=F(d,b),g,h;for(g=0;g<4;++g)h=e[g],a=g<2||g===2&&/%$/.test(h),e[g]=(/%$/.test(h)?[d,b,f,e[2]][g]*parseFloat(h)/100:parseFloat(h))+(a?c:0);e[3]>e[2]&&(e[3]=e[2]);return e}},Ja=function(){};Ja.prototype={init:function(a, -b,c){this.series=a;this.color=a.color;this.applyOptions(b,c);this.pointAttr={};if(a.options.colorByPoint&&(b=a.options.colors||a.chart.options.colors,this.color=this.color||b[a.colorCounter++],a.colorCounter===b.length))a.colorCounter=0;a.chart.pointCount++;return this},applyOptions:function(a,b){var c=this.series,d=c.options.pointValKey||c.pointValKey,a=Ja.prototype.optionsToObject.call(this,a);u(this,a);this.options=this.options?u(this.options,a):a;if(d)this.y=this[d];this.isNull=this.x===null|| -this.y===null;if(this.x===void 0&&c)this.x=b===void 0?c.autoIncrement():b;return this},optionsToObject:function(a){var b={},c=this.series,d=c.options.keys,e=d||c.pointArrayMap||["y"],f=e.length,g=0,h=0;if(J(a)||a===null)b[e[0]]=a;else if(Ea(a)){if(!d&&a.length>f){c=typeof a[0];if(c==="string")b.name=a[0];else if(c==="number")b.x=a[0];g++}for(;hn){for(c=0;j===null&&ci||this.forceCrop))if(b[d-1]q)b=[],c=[];else if(b[0]q)e=this.cropData(this.xData,this.yData,n,q),b=e.xData,c=e.yData,e=e.start,f=!0;for(i=b.length||1;--i;)d=m?k(b[i])-k(b[i-1]):b[i]-b[i-1],d>0&&(g===y||d=c){f=t(0,i-h);break}for(c=i;cd){g=c+h;break}return{xData:a.slice(f,g),yData:b.slice(f,g),start:f,end:g}},generatePoints:function(){var a=this.options.data,b=this.data,c,d=this.processedXData,e=this.processedYData,f=this.pointClass,g=d.length,h=this.cropStart||0,i,k=this.hasGroupedData,j,l=[],m;if(!b&&!k)b=[],b.length=a.length,b=this.data=b;for(m=0;m0),k=this.getExtremesFromAll||this.options.getExtremesFromAll||this.cropped||(c[l+1]||k)>=g&&(c[l-1]||k)<=h,i&&k)if(i=j.length)for(;i--;)j[i]!==null&&(e[f++]=j[i]);else e[f++]=j;this.dataMin=La(e);this.dataMax=Ga(e)},translate:function(){this.processedXData||this.processData();this.generatePoints();for(var a=this.options,b=a.stacking,c=this.xAxis,d=c.categories,e=this.yAxis,f=this.points,g=f.length,h=!!this.modifyValue,i=a.pointPlacement, -k=i==="between"||J(i),j=a.threshold,l=a.startFromThreshold?j:0,m,n,q,p,s=Number.MAX_VALUE,a=0;a=0&&n<=e.len&&m>=0&&m<=c.len;G.clientX=k?c.translate(w,0,0,0,1):m;G.negative=G.y<(j||0);G.category=d&&d[G.x]!==y?d[G.x]:G.x;G.isNull||(q!==void 0&&(s=F(s,Q(m-q))),q=m)}this.closestPointRangePx=s},getValidPoints:function(a, -b){var c=this.chart;return Ba(a||this.points||[],function(a){return b&&!c.isInsidePlot(a.plotX,a.plotY,c.inverted)?!1:!a.isNull})},setClip:function(a){var b=this.chart,c=this.options,d=b.renderer,e=b.inverted,f=this.clipBox,g=f||b.clipBox,h=this.sharedClipKey||["_sharedClip",a&&a.duration,a&&a.easing,g.height,c.xAxis,c.yAxis].join(","),i=b[h],k=b[h+"m"];if(!i){if(a)g.width=0,b[h+"m"]=k=d.clipRect(-99,e?-b.plotLeft:-b.plotTop,99,e?b.chartWidth:b.chartHeight);b[h]=i=d.clipRect(g)}a&&(i.count+=1);if(c.clip!== -!1)this.group.clip(a||f?i:b.clipRect),this.markerGroup.clip(k),this.sharedClipKey=h;a||(i.count-=1,i.count<=0&&h&&b[h]&&(f||(b[h]=b[h].destroy()),b[h+"m"]&&(b[h+"m"]=b[h+"m"].destroy())))},animate:function(a){var b=this.chart,c=this.options.animation,d;if(c&&!Z(c))c=ea[this.type].animation;a?this.setClip(c):(d=this.sharedClipKey,(a=b[d])&&a.animate({width:b.plotSizeX},c),b[d+"m"]&&b[d+"m"].animate({width:b.plotSizeX+99},c),this.animate=null)},afterAnimate:function(){this.setClip();I(this,"afterAnimate")}, -drawPoints:function(){var a,b=this.points,c=this.chart,d,e,f,g,h,i,k,j,l=this.options.marker,m=this.pointAttr[""],n,q,p,s=this.markerGroup,r=o(l.enabled,this.xAxis.isRadial,this.closestPointRangePx>2*l.radius);if(l.enabled!==!1||this._hasPointMarkers)for(f=b.length;f--;)if(g=b[f],d=V(g.plotX),e=g.plotY,j=g.graphic,n=g.marker||{},q=!!g.marker,a=r&&n.enabled===y||n.enabled,p=g.isInside,a&&J(e)&&g.y!==null)if(a=g.pointAttr[g.selected?"select":""]||m,h=a.r,i=o(n.symbol,this.symbol),k=i.indexOf("url")=== -0,j)j[p?"show":"hide"](!0).attr(a).animate(u({x:d-h,y:e-h},j.symbolName?{width:2*h,height:2*h}:{}));else{if(p&&(h>0||k))g.graphic=c.renderer.symbol(i,d-h,e-h,2*h,2*h,q?n:l).attr(a).add(s)}else if(j)g.graphic=j.destroy()},convertAttribs:function(a,b,c,d){var e=this.pointAttrToOptions,f,g,h={},a=a||{},b=b||{},c=c||{},d=d||{};for(f in e)g=e[f],h[f]=o(a[g],b[f],c[f],d[f]);return h},getAttribs:function(){var a=this,b=a.options,c=ea[a.type].marker?b.marker:b,d=c.states,e=d.hover,f,g=a.color,h=a.options.negativeColor, -i={stroke:g,fill:g},k=a.points||[],j,l=[],m,n=a.pointAttrToOptions;f=a.hasPointSpecificOptions;var q=c.lineColor,z=c.fillColor;j=b.turboThreshold;var s=a.zones,t=a.zoneAxis||"y",w,v;b.marker?(e.radius=e.radius||c.radius+e.radiusPlus,e.lineWidth=e.lineWidth||c.lineWidth+e.lineWidthPlus):(e.color=e.color||ma(e.color||g).brighten(e.brightness).get(),e.negativeColor=e.negativeColor||ma(e.negativeColor||h).brighten(e.brightness).get());l[""]=a.convertAttribs(c,i);p(["hover","select"],function(b){l[b]= -a.convertAttribs(d[b],l[""])});a.pointAttr=l;g=k.length;if(!j||g=i.value;)i=s[++f];j.color=j.fillColor=i=o(i.color,a.color)}f=b.colorByPoint||j.color;if(j.options)for(v in n)r(c[n[v]])&&(f=!0);if(f){c=c||{};m=[];d=c.states||{};f=d.hover=d.hover||{};if(!b.marker||j.negative&&!f.fillColor&&!e.fillColor)f[a.pointAttrToOptions.fill]=f.color||!j.options.color&&e[j.negative&& -h?"negativeColor":"color"]||ma(j.color).brighten(f.brightness||e.brightness).get();w={color:j.color};if(!z)w.fillColor=j.color;if(!q)w.lineColor=j.color;c.hasOwnProperty("color")&&!c.color&&delete c.color;if(i&&!e.fillColor)f.fillColor=i;m[""]=a.convertAttribs(u(w,c),l[""]);m.hover=a.convertAttribs(d.hover,l.hover,m[""]);m.select=a.convertAttribs(d.select,l.select,m[""])}else m=l;j.pointAttr=m}},destroy:function(){var a=this,b=a.chart,c=/AppleWebKit\/533/.test(za),d,e=a.data||[],f,g,h;I(a,"destroy"); -X(a);p(a.axisTypes||[],function(b){if(h=a[b])pa(h.series,a),h.isDirty=h.forceRedraw=!0});a.legendItem&&a.chart.legend.destroyItem(a);for(d=e.length;d--;)(f=e[d])&&f.destroy&&f.destroy();a.points=null;clearTimeout(a.animationTimeout);for(g in a)a[g]instanceof O&&!a[g].survive&&(d=c&&g==="group"?"hide":"destroy",a[g][d]());if(b.hoverSeries===a)b.hoverSeries=null;pa(b.series,a);for(g in a)delete a[g]},getGraphPath:function(a,b,c){var d=this,e=d.options,f=e.step,g,h=[],i,a=a||d.points;(g=a.reversed)&& -a.reverse();(f={right:1,center:2}[f]||f&&3)&&g&&(f=4-f);e.connectNulls&&!b&&!c&&(a=this.getValidPoints(a));p(a,function(g,j){var l=g.plotX,m=g.plotY,n=a[j-1];if((g.leftCliff||n&&n.rightCliff)&&!c)i=!0;g.isNull&&!r(b)&&j>0?i=!e.connectNulls:g.isNull&&!b?i=!0:(j===0||i?n=["M",g.plotX,g.plotY]:d.getPointSpline?n=d.getPointSpline(a,g,j):f?(n=f===1?["L",n.plotX,m]:f===2?["L",(n.plotX+l)/2,n.plotY,"L",(n.plotX+l)/2,m]:["L",l,n.plotY],n.push("L",l,m)):n=["L",l,m],h.push.apply(h,n),i=!1)});return d.graphPath= -h},drawGraph:function(){var a=this,b=this.options,c=[["graph",b.lineColor||this.color,b.dashStyle]],d=b.lineWidth,e=b.linecap!=="square",f=(this.gappedPath||this.getGraphPath).call(this),g=this.fillGraph&&this.color||"none";p(this.zones,function(d,e){c.push(["zoneGraph"+e,d.color||a.color,d.dashStyle||b.dashStyle])});p(c,function(c,i){var k=c[0],j=a[k];if(j)j.animate({d:f});else if((d||g)&&f.length)j={stroke:c[1],"stroke-width":d,fill:g,zIndex:1},c[2]?j.dashstyle=c[2]:e&&(j["stroke-linecap"]=j["stroke-linejoin"]= -"round"),a[k]=a.chart.renderer.path(f).attr(j).add(a.group).shadow(i<2&&b.shadow)})},applyZones:function(){var a=this,b=this.chart,c=b.renderer,d=this.zones,e,f,g=this.clips||[],h,i=this.graph,k=this.area,j=t(b.chartWidth,b.chartHeight),l=this[(this.zoneAxis||"y")+"Axis"],m,n=l.reversed,q=b.inverted,z=l.horiz,s,r,w,v=!1;if(d.length&&(i||k)&&l.min!==y)i&&i.hide(),k&&k.hide(),m=l.getExtremes(),p(d,function(d,p){e=n?z?b.plotWidth:0:z?0:l.toPixels(m.min);e=F(t(o(f,e),0),j);f=F(t(B(l.toPixels(o(d.value, -m.max),!0)),0),j);v&&(e=f=l.toPixels(m.max));s=Math.abs(e-f);r=F(e,f);w=t(e,f);if(l.isXAxis){if(h={x:q?w:r,y:0,width:s,height:j},!z)h.x=b.plotHeight-h.x}else if(h={x:0,y:q?w:r,width:j,height:s},z)h.y=b.plotWidth-h.y;b.inverted&&c.isVML&&(h=l.isXAxis?{x:0,y:n?r:w,height:h.width,width:b.chartWidth}:{x:h.y-b.plotLeft-b.spacingBox.x,y:0,width:h.height,height:b.chartHeight});g[p]?g[p].animate(h):(g[p]=c.clipRect(h),i&&a["zoneGraph"+p].clip(g[p]),k&&a["zoneArea"+p].clip(g[p]));v=d.value>m.max}),this.clips= -g},invertGroups:function(){function a(){var a={width:b.yAxis.len,height:b.xAxis.len};p(["group","markerGroup"],function(c){b[c]&&b[c].attr(a).invert()})}var b=this,c=b.chart;if(b.xAxis)N(c,"resize",a),N(b,"destroy",function(){X(c,"resize",a)}),a(),b.invertGroups=a},plotGroup:function(a,b,c,d,e){var f=this[a],g=!f;g&&(this[a]=f=this.chart.renderer.g(b).attr({zIndex:d||0.1}).add(e),f.addClass("highcharts-series-"+this.index));f.attr({visibility:c})[g?"attr":"animate"](this.getPlotBox());return f},getPlotBox:function(){var a= -this.chart,b=this.xAxis,c=this.yAxis;if(a.inverted)b=c,c=this.xAxis;return{translateX:b?b.left:a.plotLeft,translateY:c?c.top:a.plotTop,scaleX:1,scaleY:1}},render:function(){var a=this,b=a.chart,c,d=a.options,e=!!a.animate&&b.renderer.isSVG&&$a(d.animation).duration,f=a.visible?"inherit":"hidden",g=d.zIndex,h=a.hasRendered,i=b.seriesGroup;c=a.plotGroup("group","series",f,g,i);a.markerGroup=a.plotGroup("markerGroup","markers",f,g,i);e&&a.animate(!0);a.getAttribs();c.inverted=a.isCartesian?b.inverted: -!1;a.drawGraph&&(a.drawGraph(),a.applyZones());p(a.points,function(a){a.redraw&&a.redraw()});a.drawDataLabels&&a.drawDataLabels();a.visible&&a.drawPoints();a.drawTracker&&a.options.enableMouseTracking!==!1&&a.drawTracker();b.inverted&&a.invertGroups();d.clip!==!1&&!a.sharedClipKey&&!h&&c.clip(b.clipRect);e&&a.animate();if(!h)a.animationTimeout=Pa(function(){a.afterAnimate()},e);a.isDirty=a.isDirtyData=!1;a.hasRendered=!0},redraw:function(){var a=this.chart,b=this.isDirty||this.isDirtyData,c=this.group, -d=this.xAxis,e=this.yAxis;c&&(a.inverted&&c.attr({width:a.plotWidth,height:a.plotHeight}),c.animate({translateX:o(d&&d.left,a.plotLeft),translateY:o(e&&e.top,a.plotTop)}));this.translate();this.render();b&&delete this.kdTree},kdDimensions:1,kdAxisArray:["clientX","plotY"],searchPoint:function(a,b){var c=this.xAxis,d=this.yAxis,e=this.chart.inverted;return this.searchKDTree({clientX:e?c.len-a.chartY+c.pos:a.chartX-c.pos,plotY:e?d.len-a.chartX+d.pos:a.chartY-d.pos},b)},buildKDTree:function(){function a(c, -e,f){var g,h;if(h=c&&c.length)return g=b.kdAxisArray[e%f],c.sort(function(a,b){return a[g]-b[g]}),h=Math.floor(h/2),{point:c[h],left:a(c.slice(0,h),e+1,f),right:a(c.slice(h+1),e+1,f)}}var b=this,c=b.kdDimensions;delete b.kdTree;Pa(function(){b.kdTree=a(b.getValidPoints(null,!b.directTouch),c,c)},b.options.kdNow?0:1)},searchKDTree:function(a,b){function c(a,b,k,j){var l=b.point,m=d.kdAxisArray[k%j],n,q,o=l;q=r(a[e])&&r(l[e])?Math.pow(a[e]-l[e],2):null;n=r(a[f])&&r(l[f])?Math.pow(a[f]-l[f],2):null; -n=(q||0)+(n||0);l.dist=r(n)?Math.sqrt(n):Number.MAX_VALUE;l.distX=r(q)?Math.sqrt(q):Number.MAX_VALUE;m=a[m]-l[m];n=m<0?"left":"right";q=m<0?"right":"left";b[n]&&(n=c(a,b[n],k+1,j),o=n[g]0&&this.singleStacks===!1&&(s.points[r][0]=s.points[this.index+","+v+",0"][0]);e==="percent"?(p=p?i:k,j&&m[p]&&m[p][v]?(p=m[p][v],s.total=p.total=t(p.total,s.total)+Q(u)||0):s.total=ca(s.total+(Q(u)||0))):s.total=ca(s.total+(u||0));s.cum=o(s.cum,g)+(u||0);if(u!==null)s.points[r].push(s.cum),c[w]=s.cum}if(e==="percent")l.usePercentage=!0;this.stackedYData=c;l.oldStacks= -{}}};R.prototype.setPercentStacks=function(){var a=this,b=a.stackKey,c=a.yAxis.stacks,d=a.processedXData,e;p([b,"-"+b],function(b){var f;for(var g=d.length,h,i;g--;)if(h=d[g],e=a.getStackIndicator(e,h,a.index),f=(i=c[b]&&c[b][h])&&i.points[e.key],h=f)i=i.total?100/i.total:0,h[0]=ca(h[0]*i),h[1]=ca(h[1]*i),a.stackedYData[g]=h[1]})};R.prototype.getStackIndicator=function(a,b,c){!r(a)||a.x!==b?a={x:b,index:0}:a.index++;a.key=[c,b,a.index].join(",");return a};u(gb.prototype,{addSeries:function(a,b,c){var d, -e=this;a&&(b=o(b,!0),I(e,"addSeries",{options:a},function(){d=e.initSeries(a);e.isDirtyLegend=!0;e.linkSeries();b&&e.redraw(c)}));return d},addAxis:function(a,b,c,d){var e=b?"xAxis":"yAxis",f=this.options,a=E(a,{index:this[e].length,isX:b});new ha(this,a);f[e]=ta(f[e]||{});f[e].push(a);o(c,!0)&&this.redraw(d)},showLoading:function(a){var b=this,c=b.options,d=b.loadingDiv,e=c.loading,f=function(){d&&M(d,{left:b.plotLeft+"px",top:b.plotTop+"px",width:b.plotWidth+"px",height:b.plotHeight+"px"})};if(!d)b.loadingDiv= -d=ba(Ma,{className:"highcharts-loading"},u(e.style,{zIndex:10,display:"none"}),b.container),b.loadingSpan=ba("span",null,e.labelStyle,d),N(b,"redraw",f);b.loadingSpan.innerHTML=a||c.lang.loading;if(!b.loadingShown)M(d,{opacity:0,display:""}),Wa(d,{opacity:e.style.opacity},{duration:e.showDuration||0}),b.loadingShown=!0;f()},hideLoading:function(){var a=this.options,b=this.loadingDiv;b&&Wa(b,{opacity:0},{duration:a.loading.hideDuration||100,complete:function(){M(b,{display:"none"})}});this.loadingShown= -!1}});u(Ja.prototype,{update:function(a,b,c,d){function e(){f.applyOptions(a);if(f.y===null&&h)f.graphic=h.destroy();if(Z(a)&&!Ea(a))f.redraw=function(){if(h&&h.element&&a&&a.marker&&a.marker.symbol)f.graphic=h.destroy();if(a&&a.dataLabels&&f.dataLabel)f.dataLabel=f.dataLabel.destroy();f.redraw=null};i=f.index;g.updateParallelArrays(f,i);if(l&&f.name)l[f.x]=f.name;j.data[i]=Z(j.data[i])&&!Ea(j.data[i])?f.options:a;g.isDirty=g.isDirtyData=!0;if(!g.fixedBox&&g.hasCartesianSeries)k.isDirtyBox=!0;if(j.legendType=== -"point")k.isDirtyLegend=!0;b&&k.redraw(c)}var f=this,g=f.series,h=f.graphic,i,k=g.chart,j=g.options,l=g.xAxis&&g.xAxis.names,b=o(b,!0);d===!1?e():f.firePointEvent("update",{options:a},e)},remove:function(a,b){this.series.removePoint(sa(this,this.series.data),a,b)}});u(R.prototype,{addPoint:function(a,b,c,d){var e=this,f=e.options,g=e.data,h=e.graph,i=e.area,k=e.chart,j=e.xAxis&&e.xAxis.names,l=h&&h.shift||0,m=["graph","area"],h=f.data,n,q=e.xData;Ta(d,k);if(c){for(d=e.zones.length;d--;)m.push("zoneGraph"+ -d,"zoneArea"+d);p(m,function(a){if(e[a])e[a].shift=l+(f.step?2:1)})}if(i)i.isArea=!0;b=o(b,!0);i={series:e};e.pointClass.prototype.applyOptions.apply(i,[a]);m=i.x;d=q.length;if(e.requireSorting&&mm;)d--;e.updateParallelArrays(i,"splice",d,0,0);e.updateParallelArrays(i,d);if(j&&i.name)j[m]=i.name;h.splice(d,0,a);n&&(e.data.splice(d,0,null),e.processData());f.legendType==="point"&&e.generatePoints();c&&(g[0]&&g[0].remove?g[0].remove(!1):(g.shift(),e.updateParallelArrays(i, -"shift"),h.shift()));e.isDirty=!0;e.isDirtyData=!0;b&&(e.getAttribs(),k.redraw())},removePoint:function(a,b,c){var d=this,e=d.data,f=e[a],g=d.points,h=d.chart,i=function(){g&&g.length===e.length&&g.splice(a,1);e.splice(a,1);d.options.data.splice(a,1);d.updateParallelArrays(f||{series:d},"splice",a,1);f&&f.destroy();d.isDirty=!0;d.isDirtyData=!0;b&&h.redraw()};Ta(c,h);b=o(b,!0);f?f.firePointEvent("remove",null,i):i()},remove:function(a,b){var c=this,d=c.chart;I(c,"remove",null,function(){c.destroy(); -d.isDirtyLegend=d.isDirtyBox=!0;d.linkSeries();o(a,!0)&&d.redraw(b)})},update:function(a,b){var c=this,d=this.chart,e=this.userOptions,f=this.type,g=L[f].prototype,h=["group","markerGroup","dataLabelsGroup"],i;if(a.type&&a.type!==f||a.zIndex!==void 0)h.length=0;p(h,function(a){h[a]=c[a];delete c[a]});a=E(e,{animation:!1,index:this.index,pointStart:this.xData[0]},{data:this.options.data},a);this.remove(!1);for(i in g)this[i]=y;u(this,L[a.type||f].prototype);p(h,function(a){c[a]=h[a]});this.init(d, -a);d.linkSeries();o(b,!0)&&d.redraw(!1)}});u(ha.prototype,{update:function(a,b){var c=this.chart,a=c.options[this.coll][this.options.index]=E(this.userOptions,a);this.destroy(!0);this._addedPlotLB=this.chart._labelPanes=y;this.init(c,u(a,{events:y}));c.isDirtyBox=!0;o(b,!0)&&c.redraw()},remove:function(a){for(var b=this.chart,c=this.coll,d=this.series,e=d.length;e--;)d[e]&&d[e].remove(!1);pa(b.axes,this);pa(b[c],this);b.options[c].splice(this.options.index,1);p(b[c],function(a,b){a.options.index= -b});this.destroy();b.isDirtyBox=!0;o(a,!0)&&b.redraw()},setTitle:function(a,b){this.update({title:a},b)},setCategories:function(a,b){this.update({categories:a},b)}});var wa=qa(R);L.line=wa;ea.area=E(ga,{softThreshold:!1,threshold:0});var oa=qa(R,{type:"area",singleStacks:!1,getStackPoints:function(){var a=[],b=[],c=this.xAxis,d=this.yAxis,e=d.stacks[this.stackKey],f={},g=this.points,h=this.index,i=d.series,k=i.length,j,l=o(d.options.reversedStacks,!0)?1:-1,m,n;if(this.options.stacking){for(m=0;m< -g.length;m++)f[g[m].x]=g[m];for(n in e)e[n].total!==null&&b.push(n);b.sort(function(a,b){return a-b});j=Ca(i,function(){return this.visible});p(b,function(g,i){var n=0,o,r;if(f[g]&&!f[g].isNull)a.push(f[g]),p([-1,1],function(a){var c=a===1?"rightNull":"leftNull",d=0,n=e[b[i+a]];if(n)for(m=h;m>=0&&m=0&&ma&&h>e?(h=t(a,e),k=2*e-h):hc&&k>e?(k=t(c,e),h=2*e-k):k0.5;b=Math.round(b)+f;d-=b;g&&d&&(b-=1,d+=1);return{x:a,y:b,width:c,height:d}},translate:function(){var a=this,b=a.chart,c=a.options,d=a.borderWidth=o(c.borderWidth,a.closestPointRange*a.xAxis.transA<2?0:1),e=a.yAxis,f= -a.translatedThreshold=e.getThreshold(c.threshold),g=o(c.minPointLength,5),h=a.getColumnMetrics(),i=h.width,k=a.barW=t(i,1+2*d),j=a.pointXOffset=h.offset;b.inverted&&(f-=0.5);c.pointPadding&&(k=ua(k));R.prototype.translate.apply(a);p(a.points,function(c){var d=F(o(c.yBottom,f),9E4),h=999+Q(d),h=F(t(-h,c.plotY),e.len+h),q=c.plotX+j,p=k,s=F(h,d),r,w=t(h,d)-s;Q(w)g?d-g:f-(r?g:0));c.barX=q;c.pointWidth=i;c.tooltipPos=b.inverted?[e.len+ -e.pos-b.plotLeft-h,a.xAxis.len-q-p/2,w]:[q+p/2,h+e.pos-b.plotTop,w];c.shapeType="rect";c.shapeArgs=a.crispCol(q,s,p,w)})},getSymbol:Aa,drawLegendSymbol:K.drawRectangle,drawGraph:Aa,drawPoints:function(){var a=this,b=this.chart,c=a.options,d=b.renderer,e=c.animationLimit||250,f,g;p(a.points,function(h){var i=h.graphic,k;if(J(h.plotY)&&h.y!==null)f=h.shapeArgs,k=r(a.borderWidth)?{"stroke-width":a.borderWidth}:{},g=h.pointAttr[h.selected?"select":""]||a.pointAttr[""],i?(Oa(i),i.attr(k).attr(g)[b.pointCount< -e?"animate":"attr"](E(f))):h.graphic=d[h.shapeType](f).attr(k).attr(g).add(h.group||a.group).shadow(c.shadow,null,c.stacking&&!c.borderRadius);else if(i)h.graphic=i.destroy()})},animate:function(a){var b=this,c=this.yAxis,d=b.options,e=this.chart.inverted,f={};if(fa)a?(f.scaleY=0.001,a=F(c.pos+c.len,t(c.pos,c.toPixels(d.threshold))),e?f.translateX=a-c.len:f.translateY=a,b.group.attr(f)):(f[e?"translateX":"translateY"]=c.pos,b.group.animate(f,u($a(b.options.animation),{step:function(a,c){b.group.attr({scaleY:t(0.001, -c.pos)})}})),b.animate=null)},remove:function(){var a=this,b=a.chart;b.hasRendered&&p(b.series,function(b){if(b.type===a.type)b.isDirty=!0});R.prototype.remove.apply(a,arguments)}});L.column=wa;ea.bar=E(ea.column);oa=qa(wa,{type:"bar",inverted:!0});L.bar=oa;ea.scatter=E(ga,{lineWidth:0,marker:{enabled:!0},tooltip:{headerFormat:'\u25cf {series.name}
',pointFormat:"x: {point.x}
y: {point.y}
"}}); -oa=qa(R,{type:"scatter",sorted:!1,requireSorting:!1,noSharedTooltip:!0,trackerGroups:["group","markerGroup","dataLabelsGroup"],takeOrdinalPosition:!1,kdDimensions:2,drawGraph:function(){this.options.lineWidth&&R.prototype.drawGraph.call(this)}});L.scatter=oa;ea.pie=E(ga,{borderColor:"#FFFFFF",borderWidth:1,center:[null,null],clip:!1,colorByPoint:!0,dataLabels:{distance:30,enabled:!0,formatter:function(){return this.y===null?void 0:this.point.name},x:0},ignoreHiddenPoint:!0,legendType:"point",marker:null, -size:null,showInLegend:!1,slicedOffset:10,states:{hover:{brightness:0.1,shadow:!1}},stickyTracking:!1,tooltip:{followPointer:!0}});ga={type:"pie",isCartesian:!1,pointClass:qa(Ja,{init:function(){Ja.prototype.init.apply(this,arguments);var a=this,b;a.name=o(a.name,"Slice");b=function(b){a.slice(b.type==="select")};N(a,"select",b);N(a,"unselect",b);return a},setVisible:function(a,b){var c=this,d=c.series,e=d.chart,f=d.options.ignoreHiddenPoint,b=o(b,f);if(a!==c.visible){c.visible=c.options.visible= -a=a===y?!c.visible:a;d.options.data[sa(c,d.data)]=c.options;p(["graphic","dataLabel","connector","shadowGroup"],function(b){if(c[b])c[b][a?"show":"hide"](!0)});c.legendItem&&e.legend.colorizeItem(c,a);!a&&c.state==="hover"&&c.setState("");if(f)d.isDirty=!0;b&&e.redraw()}},slice:function(a,b,c){var d=this.series;Ta(c,d.chart);o(b,!0);this.sliced=this.options.sliced=a=r(a)?a:!this.sliced;d.options.data[sa(this,d.data)]=this.options;a=a?this.slicedTranslation:{translateX:0,translateY:0};this.graphic.animate(a); -this.shadowGroup&&this.shadowGroup.animate(a)},haloPath:function(a){var b=this.shapeArgs,c=this.series.chart;return this.sliced||!this.visible?[]:this.series.chart.renderer.symbols.arc(c.plotLeft+b.x,c.plotTop+b.y,b.r+a,b.r+a,{innerR:this.shapeArgs.r,start:b.start,end:b.end})}}),requireSorting:!1,directTouch:!0,noSharedTooltip:!0,trackerGroups:["group","dataLabelsGroup"],axisTypes:[],pointAttrToOptions:{stroke:"borderColor","stroke-width":"borderWidth",fill:"color"},animate:function(a){var b=this, -c=b.points,d=b.startAngleRad;if(!a)p(c,function(a){var c=a.graphic,g=a.shapeArgs;c&&(c.attr({r:a.startR||b.center[3]/2,start:d,end:d}),c.animate({r:g.r,start:g.start,end:g.end},b.options.animation))}),b.animate=null},updateTotals:function(){var a,b=0,c=this.points,d=c.length,e,f=this.options.ignoreHiddenPoint;for(a=0;a0&&(e.visible||!f)?e.y/b*100:0,e.total=b},generatePoints:function(){R.prototype.generatePoints.call(this); -this.updateTotals()},translate:function(a){this.generatePoints();var b=0,c=this.options,d=c.slicedOffset,e=d+c.borderWidth,f,g,h,i=c.startAngle||0,k=this.startAngleRad=ra/180*(i-90),i=(this.endAngleRad=ra/180*(o(c.endAngle,i+360)-90))-k,j=this.points,l=c.dataLabels.distance,c=c.ignoreHiddenPoint,m,n=j.length,q;if(!a)this.center=a=this.getCenter();this.getX=function(b,c){h=Y.asin(F((b-a[1])/(a[2]/2+l),1));return a[0]+(c?-1:1)*W(h)*(a[2]/2+l)};for(m=0;m1.5*ra?h-=2*ra:h<-ra/2&&(h+=2*ra);q.slicedTranslation={translateX:B(W(h)*d),translateY:B(da(h)*d)};f=W(h)*a[2]/2;g=da(h)*a[2]/2;q.tooltipPos=[a[0]+f*0.7,a[1]+g*0.7];q.half=h<-ra/2||h>ra/2?1:0;q.angle=h;e=F(e,l/2);q.labelPos=[a[0]+f+W(h)*l,a[1]+g+da(h)*l,a[0]+f+W(h)*e,a[1]+g+da(h)*e,a[0]+f,a[1]+g,l<0?"center":q.half?"right":"left",h]}},drawGraph:null,drawPoints:function(){var a= -this,b=a.chart.renderer,c,d,e=a.options.shadow,f,g,h,i;if(e&&!a.shadowGroup)a.shadowGroup=b.g("shadow").add(a.group);p(a.points,function(k){if(k.y!==null){d=k.graphic;h=k.shapeArgs;f=k.shadowGroup;g=k.pointAttr[k.selected?"select":""];if(!g.stroke)g.stroke=g.fill;if(e&&!f)f=k.shadowGroup=b.g("shadow").add(a.shadowGroup);c=k.sliced?k.slicedTranslation:{translateX:0,translateY:0};f&&f.attr(c);if(d)d.setRadialReference(a.center).attr(g).animate(u(h,c));else{i={"stroke-linejoin":"round"};if(!k.visible)i.visibility= -"hidden";k.graphic=d=b[k.shapeType](h).setRadialReference(a.center).attr(g).attr(i).attr(c).add(a.group).shadow(e,f)}}})},searchPoint:Aa,sortByAngle:function(a,b){a.sort(function(a,d){return a.angle!==void 0&&(d.angle-a.angle)*b})},drawLegendSymbol:K.drawRectangle,getCenter:Bb.getCenter,getSymbol:Aa};ga=qa(R,ga);L.pie=ga;R.prototype.drawDataLabels=function(){var a=this,b=a.options,c=b.cursor,d=b.dataLabels,e=a.points,f,g,h=a.hasRendered||0,i,k,j=o(d.defer,!0),l=a.chart.renderer;if(d.enabled||a._hasPointLabels)a.dlProcessOptions&& -a.dlProcessOptions(d),k=a.plotGroup("dataLabelsGroup","data-labels",j&&!h?"hidden":"visible",d.zIndex||6),j&&(k.attr({opacity:+h}),h||N(a,"afterAnimate",function(){a.visible&&k.show();k[b.animation?"animate":"attr"]({opacity:1},{duration:200})})),g=d,p(e,function(e){var h,j=e.dataLabel,p,s,t=e.connector,w=!0,v,x={};f=e.dlOptions||e.options&&e.options.dataLabels;h=o(f&&f.enabled,g.enabled)&&e.y!==null;if(j&&!h)e.dataLabel=j.destroy();else if(h){d=E(g,f);v=d.style;h=d.rotation;p=e.getLabelConfig(); -i=d.format?Ka(d.format,p):d.formatter.call(p,d);v.color=o(d.color,v.color,a.color,"black");if(j)if(r(i))j.attr({text:i}),w=!1;else{if(e.dataLabel=j=j.destroy(),t)e.connector=t.destroy()}else if(r(i)){j={fill:d.backgroundColor,stroke:d.borderColor,"stroke-width":d.borderWidth,r:d.borderRadius||0,rotation:h,padding:d.padding,zIndex:1};if(v.color==="contrast")x.color=d.inside||d.distance<0||b.stacking?l.getContrast(e.color||a.color):"#000000";if(c)x.cursor=c;for(s in j)j[s]===y&&delete j[s];j=e.dataLabel= -l[h?"text":"label"](i,0,-9999,d.shape,null,null,d.useHTML).attr(j).css(u(v,x)).add(k).shadow(d.shadow)}j&&a.alignDataLabel(e,j,d,null,w)}})};R.prototype.alignDataLabel=function(a,b,c,d,e){var f=this.chart,g=f.inverted,h=o(a.plotX,-9999),i=o(a.plotY,-9999),k=b.getBBox(),j=f.renderer.fontMetrics(c.style.fontSize).b,l=c.rotation,m=c.align,n=this.visible&&(a.series.forceDL||f.isInsidePlot(h,B(i),g)||d&&f.isInsidePlot(h,g?d.x+1:d.y+d.height-1,g)),q=o(c.overflow,"justify")==="justify";if(n)d=u({x:g?f.plotWidth- -i:h,y:B(g?f.plotHeight-h:i),width:0,height:0},d),u(c,{width:k.width,height:k.height}),l?(q=!1,g=f.renderer.rotCorr(j,l),g={x:d.x+c.x+d.width/2+g.x,y:d.y+c.y+{top:0,middle:0.5,bottom:1}[c.verticalAlign]*d.height},b[e?"attr":"animate"](g).attr({align:m}),h=(l+720)%360,h=h>180&&h<360,m==="left"?g.y-=h?k.height:0:m==="center"?(g.x-=k.width/2,g.y-=k.height/2):m==="right"&&(g.x-=k.width,g.y-=h?0:k.height)):(b.align(c,null,d),g=b.alignAttr),q?this.justifyDataLabel(b,c,g,k,d,e):o(c.crop,!0)&&(n=f.isInsidePlot(g.x, -g.y)&&f.isInsidePlot(g.x+k.width,g.y+k.height)),c.shape&&!l&&b.attr({anchorX:a.plotX,anchorY:a.plotY});if(!n)Oa(b),b.attr({y:-9999}),b.placed=!1};R.prototype.justifyDataLabel=function(a,b,c,d,e,f){var g=this.chart,h=b.align,i=b.verticalAlign,k,j,l=a.box?0:a.padding||0;k=c.x+l;if(k<0)h==="right"?b.align="left":b.x=-k,j=!0;k=c.x+d.width-l;if(k>g.plotWidth)h==="left"?b.align="right":b.x=g.plotWidth-k,j=!0;k=c.y+l;if(k<0)i==="bottom"?b.verticalAlign="top":b.y=-k,j=!0;k=c.y+d.height-l;if(k>g.plotHeight)i=== -"top"?b.verticalAlign="bottom":b.y=g.plotHeight-k,j=!0;if(j)a.placed=!f,a.align(b,null,e)};if(L.pie)L.pie.prototype.drawDataLabels=function(){var a=this,b=a.data,c,d=a.chart,e=a.options.dataLabels,f=o(e.connectorPadding,10),g=o(e.connectorWidth,1),h=d.plotWidth,i=d.plotHeight,k,j,l=o(e.softConnector,!0),m=e.distance,n=a.center,q=n[2]/2,r=n[1],s=m>0,u,w,v,x=[[],[]],y,A,D,E,C,H=[0,0,0,0],M=function(a,b){return b.y-a.y};if(a.visible&&(e.enabled||a._hasPointLabels)){R.prototype.drawDataLabels.apply(a); -p(b,function(a){if(a.dataLabel&&a.visible)x[a.half].push(a),a.dataLabel._pos=null});for(E=2;E--;){var I=[],N=[],J=x[E],L=J.length,K;if(L){a.sortByAngle(J,E-0.5);for(C=b=0;!b&&J[C];)b=J[C]&&J[C].dataLabel&&(J[C].dataLabel.getBBox().height||21),C++;if(m>0){w=F(r+q+m,d.plotHeight);for(C=t(0,r-q-m);C<=w;C+=b)I.push(C);w=I.length;if(L>w){c=[].concat(J);c.sort(M);for(C=L;C--;)c[C].rank=C;for(C=L;C--;)J[C].rank>=w&&J.splice(C,1);L=J.length}for(C=0;C0){if(w=N.pop(),K=w.i,A=w.y,c>A&&I[K+1]!==null||ch-f&&(H[1]=t(B(y+w-h+f),H[1])),A-b/2<0?H[0]=t(B(-A+b/2),H[0]):A+b/2>i&&(H[2]=t(B(A+b/2-i),H[2]))}}}if(Ga(H)===0||this.verifyDataLabelOverflow(H))this.placeDataLabels(),s&&g&&p(this.points,function(b){k=b.connector;v=b.labelPos;if((u=b.dataLabel)&&u._pos&&b.visible)D=u._attr.visibility,y=u.connX,A=u.connY,j=l?["M",y+(v[6]==="left"?5:-5),A,"C",y,A,2*v[2]-v[4],2*v[3]-v[5],v[2],v[3],"L",v[4],v[5]]:["M",y+ -(v[6]==="left"?5:-5),A,"L",v[2],v[3],"L",v[4],v[5]],k?(k.animate({d:j}),k.attr("visibility",D)):b.connector=k=a.chart.renderer.path(j).attr({"stroke-width":g,stroke:e.connectorColor||b.color||"#606060",visibility:D}).add(a.dataLabelsGroup);else if(k)b.connector=k.destroy()})}},L.pie.prototype.placeDataLabels=function(){p(this.points,function(a){var b=a.dataLabel;if(b&&a.visible)(a=b._pos)?(b.attr(b._attr),b[b.moved?"animate":"attr"](a),b.moved=!0):b&&b.attr({y:-9999})})},L.pie.prototype.alignDataLabel= -Aa,L.pie.prototype.verifyDataLabelOverflow=function(a){var b=this.center,c=this.options,d=c.center,e=c.minSize||80,f=e,g;d[0]!==null?f=t(b[2]-t(a[1],a[3]),e):(f=t(b[2]-a[1]-a[3],e),b[0]+=(a[3]-a[1])/2);d[1]!==null?f=t(F(f,b[2]-t(a[0],a[2])),e):(f=t(F(f,b[2]-a[0]-a[2]),e),b[1]+=(a[0]-a[2])/2);fo(this.translatedThreshold,g.yAxis.len)),k=o(c.inside,!!this.options.stacking);if(h){d=E(h);if(d.y<0)d.height+=d.y,d.y=0;h=d.y+d.height-g.yAxis.len;h>0&&(d.height-=h);f&&(d={x:g.yAxis.len-d.y-d.height,y:g.xAxis.len-d.x-d.width,width:d.height,height:d.width});if(!k)f?(d.x+=i?0:d.width,d.width=0):(d.y+=i?d.height:0,d.height=0)}c.align=o(c.align,!f||k?"center":i?"right":"left");c.verticalAlign=o(c.verticalAlign, -f||k?"middle":i?"top":"bottom");R.prototype.alignDataLabel.call(this,a,b,c,d,e)};(function(a){var b=a.Chart,c=a.each,d=a.pick,e=a.addEvent;b.prototype.callbacks.push(function(a){function b(){var e=[];c(a.series,function(a){var b=a.options.dataLabels,f=a.dataLabelCollections||["dataLabel"];(b.enabled||a._hasPointLabels)&&!b.allowOverlap&&a.visible&&c(f,function(b){c(a.points,function(a){if(a[b])a[b].labelrank=d(a.labelrank,a.shapeArgs&&a.shapeArgs.height),e.push(a[b])})})});a.hideOverlappingLabels(e)} -b();e(a,"redraw",b)});b.prototype.hideOverlappingLabels=function(a){var b=a.length,d,e,k,j,l,m,n,q,o;for(e=0;el.x+n.translateX+(k.width-o)||m.x+q.translateX+(j.width- -o)l.y+n.translateY+(k.height-o)||m.y+q.translateY+(j.height-o)h;if(b.series.length&&(i||l>F(j.dataMin,j.min))&&(!i||k
/g, '') - .split(//g); - - } else { - lines = [textStr]; - } - - - // Trim empty lines (#5261) - lines = grep(lines, function (line) { - return line !== ''; - }); - - - // build the lines - each(lines, function buildTextLines(line, lineNo) { - var spans, - spanNo = 0; - line = line - .replace(/^\s+|\s+$/g, '') // Trim to prevent useless/costly process on the spaces (#5258) - .replace(//g, '|||'); - spans = line.split('|||'); - - each(spans, function buildTextSpans(span) { - if (span !== '' || spans.length === 1) { - var attributes = {}, - tspan = doc.createElementNS(SVG_NS, 'tspan'), - spanStyle; // #390 - if (styleRegex.test(span)) { - spanStyle = span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2'); - attr(tspan, 'style', spanStyle); - } - if (hrefRegex.test(span) && !forExport) { // Not for export - #1529 - attr(tspan, 'onclick', 'location.href=\"' + span.match(hrefRegex)[1] + '\"'); - css(tspan, { cursor: 'pointer' }); - } - - span = unescapeAngleBrackets(span.replace(/<(.|\n)*?>/g, '') || ' '); - - // Nested tags aren't supported, and cause crash in Safari (#1596) - if (span !== ' ') { - - // add the text node - tspan.appendChild(doc.createTextNode(span)); - - if (!spanNo) { // first span in a line, align it to the left - if (lineNo && parentX !== null) { - attributes.x = parentX; - } - } else { - attributes.dx = 0; // #16 - } - - // add attributes - attr(tspan, attributes); - - // Append it - textNode.appendChild(tspan); - - // first span on subsequent line, add the line height - if (!spanNo && lineNo) { - - // allow getting the right offset height in exporting in IE - if (!hasSVG && forExport) { - css(tspan, { display: 'block' }); - } - - // Set the line height based on the font size of either - // the text element or the tspan element - attr( - tspan, - 'dy', - getLineHeight(tspan) - ); - } - - /*if (width) { - renderer.breakText(wrapper, width); - }*/ - - // Check width and apply soft breaks or ellipsis - if (width) { - var words = span.replace(/([^\^])-/g, '$1- ').split(' '), // #1273 - hasWhiteSpace = spans.length > 1 || lineNo || (words.length > 1 && textStyles.whiteSpace !== 'nowrap'), - tooLong, - actualWidth, - rest = [], - dy = getLineHeight(tspan), - softLineNo = 1, - rotation = wrapper.rotation, - wordStr = span, // for ellipsis - cursor = wordStr.length, // binary search cursor - bBox; - - while ((hasWhiteSpace || ellipsis) && (words.length || rest.length)) { - wrapper.rotation = 0; // discard rotation when computing box - bBox = wrapper.getBBox(true); - actualWidth = bBox.width; - - // Old IE cannot measure the actualWidth for SVG elements (#2314) - if (!hasSVG && renderer.forExport) { - actualWidth = renderer.measureSpanWidth(tspan.firstChild.data, wrapper.styles); - } - - tooLong = actualWidth > width; - - // For ellipsis, do a binary search for the correct string length - if (wasTooLong === undefined) { - wasTooLong = tooLong; // First time - } - if (ellipsis && wasTooLong) { - cursor /= 2; - - if (wordStr === '' || (!tooLong && cursor < 0.5)) { - words = []; // All ok, break out - } else { - wordStr = span.substring(0, wordStr.length + (tooLong ? -1 : 1) * mathCeil(cursor)); - words = [wordStr + (width > 3 ? '\u2026' : '')]; - tspan.removeChild(tspan.firstChild); - } - - // Looping down, this is the first word sequence that is not too long, - // so we can move on to build the next line. - } else if (!tooLong || words.length === 1) { - words = rest; - rest = []; - - if (words.length) { - softLineNo++; - - tspan = doc.createElementNS(SVG_NS, 'tspan'); - attr(tspan, { - dy: dy, - x: parentX - }); - if (spanStyle) { // #390 - attr(tspan, 'style', spanStyle); - } - textNode.appendChild(tspan); - } - if (actualWidth > width) { // a single word is pressing it out - width = actualWidth; - } - } else { // append to existing line tspan - tspan.removeChild(tspan.firstChild); - rest.unshift(words.pop()); - } - if (words.length) { - tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-'))); - } - } - wrapper.rotation = rotation; - } - - spanNo++; - } - } - }); - }); - - if (wasTooLong) { - wrapper.attr('title', wrapper.textStr); - } - if (tempParent) { - tempParent.removeChild(textNode); // attach it to the DOM to read offset width - } - - // Apply the text shadow - if (textShadow && wrapper.applyTextShadow) { - wrapper.applyTextShadow(textShadow); - } - } - }, - - - - /* - breakText: function (wrapper, width) { - var bBox = wrapper.getBBox(), - node = wrapper.element, - textLength = node.textContent.length, - pos = mathRound(width * textLength / bBox.width), // try this position first, based on average character width - increment = 0, - finalPos; - - if (bBox.width > width) { - while (finalPos === undefined) { - textLength = node.getSubStringLength(0, pos); - - if (textLength <= width) { - if (increment === -1) { - finalPos = pos; - } else { - increment = 1; - } - } else { - if (increment === 1) { - finalPos = pos - 1; - } else { - increment = -1; - } - } - pos += increment; - } - } - console.log('width', width, 'stringWidth', node.getSubStringLength(0, finalPos)) - }, - */ - - /** - * Returns white for dark colors and black for bright colors - */ - getContrast: function (color) { - color = Color(color).rgba; - return color[0] + color[1] + color[2] > 384 ? '#000000' : '#FFFFFF'; - }, - - /** - * Create a button with preset states - * @param {String} text - * @param {Number} x - * @param {Number} y - * @param {Function} callback - * @param {Object} normalState - * @param {Object} hoverState - * @param {Object} pressedState - */ - button: function (text, x, y, callback, normalState, hoverState, pressedState, disabledState, shape) { - var label = this.label(text, x, y, shape, null, null, null, null, 'button'), - curState = 0, - stateOptions, - stateStyle, - normalStyle, - hoverStyle, - pressedStyle, - disabledStyle, - verticalGradient = { x1: 0, y1: 0, x2: 0, y2: 1 }; - - // Normal state - prepare the attributes - normalState = merge({ - 'stroke-width': 1, - stroke: '#CCCCCC', - fill: { - linearGradient: verticalGradient, - stops: [ - [0, '#FEFEFE'], - [1, '#F6F6F6'] - ] - }, - r: 2, - padding: 5, - style: { - color: 'black' - } - }, normalState); - normalStyle = normalState.style; - delete normalState.style; - - // Hover state - hoverState = merge(normalState, { - stroke: '#68A', - fill: { - linearGradient: verticalGradient, - stops: [ - [0, '#FFF'], - [1, '#ACF'] - ] - } - }, hoverState); - hoverStyle = hoverState.style; - delete hoverState.style; - - // Pressed state - pressedState = merge(normalState, { - stroke: '#68A', - fill: { - linearGradient: verticalGradient, - stops: [ - [0, '#9BD'], - [1, '#CDF'] - ] - } - }, pressedState); - pressedStyle = pressedState.style; - delete pressedState.style; - - // Disabled state - disabledState = merge(normalState, { - style: { - color: '#CCC' - } - }, disabledState); - disabledStyle = disabledState.style; - delete disabledState.style; - - // Add the events. IE9 and IE10 need mouseover and mouseout to funciton (#667). - addEvent(label.element, isMS ? 'mouseover' : 'mouseenter', function () { - if (curState !== 3) { - label.attr(hoverState) - .css(hoverStyle); - } - }); - addEvent(label.element, isMS ? 'mouseout' : 'mouseleave', function () { - if (curState !== 3) { - stateOptions = [normalState, hoverState, pressedState][curState]; - stateStyle = [normalStyle, hoverStyle, pressedStyle][curState]; - label.attr(stateOptions) - .css(stateStyle); - } - }); - - label.setState = function (state) { - label.state = curState = state; - if (!state) { - label.attr(normalState) - .css(normalStyle); - } else if (state === 2) { - label.attr(pressedState) - .css(pressedStyle); - } else if (state === 3) { - label.attr(disabledState) - .css(disabledStyle); - } - }; - - return label - .on('click', function (e) { - if (curState !== 3) { - callback.call(label, e); - } - }) - .attr(normalState) - .css(extend({ cursor: 'default' }, normalStyle)); - }, - - /** - * Make a straight line crisper by not spilling out to neighbour pixels - * @param {Array} points - * @param {Number} width - */ - crispLine: function (points, width) { - // points format: [M, 0, 0, L, 100, 0] - // normalize to a crisp line - if (points[1] === points[4]) { - // Substract due to #1129. Now bottom and left axis gridlines behave the same. - points[1] = points[4] = mathRound(points[1]) - (width % 2 / 2); - } - if (points[2] === points[5]) { - points[2] = points[5] = mathRound(points[2]) + (width % 2 / 2); - } - return points; - }, - - - /** - * Draw a path - * @param {Array} path An SVG path in array form - */ - path: function (path) { - var attr = { - fill: NONE - }; - if (isArray(path)) { - attr.d = path; - } else if (isObject(path)) { // attributes - extend(attr, path); - } - return this.createElement('path').attr(attr); - }, - - /** - * Draw and return an SVG circle - * @param {Number} x The x position - * @param {Number} y The y position - * @param {Number} r The radius - */ - circle: function (x, y, r) { - var attr = isObject(x) ? x : { x: x, y: y, r: r }, - wrapper = this.createElement('circle'); - - // Setting x or y translates to cx and cy - wrapper.xSetter = wrapper.ySetter = function (value, key, element) { - element.setAttribute('c' + key, value); - }; - - return wrapper.attr(attr); - }, - - /** - * Draw and return an arc - * @param {Number} x X position - * @param {Number} y Y position - * @param {Number} r Radius - * @param {Number} innerR Inner radius like used in donut charts - * @param {Number} start Starting angle - * @param {Number} end Ending angle - */ - arc: function (x, y, r, innerR, start, end) { - var arc; - - if (isObject(x)) { - y = x.y; - r = x.r; - innerR = x.innerR; - start = x.start; - end = x.end; - x = x.x; - } - - // Arcs are defined as symbols for the ability to set - // attributes in attr and animate - arc = this.symbol('arc', x || 0, y || 0, r || 0, r || 0, { - innerR: innerR || 0, - start: start || 0, - end: end || 0 - }); - arc.r = r; // #959 - return arc; - }, - - /** - * Draw and return a rectangle - * @param {Number} x Left position - * @param {Number} y Top position - * @param {Number} width - * @param {Number} height - * @param {Number} r Border corner radius - * @param {Number} strokeWidth A stroke width can be supplied to allow crisp drawing - */ - rect: function (x, y, width, height, r, strokeWidth) { - - r = isObject(x) ? x.r : r; - - var wrapper = this.createElement('rect'), - attribs = isObject(x) ? x : x === UNDEFINED ? {} : { - x: x, - y: y, - width: mathMax(width, 0), - height: mathMax(height, 0) - }; - - if (strokeWidth !== UNDEFINED) { - wrapper.strokeWidth = strokeWidth; - attribs = wrapper.crisp(attribs); - } - - if (r) { - attribs.r = r; - } - - wrapper.rSetter = function (value, key, element) { - attr(element, { - rx: value, - ry: value - }); - }; - - return wrapper.attr(attribs); - }, - - /** - * Resize the box and re-align all aligned elements - * @param {Object} width - * @param {Object} height - * @param {Boolean} animate - * - */ - setSize: function (width, height, animate) { - var renderer = this, - alignedObjects = renderer.alignedObjects, - i = alignedObjects.length; - - renderer.width = width; - renderer.height = height; - - renderer.boxWrapper[pick(animate, true) ? 'animate' : 'attr']({ - width: width, - height: height - }); - - while (i--) { - alignedObjects[i].align(); - } - }, - - /** - * Create a group - * @param {String} name The group will be given a class name of 'highcharts-{name}'. - * This can be used for styling and scripting. - */ - g: function (name) { - var elem = this.createElement('g'); - return defined(name) ? elem.attr({ 'class': PREFIX + name }) : elem; - }, - - /** - * Display an image - * @param {String} src - * @param {Number} x - * @param {Number} y - * @param {Number} width - * @param {Number} height - */ - image: function (src, x, y, width, height) { - var attribs = { - preserveAspectRatio: NONE - }, - elemWrapper; - - // optional properties - if (arguments.length > 1) { - extend(attribs, { - x: x, - y: y, - width: width, - height: height - }); - } - - elemWrapper = this.createElement('image').attr(attribs); - - // set the href in the xlink namespace - if (elemWrapper.element.setAttributeNS) { - elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink', - 'href', src); - } else { - // could be exporting in IE - // using href throws "not supported" in ie7 and under, requries regex shim to fix later - elemWrapper.element.setAttribute('hc-svg-href', src); - } - return elemWrapper; - }, - - /** - * Draw a symbol out of pre-defined shape paths from the namespace 'symbol' object. - * - * @param {Object} symbol - * @param {Object} x - * @param {Object} y - * @param {Object} radius - * @param {Object} options - */ - symbol: function (symbol, x, y, width, height, options) { - - var ren = this, - obj, - - // get the symbol definition function - symbolFn = this.symbols[symbol], - - // check if there's a path defined for this symbol - path = symbolFn && symbolFn( - mathRound(x), - mathRound(y), - width, - height, - options - ), - - imageRegex = /^url\((.*?)\)$/, - imageSrc, - imageSize, - centerImage; - - if (path) { - - obj = this.path(path); - // expando properties for use in animate and attr - extend(obj, { - symbolName: symbol, - x: x, - y: y, - width: width, - height: height - }); - if (options) { - extend(obj, options); - } - - - // image symbols - } else if (imageRegex.test(symbol)) { - - // On image load, set the size and position - centerImage = function (img, size) { - if (img.element) { // it may be destroyed in the meantime (#1390) - img.attr({ - width: size[0], - height: size[1] - }); - - if (!img.alignByTranslate) { // #185 - img.translate( - mathRound((width - size[0]) / 2), // #1378 - mathRound((height - size[1]) / 2) - ); - } - } - }; - - imageSrc = symbol.match(imageRegex)[1]; - imageSize = symbolSizes[imageSrc] || (options && options.width && options.height && [options.width, options.height]); - - // Ireate the image synchronously, add attribs async - obj = this.image(imageSrc) - .attr({ - x: x, - y: y - }); - obj.isImg = true; - - if (imageSize) { - centerImage(obj, imageSize); - } else { - // Initialize image to be 0 size so export will still function if there's no cached sizes. - obj.attr({ width: 0, height: 0 }); - - // Create a dummy JavaScript image to get the width and height. Due to a bug in IE < 8, - // the created element must be assigned to a variable in order to load (#292). - createElement('img', { - onload: function () { - - // Special case for SVGs on IE11, the width is not accessible until the image is - // part of the DOM (#2854). - if (this.width === 0) { - css(this, { - position: ABSOLUTE, - top: '-999em' - }); - doc.body.appendChild(this); - } - - // Center the image - centerImage(obj, symbolSizes[imageSrc] = [this.width, this.height]); - - // Clean up after #2854 workaround. - if (this.parentNode) { - this.parentNode.removeChild(this); - } - - // Fire the load event when all external images are loaded - ren.imgCount--; - if (!ren.imgCount && charts[ren.chartIndex].onload) { - charts[ren.chartIndex].onload(); - } - }, - src: imageSrc - }); - this.imgCount++; - } - } - - return obj; - }, - - /** - * An extendable collection of functions for defining symbol paths. - */ - symbols: { - 'circle': function (x, y, w, h) { - var cpw = 0.166 * w; - return [ - M, x + w / 2, y, - 'C', x + w + cpw, y, x + w + cpw, y + h, x + w / 2, y + h, - 'C', x - cpw, y + h, x - cpw, y, x + w / 2, y, - 'Z' - ]; - }, - - 'square': function (x, y, w, h) { - return [ - M, x, y, - L, x + w, y, - x + w, y + h, - x, y + h, - 'Z' - ]; - }, - - 'triangle': function (x, y, w, h) { - return [ - M, x + w / 2, y, - L, x + w, y + h, - x, y + h, - 'Z' - ]; - }, - - 'triangle-down': function (x, y, w, h) { - return [ - M, x, y, - L, x + w, y, - x + w / 2, y + h, - 'Z' - ]; - }, - 'diamond': function (x, y, w, h) { - return [ - M, x + w / 2, y, - L, x + w, y + h / 2, - x + w / 2, y + h, - x, y + h / 2, - 'Z' - ]; - }, - 'arc': function (x, y, w, h, options) { - var start = options.start, - radius = options.r || w || h, - end = options.end - 0.001, // to prevent cos and sin of start and end from becoming equal on 360 arcs (related: #1561) - innerRadius = options.innerR, - open = options.open, - cosStart = mathCos(start), - sinStart = mathSin(start), - cosEnd = mathCos(end), - sinEnd = mathSin(end), - longArc = options.end - start < mathPI ? 0 : 1; - - return [ - M, - x + radius * cosStart, - y + radius * sinStart, - 'A', // arcTo - radius, // x radius - radius, // y radius - 0, // slanting - longArc, // long or short arc - 1, // clockwise - x + radius * cosEnd, - y + radius * sinEnd, - open ? M : L, - x + innerRadius * cosEnd, - y + innerRadius * sinEnd, - 'A', // arcTo - innerRadius, // x radius - innerRadius, // y radius - 0, // slanting - longArc, // long or short arc - 0, // clockwise - x + innerRadius * cosStart, - y + innerRadius * sinStart, - - open ? '' : 'Z' // close - ]; - }, - - /** - * Callout shape used for default tooltips, also used for rounded rectangles in VML - */ - callout: function (x, y, w, h, options) { - var arrowLength = 6, - halfDistance = 6, - r = mathMin((options && options.r) || 0, w, h), - safeDistance = r + halfDistance, - anchorX = options && options.anchorX, - anchorY = options && options.anchorY, - path; - - path = [ - 'M', x + r, y, - 'L', x + w - r, y, // top side - 'C', x + w, y, x + w, y, x + w, y + r, // top-right corner - 'L', x + w, y + h - r, // right side - 'C', x + w, y + h, x + w, y + h, x + w - r, y + h, // bottom-right corner - 'L', x + r, y + h, // bottom side - 'C', x, y + h, x, y + h, x, y + h - r, // bottom-left corner - 'L', x, y + r, // left side - 'C', x, y, x, y, x + r, y // top-right corner - ]; - - if (anchorX && anchorX > w && anchorY > y + safeDistance && anchorY < y + h - safeDistance) { // replace right side - path.splice(13, 3, - 'L', x + w, anchorY - halfDistance, - x + w + arrowLength, anchorY, - x + w, anchorY + halfDistance, - x + w, y + h - r - ); - } else if (anchorX && anchorX < 0 && anchorY > y + safeDistance && anchorY < y + h - safeDistance) { // replace left side - path.splice(33, 3, - 'L', x, anchorY + halfDistance, - x - arrowLength, anchorY, - x, anchorY - halfDistance, - x, y + r - ); - } else if (anchorY && anchorY > h && anchorX > x + safeDistance && anchorX < x + w - safeDistance) { // replace bottom - path.splice(23, 3, - 'L', anchorX + halfDistance, y + h, - anchorX, y + h + arrowLength, - anchorX - halfDistance, y + h, - x + r, y + h - ); - } else if (anchorY && anchorY < 0 && anchorX > x + safeDistance && anchorX < x + w - safeDistance) { // replace top - path.splice(3, 3, - 'L', anchorX - halfDistance, y, - anchorX, y - arrowLength, - anchorX + halfDistance, y, - w - r, y - ); - } - return path; - } - }, - - /** - * Define a clipping rectangle - * @param {String} id - * @param {Number} x - * @param {Number} y - * @param {Number} width - * @param {Number} height - */ - clipRect: function (x, y, width, height) { - var wrapper, - id = PREFIX + idCounter++, - - clipPath = this.createElement('clipPath').attr({ - id: id - }).add(this.defs); - - wrapper = this.rect(x, y, width, height, 0).add(clipPath); - wrapper.id = id; - wrapper.clipPath = clipPath; - wrapper.count = 0; - - return wrapper; - }, - - - - - - /** - * Add text to the SVG object - * @param {String} str - * @param {Number} x Left position - * @param {Number} y Top position - * @param {Boolean} useHTML Use HTML to render the text - */ - text: function (str, x, y, useHTML) { - - // declare variables - var renderer = this, - fakeSVG = useCanVG || (!hasSVG && renderer.forExport), - wrapper, - attr = {}; - - if (useHTML && (renderer.allowHTML || !renderer.forExport)) { - return renderer.html(str, x, y); - } - - attr.x = Math.round(x || 0); // X is always needed for line-wrap logic - if (y) { - attr.y = Math.round(y); - } - if (str || str === 0) { - attr.text = str; - } - - wrapper = renderer.createElement('text') - .attr(attr); - - // Prevent wrapping from creating false offsetWidths in export in legacy IE (#1079, #1063) - if (fakeSVG) { - wrapper.css({ - position: ABSOLUTE - }); - } - - if (!useHTML) { - wrapper.xSetter = function (value, key, element) { - var tspans = element.getElementsByTagName('tspan'), - tspan, - parentVal = element.getAttribute(key), - i; - for (i = 0; i < tspans.length; i++) { - tspan = tspans[i]; - // If the x values are equal, the tspan represents a linebreak - if (tspan.getAttribute(key) === parentVal) { - tspan.setAttribute(key, value); - } - } - element.setAttribute(key, value); - }; - } - - return wrapper; - }, - - /** - * Utility to return the baseline offset and total line height from the font size - */ - fontMetrics: function (fontSize, elem) { - var lineHeight, - baseline, - style; - - fontSize = fontSize || this.style.fontSize; - if (!fontSize && elem && win.getComputedStyle) { - elem = elem.element || elem; // SVGElement - style = win.getComputedStyle(elem, ''); - fontSize = style && style.fontSize; // #4309, the style doesn't exist inside a hidden iframe in Firefox - } - fontSize = /px/.test(fontSize) ? pInt(fontSize) : /em/.test(fontSize) ? parseFloat(fontSize) * 12 : 12; - - // Empirical values found by comparing font size and bounding box height. - // Applies to the default font family. http://jsfiddle.net/highcharts/7xvn7/ - lineHeight = fontSize < 24 ? fontSize + 3 : mathRound(fontSize * 1.2); - baseline = mathRound(lineHeight * 0.8); - - return { - h: lineHeight, - b: baseline, - f: fontSize - }; - }, - - /** - * Correct X and Y positioning of a label for rotation (#1764) - */ - rotCorr: function (baseline, rotation, alterY) { - var y = baseline; - if (rotation && alterY) { - y = mathMax(y * mathCos(rotation * deg2rad), 4); - } - return { - x: (-baseline / 3) * mathSin(rotation * deg2rad), - y: y - }; - }, - - /** - * Add a label, a text item that can hold a colored or gradient background - * as well as a border and shadow. - * @param {string} str - * @param {Number} x - * @param {Number} y - * @param {String} shape - * @param {Number} anchorX In case the shape has a pointer, like a flag, this is the - * coordinates it should be pinned to - * @param {Number} anchorY - * @param {Boolean} baseline Whether to position the label relative to the text baseline, - * like renderer.text, or to the upper border of the rectangle. - * @param {String} className Class name for the group - */ - label: function (str, x, y, shape, anchorX, anchorY, useHTML, baseline, className) { - - var renderer = this, - wrapper = renderer.g(className), - text = renderer.text('', 0, 0, useHTML) - .attr({ - zIndex: 1 - }), - //.add(wrapper), - box, - bBox, - alignFactor = 0, - padding = 3, - paddingLeft = 0, - width, - height, - wrapperX, - wrapperY, - crispAdjust = 0, - deferredAttr = {}, - baselineOffset, - needsBox, - updateBoxSize, - updateTextPadding, - boxAttr; - - /** - * This function runs after the label is added to the DOM (when the bounding box is - * available), and after the text of the label is updated to detect the new bounding - * box and reflect it in the border box. - */ - updateBoxSize = function () { - var boxX, - boxY, - style = text.element.style; - - bBox = (width === undefined || height === undefined || wrapper.styles.textAlign) && defined(text.textStr) && - text.getBBox(); //#3295 && 3514 box failure when string equals 0 - wrapper.width = (width || bBox.width || 0) + 2 * padding + paddingLeft; - wrapper.height = (height || bBox.height || 0) + 2 * padding; - - // update the label-scoped y offset - baselineOffset = padding + renderer.fontMetrics(style && style.fontSize, text).b; - - - if (needsBox) { - - if (!box) { - // create the border box if it is not already present - boxX = crispAdjust; - boxY = (baseline ? -baselineOffset : 0) + crispAdjust; - - wrapper.box = box = shape ? - renderer.symbol(shape, boxX, boxY, wrapper.width, wrapper.height, deferredAttr) : - renderer.rect(boxX, boxY, wrapper.width, wrapper.height, 0, deferredAttr[STROKE_WIDTH]); - - if (!box.isImg) { // #4324, fill "none" causes it to be ignored by mouse events in IE - box.attr('fill', NONE); - } - box.add(wrapper); - } - - // apply the box attributes - if (!box.isImg) { // #1630 - box.attr(extend({ - width: mathRound(wrapper.width), - height: mathRound(wrapper.height) - }, deferredAttr)); - } - deferredAttr = null; - } - }; - - /** - * This function runs after setting text or padding, but only if padding is changed - */ - updateTextPadding = function () { - var styles = wrapper.styles, - textAlign = styles && styles.textAlign, - x = paddingLeft + padding, - y; - - // determin y based on the baseline - y = baseline ? 0 : baselineOffset; - - // compensate for alignment - if (defined(width) && bBox && (textAlign === 'center' || textAlign === 'right')) { - x += { center: 0.5, right: 1 }[textAlign] * (width - bBox.width); - } - - // update if anything changed - if (x !== text.x || y !== text.y) { - text.attr('x', x); - if (y !== UNDEFINED) { - text.attr('y', y); - } - } - - // record current values - text.x = x; - text.y = y; - }; - - /** - * Set a box attribute, or defer it if the box is not yet created - * @param {Object} key - * @param {Object} value - */ - boxAttr = function (key, value) { - if (box) { - box.attr(key, value); - } else { - deferredAttr[key] = value; - } - }; - - /** - * After the text element is added, get the desired size of the border box - * and add it before the text in the DOM. - */ - wrapper.onAdd = function () { - text.add(wrapper); - wrapper.attr({ - text: (str || str === 0) ? str : '', // alignment is available now // #3295: 0 not rendered if given as a value - x: x, - y: y - }); - - if (box && defined(anchorX)) { - wrapper.attr({ - anchorX: anchorX, - anchorY: anchorY - }); - } - }; - - /* - * Add specific attribute setters. - */ - - // only change local variables - wrapper.widthSetter = function (value) { - width = value; - }; - wrapper.heightSetter = function (value) { - height = value; - }; - wrapper.paddingSetter = function (value) { - if (defined(value) && value !== padding) { - padding = wrapper.padding = value; - updateTextPadding(); - } - }; - wrapper.paddingLeftSetter = function (value) { - if (defined(value) && value !== paddingLeft) { - paddingLeft = value; - updateTextPadding(); - } - }; - - - // change local variable and prevent setting attribute on the group - wrapper.alignSetter = function (value) { - value = { left: 0, center: 0.5, right: 1 }[value]; - if (value !== alignFactor) { - alignFactor = value; - if (bBox) { // Bounding box exists, means we're dynamically changing - wrapper.attr({ x: wrapperX }); // #5134 - } - } - }; - - // apply these to the box and the text alike - wrapper.textSetter = function (value) { - if (value !== UNDEFINED) { - text.textSetter(value); - } - updateBoxSize(); - updateTextPadding(); - }; - - // apply these to the box but not to the text - wrapper['stroke-widthSetter'] = function (value, key) { - if (value) { - needsBox = true; - } - crispAdjust = value % 2 / 2; - boxAttr(key, value); - }; - wrapper.strokeSetter = wrapper.fillSetter = wrapper.rSetter = function (value, key) { - if (key === 'fill' && value) { - needsBox = true; - } - boxAttr(key, value); - }; - wrapper.anchorXSetter = function (value, key) { - anchorX = value; - boxAttr(key, mathRound(value) - crispAdjust - wrapperX); - }; - wrapper.anchorYSetter = function (value, key) { - anchorY = value; - boxAttr(key, value - wrapperY); - }; - - // rename attributes - wrapper.xSetter = function (value) { - wrapper.x = value; // for animation getter - if (alignFactor) { - value -= alignFactor * ((width || bBox.width) + 2 * padding); - } - wrapperX = mathRound(value); - wrapper.attr('translateX', wrapperX); - }; - wrapper.ySetter = function (value) { - wrapperY = wrapper.y = mathRound(value); - wrapper.attr('translateY', wrapperY); - }; - - // Redirect certain methods to either the box or the text - var baseCss = wrapper.css; - return extend(wrapper, { - /** - * Pick up some properties and apply them to the text instead of the wrapper - */ - css: function (styles) { - if (styles) { - var textStyles = {}; - styles = merge(styles); // create a copy to avoid altering the original object (#537) - each(wrapper.textProps, function (prop) { - if (styles[prop] !== UNDEFINED) { - textStyles[prop] = styles[prop]; - delete styles[prop]; - } - }); - text.css(textStyles); - } - return baseCss.call(wrapper, styles); - }, - /** - * Return the bounding box of the box, not the group - */ - getBBox: function () { - return { - width: bBox.width + 2 * padding, - height: bBox.height + 2 * padding, - x: bBox.x - padding, - y: bBox.y - padding - }; - }, - /** - * Apply the shadow to the box - */ - shadow: function (b) { - if (box) { - box.shadow(b); - } - return wrapper; - }, - /** - * Destroy and release memory. - */ - destroy: function () { - - // Added by button implementation - removeEvent(wrapper.element, 'mouseenter'); - removeEvent(wrapper.element, 'mouseleave'); - - if (text) { - text = text.destroy(); - } - if (box) { - box = box.destroy(); - } - // Call base implementation to destroy the rest - SVGElement.prototype.destroy.call(wrapper); - - // Release local pointers (#1298) - wrapper = renderer = updateBoxSize = updateTextPadding = boxAttr = null; - } - }); - } - }; // end SVGRenderer - - - // general renderer - Renderer = SVGRenderer; - // extend SvgElement for useHTML option - extend(SVGElement.prototype, { - /** - * Apply CSS to HTML elements. This is used in text within SVG rendering and - * by the VML renderer - */ - htmlCss: function (styles) { - var wrapper = this, - element = wrapper.element, - textWidth = styles && element.tagName === 'SPAN' && styles.width; - - if (textWidth) { - delete styles.width; - wrapper.textWidth = textWidth; - wrapper.updateTransform(); - } - if (styles && styles.textOverflow === 'ellipsis') { - styles.whiteSpace = 'nowrap'; - styles.overflow = 'hidden'; - } - wrapper.styles = extend(wrapper.styles, styles); - css(wrapper.element, styles); - - return wrapper; - }, - - /** - * VML and useHTML method for calculating the bounding box based on offsets - * @param {Boolean} refresh Whether to force a fresh value from the DOM or to - * use the cached value - * - * @return {Object} A hash containing values for x, y, width and height - */ - - htmlGetBBox: function () { - var wrapper = this, - element = wrapper.element; - - // faking getBBox in exported SVG in legacy IE - // faking getBBox in exported SVG in legacy IE (is this a duplicate of the fix for #1079?) - if (element.nodeName === 'text') { - element.style.position = ABSOLUTE; - } - - return { - x: element.offsetLeft, - y: element.offsetTop, - width: element.offsetWidth, - height: element.offsetHeight - }; - }, - - /** - * VML override private method to update elements based on internal - * properties based on SVG transform - */ - htmlUpdateTransform: function () { - // aligning non added elements is expensive - if (!this.added) { - this.alignOnAdd = true; - return; - } - - var wrapper = this, - renderer = wrapper.renderer, - elem = wrapper.element, - translateX = wrapper.translateX || 0, - translateY = wrapper.translateY || 0, - x = wrapper.x || 0, - y = wrapper.y || 0, - align = wrapper.textAlign || 'left', - alignCorrection = { left: 0, center: 0.5, right: 1 }[align], - shadows = wrapper.shadows, - styles = wrapper.styles; - - // apply translate - css(elem, { - marginLeft: translateX, - marginTop: translateY - }); - if (shadows) { // used in labels/tooltip - each(shadows, function (shadow) { - css(shadow, { - marginLeft: translateX + 1, - marginTop: translateY + 1 - }); - }); - } - - // apply inversion - if (wrapper.inverted) { // wrapper is a group - each(elem.childNodes, function (child) { - renderer.invertChild(child, elem); - }); - } - - if (elem.tagName === 'SPAN') { - - var rotation = wrapper.rotation, - baseline, - textWidth = pInt(wrapper.textWidth), - whiteSpace = styles && styles.whiteSpace, - currentTextTransform = [rotation, align, elem.innerHTML, wrapper.textWidth, wrapper.textAlign].join(','); - - if (currentTextTransform !== wrapper.cTT) { // do the calculations and DOM access only if properties changed - - - baseline = renderer.fontMetrics(elem.style.fontSize).b; - - // Renderer specific handling of span rotation - if (defined(rotation)) { - wrapper.setSpanRotation(rotation, alignCorrection, baseline); - } - - // Update textWidth - if (elem.offsetWidth > textWidth && /[ \-]/.test(elem.textContent || elem.innerText)) { // #983, #1254 - css(elem, { - width: textWidth + PX, - display: 'block', - whiteSpace: whiteSpace || 'normal' // #3331 - }); - wrapper.hasTextWidth = true; - } else if (wrapper.hasTextWidth) { // #4928 - css(elem, { - width: '', - display: '', - whiteSpace: whiteSpace || 'nowrap' - }); - wrapper.hasTextWidth = false; - } - - wrapper.getSpanCorrection(wrapper.hasTextWidth ? textWidth : elem.offsetWidth, baseline, alignCorrection, rotation, align); - } - - // apply position with correction - css(elem, { - left: (x + (wrapper.xCorr || 0)) + PX, - top: (y + (wrapper.yCorr || 0)) + PX - }); - - // force reflow in webkit to apply the left and top on useHTML element (#1249) - if (isWebKit) { - baseline = elem.offsetHeight; // assigned to baseline for lint purpose - } - - // record current text transform - wrapper.cTT = currentTextTransform; - } - }, - - /** - * Set the rotation of an individual HTML span - */ - setSpanRotation: function (rotation, alignCorrection, baseline) { - var rotationStyle = {}, - cssTransformKey = isMS ? '-ms-transform' : isWebKit ? '-webkit-transform' : isFirefox ? 'MozTransform' : isOpera ? '-o-transform' : ''; - - rotationStyle[cssTransformKey] = rotationStyle.transform = 'rotate(' + rotation + 'deg)'; - rotationStyle[cssTransformKey + (isFirefox ? 'Origin' : '-origin')] = rotationStyle.transformOrigin = (alignCorrection * 100) + '% ' + baseline + 'px'; - css(this.element, rotationStyle); - }, - - /** - * Get the correction in X and Y positioning as the element is rotated. - */ - getSpanCorrection: function (width, baseline, alignCorrection) { - this.xCorr = -width * alignCorrection; - this.yCorr = -baseline; - } - }); - - // Extend SvgRenderer for useHTML option. - extend(SVGRenderer.prototype, { - /** - * Create HTML text node. This is used by the VML renderer as well as the SVG - * renderer through the useHTML option. - * - * @param {String} str - * @param {Number} x - * @param {Number} y - */ - html: function (str, x, y) { - var wrapper = this.createElement('span'), - element = wrapper.element, - renderer = wrapper.renderer, - isSVG = renderer.isSVG, - addSetters = function (element, style) { - // These properties are set as attributes on the SVG group, and as - // identical CSS properties on the div. (#3542) - each(['opacity', 'visibility'], function (prop) { - wrap(element, prop + 'Setter', function (proceed, value, key, elem) { - proceed.call(this, value, key, elem); - style[key] = value; - }); - }); - }; - - // Text setter - wrapper.textSetter = function (value) { - if (value !== element.innerHTML) { - delete this.bBox; - } - element.innerHTML = this.textStr = value; - wrapper.htmlUpdateTransform(); - }; - - // Add setters for the element itself (#4938) - if (isSVG) { // #4938, only for HTML within SVG - addSetters(wrapper, wrapper.element.style); - } - - // Various setters which rely on update transform - wrapper.xSetter = wrapper.ySetter = wrapper.alignSetter = wrapper.rotationSetter = function (value, key) { - if (key === 'align') { - key = 'textAlign'; // Do not overwrite the SVGElement.align method. Same as VML. - } - wrapper[key] = value; - wrapper.htmlUpdateTransform(); - }; - - // Set the default attributes - wrapper - .attr({ - text: str, - x: mathRound(x), - y: mathRound(y) - }) - .css({ - position: ABSOLUTE, - fontFamily: this.style.fontFamily, - fontSize: this.style.fontSize - }); - - // Keep the whiteSpace style outside the wrapper.styles collection - element.style.whiteSpace = 'nowrap'; - - // Use the HTML specific .css method - wrapper.css = wrapper.htmlCss; - - // This is specific for HTML within SVG - if (isSVG) { - wrapper.add = function (svgGroupWrapper) { - - var htmlGroup, - container = renderer.box.parentNode, - parentGroup, - parents = []; - - this.parentGroup = svgGroupWrapper; - - // Create a mock group to hold the HTML elements - if (svgGroupWrapper) { - htmlGroup = svgGroupWrapper.div; - if (!htmlGroup) { - - // Read the parent chain into an array and read from top down - parentGroup = svgGroupWrapper; - while (parentGroup) { - - parents.push(parentGroup); - - // Move up to the next parent group - parentGroup = parentGroup.parentGroup; - } - - // Ensure dynamically updating position when any parent is translated - each(parents.reverse(), function (parentGroup) { - var htmlGroupStyle, - cls = attr(parentGroup.element, 'class'); - - if (cls) { - cls = { className: cls }; - } // else null - - // Create a HTML div and append it to the parent div to emulate - // the SVG group structure - htmlGroup = parentGroup.div = parentGroup.div || createElement(DIV, cls, { - position: ABSOLUTE, - left: (parentGroup.translateX || 0) + PX, - top: (parentGroup.translateY || 0) + PX, - opacity: parentGroup.opacity // #5075 - }, htmlGroup || container); // the top group is appended to container - - // Shortcut - htmlGroupStyle = htmlGroup.style; - - // Set listeners to update the HTML div's position whenever the SVG group - // position is changed - extend(parentGroup, { - translateXSetter: function (value, key) { - htmlGroupStyle.left = value + PX; - parentGroup[key] = value; - parentGroup.doTransform = true; - }, - translateYSetter: function (value, key) { - htmlGroupStyle.top = value + PX; - parentGroup[key] = value; - parentGroup.doTransform = true; - } - }); - addSetters(parentGroup, htmlGroupStyle); - }); - - } - } else { - htmlGroup = container; - } - - htmlGroup.appendChild(element); - - // Shared with VML: - wrapper.added = true; - if (wrapper.alignOnAdd) { - wrapper.htmlUpdateTransform(); - } - - return wrapper; - }; - } - return wrapper; - } - }); - - - /* **************************************************************************** - * * - * START OF INTERNET EXPLORER <= 8 SPECIFIC CODE * - * * - * For applications and websites that don't need IE support, like platform * - * targeted mobile apps and web apps, this code can be removed. * - * * - *****************************************************************************/ - - /** - * @constructor - */ - var VMLRenderer, VMLElement; - if (!hasSVG && !useCanVG) { - - /** - * The VML element wrapper. - */ - VMLElement = { - - /** - * Initialize a new VML element wrapper. It builds the markup as a string - * to minimize DOM traffic. - * @param {Object} renderer - * @param {Object} nodeName - */ - init: function (renderer, nodeName) { - var wrapper = this, - markup = ['<', nodeName, ' filled="f" stroked="f"'], - style = ['position: ', ABSOLUTE, ';'], - isDiv = nodeName === DIV; - - // divs and shapes need size - if (nodeName === 'shape' || isDiv) { - style.push('left:0;top:0;width:1px;height:1px;'); - } - style.push('visibility: ', isDiv ? HIDDEN : VISIBLE); - - markup.push(' style="', style.join(''), '"/>'); - - // create element with default attributes and style - if (nodeName) { - markup = isDiv || nodeName === 'span' || nodeName === 'img' ? - markup.join('') : - renderer.prepVML(markup); - wrapper.element = createElement(markup); - } - - wrapper.renderer = renderer; - }, - - /** - * Add the node to the given parent - * @param {Object} parent - */ - add: function (parent) { - var wrapper = this, - renderer = wrapper.renderer, - element = wrapper.element, - box = renderer.box, - inverted = parent && parent.inverted, - - // get the parent node - parentNode = parent ? - parent.element || parent : - box; - - if (parent) { - this.parentGroup = parent; - } - - // if the parent group is inverted, apply inversion on all children - if (inverted) { // only on groups - renderer.invertChild(element, parentNode); - } - - // append it - parentNode.appendChild(element); - - // align text after adding to be able to read offset - wrapper.added = true; - if (wrapper.alignOnAdd && !wrapper.deferUpdateTransform) { - wrapper.updateTransform(); - } - - // fire an event for internal hooks - if (wrapper.onAdd) { - wrapper.onAdd(); - } - - return wrapper; - }, - - /** - * VML always uses htmlUpdateTransform - */ - updateTransform: SVGElement.prototype.htmlUpdateTransform, - - /** - * Set the rotation of a span with oldIE's filter - */ - setSpanRotation: function () { - // Adjust for alignment and rotation. Rotation of useHTML content is not yet implemented - // but it can probably be implemented for Firefox 3.5+ on user request. FF3.5+ - // has support for CSS3 transform. The getBBox method also needs to be updated - // to compensate for the rotation, like it currently does for SVG. - // Test case: http://jsfiddle.net/highcharts/Ybt44/ - - var rotation = this.rotation, - costheta = mathCos(rotation * deg2rad), - sintheta = mathSin(rotation * deg2rad); - - css(this.element, { - filter: rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta, - ', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta, - ', sizingMethod=\'auto expand\')'].join('') : NONE - }); - }, - - /** - * Get the positioning correction for the span after rotating. - */ - getSpanCorrection: function (width, baseline, alignCorrection, rotation, align) { - - var costheta = rotation ? mathCos(rotation * deg2rad) : 1, - sintheta = rotation ? mathSin(rotation * deg2rad) : 0, - height = pick(this.elemHeight, this.element.offsetHeight), - quad, - nonLeft = align && align !== 'left'; - - // correct x and y - this.xCorr = costheta < 0 && -width; - this.yCorr = sintheta < 0 && -height; - - // correct for baseline and corners spilling out after rotation - quad = costheta * sintheta < 0; - this.xCorr += sintheta * baseline * (quad ? 1 - alignCorrection : alignCorrection); - this.yCorr -= costheta * baseline * (rotation ? (quad ? alignCorrection : 1 - alignCorrection) : 1); - // correct for the length/height of the text - if (nonLeft) { - this.xCorr -= width * alignCorrection * (costheta < 0 ? -1 : 1); - if (rotation) { - this.yCorr -= height * alignCorrection * (sintheta < 0 ? -1 : 1); - } - css(this.element, { - textAlign: align - }); - } - }, - - /** - * Converts a subset of an SVG path definition to its VML counterpart. Takes an array - * as the parameter and returns a string. - */ - pathToVML: function (value) { - // convert paths - var i = value.length, - path = []; - - while (i--) { - - // Multiply by 10 to allow subpixel precision. - // Substracting half a pixel seems to make the coordinates - // align with SVG, but this hasn't been tested thoroughly - if (isNumber(value[i])) { - path[i] = mathRound(value[i] * 10) - 5; - } else if (value[i] === 'Z') { // close the path - path[i] = 'x'; - } else { - path[i] = value[i]; - - // When the start X and end X coordinates of an arc are too close, - // they are rounded to the same value above. In this case, substract or - // add 1 from the end X and Y positions. #186, #760, #1371, #1410. - if (value.isArc && (value[i] === 'wa' || value[i] === 'at')) { - // Start and end X - if (path[i + 5] === path[i + 7]) { - path[i + 7] += value[i + 7] > value[i + 5] ? 1 : -1; - } - // Start and end Y - if (path[i + 6] === path[i + 8]) { - path[i + 8] += value[i + 8] > value[i + 6] ? 1 : -1; - } - } - } - } - - - // Loop up again to handle path shortcuts (#2132) - /*while (i++ < path.length) { - if (path[i] === 'H') { // horizontal line to - path[i] = 'L'; - path.splice(i + 2, 0, path[i - 1]); - } else if (path[i] === 'V') { // vertical line to - path[i] = 'L'; - path.splice(i + 1, 0, path[i - 2]); - } - }*/ - return path.join(' ') || 'x'; - }, - - /** - * Set the element's clipping to a predefined rectangle - * - * @param {String} id The id of the clip rectangle - */ - clip: function (clipRect) { - var wrapper = this, - clipMembers, - cssRet; - - if (clipRect) { - clipMembers = clipRect.members; - erase(clipMembers, wrapper); // Ensure unique list of elements (#1258) - clipMembers.push(wrapper); - wrapper.destroyClip = function () { - erase(clipMembers, wrapper); - }; - cssRet = clipRect.getCSS(wrapper); - - } else { - if (wrapper.destroyClip) { - wrapper.destroyClip(); - } - cssRet = { clip: docMode8 ? 'inherit' : 'rect(auto)' }; // #1214 - } - - return wrapper.css(cssRet); - - }, - - /** - * Set styles for the element - * @param {Object} styles - */ - css: SVGElement.prototype.htmlCss, - - /** - * Removes a child either by removeChild or move to garbageBin. - * Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not. - */ - safeRemoveChild: function (element) { - // discardElement will detach the node from its parent before attaching it - // to the garbage bin. Therefore it is important that the node is attached and have parent. - if (element.parentNode) { - discardElement(element); - } - }, - - /** - * Extend element.destroy by removing it from the clip members array - */ - destroy: function () { - if (this.destroyClip) { - this.destroyClip(); - } - - return SVGElement.prototype.destroy.apply(this); - }, - - /** - * Add an event listener. VML override for normalizing event parameters. - * @param {String} eventType - * @param {Function} handler - */ - on: function (eventType, handler) { - // simplest possible event model for internal use - this.element['on' + eventType] = function () { - var evt = win.event; - evt.target = evt.srcElement; - handler(evt); - }; - return this; - }, - - /** - * In stacked columns, cut off the shadows so that they don't overlap - */ - cutOffPath: function (path, length) { - - var len; - - path = path.split(/[ ,]/); - len = path.length; - - if (len === 9 || len === 11) { - path[len - 4] = path[len - 2] = pInt(path[len - 2]) - 10 * length; - } - return path.join(' '); - }, - - /** - * Apply a drop shadow by copying elements and giving them different strokes - * @param {Boolean|Object} shadowOptions - */ - shadow: function (shadowOptions, group, cutOff) { - var shadows = [], - i, - element = this.element, - renderer = this.renderer, - shadow, - elemStyle = element.style, - markup, - path = element.path, - strokeWidth, - modifiedPath, - shadowWidth, - shadowElementOpacity; - - // some times empty paths are not strings - if (path && typeof path.value !== 'string') { - path = 'x'; - } - modifiedPath = path; - - if (shadowOptions) { - shadowWidth = pick(shadowOptions.width, 3); - shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth; - for (i = 1; i <= 3; i++) { - - strokeWidth = (shadowWidth * 2) + 1 - (2 * i); - - // Cut off shadows for stacked column items - if (cutOff) { - modifiedPath = this.cutOffPath(path.value, strokeWidth + 0.5); - } - - markup = ['']; - - shadow = createElement(renderer.prepVML(markup), - null, { - left: pInt(elemStyle.left) + pick(shadowOptions.offsetX, 1), - top: pInt(elemStyle.top) + pick(shadowOptions.offsetY, 1) - } - ); - if (cutOff) { - shadow.cutOff = strokeWidth + 1; - } - - // apply the opacity - markup = ['']; - createElement(renderer.prepVML(markup), null, null, shadow); - - - // insert it - if (group) { - group.element.appendChild(shadow); - } else { - element.parentNode.insertBefore(shadow, element); - } - - // record it - shadows.push(shadow); - - } - - this.shadows = shadows; - } - return this; - }, - updateShadows: noop, // Used in SVG only - - setAttr: function (key, value) { - if (docMode8) { // IE8 setAttribute bug - this.element[key] = value; - } else { - this.element.setAttribute(key, value); - } - }, - classSetter: function (value) { - // IE8 Standards mode has problems retrieving the className unless set like this - this.element.className = value; - }, - dashstyleSetter: function (value, key, element) { - var strokeElem = element.getElementsByTagName('stroke')[0] || - createElement(this.renderer.prepVML(['']), null, null, element); - strokeElem[key] = value || 'solid'; - this[key] = value; /* because changing stroke-width will change the dash length - and cause an epileptic effect */ - }, - dSetter: function (value, key, element) { - var i, - shadows = this.shadows; - value = value || []; - this.d = value.join && value.join(' '); // used in getter for animation - - element.path = value = this.pathToVML(value); - - // update shadows - if (shadows) { - i = shadows.length; - while (i--) { - shadows[i].path = shadows[i].cutOff ? this.cutOffPath(value, shadows[i].cutOff) : value; - } - } - this.setAttr(key, value); - }, - fillSetter: function (value, key, element) { - var nodeName = element.nodeName; - if (nodeName === 'SPAN') { // text color - element.style.color = value; - } else if (nodeName !== 'IMG') { // #1336 - element.filled = value !== NONE; - this.setAttr('fillcolor', this.renderer.color(value, element, key, this)); - } - }, - 'fill-opacitySetter': function (value, key, element) { - createElement( - this.renderer.prepVML(['<', key.split('-')[0], ' opacity="', value, '"/>']), - null, - null, - element - ); - }, - opacitySetter: noop, // Don't bother - animation is too slow and filters introduce artifacts - rotationSetter: function (value, key, element) { - var style = element.style; - this[key] = style[key] = value; // style is for #1873 - - // Correction for the 1x1 size of the shape container. Used in gauge needles. - style.left = -mathRound(mathSin(value * deg2rad) + 1) + PX; - style.top = mathRound(mathCos(value * deg2rad)) + PX; - }, - strokeSetter: function (value, key, element) { - this.setAttr('strokecolor', this.renderer.color(value, element, key, this)); - }, - 'stroke-widthSetter': function (value, key, element) { - element.stroked = !!value; // VML "stroked" attribute - this[key] = value; // used in getter, issue #113 - if (isNumber(value)) { - value += PX; - } - this.setAttr('strokeweight', value); - }, - titleSetter: function (value, key) { - this.setAttr(key, value); - }, - visibilitySetter: function (value, key, element) { - - // Handle inherited visibility - if (value === 'inherit') { - value = VISIBLE; - } - - // Let the shadow follow the main element - if (this.shadows) { - each(this.shadows, function (shadow) { - shadow.style[key] = value; - }); - } - - // Instead of toggling the visibility CSS property, move the div out of the viewport. - // This works around #61 and #586 - if (element.nodeName === 'DIV') { - value = value === HIDDEN ? '-999em' : 0; - - // In order to redraw, IE7 needs the div to be visible when tucked away - // outside the viewport. So the visibility is actually opposite of - // the expected value. This applies to the tooltip only. - if (!docMode8) { - element.style[key] = value ? VISIBLE : HIDDEN; - } - key = 'top'; - } - element.style[key] = value; - }, - xSetter: function (value, key, element) { - this[key] = value; // used in getter - - if (key === 'x') { - key = 'left'; - } else if (key === 'y') { - key = 'top'; - }/* else { - value = mathMax(0, value); // don't set width or height below zero (#311) - }*/ - - // clipping rectangle special - if (this.updateClipping) { - this[key] = value; // the key is now 'left' or 'top' for 'x' and 'y' - this.updateClipping(); - } else { - // normal - element.style[key] = value; - } - }, - zIndexSetter: function (value, key, element) { - element.style[key] = value; - } - }; - VMLElement['stroke-opacitySetter'] = VMLElement['fill-opacitySetter']; - - Highcharts.VMLElement = VMLElement = extendClass(SVGElement, VMLElement); - - // Some shared setters - VMLElement.prototype.ySetter = - VMLElement.prototype.widthSetter = - VMLElement.prototype.heightSetter = - VMLElement.prototype.xSetter; - - - /** - * The VML renderer - */ - var VMLRendererExtension = { // inherit SVGRenderer - - Element: VMLElement, - isIE8: userAgent.indexOf('MSIE 8.0') > -1, - - - /** - * Initialize the VMLRenderer - * @param {Object} container - * @param {Number} width - * @param {Number} height - */ - init: function (container, width, height, style) { - var renderer = this, - boxWrapper, - box, - css; - - renderer.alignedObjects = []; - - boxWrapper = renderer.createElement(DIV) - .css(extend(this.getStyle(style), { position: 'relative' })); - box = boxWrapper.element; - container.appendChild(boxWrapper.element); - - - // generate the containing box - renderer.isVML = true; - renderer.box = box; - renderer.boxWrapper = boxWrapper; - renderer.gradients = {}; - renderer.cache = {}; // Cache for numerical bounding boxes - renderer.cacheKeys = []; - renderer.imgCount = 0; - - - renderer.setSize(width, height, false); - - // The only way to make IE6 and IE7 print is to use a global namespace. However, - // with IE8 the only way to make the dynamic shapes visible in screen and print mode - // seems to be to add the xmlns attribute and the behaviour style inline. - if (!doc.namespaces.hcv) { - - doc.namespaces.add('hcv', 'urn:schemas-microsoft-com:vml'); - - // Setup default CSS (#2153, #2368, #2384) - css = 'hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke' + - '{ behavior:url(#default#VML); display: inline-block; } '; - try { - doc.createStyleSheet().cssText = css; - } catch (e) { - doc.styleSheets[0].cssText += css; - } - - } - }, - - - /** - * Detect whether the renderer is hidden. This happens when one of the parent elements - * has display: none - */ - isHidden: function () { - return !this.box.offsetWidth; - }, - - /** - * Define a clipping rectangle. In VML it is accomplished by storing the values - * for setting the CSS style to all associated members. - * - * @param {Number} x - * @param {Number} y - * @param {Number} width - * @param {Number} height - */ - clipRect: function (x, y, width, height) { - - // create a dummy element - var clipRect = this.createElement(), - isObj = isObject(x); - - // mimic a rectangle with its style object for automatic updating in attr - return extend(clipRect, { - members: [], - count: 0, - left: (isObj ? x.x : x) + 1, - top: (isObj ? x.y : y) + 1, - width: (isObj ? x.width : width) - 1, - height: (isObj ? x.height : height) - 1, - getCSS: function (wrapper) { - var element = wrapper.element, - nodeName = element.nodeName, - isShape = nodeName === 'shape', - inverted = wrapper.inverted, - rect = this, - top = rect.top - (isShape ? element.offsetTop : 0), - left = rect.left, - right = left + rect.width, - bottom = top + rect.height, - ret = { - clip: 'rect(' + - mathRound(inverted ? left : top) + 'px,' + - mathRound(inverted ? bottom : right) + 'px,' + - mathRound(inverted ? right : bottom) + 'px,' + - mathRound(inverted ? top : left) + 'px)' - }; - - // issue 74 workaround - if (!inverted && docMode8 && nodeName === 'DIV') { - extend(ret, { - width: right + PX, - height: bottom + PX - }); - } - return ret; - }, - - // used in attr and animation to update the clipping of all members - updateClipping: function () { - each(clipRect.members, function (member) { - if (member.element) { // Deleted series, like in stock/members/series-remove demo. Should be removed from members, but this will do. - member.css(clipRect.getCSS(member)); - } - }); - } - }); - - }, - - - /** - * Take a color and return it if it's a string, make it a gradient if it's a - * gradient configuration object, and apply opacity. - * - * @param {Object} color The color or config object - */ - color: function (color, elem, prop, wrapper) { - var renderer = this, - colorObject, - regexRgba = /^rgba/, - markup, - fillType, - ret = NONE; - - // Check for linear or radial gradient - if (color && color.linearGradient) { - fillType = 'gradient'; - } else if (color && color.radialGradient) { - fillType = 'pattern'; - } - - - if (fillType) { - - var stopColor, - stopOpacity, - gradient = color.linearGradient || color.radialGradient, - x1, - y1, - x2, - y2, - opacity1, - opacity2, - color1, - color2, - fillAttr = '', - stops = color.stops, - firstStop, - lastStop, - colors = [], - addFillNode = function () { - // Add the fill subnode. When colors attribute is used, the meanings of opacity and o:opacity2 - // are reversed. - markup = ['']; - createElement(renderer.prepVML(markup), null, null, elem); - }; - - // Extend from 0 to 1 - firstStop = stops[0]; - lastStop = stops[stops.length - 1]; - if (firstStop[0] > 0) { - stops.unshift([ - 0, - firstStop[1] - ]); - } - if (lastStop[0] < 1) { - stops.push([ - 1, - lastStop[1] - ]); - } - - // Compute the stops - each(stops, function (stop, i) { - if (regexRgba.test(stop[1])) { - colorObject = Color(stop[1]); - stopColor = colorObject.get('rgb'); - stopOpacity = colorObject.get('a'); - } else { - stopColor = stop[1]; - stopOpacity = 1; - } - - // Build the color attribute - colors.push((stop[0] * 100) + '% ' + stopColor); - - // Only start and end opacities are allowed, so we use the first and the last - if (!i) { - opacity1 = stopOpacity; - color2 = stopColor; - } else { - opacity2 = stopOpacity; - color1 = stopColor; - } - }); - - // Apply the gradient to fills only. - if (prop === 'fill') { - - // Handle linear gradient angle - if (fillType === 'gradient') { - x1 = gradient.x1 || gradient[0] || 0; - y1 = gradient.y1 || gradient[1] || 0; - x2 = gradient.x2 || gradient[2] || 0; - y2 = gradient.y2 || gradient[3] || 0; - fillAttr = 'angle="' + (90 - math.atan( - (y2 - y1) / // y vector - (x2 - x1) // x vector - ) * 180 / mathPI) + '"'; - - addFillNode(); - - // Radial (circular) gradient - } else { - - var r = gradient.r, - sizex = r * 2, - sizey = r * 2, - cx = gradient.cx, - cy = gradient.cy, - radialReference = elem.radialReference, - bBox, - applyRadialGradient = function () { - if (radialReference) { - bBox = wrapper.getBBox(); - cx += (radialReference[0] - bBox.x) / bBox.width - 0.5; - cy += (radialReference[1] - bBox.y) / bBox.height - 0.5; - sizex *= radialReference[2] / bBox.width; - sizey *= radialReference[2] / bBox.height; - } - fillAttr = 'src="' + defaultOptions.global.VMLRadialGradientURL + '" ' + - 'size="' + sizex + ',' + sizey + '" ' + - 'origin="0.5,0.5" ' + - 'position="' + cx + ',' + cy + '" ' + - 'color2="' + color2 + '" '; - - addFillNode(); - }; - - // Apply radial gradient - if (wrapper.added) { - applyRadialGradient(); - } else { - // We need to know the bounding box to get the size and position right - wrapper.onAdd = applyRadialGradient; - } - - // The fill element's color attribute is broken in IE8 standards mode, so we - // need to set the parent shape's fillcolor attribute instead. - ret = color1; - } - - // Gradients are not supported for VML stroke, return the first color. #722. - } else { - ret = stopColor; - } - - // If the color is an rgba color, split it and add a fill node - // to hold the opacity component - } else if (regexRgba.test(color) && elem.tagName !== 'IMG') { - - colorObject = Color(color); - - wrapper[prop + '-opacitySetter'](colorObject.get('a'), prop, elem); - - ret = colorObject.get('rgb'); - - - } else { - var propNodes = elem.getElementsByTagName(prop); // 'stroke' or 'fill' node - if (propNodes.length) { - propNodes[0].opacity = 1; - propNodes[0].type = 'solid'; - } - ret = color; - } - - return ret; - }, - - /** - * Take a VML string and prepare it for either IE8 or IE6/IE7. - * @param {Array} markup A string array of the VML markup to prepare - */ - prepVML: function (markup) { - var vmlStyle = 'display:inline-block;behavior:url(#default#VML);', - isIE8 = this.isIE8; - - markup = markup.join(''); - - if (isIE8) { // add xmlns and style inline - markup = markup.replace('/>', ' xmlns="urn:schemas-microsoft-com:vml" />'); - if (markup.indexOf('style="') === -1) { - markup = markup.replace('/>', ' style="' + vmlStyle + '" />'); - } else { - markup = markup.replace('style="', 'style="' + vmlStyle); - } - - } else { // add namespace - markup = markup.replace('<', ' 1) { - obj.attr({ - x: x, - y: y, - width: width, - height: height - }); - } - return obj; - }, - - /** - * For rectangles, VML uses a shape for rect to overcome bugs and rotation problems - */ - createElement: function (nodeName) { - return nodeName === 'rect' ? this.symbol(nodeName) : SVGRenderer.prototype.createElement.call(this, nodeName); - }, - - /** - * In the VML renderer, each child of an inverted div (group) is inverted - * @param {Object} element - * @param {Object} parentNode - */ - invertChild: function (element, parentNode) { - var ren = this, - parentStyle = parentNode.style, - imgStyle = element.tagName === 'IMG' && element.style; // #1111 - - css(element, { - flip: 'x', - left: pInt(parentStyle.width) - (imgStyle ? pInt(imgStyle.top) : 1), - top: pInt(parentStyle.height) - (imgStyle ? pInt(imgStyle.left) : 1), - rotation: -90 - }); - - // Recursively invert child elements, needed for nested composite shapes like box plots and error bars. #1680, #1806. - each(element.childNodes, function (child) { - ren.invertChild(child, element); - }); - }, - - /** - * Symbol definitions that override the parent SVG renderer's symbols - * - */ - symbols: { - // VML specific arc function - arc: function (x, y, w, h, options) { - var start = options.start, - end = options.end, - radius = options.r || w || h, - innerRadius = options.innerR, - cosStart = mathCos(start), - sinStart = mathSin(start), - cosEnd = mathCos(end), - sinEnd = mathSin(end), - ret; - - if (end - start === 0) { // no angle, don't show it. - return ['x']; - } - - ret = [ - 'wa', // clockwise arc to - x - radius, // left - y - radius, // top - x + radius, // right - y + radius, // bottom - x + radius * cosStart, // start x - y + radius * sinStart, // start y - x + radius * cosEnd, // end x - y + radius * sinEnd // end y - ]; - - if (options.open && !innerRadius) { - ret.push( - 'e', - M, - x, // - innerRadius, - y// - innerRadius - ); - } - - ret.push( - 'at', // anti clockwise arc to - x - innerRadius, // left - y - innerRadius, // top - x + innerRadius, // right - y + innerRadius, // bottom - x + innerRadius * cosEnd, // start x - y + innerRadius * sinEnd, // start y - x + innerRadius * cosStart, // end x - y + innerRadius * sinStart, // end y - 'x', // finish path - 'e' // close - ); - - ret.isArc = true; - return ret; - - }, - // Add circle symbol path. This performs significantly faster than v:oval. - circle: function (x, y, w, h, wrapper) { - - if (wrapper) { - w = h = 2 * wrapper.r; - } - - // Center correction, #1682 - if (wrapper && wrapper.isCircle) { - x -= w / 2; - y -= h / 2; - } - - // Return the path - return [ - 'wa', // clockwisearcto - x, // left - y, // top - x + w, // right - y + h, // bottom - x + w, // start x - y + h / 2, // start y - x + w, // end x - y + h / 2, // end y - //'x', // finish path - 'e' // close - ]; - }, - /** - * Add rectangle symbol path which eases rotation and omits arcsize problems - * compared to the built-in VML roundrect shape. When borders are not rounded, - * use the simpler square path, else use the callout path without the arrow. - */ - rect: function (x, y, w, h, options) { - return SVGRenderer.prototype.symbols[ - !defined(options) || !options.r ? 'square' : 'callout' - ].call(0, x, y, w, h, options); - } - } - }; - Highcharts.VMLRenderer = VMLRenderer = function () { - this.init.apply(this, arguments); - }; - VMLRenderer.prototype = merge(SVGRenderer.prototype, VMLRendererExtension); - - // general renderer - Renderer = VMLRenderer; - } - - // This method is used with exporting in old IE, when emulating SVG (see #2314) - SVGRenderer.prototype.measureSpanWidth = function (text, styles) { - var measuringSpan = doc.createElement('span'), - offsetWidth, - textNode = doc.createTextNode(text); - - measuringSpan.appendChild(textNode); - css(measuringSpan, styles); - this.box.appendChild(measuringSpan); - offsetWidth = measuringSpan.offsetWidth; - discardElement(measuringSpan); // #2463 - return offsetWidth; - }; - - - /* **************************************************************************** - * * - * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE * - * * - *****************************************************************************/ - /* **************************************************************************** - * * - * START OF ANDROID < 3 SPECIFIC CODE. THIS CAN BE REMOVED IF YOU'RE NOT * - * TARGETING THAT SYSTEM. * - * * - *****************************************************************************/ - var CanVGRenderer, - CanVGController; - - /** - * Downloads a script and executes a callback when done. - * @param {String} scriptLocation - * @param {Function} callback - */ - function getScript(scriptLocation, callback) { - var head = doc.getElementsByTagName('head')[0], - script = doc.createElement('script'); - - script.type = 'text/javascript'; - script.src = scriptLocation; - script.onload = callback; - - head.appendChild(script); - } - - if (useCanVG) { - /** - * The CanVGRenderer is empty from start to keep the source footprint small. - * When requested, the CanVGController downloads the rest of the source packaged - * together with the canvg library. - */ - Highcharts.CanVGRenderer = CanVGRenderer = function () { - // Override the global SVG namespace to fake SVG/HTML that accepts CSS - SVG_NS = 'http://www.w3.org/1999/xhtml'; - }; - - /** - * Start with an empty symbols object. This is needed when exporting is used (exporting.src.js will add a few symbols), but - * the implementation from SvgRenderer will not be merged in until first render. - */ - CanVGRenderer.prototype.symbols = {}; - - /** - * Handles on demand download of canvg rendering support. - */ - CanVGController = (function () { - // List of renderering calls - var deferredRenderCalls = []; - - /** - * When downloaded, we are ready to draw deferred charts. - */ - function drawDeferred() { - var callLength = deferredRenderCalls.length, - callIndex; - - // Draw all pending render calls - for (callIndex = 0; callIndex < callLength; callIndex++) { - deferredRenderCalls[callIndex](); - } - // Clear the list - deferredRenderCalls = []; - } - - return { - push: function (func, scriptLocation) { - // Only get the script once - if (deferredRenderCalls.length === 0) { - getScript(scriptLocation, drawDeferred); - } - // Register render call - deferredRenderCalls.push(func); - } - }; - }()); - - Renderer = CanVGRenderer; - } // end CanVGRenderer - - /* **************************************************************************** - * * - * END OF ANDROID < 3 SPECIFIC CODE * - * * - *****************************************************************************/ - - /** - * The Tick class - */ - function Tick(axis, pos, type, noLabel) { - this.axis = axis; - this.pos = pos; - this.type = type || ''; - this.isNew = true; - - if (!type && !noLabel) { - this.addLabel(); - } - } - - Tick.prototype = { - /** - * Write the tick label - */ - addLabel: function () { - var tick = this, - axis = tick.axis, - options = axis.options, - chart = axis.chart, - categories = axis.categories, - names = axis.names, - pos = tick.pos, - labelOptions = options.labels, - str, - tickPositions = axis.tickPositions, - isFirst = pos === tickPositions[0], - isLast = pos === tickPositions[tickPositions.length - 1], - value = categories ? - pick(categories[pos], names[pos], pos) : - pos, - label = tick.label, - tickPositionInfo = tickPositions.info, - dateTimeLabelFormat; - - // Set the datetime label format. If a higher rank is set for this position, use that. If not, - // use the general format. - if (axis.isDatetimeAxis && tickPositionInfo) { - dateTimeLabelFormat = options.dateTimeLabelFormats[tickPositionInfo.higherRanks[pos] || tickPositionInfo.unitName]; - } - // set properties for access in render method - tick.isFirst = isFirst; - tick.isLast = isLast; - - // get the string - str = axis.labelFormatter.call({ - axis: axis, - chart: chart, - isFirst: isFirst, - isLast: isLast, - dateTimeLabelFormat: dateTimeLabelFormat, - value: axis.isLog ? correctFloat(axis.lin2log(value)) : value - }); - - // prepare CSS - //css = width && { width: mathMax(1, mathRound(width - 2 * (labelOptions.padding || 10))) + PX }; - - // first call - if (!defined(label)) { - - tick.label = label = - defined(str) && labelOptions.enabled ? - chart.renderer.text( - str, - 0, - 0, - labelOptions.useHTML - ) - //.attr(attr) - // without position absolute, IE export sometimes is wrong - .css(merge(labelOptions.style)) - .add(axis.labelGroup) : - null; - tick.labelLength = label && label.getBBox().width; // Un-rotated length - tick.rotation = 0; // Base value to detect change for new calls to getBBox - - // update - } else if (label) { - label.attr({ text: str }); - } - }, - - /** - * Get the offset height or width of the label - */ - getLabelSize: function () { - return this.label ? - this.label.getBBox()[this.axis.horiz ? 'height' : 'width'] : - 0; - }, - - /** - * Handle the label overflow by adjusting the labels to the left and right edge, or - * hide them if they collide into the neighbour label. - */ - handleOverflow: function (xy) { - var axis = this.axis, - pxPos = xy.x, - chartWidth = axis.chart.chartWidth, - spacing = axis.chart.spacing, - leftBound = pick(axis.labelLeft, mathMin(axis.pos, spacing[3])), - rightBound = pick(axis.labelRight, mathMax(axis.pos + axis.len, chartWidth - spacing[1])), - label = this.label, - rotation = this.rotation, - factor = { left: 0, center: 0.5, right: 1 }[axis.labelAlign], - labelWidth = label.getBBox().width, - slotWidth = axis.getSlotWidth(), - modifiedSlotWidth = slotWidth, - xCorrection = factor, - goRight = 1, - leftPos, - rightPos, - textWidth, - css = {}; - - // Check if the label overshoots the chart spacing box. If it does, move it. - // If it now overshoots the slotWidth, add ellipsis. - if (!rotation) { - leftPos = pxPos - factor * labelWidth; - rightPos = pxPos + (1 - factor) * labelWidth; - - if (leftPos < leftBound) { - modifiedSlotWidth = xy.x + modifiedSlotWidth * (1 - factor) - leftBound; - } else if (rightPos > rightBound) { - modifiedSlotWidth = rightBound - xy.x + modifiedSlotWidth * factor; - goRight = -1; - } - - modifiedSlotWidth = mathMin(slotWidth, modifiedSlotWidth); // #4177 - if (modifiedSlotWidth < slotWidth && axis.labelAlign === 'center') { - xy.x += goRight * (slotWidth - modifiedSlotWidth - xCorrection * (slotWidth - mathMin(labelWidth, modifiedSlotWidth))); - } - // If the label width exceeds the available space, set a text width to be - // picked up below. Also, if a width has been set before, we need to set a new - // one because the reported labelWidth will be limited by the box (#3938). - if (labelWidth > modifiedSlotWidth || (axis.autoRotation && label.styles.width)) { - textWidth = modifiedSlotWidth; - } - - // Add ellipsis to prevent rotated labels to be clipped against the edge of the chart - } else if (rotation < 0 && pxPos - factor * labelWidth < leftBound) { - textWidth = mathRound(pxPos / mathCos(rotation * deg2rad) - leftBound); - } else if (rotation > 0 && pxPos + factor * labelWidth > rightBound) { - textWidth = mathRound((chartWidth - pxPos) / mathCos(rotation * deg2rad)); - } - - if (textWidth) { - css.width = textWidth; - if (!axis.options.labels.style.textOverflow) { - css.textOverflow = 'ellipsis'; - } - label.css(css); - } - }, - - /** - * Get the x and y position for ticks and labels - */ - getPosition: function (horiz, pos, tickmarkOffset, old) { - var axis = this.axis, - chart = axis.chart, - cHeight = (old && chart.oldChartHeight) || chart.chartHeight; - - return { - x: horiz ? - axis.translate(pos + tickmarkOffset, null, null, old) + axis.transB : - axis.left + axis.offset + (axis.opposite ? ((old && chart.oldChartWidth) || chart.chartWidth) - axis.right - axis.left : 0), - - y: horiz ? - cHeight - axis.bottom + axis.offset - (axis.opposite ? axis.height : 0) : - cHeight - axis.translate(pos + tickmarkOffset, null, null, old) - axis.transB - }; - - }, - - /** - * Get the x, y position of the tick label - */ - getLabelPosition: function (x, y, label, horiz, labelOptions, tickmarkOffset, index, step) { - var axis = this.axis, - transA = axis.transA, - reversed = axis.reversed, - staggerLines = axis.staggerLines, - rotCorr = axis.tickRotCorr || { x: 0, y: 0 }, - yOffset = labelOptions.y, - line; - - if (!defined(yOffset)) { - if (axis.side === 0) { - yOffset = label.rotation ? -8 : -label.getBBox().height; - } else if (axis.side === 2) { - yOffset = rotCorr.y + 8; - } else { - // #3140, #3140 - yOffset = mathCos(label.rotation * deg2rad) * (rotCorr.y - label.getBBox(false, 0).height / 2); - } - } - - x = x + labelOptions.x + rotCorr.x - (tickmarkOffset && horiz ? - tickmarkOffset * transA * (reversed ? -1 : 1) : 0); - y = y + yOffset - (tickmarkOffset && !horiz ? - tickmarkOffset * transA * (reversed ? 1 : -1) : 0); - - // Correct for staggered labels - if (staggerLines) { - line = (index / (step || 1) % staggerLines); - if (axis.opposite) { - line = staggerLines - line - 1; - } - y += line * (axis.labelOffset / staggerLines); - } - - return { - x: x, - y: mathRound(y) - }; - }, - - /** - * Extendible method to return the path of the marker - */ - getMarkPath: function (x, y, tickLength, tickWidth, horiz, renderer) { - return renderer.crispLine([ - M, - x, - y, - L, - x + (horiz ? 0 : -tickLength), - y + (horiz ? tickLength : 0) - ], tickWidth); - }, - - /** - * Put everything in place - * - * @param index {Number} - * @param old {Boolean} Use old coordinates to prepare an animation into new position - */ - render: function (index, old, opacity) { - var tick = this, - axis = tick.axis, - options = axis.options, - chart = axis.chart, - renderer = chart.renderer, - horiz = axis.horiz, - type = tick.type, - label = tick.label, - pos = tick.pos, - labelOptions = options.labels, - gridLine = tick.gridLine, - gridPrefix = type ? type + 'Grid' : 'grid', - tickPrefix = type ? type + 'Tick' : 'tick', - gridLineWidth = options[gridPrefix + 'LineWidth'], - gridLineColor = options[gridPrefix + 'LineColor'], - dashStyle = options[gridPrefix + 'LineDashStyle'], - tickSize = axis.tickSize(tickPrefix), - tickColor = options[tickPrefix + 'Color'], - gridLinePath, - mark = tick.mark, - markPath, - step = /*axis.labelStep || */labelOptions.step, - attribs, - show = true, - tickmarkOffset = axis.tickmarkOffset, - xy = tick.getPosition(horiz, pos, tickmarkOffset, old), - x = xy.x, - y = xy.y, - reverseCrisp = ((horiz && x === axis.pos + axis.len) || (!horiz && y === axis.pos)) ? -1 : 1; // #1480, #1687 - - opacity = pick(opacity, 1); - this.isActive = true; - - // create the grid line - if (gridLineWidth) { - gridLinePath = axis.getPlotLinePath(pos + tickmarkOffset, gridLineWidth * reverseCrisp, old, true); - - if (gridLine === UNDEFINED) { - attribs = { - stroke: gridLineColor, - 'stroke-width': gridLineWidth - }; - if (dashStyle) { - attribs.dashstyle = dashStyle; - } - if (!type) { - attribs.zIndex = 1; - } - if (old) { - attribs.opacity = 0; - } - tick.gridLine = gridLine = - gridLineWidth ? - renderer.path(gridLinePath) - .attr(attribs).add(axis.gridGroup) : - null; - } - - // If the parameter 'old' is set, the current call will be followed - // by another call, therefore do not do any animations this time - if (!old && gridLine && gridLinePath) { - gridLine[tick.isNew ? 'attr' : 'animate']({ - d: gridLinePath, - opacity: opacity - }); - } - } - - // create the tick mark - if (tickSize) { - if (axis.opposite) { - tickSize[0] = -tickSize[0]; - } - markPath = tick.getMarkPath(x, y, tickSize[0], tickSize[1] * reverseCrisp, horiz, renderer); - if (mark) { // updating - mark.animate({ - d: markPath, - opacity: opacity - }); - } else { // first time - tick.mark = renderer.path( - markPath - ).attr({ - stroke: tickColor, - 'stroke-width': tickSize[1], - opacity: opacity - }).add(axis.axisGroup); - } - } - - // the label is created on init - now move it into place - if (label && isNumber(x)) { - label.xy = xy = tick.getLabelPosition(x, y, label, horiz, labelOptions, tickmarkOffset, index, step); - - // Apply show first and show last. If the tick is both first and last, it is - // a single centered tick, in which case we show the label anyway (#2100). - if ((tick.isFirst && !tick.isLast && !pick(options.showFirstLabel, 1)) || - (tick.isLast && !tick.isFirst && !pick(options.showLastLabel, 1))) { - show = false; - - // Handle label overflow and show or hide accordingly - } else if (horiz && !axis.isRadial && !labelOptions.step && !labelOptions.rotation && !old && opacity !== 0) { - tick.handleOverflow(xy); - } - - // apply step - if (step && index % step) { - // show those indices dividable by step - show = false; - } - - // Set the new position, and show or hide - if (show && isNumber(xy.y)) { - xy.opacity = opacity; - label[tick.isNew ? 'attr' : 'animate'](xy); - tick.isNew = false; - } else { - label.attr('y', -9999); // #1338 - } - } - }, - - /** - * Destructor for the tick prototype - */ - destroy: function () { - destroyObjectProperties(this, this.axis); - } - }; - - /** - * The object wrapper for plot lines and plot bands - * @param {Object} options - */ - Highcharts.PlotLineOrBand = function (axis, options) { - this.axis = axis; - - if (options) { - this.options = options; - this.id = options.id; - } - }; - - Highcharts.PlotLineOrBand.prototype = { - - /** - * Render the plot line or plot band. If it is already existing, - * move it. - */ - render: function () { - var plotLine = this, - axis = plotLine.axis, - horiz = axis.horiz, - options = plotLine.options, - optionsLabel = options.label, - label = plotLine.label, - width = options.width, - to = options.to, - from = options.from, - isBand = defined(from) && defined(to), - value = options.value, - dashStyle = options.dashStyle, - svgElem = plotLine.svgElem, - path = [], - addEvent, - eventType, - color = options.color, - zIndex = pick(options.zIndex, 0), - events = options.events, - attribs = {}, - renderer = axis.chart.renderer, - log2lin = axis.log2lin; - - // logarithmic conversion - if (axis.isLog) { - from = log2lin(from); - to = log2lin(to); - value = log2lin(value); - } - - // plot line - if (width) { - path = axis.getPlotLinePath(value, width); - attribs = { - stroke: color, - 'stroke-width': width - }; - if (dashStyle) { - attribs.dashstyle = dashStyle; - } - } else if (isBand) { // plot band - - path = axis.getPlotBandPath(from, to, options); - if (color) { - attribs.fill = color; - } - if (options.borderWidth) { - attribs.stroke = options.borderColor; - attribs['stroke-width'] = options.borderWidth; - } - } else { - return; - } - // zIndex - attribs.zIndex = zIndex; - - // common for lines and bands - if (svgElem) { - if (path) { - svgElem.show(); - svgElem.animate({ d: path }); - } else { - svgElem.hide(); - if (label) { - plotLine.label = label = label.destroy(); - } - } - } else if (path && path.length) { - plotLine.svgElem = svgElem = renderer.path(path) - .attr(attribs).add(); - - // events - if (events) { - addEvent = function (eventType) { - svgElem.on(eventType, function (e) { - events[eventType].apply(plotLine, [e]); - }); - }; - for (eventType in events) { - addEvent(eventType); - } - } - } - - // the plot band/line label - if (optionsLabel && defined(optionsLabel.text) && path && path.length && - axis.width > 0 && axis.height > 0 && !path.flat) { - // apply defaults - optionsLabel = merge({ - align: horiz && isBand && 'center', - x: horiz ? !isBand && 4 : 10, - verticalAlign: !horiz && isBand && 'middle', - y: horiz ? isBand ? 16 : 10 : isBand ? 6 : -4, - rotation: horiz && !isBand && 90 - }, optionsLabel); - - this.renderLabel(optionsLabel, path, isBand, zIndex); - - } else if (label) { // move out of sight - label.hide(); - } - - // chainable - return plotLine; - }, - - /** - * Render and align label for plot line or band. - */ - renderLabel: function (optionsLabel, path, isBand, zIndex) { - var plotLine = this, - label = plotLine.label, - renderer = plotLine.axis.chart.renderer, - attribs, - xs, - ys, - x, - y; - - // add the SVG element - if (!label) { - attribs = { - align: optionsLabel.textAlign || optionsLabel.align, - rotation: optionsLabel.rotation - }; - - attribs.zIndex = zIndex; - - plotLine.label = label = renderer.text( - optionsLabel.text, - 0, - 0, - optionsLabel.useHTML - ) - .attr(attribs) - .css(optionsLabel.style) - .add(); - } - - // get the bounding box and align the label - // #3000 changed to better handle choice between plotband or plotline - xs = [path[1], path[4], (isBand ? path[6] : path[1])]; - ys = [path[2], path[5], (isBand ? path[7] : path[2])]; - x = arrayMin(xs); - y = arrayMin(ys); - - label.align(optionsLabel, false, { - x: x, - y: y, - width: arrayMax(xs) - x, - height: arrayMax(ys) - y - }); - label.show(); - }, - - /** - * Remove the plot line or band - */ - destroy: function () { - // remove it from the lookup - erase(this.axis.plotLinesAndBands, this); - - delete this.axis; - destroyObjectProperties(this); - } - }; - - /** - * Object with members for extending the Axis prototype - */ - - AxisPlotLineOrBandExtension = { - - /** - * Create the path for a plot band - */ - getPlotBandPath: function (from, to) { - var toPath = this.getPlotLinePath(to, null, null, true), - path = this.getPlotLinePath(from, null, null, true); - - if (path && toPath) { - - // Flat paths don't need labels (#3836) - path.flat = path.toString() === toPath.toString(); - - path.push( - toPath[4], - toPath[5], - toPath[1], - toPath[2] - ); - } else { // outside the axis area - path = null; - } - - return path; - }, - - addPlotBand: function (options) { - return this.addPlotBandOrLine(options, 'plotBands'); - }, - - addPlotLine: function (options) { - return this.addPlotBandOrLine(options, 'plotLines'); - }, - - /** - * Add a plot band or plot line after render time - * - * @param options {Object} The plotBand or plotLine configuration object - */ - addPlotBandOrLine: function (options, coll) { - var obj = new Highcharts.PlotLineOrBand(this, options).render(), - userOptions = this.userOptions; - - if (obj) { // #2189 - // Add it to the user options for exporting and Axis.update - if (coll) { - userOptions[coll] = userOptions[coll] || []; - userOptions[coll].push(options); - } - this.plotLinesAndBands.push(obj); - } - - return obj; - }, - - /** - * Remove a plot band or plot line from the chart by id - * @param {Object} id - */ - removePlotBandOrLine: function (id) { - var plotLinesAndBands = this.plotLinesAndBands, - options = this.options, - userOptions = this.userOptions, - i = plotLinesAndBands.length; - while (i--) { - if (plotLinesAndBands[i].id === id) { - plotLinesAndBands[i].destroy(); - } - } - each([options.plotLines || [], userOptions.plotLines || [], options.plotBands || [], userOptions.plotBands || []], function (arr) { - i = arr.length; - while (i--) { - if (arr[i].id === id) { - erase(arr, arr[i]); - } - } - }); - } - }; - - /** - * Create a new axis object - * @param {Object} chart - * @param {Object} options - */ - var Axis = Highcharts.Axis = function () { - this.init.apply(this, arguments); - }; - - Axis.prototype = { - - /** - * Default options for the X axis - the Y axis has extended defaults - */ - defaultOptions: { - // allowDecimals: null, - // alternateGridColor: null, - // categories: [], - dateTimeLabelFormats: { - millisecond: '%H:%M:%S.%L', - second: '%H:%M:%S', - minute: '%H:%M', - hour: '%H:%M', - day: '%e. %b', - week: '%e. %b', - month: '%b \'%y', - year: '%Y' - }, - endOnTick: false, - gridLineColor: '#D8D8D8', - // gridLineDashStyle: 'solid', - // gridLineWidth: 0, - // reversed: false, - - labels: { - enabled: true, - // rotation: 0, - // align: 'center', - // step: null, - style: { - color: '#606060', - cursor: 'default', - fontSize: '11px' - }, - x: 0 - //y: undefined - /*formatter: function () { - return this.value; - },*/ - }, - lineColor: '#C0D0E0', - lineWidth: 1, - //linkedTo: null, - //max: undefined, - //min: undefined, - minPadding: 0.01, - maxPadding: 0.01, - //minRange: null, - minorGridLineColor: '#E0E0E0', - // minorGridLineDashStyle: null, - minorGridLineWidth: 1, - minorTickColor: '#A0A0A0', - //minorTickInterval: null, - minorTickLength: 2, - minorTickPosition: 'outside', // inside or outside - //minorTickWidth: 0, - //opposite: false, - //offset: 0, - //plotBands: [{ - // events: {}, - // zIndex: 1, - // labels: { align, x, verticalAlign, y, style, rotation, textAlign } - //}], - //plotLines: [{ - // events: {} - // dashStyle: {} - // zIndex: - // labels: { align, x, verticalAlign, y, style, rotation, textAlign } - //}], - //reversed: false, - // showFirstLabel: true, - // showLastLabel: true, - startOfWeek: 1, - startOnTick: false, - tickColor: '#C0D0E0', - //tickInterval: null, - tickLength: 10, - tickmarkPlacement: 'between', // on or between - tickPixelInterval: 100, - tickPosition: 'outside', - //tickWidth: 1, - title: { - //text: null, - align: 'middle', // low, middle or high - //margin: 0 for horizontal, 10 for vertical axes, - //rotation: 0, - //side: 'outside', - style: { - color: '#707070' - } - //x: 0, - //y: 0 - }, - type: 'linear' // linear, logarithmic or datetime - //visible: true - }, - - /** - * This options set extends the defaultOptions for Y axes - */ - defaultYAxisOptions: { - endOnTick: true, - gridLineWidth: 1, - tickPixelInterval: 72, - showLastLabel: true, - labels: { - x: -8 - }, - lineWidth: 0, - maxPadding: 0.05, - minPadding: 0.05, - startOnTick: true, - //tickWidth: 0, - title: { - rotation: 270, - text: 'Values' - }, - stackLabels: { - enabled: false, - //align: dynamic, - //y: dynamic, - //x: dynamic, - //verticalAlign: dynamic, - //textAlign: dynamic, - //rotation: 0, - formatter: function () { - return Highcharts.numberFormat(this.total, -1); - }, - style: merge(defaultPlotOptions.line.dataLabels.style, { color: '#000000' }) - } - }, - - /** - * These options extend the defaultOptions for left axes - */ - defaultLeftAxisOptions: { - labels: { - x: -15 - }, - title: { - rotation: 270 - } - }, - - /** - * These options extend the defaultOptions for right axes - */ - defaultRightAxisOptions: { - labels: { - x: 15 - }, - title: { - rotation: 90 - } - }, - - /** - * These options extend the defaultOptions for bottom axes - */ - defaultBottomAxisOptions: { - labels: { - autoRotation: [-45], - x: 0 - // overflow: undefined, - // staggerLines: null - }, - title: { - rotation: 0 - } - }, - /** - * These options extend the defaultOptions for top axes - */ - defaultTopAxisOptions: { - labels: { - autoRotation: [-45], - x: 0 - // overflow: undefined - // staggerLines: null - }, - title: { - rotation: 0 - } - }, - - /** - * Initialize the axis - */ - init: function (chart, userOptions) { - - - var isXAxis = userOptions.isX, - axis = this; - - axis.chart = chart; - - // Flag, is the axis horizontal - axis.horiz = chart.inverted ? !isXAxis : isXAxis; - - // Flag, isXAxis - axis.isXAxis = isXAxis; - axis.coll = isXAxis ? 'xAxis' : 'yAxis'; - - axis.opposite = userOptions.opposite; // needed in setOptions - axis.side = userOptions.side || (axis.horiz ? - (axis.opposite ? 0 : 2) : // top : bottom - (axis.opposite ? 1 : 3)); // right : left - - axis.setOptions(userOptions); - - - var options = this.options, - type = options.type, - isDatetimeAxis = type === 'datetime'; - - axis.labelFormatter = options.labels.formatter || axis.defaultLabelFormatter; // can be overwritten by dynamic format - - - // Flag, stagger lines or not - axis.userOptions = userOptions; - - //axis.axisTitleMargin = UNDEFINED,// = options.title.margin, - axis.minPixelPadding = 0; - - axis.reversed = options.reversed; - axis.visible = options.visible !== false; - axis.zoomEnabled = options.zoomEnabled !== false; - - // Initial categories - axis.categories = options.categories || type === 'category'; - axis.names = axis.names || []; // Preserve on update (#3830) - - // Elements - //axis.axisGroup = UNDEFINED; - //axis.gridGroup = UNDEFINED; - //axis.axisTitle = UNDEFINED; - //axis.axisLine = UNDEFINED; - - // Shorthand types - axis.isLog = type === 'logarithmic'; - axis.isDatetimeAxis = isDatetimeAxis; - - // Flag, if axis is linked to another axis - axis.isLinked = defined(options.linkedTo); - // Linked axis. - //axis.linkedParent = UNDEFINED; - - // Tick positions - //axis.tickPositions = UNDEFINED; // array containing predefined positions - // Tick intervals - //axis.tickInterval = UNDEFINED; - //axis.minorTickInterval = UNDEFINED; - - - // Major ticks - axis.ticks = {}; - axis.labelEdge = []; - // Minor ticks - axis.minorTicks = {}; - - // List of plotLines/Bands - axis.plotLinesAndBands = []; - - // Alternate bands - axis.alternateBands = {}; - - // Axis metrics - //axis.left = UNDEFINED; - //axis.top = UNDEFINED; - //axis.width = UNDEFINED; - //axis.height = UNDEFINED; - //axis.bottom = UNDEFINED; - //axis.right = UNDEFINED; - //axis.transA = UNDEFINED; - //axis.transB = UNDEFINED; - //axis.oldTransA = UNDEFINED; - axis.len = 0; - //axis.oldMin = UNDEFINED; - //axis.oldMax = UNDEFINED; - //axis.oldUserMin = UNDEFINED; - //axis.oldUserMax = UNDEFINED; - //axis.oldAxisLength = UNDEFINED; - axis.minRange = axis.userMinRange = options.minRange || options.maxZoom; - axis.range = options.range; - axis.offset = options.offset || 0; - - - // Dictionary for stacks - axis.stacks = {}; - axis.oldStacks = {}; - axis.stacksTouched = 0; - - // Min and max in the data - //axis.dataMin = UNDEFINED, - //axis.dataMax = UNDEFINED, - - // The axis range - axis.max = null; - axis.min = null; - - // User set min and max - //axis.userMin = UNDEFINED, - //axis.userMax = UNDEFINED, - - // Crosshair options - axis.crosshair = pick(options.crosshair, splat(chart.options.tooltip.crosshairs)[isXAxis ? 0 : 1], false); - // Run Axis - - var eventType, - events = axis.options.events; - - // Register - if (inArray(axis, chart.axes) === -1) { // don't add it again on Axis.update() - if (isXAxis && !this.isColorAxis) { // #2713 - chart.axes.splice(chart.xAxis.length, 0, axis); - } else { - chart.axes.push(axis); - } - - chart[axis.coll].push(axis); - } - - axis.series = axis.series || []; // populated by Series - - // inverted charts have reversed xAxes as default - if (chart.inverted && isXAxis && axis.reversed === UNDEFINED) { - axis.reversed = true; - } - - axis.removePlotBand = axis.removePlotBandOrLine; - axis.removePlotLine = axis.removePlotBandOrLine; - - - // register event listeners - for (eventType in events) { - addEvent(axis, eventType, events[eventType]); - } - - // extend logarithmic axis - if (axis.isLog) { - axis.val2lin = axis.log2lin; - axis.lin2val = axis.lin2log; - } - }, - - /** - * Merge and set options - */ - setOptions: function (userOptions) { - this.options = merge( - this.defaultOptions, - this.isXAxis ? {} : this.defaultYAxisOptions, - [this.defaultTopAxisOptions, this.defaultRightAxisOptions, - this.defaultBottomAxisOptions, this.defaultLeftAxisOptions][this.side], - merge( - defaultOptions[this.coll], // if set in setOptions (#1053) - userOptions - ) - ); - }, - - /** - * The default label formatter. The context is a special config object for the label. - */ - defaultLabelFormatter: function () { - var axis = this.axis, - value = this.value, - categories = axis.categories, - dateTimeLabelFormat = this.dateTimeLabelFormat, - numericSymbols = defaultOptions.lang.numericSymbols, - i = numericSymbols && numericSymbols.length, - multi, - ret, - formatOption = axis.options.labels.format, - - // make sure the same symbol is added for all labels on a linear axis - numericSymbolDetector = axis.isLog ? value : axis.tickInterval; - - if (formatOption) { - ret = format(formatOption, this); - - } else if (categories) { - ret = value; - - } else if (dateTimeLabelFormat) { // datetime axis - ret = dateFormat(dateTimeLabelFormat, value); - - } else if (i && numericSymbolDetector >= 1000) { - // Decide whether we should add a numeric symbol like k (thousands) or M (millions). - // If we are to enable this in tooltip or other places as well, we can move this - // logic to the numberFormatter and enable it by a parameter. - while (i-- && ret === UNDEFINED) { - multi = Math.pow(1000, i + 1); - if (numericSymbolDetector >= multi && (value * 10) % multi === 0 && numericSymbols[i] !== null) { - ret = Highcharts.numberFormat(value / multi, -1) + numericSymbols[i]; - } - } - } - - if (ret === UNDEFINED) { - if (mathAbs(value) >= 10000) { // add thousands separators - ret = Highcharts.numberFormat(value, -1); - - } else { // small numbers - ret = Highcharts.numberFormat(value, -1, UNDEFINED, ''); // #2466 - } - } - - return ret; - }, - - /** - * Get the minimum and maximum for the series of each axis - */ - getSeriesExtremes: function () { - var axis = this, - chart = axis.chart; - - axis.hasVisibleSeries = false; - - // Reset properties in case we're redrawing (#3353) - axis.dataMin = axis.dataMax = axis.threshold = null; - axis.softThreshold = !axis.isXAxis; - - if (axis.buildStacks) { - axis.buildStacks(); - } - - // loop through this axis' series - each(axis.series, function (series) { - - if (series.visible || !chart.options.chart.ignoreHiddenSeries) { - - var seriesOptions = series.options, - xData, - threshold = seriesOptions.threshold, - seriesDataMin, - seriesDataMax; - - axis.hasVisibleSeries = true; - - // Validate threshold in logarithmic axes - if (axis.isLog && threshold <= 0) { - threshold = null; - } - - // Get dataMin and dataMax for X axes - if (axis.isXAxis) { - xData = series.xData; - if (xData.length) { - // If xData contains values which is not numbers, then filter them out. - // To prevent performance hit, we only do this after we have already - // found seriesDataMin because in most cases all data is valid. #5234. - seriesDataMin = arrayMin(xData); - if (!isNumber(seriesDataMin) && !(seriesDataMin instanceof Date)) { // Date for #5010 - xData = grep(xData, function (x) { - return isNumber(x); - }); - seriesDataMin = arrayMin(xData); // Do it again with valid data - } - - axis.dataMin = mathMin(pick(axis.dataMin, xData[0]), seriesDataMin); - axis.dataMax = mathMax(pick(axis.dataMax, xData[0]), arrayMax(xData)); - - } - - // Get dataMin and dataMax for Y axes, as well as handle stacking and processed data - } else { - - // Get this particular series extremes - series.getExtremes(); - seriesDataMax = series.dataMax; - seriesDataMin = series.dataMin; - - // Get the dataMin and dataMax so far. If percentage is used, the min and max are - // always 0 and 100. If seriesDataMin and seriesDataMax is null, then series - // doesn't have active y data, we continue with nulls - if (defined(seriesDataMin) && defined(seriesDataMax)) { - axis.dataMin = mathMin(pick(axis.dataMin, seriesDataMin), seriesDataMin); - axis.dataMax = mathMax(pick(axis.dataMax, seriesDataMax), seriesDataMax); - } - - // Adjust to threshold - if (defined(threshold)) { - axis.threshold = threshold; - } - // If any series has a hard threshold, it takes precedence - if (!seriesOptions.softThreshold || axis.isLog) { - axis.softThreshold = false; - } - } - } - }); - }, - - /** - * Translate from axis value to pixel position on the chart, or back - * - */ - translate: function (val, backwards, cvsCoord, old, handleLog, pointPlacement) { - var axis = this.linkedParent || this, // #1417 - sign = 1, - cvsOffset = 0, - localA = old ? axis.oldTransA : axis.transA, - localMin = old ? axis.oldMin : axis.min, - returnValue, - minPixelPadding = axis.minPixelPadding, - doPostTranslate = (axis.isOrdinal || axis.isBroken || (axis.isLog && handleLog)) && axis.lin2val; - - if (!localA) { - localA = axis.transA; - } - - // In vertical axes, the canvas coordinates start from 0 at the top like in - // SVG. - if (cvsCoord) { - sign *= -1; // canvas coordinates inverts the value - cvsOffset = axis.len; - } - - // Handle reversed axis - if (axis.reversed) { - sign *= -1; - cvsOffset -= sign * (axis.sector || axis.len); - } - - // From pixels to value - if (backwards) { // reverse translation - - val = val * sign + cvsOffset; - val -= minPixelPadding; - returnValue = val / localA + localMin; // from chart pixel to value - if (doPostTranslate) { // log and ordinal axes - returnValue = axis.lin2val(returnValue); - } - - // From value to pixels - } else { - if (doPostTranslate) { // log and ordinal axes - val = axis.val2lin(val); - } - if (pointPlacement === 'between') { - pointPlacement = 0.5; - } - returnValue = sign * (val - localMin) * localA + cvsOffset + (sign * minPixelPadding) + - (isNumber(pointPlacement) ? localA * pointPlacement * axis.pointRange : 0); - } - - return returnValue; - }, - - /** - * Utility method to translate an axis value to pixel position. - * @param {Number} value A value in terms of axis units - * @param {Boolean} paneCoordinates Whether to return the pixel coordinate relative to the chart - * or just the axis/pane itself. - */ - toPixels: function (value, paneCoordinates) { - return this.translate(value, false, !this.horiz, null, true) + (paneCoordinates ? 0 : this.pos); - }, - - /* - * Utility method to translate a pixel position in to an axis value - * @param {Number} pixel The pixel value coordinate - * @param {Boolean} paneCoordiantes Whether the input pixel is relative to the chart or just the - * axis/pane itself. - */ - toValue: function (pixel, paneCoordinates) { - return this.translate(pixel - (paneCoordinates ? 0 : this.pos), true, !this.horiz, null, true); - }, - - /** - * Create the path for a plot line that goes from the given value on - * this axis, across the plot to the opposite side - * @param {Number} value - * @param {Number} lineWidth Used for calculation crisp line - * @param {Number] old Use old coordinates (for resizing and rescaling) - */ - getPlotLinePath: function (value, lineWidth, old, force, translatedValue) { - var axis = this, - chart = axis.chart, - axisLeft = axis.left, - axisTop = axis.top, - x1, - y1, - x2, - y2, - cHeight = (old && chart.oldChartHeight) || chart.chartHeight, - cWidth = (old && chart.oldChartWidth) || chart.chartWidth, - skip, - transB = axis.transB, - /** - * Check if x is between a and b. If not, either move to a/b or skip, - * depending on the force parameter. - */ - between = function (x, a, b) { - if (x < a || x > b) { - if (force) { - x = mathMin(mathMax(a, x), b); - } else { - skip = true; - } - } - return x; - }; - - translatedValue = pick(translatedValue, axis.translate(value, null, null, old)); - x1 = x2 = mathRound(translatedValue + transB); - y1 = y2 = mathRound(cHeight - translatedValue - transB); - if (!isNumber(translatedValue)) { // no min or max - skip = true; - - } else if (axis.horiz) { - y1 = axisTop; - y2 = cHeight - axis.bottom; - x1 = x2 = between(x1, axisLeft, axisLeft + axis.width); - } else { - x1 = axisLeft; - x2 = cWidth - axis.right; - y1 = y2 = between(y1, axisTop, axisTop + axis.height); - } - return skip && !force ? - null : - chart.renderer.crispLine([M, x1, y1, L, x2, y2], lineWidth || 1); - }, - - /** - * Set the tick positions of a linear axis to round values like whole tens or every five. - */ - getLinearTickPositions: function (tickInterval, min, max) { - var pos, - lastPos, - roundedMin = correctFloat(mathFloor(min / tickInterval) * tickInterval), - roundedMax = correctFloat(mathCeil(max / tickInterval) * tickInterval), - tickPositions = []; - - // For single points, add a tick regardless of the relative position (#2662) - if (min === max && isNumber(min)) { - return [min]; - } - - // Populate the intermediate values - pos = roundedMin; - while (pos <= roundedMax) { - - // Place the tick on the rounded value - tickPositions.push(pos); - - // Always add the raw tickInterval, not the corrected one. - pos = correctFloat(pos + tickInterval); - - // If the interval is not big enough in the current min - max range to actually increase - // the loop variable, we need to break out to prevent endless loop. Issue #619 - if (pos === lastPos) { - break; - } - - // Record the last value - lastPos = pos; - } - return tickPositions; - }, - - /** - * Return the minor tick positions. For logarithmic axes, reuse the same logic - * as for major ticks. - */ - getMinorTickPositions: function () { - var axis = this, - options = axis.options, - tickPositions = axis.tickPositions, - minorTickInterval = axis.minorTickInterval, - minorTickPositions = [], - pos, - i, - pointRangePadding = axis.pointRangePadding || 0, - min = axis.min - pointRangePadding, // #1498 - max = axis.max + pointRangePadding, // #1498 - range = max - min, - len; - - // If minor ticks get too dense, they are hard to read, and may cause long running script. So we don't draw them. - if (range && range / minorTickInterval < axis.len / 3) { // #3875 - - if (axis.isLog) { - len = tickPositions.length; - for (i = 1; i < len; i++) { - minorTickPositions = minorTickPositions.concat( - axis.getLogTickPositions(minorTickInterval, tickPositions[i - 1], tickPositions[i], true) - ); - } - } else if (axis.isDatetimeAxis && options.minorTickInterval === 'auto') { // #1314 - minorTickPositions = minorTickPositions.concat( - axis.getTimeTicks( - axis.normalizeTimeTickInterval(minorTickInterval), - min, - max, - options.startOfWeek - ) - ); - } else { - for (pos = min + (tickPositions[0] - min) % minorTickInterval; pos <= max; pos += minorTickInterval) { - minorTickPositions.push(pos); - } - } - } - - if (minorTickPositions.length !== 0) { // don't change the extremes, when there is no minor ticks - axis.trimTicks(minorTickPositions, options.startOnTick, options.endOnTick); // #3652 #3743 #1498 - } - return minorTickPositions; - }, - - /** - * Adjust the min and max for the minimum range. Keep in mind that the series data is - * not yet processed, so we don't have information on data cropping and grouping, or - * updated axis.pointRange or series.pointRange. The data can't be processed until - * we have finally established min and max. - */ - adjustForMinRange: function () { - var axis = this, - options = axis.options, - min = axis.min, - max = axis.max, - zoomOffset, - spaceAvailable = axis.dataMax - axis.dataMin >= axis.minRange, - closestDataRange, - i, - distance, - xData, - loopLength, - minArgs, - maxArgs, - minRange; - - // Set the automatic minimum range based on the closest point distance - if (axis.isXAxis && axis.minRange === UNDEFINED && !axis.isLog) { - - if (defined(options.min) || defined(options.max)) { - axis.minRange = null; // don't do this again - - } else { - - // Find the closest distance between raw data points, as opposed to - // closestPointRange that applies to processed points (cropped and grouped) - each(axis.series, function (series) { - xData = series.xData; - loopLength = series.xIncrement ? 1 : xData.length - 1; - for (i = loopLength; i > 0; i--) { - distance = xData[i] - xData[i - 1]; - if (closestDataRange === UNDEFINED || distance < closestDataRange) { - closestDataRange = distance; - } - } - }); - axis.minRange = mathMin(closestDataRange * 5, axis.dataMax - axis.dataMin); - } - } - - // if minRange is exceeded, adjust - if (max - min < axis.minRange) { - minRange = axis.minRange; - zoomOffset = (minRange - max + min) / 2; - - // if min and max options have been set, don't go beyond it - minArgs = [min - zoomOffset, pick(options.min, min - zoomOffset)]; - if (spaceAvailable) { // if space is available, stay within the data range - minArgs[2] = axis.dataMin; - } - min = arrayMax(minArgs); - - maxArgs = [min + minRange, pick(options.max, min + minRange)]; - if (spaceAvailable) { // if space is availabe, stay within the data range - maxArgs[2] = axis.dataMax; - } - - max = arrayMin(maxArgs); - - // now if the max is adjusted, adjust the min back - if (max - min < minRange) { - minArgs[0] = max - minRange; - minArgs[1] = pick(options.min, max - minRange); - min = arrayMax(minArgs); - } - } - - // Record modified extremes - axis.min = min; - axis.max = max; - }, - - /** - * Find the closestPointRange across all series - */ - getClosest: function () { - var ret; - each(this.series, function (series) { - var seriesClosest = series.closestPointRange; - if (!series.noSharedTooltip && defined(seriesClosest)) { - ret = defined(ret) ? - mathMin(ret, seriesClosest) : - seriesClosest; - } - }); - return ret; - }, - - /** - * Update translation information - */ - setAxisTranslation: function (saveOld) { - var axis = this, - range = axis.max - axis.min, - pointRange = axis.axisPointRange || 0, - closestPointRange, - minPointOffset = 0, - pointRangePadding = 0, - linkedParent = axis.linkedParent, - ordinalCorrection, - hasCategories = !!axis.categories, - transA = axis.transA, - isXAxis = axis.isXAxis; - - // Adjust translation for padding. Y axis with categories need to go through the same (#1784). - if (isXAxis || hasCategories || pointRange) { - if (linkedParent) { - minPointOffset = linkedParent.minPointOffset; - pointRangePadding = linkedParent.pointRangePadding; - - } else { - - // Get the closest points - closestPointRange = axis.getClosest(); - - each(axis.series, function (series) { - var seriesPointRange = hasCategories ? - 1 : - (isXAxis ? - pick(series.options.pointRange, closestPointRange, 0) : - (axis.axisPointRange || 0)), // #2806 - pointPlacement = series.options.pointPlacement; - - pointRange = mathMax(pointRange, seriesPointRange); - - if (!axis.single) { - // minPointOffset is the value padding to the left of the axis in order to make - // room for points with a pointRange, typically columns. When the pointPlacement option - // is 'between' or 'on', this padding does not apply. - minPointOffset = mathMax( - minPointOffset, - isString(pointPlacement) ? 0 : seriesPointRange / 2 - ); - - // Determine the total padding needed to the length of the axis to make room for the - // pointRange. If the series' pointPlacement is 'on', no padding is added. - pointRangePadding = mathMax( - pointRangePadding, - pointPlacement === 'on' ? 0 : seriesPointRange - ); - } - }); - } - - // Record minPointOffset and pointRangePadding - ordinalCorrection = axis.ordinalSlope && closestPointRange ? axis.ordinalSlope / closestPointRange : 1; // #988, #1853 - axis.minPointOffset = minPointOffset = minPointOffset * ordinalCorrection; - axis.pointRangePadding = pointRangePadding = pointRangePadding * ordinalCorrection; - - // pointRange means the width reserved for each point, like in a column chart - axis.pointRange = mathMin(pointRange, range); - - // closestPointRange means the closest distance between points. In columns - // it is mostly equal to pointRange, but in lines pointRange is 0 while closestPointRange - // is some other value - if (isXAxis) { - axis.closestPointRange = closestPointRange; - } - } - - // Secondary values - if (saveOld) { - axis.oldTransA = transA; - } - axis.translationSlope = axis.transA = transA = axis.len / ((range + pointRangePadding) || 1); - axis.transB = axis.horiz ? axis.left : axis.bottom; // translation addend - axis.minPixelPadding = transA * minPointOffset; - }, - - minFromRange: function () { - return this.max - this.range; - }, - - /** - * Set the tick positions to round values and optionally extend the extremes - * to the nearest tick - */ - setTickInterval: function (secondPass) { - var axis = this, - chart = axis.chart, - options = axis.options, - isLog = axis.isLog, - log2lin = axis.log2lin, - isDatetimeAxis = axis.isDatetimeAxis, - isXAxis = axis.isXAxis, - isLinked = axis.isLinked, - maxPadding = options.maxPadding, - minPadding = options.minPadding, - length, - linkedParentExtremes, - tickIntervalOption = options.tickInterval, - minTickInterval, - tickPixelIntervalOption = options.tickPixelInterval, - categories = axis.categories, - threshold = axis.threshold, - softThreshold = axis.softThreshold, - thresholdMin, - thresholdMax, - hardMin, - hardMax; - - if (!isDatetimeAxis && !categories && !isLinked) { - this.getTickAmount(); - } - - // Min or max set either by zooming/setExtremes or initial options - hardMin = pick(axis.userMin, options.min); - hardMax = pick(axis.userMax, options.max); - - // Linked axis gets the extremes from the parent axis - if (isLinked) { - axis.linkedParent = chart[axis.coll][options.linkedTo]; - linkedParentExtremes = axis.linkedParent.getExtremes(); - axis.min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin); - axis.max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax); - if (options.type !== axis.linkedParent.options.type) { - error(11, 1); // Can't link axes of different type - } - - // Initial min and max from the extreme data values - } else { - - // Adjust to hard threshold - if (!softThreshold && defined(threshold)) { - if (axis.dataMin >= threshold) { - thresholdMin = threshold; - minPadding = 0; - } else if (axis.dataMax <= threshold) { - thresholdMax = threshold; - maxPadding = 0; - } - } - - axis.min = pick(hardMin, thresholdMin, axis.dataMin); - axis.max = pick(hardMax, thresholdMax, axis.dataMax); - - } - - if (isLog) { - if (!secondPass && mathMin(axis.min, pick(axis.dataMin, axis.min)) <= 0) { // #978 - error(10, 1); // Can't plot negative values on log axis - } - // The correctFloat cures #934, float errors on full tens. But it - // was too aggressive for #4360 because of conversion back to lin, - // therefore use precision 15. - axis.min = correctFloat(log2lin(axis.min), 15); - axis.max = correctFloat(log2lin(axis.max), 15); - } - - // handle zoomed range - if (axis.range && defined(axis.max)) { - axis.userMin = axis.min = hardMin = mathMax(axis.min, axis.minFromRange()); // #618 - axis.userMax = hardMax = axis.max; - - axis.range = null; // don't use it when running setExtremes - } - - // Hook for Highstock Scroller. Consider combining with beforePadding. - fireEvent(axis, 'foundExtremes'); - - // Hook for adjusting this.min and this.max. Used by bubble series. - if (axis.beforePadding) { - axis.beforePadding(); - } - - // adjust min and max for the minimum range - axis.adjustForMinRange(); - - // Pad the values to get clear of the chart's edges. To avoid tickInterval taking the padding - // into account, we do this after computing tick interval (#1337). - if (!categories && !axis.axisPointRange && !axis.usePercentage && !isLinked && defined(axis.min) && defined(axis.max)) { - length = axis.max - axis.min; - if (length) { - if (!defined(hardMin) && minPadding) { - axis.min -= length * minPadding; - } - if (!defined(hardMax) && maxPadding) { - axis.max += length * maxPadding; - } - } - } - - // Stay within floor and ceiling - if (isNumber(options.floor)) { - axis.min = mathMax(axis.min, options.floor); - } - if (isNumber(options.ceiling)) { - axis.max = mathMin(axis.max, options.ceiling); - } - - // When the threshold is soft, adjust the extreme value only if - // the data extreme and the padded extreme land on either side of the threshold. For example, - // a series of [0, 1, 2, 3] would make the yAxis add a tick for -1 because of the - // default minPadding and startOnTick options. This is prevented by the softThreshold - // option. - if (softThreshold && defined(axis.dataMin)) { - threshold = threshold || 0; - if (!defined(hardMin) && axis.min < threshold && axis.dataMin >= threshold) { - axis.min = threshold; - } else if (!defined(hardMax) && axis.max > threshold && axis.dataMax <= threshold) { - axis.max = threshold; - } - } - - - // get tickInterval - if (axis.min === axis.max || axis.min === undefined || axis.max === undefined) { - axis.tickInterval = 1; - } else if (isLinked && !tickIntervalOption && - tickPixelIntervalOption === axis.linkedParent.options.tickPixelInterval) { - axis.tickInterval = tickIntervalOption = axis.linkedParent.tickInterval; - } else { - axis.tickInterval = pick( - tickIntervalOption, - this.tickAmount ? ((axis.max - axis.min) / mathMax(this.tickAmount - 1, 1)) : undefined, - categories ? // for categoried axis, 1 is default, for linear axis use tickPix - 1 : - // don't let it be more than the data range - (axis.max - axis.min) * tickPixelIntervalOption / mathMax(axis.len, tickPixelIntervalOption) - ); - } - - // Now we're finished detecting min and max, crop and group series data. This - // is in turn needed in order to find tick positions in ordinal axes. - if (isXAxis && !secondPass) { - each(axis.series, function (series) { - series.processData(axis.min !== axis.oldMin || axis.max !== axis.oldMax); - }); - } - - // set the translation factor used in translate function - axis.setAxisTranslation(true); - - // hook for ordinal axes and radial axes - if (axis.beforeSetTickPositions) { - axis.beforeSetTickPositions(); - } - - // hook for extensions, used in Highstock ordinal axes - if (axis.postProcessTickInterval) { - axis.tickInterval = axis.postProcessTickInterval(axis.tickInterval); - } - - // In column-like charts, don't cramp in more ticks than there are points (#1943, #4184) - if (axis.pointRange && !tickIntervalOption) { - axis.tickInterval = mathMax(axis.pointRange, axis.tickInterval); - } - - // Before normalizing the tick interval, handle minimum tick interval. This applies only if tickInterval is not defined. - minTickInterval = pick(options.minTickInterval, axis.isDatetimeAxis && axis.closestPointRange); - if (!tickIntervalOption && axis.tickInterval < minTickInterval) { - axis.tickInterval = minTickInterval; - } - - // for linear axes, get magnitude and normalize the interval - if (!isDatetimeAxis && !isLog && !tickIntervalOption) { - axis.tickInterval = normalizeTickInterval( - axis.tickInterval, - null, - getMagnitude(axis.tickInterval), - // If the tick interval is between 0.5 and 5 and the axis max is in the order of - // thousands, chances are we are dealing with years. Don't allow decimals. #3363. - pick(options.allowDecimals, !(axis.tickInterval > 0.5 && axis.tickInterval < 5 && axis.max > 1000 && axis.max < 9999)), - !!this.tickAmount - ); - } - - // Prevent ticks from getting so close that we can't draw the labels - if (!this.tickAmount && this.len) { // Color axis with disabled legend has no length - axis.tickInterval = axis.unsquish(); - } - - this.setTickPositions(); - }, - - /** - * Now we have computed the normalized tickInterval, get the tick positions - */ - setTickPositions: function () { - - var options = this.options, - tickPositions, - tickPositionsOption = options.tickPositions, - tickPositioner = options.tickPositioner, - startOnTick = options.startOnTick, - endOnTick = options.endOnTick, - single; - - // Set the tickmarkOffset - this.tickmarkOffset = (this.categories && options.tickmarkPlacement === 'between' && - this.tickInterval === 1) ? 0.5 : 0; // #3202 - - - // get minorTickInterval - this.minorTickInterval = options.minorTickInterval === 'auto' && this.tickInterval ? - this.tickInterval / 5 : options.minorTickInterval; - - // Find the tick positions - this.tickPositions = tickPositions = tickPositionsOption && tickPositionsOption.slice(); // Work on a copy (#1565) - if (!tickPositions) { - - if (this.isDatetimeAxis) { - tickPositions = this.getTimeTicks( - this.normalizeTimeTickInterval(this.tickInterval, options.units), - this.min, - this.max, - options.startOfWeek, - this.ordinalPositions, - this.closestPointRange, - true - ); - } else if (this.isLog) { - tickPositions = this.getLogTickPositions(this.tickInterval, this.min, this.max); - } else { - tickPositions = this.getLinearTickPositions(this.tickInterval, this.min, this.max); - } - - // Too dense ticks, keep only the first and last (#4477) - if (tickPositions.length > this.len) { - tickPositions = [tickPositions[0], tickPositions.pop()]; - } - - this.tickPositions = tickPositions; - - // Run the tick positioner callback, that allows modifying auto tick positions. - if (tickPositioner) { - tickPositioner = tickPositioner.apply(this, [this.min, this.max]); - if (tickPositioner) { - this.tickPositions = tickPositions = tickPositioner; - } - } - - } - - if (!this.isLinked) { - - // reset min/max or remove extremes based on start/end on tick - this.trimTicks(tickPositions, startOnTick, endOnTick); - - // When there is only one point, or all points have the same value on this axis, then min - // and max are equal and tickPositions.length is 0 or 1. In this case, add some padding - // in order to center the point, but leave it with one tick. #1337. - if (this.min === this.max && defined(this.min) && !this.tickAmount) { - // Substract half a unit (#2619, #2846, #2515, #3390) - single = true; - this.min -= 0.5; - this.max += 0.5; - } - this.single = single; - - if (!tickPositionsOption && !tickPositioner) { - this.adjustTickAmount(); - } - } - }, - - /** - * Handle startOnTick and endOnTick by either adapting to padding min/max or rounded min/max - */ - trimTicks: function (tickPositions, startOnTick, endOnTick) { - var roundedMin = tickPositions[0], - roundedMax = tickPositions[tickPositions.length - 1], - minPointOffset = this.minPointOffset || 0; - - if (startOnTick) { - this.min = roundedMin; - } else { - while (this.min - minPointOffset > tickPositions[0]) { - tickPositions.shift(); - } - } - - if (endOnTick) { - this.max = roundedMax; - } else { - while (this.max + minPointOffset < tickPositions[tickPositions.length - 1]) { - tickPositions.pop(); - } - } - - // If no tick are left, set one tick in the middle (#3195) - if (tickPositions.length === 0 && defined(roundedMin)) { - tickPositions.push((roundedMax + roundedMin) / 2); - } - }, - - /** - * Check if there are multiple axes in the same pane - * @returns {Boolean} There are other axes - */ - alignToOthers: function () { - var others = {}, // Whether there is another axis to pair with this one - hasOther, - options = this.options; - - if (this.chart.options.chart.alignTicks !== false && options.alignTicks !== false) { - each(this.chart[this.coll], function (axis) { - var otherOptions = axis.options, - horiz = axis.horiz, - key = [ - horiz ? otherOptions.left : otherOptions.top, - otherOptions.width, - otherOptions.height, - otherOptions.pane - ].join(','); - - - if (axis.series.length) { // #4442 - if (others[key]) { - hasOther = true; // #4201 - } else { - others[key] = 1; - } - } - }); - } - return hasOther; - }, - - /** - * Set the max ticks of either the x and y axis collection - */ - getTickAmount: function () { - var options = this.options, - tickAmount = options.tickAmount, - tickPixelInterval = options.tickPixelInterval; - - if (!defined(options.tickInterval) && this.len < tickPixelInterval && !this.isRadial && - !this.isLog && options.startOnTick && options.endOnTick) { - tickAmount = 2; - } - - if (!tickAmount && this.alignToOthers()) { - // Add 1 because 4 tick intervals require 5 ticks (including first and last) - tickAmount = mathCeil(this.len / tickPixelInterval) + 1; - } - - // For tick amounts of 2 and 3, compute five ticks and remove the intermediate ones. This - // prevents the axis from adding ticks that are too far away from the data extremes. - if (tickAmount < 4) { - this.finalTickAmt = tickAmount; - tickAmount = 5; - } - - this.tickAmount = tickAmount; - }, - - /** - * When using multiple axes, adjust the number of ticks to match the highest - * number of ticks in that group - */ - adjustTickAmount: function () { - var tickInterval = this.tickInterval, - tickPositions = this.tickPositions, - tickAmount = this.tickAmount, - finalTickAmt = this.finalTickAmt, - currentTickAmount = tickPositions && tickPositions.length, - i, - len; - - if (currentTickAmount < tickAmount) { - while (tickPositions.length < tickAmount) { - tickPositions.push(correctFloat( - tickPositions[tickPositions.length - 1] + tickInterval - )); - } - this.transA *= (currentTickAmount - 1) / (tickAmount - 1); - this.max = tickPositions[tickPositions.length - 1]; - - // We have too many ticks, run second pass to try to reduce ticks - } else if (currentTickAmount > tickAmount) { - this.tickInterval *= 2; - this.setTickPositions(); - } - - // The finalTickAmt property is set in getTickAmount - if (defined(finalTickAmt)) { - i = len = tickPositions.length; - while (i--) { - if ( - (finalTickAmt === 3 && i % 2 === 1) || // Remove every other tick - (finalTickAmt <= 2 && i > 0 && i < len - 1) // Remove all but first and last - ) { - tickPositions.splice(i, 1); - } - } - this.finalTickAmt = UNDEFINED; - } - }, - - /** - * Set the scale based on data min and max, user set min and max or options - * - */ - setScale: function () { - var axis = this, - isDirtyData, - isDirtyAxisLength; - - axis.oldMin = axis.min; - axis.oldMax = axis.max; - axis.oldAxisLength = axis.len; - - // set the new axisLength - axis.setAxisSize(); - //axisLength = horiz ? axisWidth : axisHeight; - isDirtyAxisLength = axis.len !== axis.oldAxisLength; - - // is there new data? - each(axis.series, function (series) { - if (series.isDirtyData || series.isDirty || - series.xAxis.isDirty) { // when x axis is dirty, we need new data extremes for y as well - isDirtyData = true; - } - }); - - // do we really need to go through all this? - if (isDirtyAxisLength || isDirtyData || axis.isLinked || axis.forceRedraw || - axis.userMin !== axis.oldUserMin || axis.userMax !== axis.oldUserMax || axis.alignToOthers()) { - - if (axis.resetStacks) { - axis.resetStacks(); - } - - axis.forceRedraw = false; - - // get data extremes if needed - axis.getSeriesExtremes(); - - // get fixed positions based on tickInterval - axis.setTickInterval(); - - // record old values to decide whether a rescale is necessary later on (#540) - axis.oldUserMin = axis.userMin; - axis.oldUserMax = axis.userMax; - - // Mark as dirty if it is not already set to dirty and extremes have changed. #595. - if (!axis.isDirty) { - axis.isDirty = isDirtyAxisLength || axis.min !== axis.oldMin || axis.max !== axis.oldMax; - } - } else if (axis.cleanStacks) { - axis.cleanStacks(); - } - }, - - /** - * Set the extremes and optionally redraw - * @param {Number} newMin - * @param {Number} newMax - * @param {Boolean} redraw - * @param {Boolean|Object} animation Whether to apply animation, and optionally animation - * configuration - * @param {Object} eventArguments - * - */ - setExtremes: function (newMin, newMax, redraw, animation, eventArguments) { - var axis = this, - chart = axis.chart; - - redraw = pick(redraw, true); // defaults to true - - each(axis.series, function (serie) { - delete serie.kdTree; - }); - - // Extend the arguments with min and max - eventArguments = extend(eventArguments, { - min: newMin, - max: newMax - }); - - // Fire the event - fireEvent(axis, 'setExtremes', eventArguments, function () { // the default event handler - - axis.userMin = newMin; - axis.userMax = newMax; - axis.eventArgs = eventArguments; - - if (redraw) { - chart.redraw(animation); - } - }); - }, - - /** - * Overridable method for zooming chart. Pulled out in a separate method to allow overriding - * in stock charts. - */ - zoom: function (newMin, newMax) { - var dataMin = this.dataMin, - dataMax = this.dataMax, - options = this.options, - min = mathMin(dataMin, pick(options.min, dataMin)), - max = mathMax(dataMax, pick(options.max, dataMax)); - - // Prevent pinch zooming out of range. Check for defined is for #1946. #1734. - if (!this.allowZoomOutside) { - if (defined(dataMin) && newMin <= min) { - newMin = min; - } - if (defined(dataMax) && newMax >= max) { - newMax = max; - } - } - - // In full view, displaying the reset zoom button is not required - this.displayBtn = newMin !== UNDEFINED || newMax !== UNDEFINED; - - // Do it - this.setExtremes( - newMin, - newMax, - false, - UNDEFINED, - { trigger: 'zoom' } - ); - return true; - }, - - /** - * Update the axis metrics - */ - setAxisSize: function () { - var chart = this.chart, - options = this.options, - offsetLeft = options.offsetLeft || 0, - offsetRight = options.offsetRight || 0, - horiz = this.horiz, - width = pick(options.width, chart.plotWidth - offsetLeft + offsetRight), - height = pick(options.height, chart.plotHeight), - top = pick(options.top, chart.plotTop), - left = pick(options.left, chart.plotLeft + offsetLeft), - percentRegex = /%$/; - - // Check for percentage based input values. Rounding fixes problems with - // column overflow and plot line filtering (#4898, #4899) - if (percentRegex.test(height)) { - height = Math.round(parseFloat(height) / 100 * chart.plotHeight); - } - if (percentRegex.test(top)) { - top = Math.round(parseFloat(top) / 100 * chart.plotHeight + chart.plotTop); - } - - // Expose basic values to use in Series object and navigator - this.left = left; - this.top = top; - this.width = width; - this.height = height; - this.bottom = chart.chartHeight - height - top; - this.right = chart.chartWidth - width - left; - - // Direction agnostic properties - this.len = mathMax(horiz ? width : height, 0); // mathMax fixes #905 - this.pos = horiz ? left : top; // distance from SVG origin - }, - - /** - * Get the actual axis extremes - */ - getExtremes: function () { - var axis = this, - isLog = axis.isLog, - lin2log = axis.lin2log; - - return { - min: isLog ? correctFloat(lin2log(axis.min)) : axis.min, - max: isLog ? correctFloat(lin2log(axis.max)) : axis.max, - dataMin: axis.dataMin, - dataMax: axis.dataMax, - userMin: axis.userMin, - userMax: axis.userMax - }; - }, - - /** - * Get the zero plane either based on zero or on the min or max value. - * Used in bar and area plots - */ - getThreshold: function (threshold) { - var axis = this, - isLog = axis.isLog, - lin2log = axis.lin2log, - realMin = isLog ? lin2log(axis.min) : axis.min, - realMax = isLog ? lin2log(axis.max) : axis.max; - - // With a threshold of null, make the columns/areas rise from the top or bottom - // depending on the value, assuming an actual threshold of 0 (#4233). - if (threshold === null) { - threshold = realMax < 0 ? realMax : realMin; - } else if (realMin > threshold) { - threshold = realMin; - } else if (realMax < threshold) { - threshold = realMax; - } - - return axis.translate(threshold, 0, 1, 0, 1); - }, - - /** - * Compute auto alignment for the axis label based on which side the axis is on - * and the given rotation for the label - */ - autoLabelAlign: function (rotation) { - var ret, - angle = (pick(rotation, 0) - (this.side * 90) + 720) % 360; - - if (angle > 15 && angle < 165) { - ret = 'right'; - } else if (angle > 195 && angle < 345) { - ret = 'left'; - } else { - ret = 'center'; - } - return ret; - }, - - /** - * Get the tick length and width for the axis. - * @param {String} prefix 'tick' or 'minorTick' - * @returns {Array} An array of tickLength and tickWidth - */ - tickSize: function (prefix) { - var options = this.options, - tickLength = options[prefix + 'Length'], - tickWidth = pick(options[prefix + 'Width'], prefix === 'tick' && this.isXAxis ? 1 : 0); // X axis defaults to 1 - - if (tickWidth && tickLength) { - // Negate the length - if (options[prefix + 'Position'] === 'inside') { - tickLength = -tickLength; - } - return [tickLength, tickWidth]; - } - - }, - - /** - * Return the size of the labels - */ - labelMetrics: function () { - return this.chart.renderer.fontMetrics( - this.options.labels.style.fontSize, - this.ticks[0] && this.ticks[0].label - ); - }, - - /** - * Prevent the ticks from getting so close we can't draw the labels. On a horizontal - * axis, this is handled by rotating the labels, removing ticks and adding ellipsis. - * On a vertical axis remove ticks and add ellipsis. - */ - unsquish: function () { - var labelOptions = this.options.labels, - horiz = this.horiz, - tickInterval = this.tickInterval, - newTickInterval = tickInterval, - slotSize = this.len / (((this.categories ? 1 : 0) + this.max - this.min) / tickInterval), - rotation, - rotationOption = labelOptions.rotation, - labelMetrics = this.labelMetrics(), - step, - bestScore = Number.MAX_VALUE, - autoRotation, - // Return the multiple of tickInterval that is needed to avoid collision - getStep = function (spaceNeeded) { - var step = spaceNeeded / (slotSize || 1); - step = step > 1 ? mathCeil(step) : 1; - return step * tickInterval; - }; - - if (horiz) { - autoRotation = !labelOptions.staggerLines && !labelOptions.step && ( // #3971 - defined(rotationOption) ? - [rotationOption] : - slotSize < pick(labelOptions.autoRotationLimit, 80) && labelOptions.autoRotation - ); - - if (autoRotation) { - - // Loop over the given autoRotation options, and determine which gives the best score. The - // best score is that with the lowest number of steps and a rotation closest to horizontal. - each(autoRotation, function (rot) { - var score; - - if (rot === rotationOption || (rot && rot >= -90 && rot <= 90)) { // #3891 - - step = getStep(mathAbs(labelMetrics.h / mathSin(deg2rad * rot))); - - score = step + mathAbs(rot / 360); - - if (score < bestScore) { - bestScore = score; - rotation = rot; - newTickInterval = step; - } - } - }); - } - - } else if (!labelOptions.step) { // #4411 - newTickInterval = getStep(labelMetrics.h); - } - - this.autoRotation = autoRotation; - this.labelRotation = pick(rotation, rotationOption); - - return newTickInterval; - }, - - /** - * Get the general slot width for this axis. This may change between the pre-render (from Axis.getOffset) - * and the final tick rendering and placement (#5086). - */ - getSlotWidth: function () { - var chart = this.chart, - horiz = this.horiz, - labelOptions = this.options.labels, - slotCount = Math.max(this.tickPositions.length - (this.categories ? 0 : 1), 1), - marginLeft = chart.margin[3]; - - return (horiz && (labelOptions.step || 0) < 2 && !labelOptions.rotation && // #4415 - ((this.staggerLines || 1) * chart.plotWidth) / slotCount) || - (!horiz && ((marginLeft && (marginLeft - chart.spacing[3])) || chart.chartWidth * 0.33)); // #1580, #1931 - - }, - - /** - * Render the axis labels and determine whether ellipsis or rotation need to be applied - */ - renderUnsquish: function () { - var chart = this.chart, - renderer = chart.renderer, - tickPositions = this.tickPositions, - ticks = this.ticks, - labelOptions = this.options.labels, - horiz = this.horiz, - slotWidth = this.getSlotWidth(), - innerWidth = mathMax(1, mathRound(slotWidth - 2 * (labelOptions.padding || 5))), - attr = {}, - labelMetrics = this.labelMetrics(), - textOverflowOption = labelOptions.style.textOverflow, - css, - labelLength = 0, - label, - i, - pos; - - // Set rotation option unless it is "auto", like in gauges - if (!isString(labelOptions.rotation)) { - attr.rotation = labelOptions.rotation || 0; // #4443 - } - - // Handle auto rotation on horizontal axis - if (this.autoRotation) { - - // Get the longest label length - each(tickPositions, function (tick) { - tick = ticks[tick]; - if (tick && tick.labelLength > labelLength) { - labelLength = tick.labelLength; - } - }); - - // Apply rotation only if the label is too wide for the slot, and - // the label is wider than its height. - if (labelLength > innerWidth && labelLength > labelMetrics.h) { - attr.rotation = this.labelRotation; - } else { - this.labelRotation = 0; - } - - // Handle word-wrap or ellipsis on vertical axis - } else if (slotWidth) { - // For word-wrap or ellipsis - css = { width: innerWidth + PX }; - - if (!textOverflowOption) { - css.textOverflow = 'clip'; - - // On vertical axis, only allow word wrap if there is room for more lines. - i = tickPositions.length; - while (!horiz && i--) { - pos = tickPositions[i]; - label = ticks[pos].label; - if (label) { - // Reset ellipsis in order to get the correct bounding box (#4070) - if (label.styles.textOverflow === 'ellipsis') { - label.css({ textOverflow: 'clip' }); - - // Set the correct width in order to read the bounding box height (#4678, #5034) - } else if (ticks[pos].labelLength > slotWidth) { - label.css({ width: slotWidth + 'px' }); - } - - if (label.getBBox().height > this.len / tickPositions.length - (labelMetrics.h - labelMetrics.f)) { - label.specCss = { textOverflow: 'ellipsis' }; - } - } - } - } - } - - - // Add ellipsis if the label length is significantly longer than ideal - if (attr.rotation) { - css = { - width: (labelLength > chart.chartHeight * 0.5 ? chart.chartHeight * 0.33 : chart.chartHeight) + PX - }; - if (!textOverflowOption) { - css.textOverflow = 'ellipsis'; - } - } - - // Set the explicit or automatic label alignment - this.labelAlign = labelOptions.align || this.autoLabelAlign(this.labelRotation); - if (this.labelAlign) { - attr.align = this.labelAlign; - } - - // Apply general and specific CSS - each(tickPositions, function (pos) { - var tick = ticks[pos], - label = tick && tick.label; - if (label) { - label.attr(attr); // This needs to go before the CSS in old IE (#4502) - if (css) { - label.css(merge(css, label.specCss)); - } - delete label.specCss; - tick.rotation = attr.rotation; - } - }); - - // Note: Why is this not part of getLabelPosition? - this.tickRotCorr = renderer.rotCorr(labelMetrics.b, this.labelRotation || 0, this.side !== 0); - }, - - /** - * Return true if the axis has associated data - */ - hasData: function () { - return this.hasVisibleSeries || (defined(this.min) && defined(this.max) && !!this.tickPositions); - }, - - /** - * Render the tick labels to a preliminary position to get their sizes - */ - getOffset: function () { - var axis = this, - chart = axis.chart, - renderer = chart.renderer, - options = axis.options, - tickPositions = axis.tickPositions, - ticks = axis.ticks, - horiz = axis.horiz, - side = axis.side, - invertedSide = chart.inverted ? [1, 0, 3, 2][side] : side, - hasData, - showAxis, - titleOffset = 0, - titleOffsetOption, - titleMargin = 0, - axisTitleOptions = options.title, - labelOptions = options.labels, - labelOffset = 0, // reset - labelOffsetPadded, - opposite = axis.opposite, - axisOffset = chart.axisOffset, - clipOffset = chart.clipOffset, - clip, - directionFactor = [-1, 1, 1, -1][side], - n, - textAlign, - axisParent = axis.axisParent, // Used in color axis - lineHeightCorrection, - tickSize = this.tickSize('tick'); - - // For reuse in Axis.render - hasData = axis.hasData(); - axis.showAxis = showAxis = hasData || pick(options.showEmpty, true); - - // Set/reset staggerLines - axis.staggerLines = axis.horiz && labelOptions.staggerLines; - - // Create the axisGroup and gridGroup elements on first iteration - if (!axis.axisGroup) { - axis.gridGroup = renderer.g('grid') - .attr({ zIndex: options.gridZIndex || 1 }) - .add(axisParent); - axis.axisGroup = renderer.g('axis') - .attr({ zIndex: options.zIndex || 2 }) - .add(axisParent); - axis.labelGroup = renderer.g('axis-labels') - .attr({ zIndex: labelOptions.zIndex || 7 }) - .addClass(PREFIX + axis.coll.toLowerCase() + '-labels') - .add(axisParent); - } - - if (hasData || axis.isLinked) { - - // Generate ticks - each(tickPositions, function (pos) { - if (!ticks[pos]) { - ticks[pos] = new Tick(axis, pos); - } else { - ticks[pos].addLabel(); // update labels depending on tick interval - } - }); - - axis.renderUnsquish(); - - - // Left side must be align: right and right side must have align: left for labels - if (labelOptions.reserveSpace !== false && (side === 0 || side === 2 || - { 1: 'left', 3: 'right' }[side] === axis.labelAlign || axis.labelAlign === 'center')) { - each(tickPositions, function (pos) { - - // get the highest offset - labelOffset = mathMax( - ticks[pos].getLabelSize(), - labelOffset - ); - }); - } - - if (axis.staggerLines) { - labelOffset *= axis.staggerLines; - axis.labelOffset = labelOffset * (axis.opposite ? -1 : 1); - } - - - } else { // doesn't have data - for (n in ticks) { - ticks[n].destroy(); - delete ticks[n]; - } - } - - if (axisTitleOptions && axisTitleOptions.text && axisTitleOptions.enabled !== false) { - if (!axis.axisTitle) { - textAlign = axisTitleOptions.textAlign; - if (!textAlign) { - textAlign = (horiz ? { - low: 'left', - middle: 'center', - high: 'right' - } : { - low: opposite ? 'right' : 'left', - middle: 'center', - high: opposite ? 'left' : 'right' - })[axisTitleOptions.align]; - } - axis.axisTitle = renderer.text( - axisTitleOptions.text, - 0, - 0, - axisTitleOptions.useHTML - ) - .attr({ - zIndex: 7, - rotation: axisTitleOptions.rotation || 0, - align: textAlign - }) - .addClass(PREFIX + this.coll.toLowerCase() + '-title') - .css(axisTitleOptions.style) - .add(axis.axisGroup); - axis.axisTitle.isNew = true; - } - - if (showAxis) { - titleOffset = axis.axisTitle.getBBox()[horiz ? 'height' : 'width']; - titleOffsetOption = axisTitleOptions.offset; - titleMargin = defined(titleOffsetOption) ? 0 : pick(axisTitleOptions.margin, horiz ? 5 : 10); - } - - // hide or show the title depending on whether showEmpty is set - axis.axisTitle[showAxis ? 'show' : 'hide'](true); - } - - // handle automatic or user set offset - axis.offset = directionFactor * pick(options.offset, axisOffset[side]); - - axis.tickRotCorr = axis.tickRotCorr || { x: 0, y: 0 }; // polar - if (side === 0) { - lineHeightCorrection = -axis.labelMetrics().h; - } else if (side === 2) { - lineHeightCorrection = axis.tickRotCorr.y; - } else { - lineHeightCorrection = 0; - } - - // Find the padded label offset - labelOffsetPadded = Math.abs(labelOffset) + titleMargin; - if (labelOffset) { - labelOffsetPadded -= lineHeightCorrection; - labelOffsetPadded += directionFactor * (horiz ? pick(labelOptions.y, axis.tickRotCorr.y + directionFactor * 8) : labelOptions.x); - } - axis.axisTitleMargin = pick(titleOffsetOption, labelOffsetPadded); - - axisOffset[side] = mathMax( - axisOffset[side], - axis.axisTitleMargin + titleOffset + directionFactor * axis.offset, - labelOffsetPadded, // #3027 - hasData && tickPositions.length && tickSize ? tickSize[0] : 0 // #4866 - ); - - // Decide the clipping needed to keep the graph inside the plot area and axis lines - clip = options.offset ? 0 : mathFloor(options.lineWidth / 2) * 2; // #4308, #4371 - clipOffset[invertedSide] = mathMax(clipOffset[invertedSide], clip); - }, - - /** - * Get the path for the axis line - */ - getLinePath: function (lineWidth) { - var chart = this.chart, - opposite = this.opposite, - offset = this.offset, - horiz = this.horiz, - lineLeft = this.left + (opposite ? this.width : 0) + offset, - lineTop = chart.chartHeight - this.bottom - (opposite ? this.height : 0) + offset; - - if (opposite) { - lineWidth *= -1; // crispify the other way - #1480, #1687 - } - - return chart.renderer - .crispLine([ - M, - horiz ? - this.left : - lineLeft, - horiz ? - lineTop : - this.top, - L, - horiz ? - chart.chartWidth - this.right : - lineLeft, - horiz ? - lineTop : - chart.chartHeight - this.bottom - ], lineWidth); - }, - - /** - * Position the title - */ - getTitlePosition: function () { - // compute anchor points for each of the title align options - var horiz = this.horiz, - axisLeft = this.left, - axisTop = this.top, - axisLength = this.len, - axisTitleOptions = this.options.title, - margin = horiz ? axisLeft : axisTop, - opposite = this.opposite, - offset = this.offset, - xOption = axisTitleOptions.x || 0, - yOption = axisTitleOptions.y || 0, - fontSize = pInt(axisTitleOptions.style.fontSize || 12), - - // the position in the length direction of the axis - alongAxis = { - low: margin + (horiz ? 0 : axisLength), - middle: margin + axisLength / 2, - high: margin + (horiz ? axisLength : 0) - }[axisTitleOptions.align], - - // the position in the perpendicular direction of the axis - offAxis = (horiz ? axisTop + this.height : axisLeft) + - (horiz ? 1 : -1) * // horizontal axis reverses the margin - (opposite ? -1 : 1) * // so does opposite axes - this.axisTitleMargin + - (this.side === 2 ? fontSize : 0); - - return { - x: horiz ? - alongAxis + xOption : - offAxis + (opposite ? this.width : 0) + offset + xOption, - y: horiz ? - offAxis + yOption - (opposite ? this.height : 0) + offset : - alongAxis + yOption - }; - }, - - /** - * Render the axis - */ - render: function () { - var axis = this, - chart = axis.chart, - renderer = chart.renderer, - options = axis.options, - isLog = axis.isLog, - lin2log = axis.lin2log, - isLinked = axis.isLinked, - tickPositions = axis.tickPositions, - axisTitle = axis.axisTitle, - ticks = axis.ticks, - minorTicks = axis.minorTicks, - alternateBands = axis.alternateBands, - stackLabelOptions = options.stackLabels, - alternateGridColor = options.alternateGridColor, - tickmarkOffset = axis.tickmarkOffset, - lineWidth = options.lineWidth, - linePath, - hasRendered = chart.hasRendered, - slideInTicks = hasRendered && isNumber(axis.oldMin), - showAxis = axis.showAxis, - animation = animObject(renderer.globalAnimation), - from, - to; - - // Reset - axis.labelEdge.length = 0; - //axis.justifyToPlot = overflow === 'justify'; - axis.overlap = false; - - // Mark all elements inActive before we go over and mark the active ones - each([ticks, minorTicks, alternateBands], function (coll) { - var pos; - for (pos in coll) { - coll[pos].isActive = false; - } - }); - - // If the series has data draw the ticks. Else only the line and title - if (axis.hasData() || isLinked) { - - // minor ticks - if (axis.minorTickInterval && !axis.categories) { - each(axis.getMinorTickPositions(), function (pos) { - if (!minorTicks[pos]) { - minorTicks[pos] = new Tick(axis, pos, 'minor'); - } - - // render new ticks in old position - if (slideInTicks && minorTicks[pos].isNew) { - minorTicks[pos].render(null, true); - } - - minorTicks[pos].render(null, false, 1); - }); - } - - // Major ticks. Pull out the first item and render it last so that - // we can get the position of the neighbour label. #808. - if (tickPositions.length) { // #1300 - each(tickPositions, function (pos, i) { - - // linked axes need an extra check to find out if - if (!isLinked || (pos >= axis.min && pos <= axis.max)) { - - if (!ticks[pos]) { - ticks[pos] = new Tick(axis, pos); - } - - // render new ticks in old position - if (slideInTicks && ticks[pos].isNew) { - ticks[pos].render(i, true, 0.1); - } - - ticks[pos].render(i); - } - - }); - // In a categorized axis, the tick marks are displayed between labels. So - // we need to add a tick mark and grid line at the left edge of the X axis. - if (tickmarkOffset && (axis.min === 0 || axis.single)) { - if (!ticks[-1]) { - ticks[-1] = new Tick(axis, -1, null, true); - } - ticks[-1].render(-1); - } - - } - - // alternate grid color - if (alternateGridColor) { - each(tickPositions, function (pos, i) { - to = tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] + tickmarkOffset : axis.max - tickmarkOffset; - if (i % 2 === 0 && pos < axis.max && to <= axis.max + (chart.polar ? -tickmarkOffset : tickmarkOffset)) { // #2248, #4660 - if (!alternateBands[pos]) { - alternateBands[pos] = new Highcharts.PlotLineOrBand(axis); - } - from = pos + tickmarkOffset; // #949 - alternateBands[pos].options = { - from: isLog ? lin2log(from) : from, - to: isLog ? lin2log(to) : to, - color: alternateGridColor - }; - alternateBands[pos].render(); - alternateBands[pos].isActive = true; - } - }); - } - - // custom plot lines and bands - if (!axis._addedPlotLB) { // only first time - each((options.plotLines || []).concat(options.plotBands || []), function (plotLineOptions) { - axis.addPlotBandOrLine(plotLineOptions); - }); - axis._addedPlotLB = true; - } - - } // end if hasData - - // Remove inactive ticks - each([ticks, minorTicks, alternateBands], function (coll) { - var pos, - i, - forDestruction = [], - delay = animation.duration, - destroyInactiveItems = function () { - i = forDestruction.length; - while (i--) { - // When resizing rapidly, the same items may be destroyed in different timeouts, - // or the may be reactivated - if (coll[forDestruction[i]] && !coll[forDestruction[i]].isActive) { - coll[forDestruction[i]].destroy(); - delete coll[forDestruction[i]]; - } - } - - }; - - for (pos in coll) { - - if (!coll[pos].isActive) { - // Render to zero opacity - coll[pos].render(pos, false, 0); - coll[pos].isActive = false; - forDestruction.push(pos); - } - } - - // When the objects are finished fading out, destroy them - syncTimeout( - destroyInactiveItems, - coll === alternateBands || !chart.hasRendered || !delay ? 0 : delay - ); - }); - - // Static items. As the axis group is cleared on subsequent calls - // to render, these items are added outside the group. - // axis line - if (lineWidth) { - linePath = axis.getLinePath(lineWidth); - if (!axis.axisLine) { - axis.axisLine = renderer.path(linePath) - .attr({ - stroke: options.lineColor, - 'stroke-width': lineWidth, - zIndex: 7 - }) - .add(axis.axisGroup); - } else { - axis.axisLine.animate({ d: linePath }); - } - - // show or hide the line depending on options.showEmpty - axis.axisLine[showAxis ? 'show' : 'hide'](true); - } - - if (axisTitle && showAxis) { - - axisTitle[axisTitle.isNew ? 'attr' : 'animate']( - axis.getTitlePosition() - ); - axisTitle.isNew = false; - } - - // Stacked totals: - if (stackLabelOptions && stackLabelOptions.enabled) { - axis.renderStackTotals(); - } - // End stacked totals - - axis.isDirty = false; - }, - - /** - * Redraw the axis to reflect changes in the data or axis extremes - */ - redraw: function () { - - if (this.visible) { - // render the axis - this.render(); - - // move plot lines and bands - each(this.plotLinesAndBands, function (plotLine) { - plotLine.render(); - }); - } - - // mark associated series as dirty and ready for redraw - each(this.series, function (series) { - series.isDirty = true; - }); - - }, - - /** - * Destroys an Axis instance. - */ - destroy: function (keepEvents) { - var axis = this, - stacks = axis.stacks, - stackKey, - plotLinesAndBands = axis.plotLinesAndBands, - i; - - // Remove the events - if (!keepEvents) { - removeEvent(axis); - } - - // Destroy each stack total - for (stackKey in stacks) { - destroyObjectProperties(stacks[stackKey]); - - stacks[stackKey] = null; - } - - // Destroy collections - each([axis.ticks, axis.minorTicks, axis.alternateBands], function (coll) { - destroyObjectProperties(coll); - }); - i = plotLinesAndBands.length; - while (i--) { // #1975 - plotLinesAndBands[i].destroy(); - } - - // Destroy local variables - each(['stackTotalGroup', 'axisLine', 'axisTitle', 'axisGroup', 'cross', 'gridGroup', 'labelGroup'], function (prop) { - if (axis[prop]) { - axis[prop] = axis[prop].destroy(); - } - }); - - // Destroy crosshair - if (this.cross) { - this.cross.destroy(); - } - }, - - /** - * Draw the crosshair - * - * @param {Object} e The event arguments from the modified pointer event - * @param {Object} point The Point object - */ - drawCrosshair: function (e, point) { - - var path, - options = this.crosshair, - pos, - attribs, - categorized, - strokeWidth; - - if ( - // Disabled in options - !this.crosshair || - // Snap - ((defined(point) || !pick(options.snap, true)) === false) - ) { - this.hideCrosshair(); - - } else { - - // Get the path - if (!pick(options.snap, true)) { - pos = (this.horiz ? e.chartX - this.pos : this.len - e.chartY + this.pos); - } else if (defined(point)) { - pos = this.isXAxis ? point.plotX : this.len - point.plotY; // #3834 - } - - if (this.isRadial) { - path = this.getPlotLinePath(this.isXAxis ? point.x : pick(point.stackY, point.y)) || null; // #3189 - } else { - path = this.getPlotLinePath(null, null, null, null, pos) || null; // #3189 - } - - if (path === null) { - this.hideCrosshair(); - return; - } - - categorized = this.categories && !this.isRadial; - strokeWidth = pick(options.width, (categorized ? this.transA : 1)); - - // Draw the cross - if (this.cross) { - this.cross - .attr({ - d: path, - visibility: 'visible', - 'stroke-width': strokeWidth // #4737 - }); - } else { - attribs = { - 'pointer-events': 'none', // #5259 - 'stroke-width': strokeWidth, - stroke: options.color || (categorized ? 'rgba(155,200,255,0.2)' : '#C0C0C0'), - zIndex: pick(options.zIndex, 2) - }; - if (options.dashStyle) { - attribs.dashstyle = options.dashStyle; - } - this.cross = this.chart.renderer.path(path).attr(attribs).add(); - } - - } - - }, - - /** - * Hide the crosshair. - */ - hideCrosshair: function () { - if (this.cross) { - this.cross.hide(); - } - } - }; // end Axis - - extend(Axis.prototype, AxisPlotLineOrBandExtension); - - /** - * Set the tick positions to a time unit that makes sense, for example - * on the first of each month or on every Monday. Return an array - * with the time positions. Used in datetime axes as well as for grouping - * data on a datetime axis. - * - * @param {Object} normalizedInterval The interval in axis values (ms) and the count - * @param {Number} min The minimum in axis values - * @param {Number} max The maximum in axis values - * @param {Number} startOfWeek - */ - Axis.prototype.getTimeTicks = function (normalizedInterval, min, max, startOfWeek) { - var tickPositions = [], - i, - higherRanks = {}, - useUTC = defaultOptions.global.useUTC, - minYear, // used in months and years as a basis for Date.UTC() - minDate = new Date(min - getTZOffset(min)), - interval = normalizedInterval.unitRange, - count = normalizedInterval.count; - - if (defined(min)) { // #1300 - minDate[setMilliseconds](interval >= timeUnits.second ? 0 : // #3935 - count * mathFloor(minDate.getMilliseconds() / count)); // #3652, #3654 - - if (interval >= timeUnits.second) { // second - minDate[setSeconds](interval >= timeUnits.minute ? 0 : // #3935 - count * mathFloor(minDate.getSeconds() / count)); - } - - if (interval >= timeUnits.minute) { // minute - minDate[setMinutes](interval >= timeUnits.hour ? 0 : - count * mathFloor(minDate[getMinutes]() / count)); - } - - if (interval >= timeUnits.hour) { // hour - minDate[setHours](interval >= timeUnits.day ? 0 : - count * mathFloor(minDate[getHours]() / count)); - } - - if (interval >= timeUnits.day) { // day - minDate[setDate](interval >= timeUnits.month ? 1 : - count * mathFloor(minDate[getDate]() / count)); - } - - if (interval >= timeUnits.month) { // month - minDate[setMonth](interval >= timeUnits.year ? 0 : - count * mathFloor(minDate[getMonth]() / count)); - minYear = minDate[getFullYear](); - } - - if (interval >= timeUnits.year) { // year - minYear -= minYear % count; - minDate[setFullYear](minYear); - } - - // week is a special case that runs outside the hierarchy - if (interval === timeUnits.week) { - // get start of current week, independent of count - minDate[setDate](minDate[getDate]() - minDate[getDay]() + - pick(startOfWeek, 1)); - } - - - // get tick positions - i = 1; - if (timezoneOffset || getTimezoneOffset) { - minDate = minDate.getTime(); - minDate = new Date(minDate + getTZOffset(minDate)); - } - minYear = minDate[getFullYear](); - var time = minDate.getTime(), - minMonth = minDate[getMonth](), - minDateDate = minDate[getDate](), - variableDayLength = !useUTC || !!getTimezoneOffset, // #4951 - localTimezoneOffset = (timeUnits.day + - (useUTC ? getTZOffset(minDate) : minDate.getTimezoneOffset() * 60 * 1000) - ) % timeUnits.day; // #950, #3359 - - // iterate and add tick positions at appropriate values - while (time < max) { - tickPositions.push(time); - - // if the interval is years, use Date.UTC to increase years - if (interval === timeUnits.year) { - time = makeTime(minYear + i * count, 0); - - // if the interval is months, use Date.UTC to increase months - } else if (interval === timeUnits.month) { - time = makeTime(minYear, minMonth + i * count); - - // if we're using global time, the interval is not fixed as it jumps - // one hour at the DST crossover - } else if (variableDayLength && (interval === timeUnits.day || interval === timeUnits.week)) { - time = makeTime(minYear, minMonth, minDateDate + - i * count * (interval === timeUnits.day ? 1 : 7)); - - // else, the interval is fixed and we use simple addition - } else { - time += interval * count; - } - - i++; - } - - // push the last time - tickPositions.push(time); - - - // mark new days if the time is dividible by day (#1649, #1760) - each(grep(tickPositions, function (time) { - return interval <= timeUnits.hour && time % timeUnits.day === localTimezoneOffset; - }), function (time) { - higherRanks[time] = 'day'; - }); - } - - - // record information on the chosen unit - for dynamic label formatter - tickPositions.info = extend(normalizedInterval, { - higherRanks: higherRanks, - totalRange: interval * count - }); - - return tickPositions; - }; - - /** - * Get a normalized tick interval for dates. Returns a configuration object with - * unit range (interval), count and name. Used to prepare data for getTimeTicks. - * Previously this logic was part of getTimeTicks, but as getTimeTicks now runs - * of segments in stock charts, the normalizing logic was extracted in order to - * prevent it for running over again for each segment having the same interval. - * #662, #697. - */ - Axis.prototype.normalizeTimeTickInterval = function (tickInterval, unitsOption) { - var units = unitsOption || [[ - 'millisecond', // unit name - [1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples - ], [ - 'second', - [1, 2, 5, 10, 15, 30] - ], [ - 'minute', - [1, 2, 5, 10, 15, 30] - ], [ - 'hour', - [1, 2, 3, 4, 6, 8, 12] - ], [ - 'day', - [1, 2] - ], [ - 'week', - [1, 2] - ], [ - 'month', - [1, 2, 3, 4, 6] - ], [ - 'year', - null - ]], - unit = units[units.length - 1], // default unit is years - interval = timeUnits[unit[0]], - multiples = unit[1], - count, - i; - - // loop through the units to find the one that best fits the tickInterval - for (i = 0; i < units.length; i++) { - unit = units[i]; - interval = timeUnits[unit[0]]; - multiples = unit[1]; - - - if (units[i + 1]) { - // lessThan is in the middle between the highest multiple and the next unit. - var lessThan = (interval * multiples[multiples.length - 1] + - timeUnits[units[i + 1][0]]) / 2; - - // break and keep the current unit - if (tickInterval <= lessThan) { - break; - } - } - } - - // prevent 2.5 years intervals, though 25, 250 etc. are allowed - if (interval === timeUnits.year && tickInterval < 5 * interval) { - multiples = [1, 2, 5]; - } - - // get the count - count = normalizeTickInterval( - tickInterval / interval, - multiples, - unit[0] === 'year' ? mathMax(getMagnitude(tickInterval / interval), 1) : 1 // #1913, #2360 - ); - - return { - unitRange: interval, - count: count, - unitName: unit[0] - }; - }; - /** - * Methods defined on the Axis prototype - */ - - /** - * Set the tick positions of a logarithmic axis - */ - Axis.prototype.getLogTickPositions = function (interval, min, max, minor) { - var axis = this, - options = axis.options, - axisLength = axis.len, - lin2log = axis.lin2log, - log2lin = axis.log2lin, - // Since we use this method for both major and minor ticks, - // use a local variable and return the result - positions = []; - - // Reset - if (!minor) { - axis._minorAutoInterval = null; - } - - // First case: All ticks fall on whole logarithms: 1, 10, 100 etc. - if (interval >= 0.5) { - interval = mathRound(interval); - positions = axis.getLinearTickPositions(interval, min, max); - - // Second case: We need intermediary ticks. For example - // 1, 2, 4, 6, 8, 10, 20, 40 etc. - } else if (interval >= 0.08) { - var roundedMin = mathFloor(min), - intermediate, - i, - j, - len, - pos, - lastPos, - break2; - - if (interval > 0.3) { - intermediate = [1, 2, 4]; - } else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc - intermediate = [1, 2, 4, 6, 8]; - } else { // 0.1 equals ten minor ticks per 1, 10, 100 etc - intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9]; - } - - for (i = roundedMin; i < max + 1 && !break2; i++) { - len = intermediate.length; - for (j = 0; j < len && !break2; j++) { - pos = log2lin(lin2log(i) * intermediate[j]); - if (pos > min && (!minor || lastPos <= max) && lastPos !== UNDEFINED) { // #1670, lastPos is #3113 - positions.push(lastPos); - } - - if (lastPos > max) { - break2 = true; - } - lastPos = pos; - } - } - - // Third case: We are so deep in between whole logarithmic values that - // we might as well handle the tick positions like a linear axis. For - // example 1.01, 1.02, 1.03, 1.04. - } else { - var realMin = lin2log(min), - realMax = lin2log(max), - tickIntervalOption = options[minor ? 'minorTickInterval' : 'tickInterval'], - filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption, - tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1), - totalPixelLength = minor ? axisLength / axis.tickPositions.length : axisLength; - - interval = pick( - filteredTickIntervalOption, - axis._minorAutoInterval, - (realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1) - ); - - interval = normalizeTickInterval( - interval, - null, - getMagnitude(interval) - ); - - positions = map(axis.getLinearTickPositions( - interval, - realMin, - realMax - ), log2lin); - - if (!minor) { - axis._minorAutoInterval = interval / 5; - } - } - - // Set the axis-level tickInterval variable - if (!minor) { - axis.tickInterval = interval; - } - return positions; - }; - - Axis.prototype.log2lin = function (num) { - return math.log(num) / math.LN10; - }; - - Axis.prototype.lin2log = function (num) { - return math.pow(10, num); - }; - /** - * The tooltip object - * @param {Object} chart The chart instance - * @param {Object} options Tooltip options - */ - var Tooltip = Highcharts.Tooltip = function () { - this.init.apply(this, arguments); - }; - - Tooltip.prototype = { - - init: function (chart, options) { - - var borderWidth = options.borderWidth, - style = options.style, - padding = pInt(style.padding); - - // Save the chart and options - this.chart = chart; - this.options = options; - - // Keep track of the current series - //this.currentSeries = UNDEFINED; - - // List of crosshairs - this.crosshairs = []; - - // Current values of x and y when animating - this.now = { x: 0, y: 0 }; - - // The tooltip is initially hidden - this.isHidden = true; - - - // create the label - this.label = chart.renderer.label('', 0, 0, options.shape || 'callout', null, null, options.useHTML, null, 'tooltip') - .attr({ - padding: padding, - fill: options.backgroundColor, - 'stroke-width': borderWidth, - r: options.borderRadius, - zIndex: 8 - }) - .css(style) - .css({ padding: 0 }) // Remove it from VML, the padding is applied as an attribute instead (#1117) - .add() - .attr({ y: -9999 }); // #2301, #2657 - - // When using canVG the shadow shows up as a gray circle - // even if the tooltip is hidden. - if (!useCanVG) { - this.label.shadow(options.shadow); - } - - // Public property for getting the shared state. - this.shared = options.shared; - }, - - /** - * Destroy the tooltip and its elements. - */ - destroy: function () { - // Destroy and clear local variables - if (this.label) { - this.label = this.label.destroy(); - } - clearTimeout(this.hideTimer); - clearTimeout(this.tooltipTimeout); - }, - - /** - * Provide a soft movement for the tooltip - * - * @param {Number} x - * @param {Number} y - * @private - */ - move: function (x, y, anchorX, anchorY) { - var tooltip = this, - now = tooltip.now, - animate = tooltip.options.animation !== false && !tooltip.isHidden && - // When we get close to the target position, abort animation and land on the right place (#3056) - (mathAbs(x - now.x) > 1 || mathAbs(y - now.y) > 1), - skipAnchor = tooltip.followPointer || tooltip.len > 1; - - // Get intermediate values for animation - extend(now, { - x: animate ? (2 * now.x + x) / 3 : x, - y: animate ? (now.y + y) / 2 : y, - anchorX: skipAnchor ? UNDEFINED : animate ? (2 * now.anchorX + anchorX) / 3 : anchorX, - anchorY: skipAnchor ? UNDEFINED : animate ? (now.anchorY + anchorY) / 2 : anchorY - }); - - // Move to the intermediate value - tooltip.label.attr(now); - - - // Run on next tick of the mouse tracker - if (animate) { - - // Never allow two timeouts - clearTimeout(this.tooltipTimeout); - - // Set the fixed interval ticking for the smooth tooltip - this.tooltipTimeout = setTimeout(function () { - // The interval function may still be running during destroy, so check that the chart is really there before calling. - if (tooltip) { - tooltip.move(x, y, anchorX, anchorY); - } - }, 32); - - } - }, - - /** - * Hide the tooltip - */ - hide: function (delay) { - var tooltip = this; - clearTimeout(this.hideTimer); // disallow duplicate timers (#1728, #1766) - delay = pick(delay, this.options.hideDelay, 500); - if (!this.isHidden) { - this.hideTimer = syncTimeout(function () { - tooltip.label[delay ? 'fadeOut' : 'hide'](); - tooltip.isHidden = true; - }, delay); - } - }, - - /** - * Extendable method to get the anchor position of the tooltip - * from a point or set of points - */ - getAnchor: function (points, mouseEvent) { - var ret, - chart = this.chart, - inverted = chart.inverted, - plotTop = chart.plotTop, - plotLeft = chart.plotLeft, - plotX = 0, - plotY = 0, - yAxis, - xAxis; - - points = splat(points); - - // Pie uses a special tooltipPos - ret = points[0].tooltipPos; - - // When tooltip follows mouse, relate the position to the mouse - if (this.followPointer && mouseEvent) { - if (mouseEvent.chartX === UNDEFINED) { - mouseEvent = chart.pointer.normalize(mouseEvent); - } - ret = [ - mouseEvent.chartX - chart.plotLeft, - mouseEvent.chartY - plotTop - ]; - } - // When shared, use the average position - if (!ret) { - each(points, function (point) { - yAxis = point.series.yAxis; - xAxis = point.series.xAxis; - plotX += point.plotX + (!inverted && xAxis ? xAxis.left - plotLeft : 0); - plotY += (point.plotLow ? (point.plotLow + point.plotHigh) / 2 : point.plotY) + - (!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151 - }); - - plotX /= points.length; - plotY /= points.length; - - ret = [ - inverted ? chart.plotWidth - plotY : plotX, - this.shared && !inverted && points.length > 1 && mouseEvent ? - mouseEvent.chartY - plotTop : // place shared tooltip next to the mouse (#424) - inverted ? chart.plotHeight - plotX : plotY - ]; - } - - return map(ret, mathRound); - }, - - /** - * Place the tooltip in a chart without spilling over - * and not covering the point it self. - */ - getPosition: function (boxWidth, boxHeight, point) { - - var chart = this.chart, - distance = this.distance, - ret = {}, - h = point.h || 0, // #4117 - swapped, - first = ['y', chart.chartHeight, boxHeight, point.plotY + chart.plotTop, chart.plotTop, chart.plotTop + chart.plotHeight], - second = ['x', chart.chartWidth, boxWidth, point.plotX + chart.plotLeft, chart.plotLeft, chart.plotLeft + chart.plotWidth], - // The far side is right or bottom - preferFarSide = !this.followPointer && pick(point.ttBelow, !chart.inverted === !!point.negative), // #4984 - /** - * Handle the preferred dimension. When the preferred dimension is tooltip - * on top or bottom of the point, it will look for space there. - */ - firstDimension = function (dim, outerSize, innerSize, point, min, max) { - var roomLeft = innerSize < point - distance, - roomRight = point + distance + innerSize < outerSize, - alignedLeft = point - distance - innerSize, - alignedRight = point + distance; - - if (preferFarSide && roomRight) { - ret[dim] = alignedRight; - } else if (!preferFarSide && roomLeft) { - ret[dim] = alignedLeft; - } else if (roomLeft) { - ret[dim] = mathMin(max - innerSize, alignedLeft - h < 0 ? alignedLeft : alignedLeft - h); - } else if (roomRight) { - ret[dim] = mathMax(min, alignedRight + h + innerSize > outerSize ? alignedRight : alignedRight + h); - } else { - return false; - } - }, - /** - * Handle the secondary dimension. If the preferred dimension is tooltip - * on top or bottom of the point, the second dimension is to align the tooltip - * above the point, trying to align center but allowing left or right - * align within the chart box. - */ - secondDimension = function (dim, outerSize, innerSize, point) { - var retVal; - - // Too close to the edge, return false and swap dimensions - if (point < distance || point > outerSize - distance) { - retVal = false; - // Align left/top - } else if (point < innerSize / 2) { - ret[dim] = 1; - // Align right/bottom - } else if (point > outerSize - innerSize / 2) { - ret[dim] = outerSize - innerSize - 2; - // Align center - } else { - ret[dim] = point - innerSize / 2; - } - return retVal; - }, - /** - * Swap the dimensions - */ - swap = function (count) { - var temp = first; - first = second; - second = temp; - swapped = count; - }, - run = function () { - if (firstDimension.apply(0, first) !== false) { - if (secondDimension.apply(0, second) === false && !swapped) { - swap(true); - run(); - } - } else if (!swapped) { - swap(true); - run(); - } else { - ret.x = ret.y = 0; - } - }; - - // Under these conditions, prefer the tooltip on the side of the point - if (chart.inverted || this.len > 1) { - swap(); - } - run(); - - return ret; - - }, - - /** - * In case no user defined formatter is given, this will be used. Note that the context - * here is an object holding point, series, x, y etc. - */ - defaultFormatter: function (tooltip) { - var items = this.points || splat(this), - s; - - // build the header - s = [tooltip.tooltipFooterHeaderFormatter(items[0])]; //#3397: abstraction to enable formatting of footer and header - - // build the values - s = s.concat(tooltip.bodyFormatter(items)); - - // footer - s.push(tooltip.tooltipFooterHeaderFormatter(items[0], true)); //#3397: abstraction to enable formatting of footer and header - - return s.join(''); - }, - - /** - * Refresh the tooltip's text and position. - * @param {Object} point - */ - refresh: function (point, mouseEvent) { - var tooltip = this, - chart = tooltip.chart, - label = tooltip.label, - options = tooltip.options, - x, - y, - anchor, - textConfig = {}, - text, - pointConfig = [], - formatter = options.formatter || tooltip.defaultFormatter, - hoverPoints = chart.hoverPoints, - borderColor, - shared = tooltip.shared, - currentSeries; - - clearTimeout(this.hideTimer); - - // get the reference point coordinates (pie charts use tooltipPos) - tooltip.followPointer = splat(point)[0].series.tooltipOptions.followPointer; - anchor = tooltip.getAnchor(point, mouseEvent); - x = anchor[0]; - y = anchor[1]; - - // shared tooltip, array is sent over - if (shared && !(point.series && point.series.noSharedTooltip)) { - - // hide previous hoverPoints and set new - - chart.hoverPoints = point; - if (hoverPoints) { - each(hoverPoints, function (point) { - point.setState(); - }); - } - - each(point, function (item) { - item.setState(HOVER_STATE); - - pointConfig.push(item.getLabelConfig()); - }); - - textConfig = { - x: point[0].category, - y: point[0].y - }; - textConfig.points = pointConfig; - this.len = pointConfig.length; - point = point[0]; - - // single point tooltip - } else { - textConfig = point.getLabelConfig(); - } - text = formatter.call(textConfig, tooltip); - - // register the current series - currentSeries = point.series; - this.distance = pick(currentSeries.tooltipOptions.distance, 16); - - // update the inner HTML - if (text === false) { - this.hide(); - } else { - - // show it - if (tooltip.isHidden) { - stop(label); - label.attr('opacity', 1).show(); - } - - // update text - label.attr({ - text: text - }); - - // set the stroke color of the box - borderColor = options.borderColor || point.color || currentSeries.color || '#606060'; - label.attr({ - stroke: borderColor - }); - tooltip.updatePosition({ - plotX: x, - plotY: y, - negative: point.negative, - ttBelow: point.ttBelow, - h: anchor[2] || 0 - }); - - this.isHidden = false; - } - fireEvent(chart, 'tooltipRefresh', { - text: text, - x: x + chart.plotLeft, - y: y + chart.plotTop, - borderColor: borderColor - }); - }, - - /** - * Find the new position and perform the move - */ - updatePosition: function (point) { - var chart = this.chart, - label = this.label, - pos = (this.options.positioner || this.getPosition).call( - this, - label.width, - label.height, - point - ); - - // do the move - this.move( - mathRound(pos.x), - mathRound(pos.y || 0), // can be undefined (#3977) - point.plotX + chart.plotLeft, - point.plotY + chart.plotTop - ); - }, - - /** - * Get the best X date format based on the closest point range on the axis. - */ - getXDateFormat: function (point, options, xAxis) { - var xDateFormat, - dateTimeLabelFormats = options.dateTimeLabelFormats, - closestPointRange = xAxis && xAxis.closestPointRange, - n, - blank = '01-01 00:00:00.000', - strpos = { - millisecond: 15, - second: 12, - minute: 9, - hour: 6, - day: 3 - }, - date, - lastN = 'millisecond'; // for sub-millisecond data, #4223 - - if (closestPointRange) { - date = dateFormat('%m-%d %H:%M:%S.%L', point.x); - for (n in timeUnits) { - - // If the range is exactly one week and we're looking at a Sunday/Monday, go for the week format - if (closestPointRange === timeUnits.week && +dateFormat('%w', point.x) === xAxis.options.startOfWeek && - date.substr(6) === blank.substr(6)) { - n = 'week'; - break; - } - - // The first format that is too great for the range - if (timeUnits[n] > closestPointRange) { - n = lastN; - break; - } - - // If the point is placed every day at 23:59, we need to show - // the minutes as well. #2637. - if (strpos[n] && date.substr(strpos[n]) !== blank.substr(strpos[n])) { - break; - } - - // Weeks are outside the hierarchy, only apply them on Mondays/Sundays like in the first condition - if (n !== 'week') { - lastN = n; - } - } - - if (n) { - xDateFormat = dateTimeLabelFormats[n]; - } - } else { - xDateFormat = dateTimeLabelFormats.day; - } - - return xDateFormat || dateTimeLabelFormats.year; // #2546, 2581 - }, - - /** - * Format the footer/header of the tooltip - * #3397: abstraction to enable formatting of footer and header - */ - tooltipFooterHeaderFormatter: function (point, isFooter) { - var footOrHead = isFooter ? 'footer' : 'header', - series = point.series, - tooltipOptions = series.tooltipOptions, - xDateFormat = tooltipOptions.xDateFormat, - xAxis = series.xAxis, - isDateTime = xAxis && xAxis.options.type === 'datetime' && isNumber(point.key), - formatString = tooltipOptions[footOrHead + 'Format']; - - // Guess the best date format based on the closest point distance (#568, #3418) - if (isDateTime && !xDateFormat) { - xDateFormat = this.getXDateFormat(point, tooltipOptions, xAxis); - } - - // Insert the footer date format if any - if (isDateTime && xDateFormat) { - formatString = formatString.replace('{point.key}', '{point.key:' + xDateFormat + '}'); - } - - return format(formatString, { - point: point, - series: series - }); - }, - - /** - * Build the body (lines) of the tooltip by iterating over the items and returning one entry for each item, - * abstracting this functionality allows to easily overwrite and extend it. - */ - bodyFormatter: function (items) { - return map(items, function (item) { - var tooltipOptions = item.series.tooltipOptions; - return (tooltipOptions.pointFormatter || item.point.tooltipFormatter).call(item.point, tooltipOptions.pointFormat); - }); - } - - }; - - var hoverChartIndex; - - // Global flag for touch support - hasTouch = doc && doc.documentElement.ontouchstart !== UNDEFINED; - - /** - * The mouse tracker object. All methods starting with "on" are primary DOM event handlers. - * Subsequent methods should be named differently from what they are doing. - * @param {Object} chart The Chart instance - * @param {Object} options The root options object - */ - var Pointer = Highcharts.Pointer = function (chart, options) { - this.init(chart, options); - }; - - Pointer.prototype = { - /** - * Initialize Pointer - */ - init: function (chart, options) { - - var chartOptions = options.chart, - chartEvents = chartOptions.events, - zoomType = useCanVG ? '' : chartOptions.zoomType, - inverted = chart.inverted, - zoomX, - zoomY; - - // Store references - this.options = options; - this.chart = chart; - - // Zoom status - this.zoomX = zoomX = /x/.test(zoomType); - this.zoomY = zoomY = /y/.test(zoomType); - this.zoomHor = (zoomX && !inverted) || (zoomY && inverted); - this.zoomVert = (zoomY && !inverted) || (zoomX && inverted); - this.hasZoom = zoomX || zoomY; - - // Do we need to handle click on a touch device? - this.runChartClick = chartEvents && !!chartEvents.click; - - this.pinchDown = []; - this.lastValidTouch = {}; - - if (Highcharts.Tooltip && options.tooltip.enabled) { - chart.tooltip = new Tooltip(chart, options.tooltip); - this.followTouchMove = pick(options.tooltip.followTouchMove, true); - } - - this.setDOMEvents(); - }, - - /** - * Add crossbrowser support for chartX and chartY - * @param {Object} e The event object in standard browsers - */ - normalize: function (e, chartPosition) { - var chartX, - chartY, - ePos; - - // IE normalizing - e = e || win.event; - if (!e.target) { - e.target = e.srcElement; - } - - // iOS (#2757) - ePos = e.touches ? (e.touches.length ? e.touches.item(0) : e.changedTouches[0]) : e; - - // Get mouse position - if (!chartPosition) { - this.chartPosition = chartPosition = offset(this.chart.container); - } - - // chartX and chartY - if (ePos.pageX === UNDEFINED) { // IE < 9. #886. - chartX = mathMax(e.x, e.clientX - chartPosition.left); // #2005, #2129: the second case is - // for IE10 quirks mode within framesets - chartY = e.y; - } else { - chartX = ePos.pageX - chartPosition.left; - chartY = ePos.pageY - chartPosition.top; - } - - return extend(e, { - chartX: mathRound(chartX), - chartY: mathRound(chartY) - }); - }, - - /** - * Get the click position in terms of axis values. - * - * @param {Object} e A pointer event - */ - getCoordinates: function (e) { - var coordinates = { - xAxis: [], - yAxis: [] - }; - - each(this.chart.axes, function (axis) { - coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({ - axis: axis, - value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY']) - }); - }); - return coordinates; - }, - - /** - * With line type charts with a single tracker, get the point closest to the mouse. - * Run Point.onMouseOver and display tooltip for the point or points. - */ - runPointActions: function (e) { - - var pointer = this, - chart = pointer.chart, - series = chart.series, - tooltip = chart.tooltip, - shared = tooltip ? tooltip.shared : false, - followPointer, - hoverPoint = chart.hoverPoint, - hoverSeries = chart.hoverSeries, - i, - distance = [Number.MAX_VALUE, Number.MAX_VALUE], // #4511 - anchor, - noSharedTooltip, - stickToHoverSeries, - directTouch, - kdpoints = [], - kdpoint = [], - kdpointT; - - // For hovering over the empty parts of the plot area (hoverSeries is undefined). - // If there is one series with point tracking (combo chart), don't go to nearest neighbour. - if (!shared && !hoverSeries) { - for (i = 0; i < series.length; i++) { - if (series[i].directTouch || !series[i].options.stickyTracking) { - series = []; - } - } - } - - // If it has a hoverPoint and that series requires direct touch (like columns, #3899), or we're on - // a noSharedTooltip series among shared tooltip series (#4546), use the hoverPoint . Otherwise, - // search the k-d tree. - stickToHoverSeries = hoverSeries && (shared ? hoverSeries.noSharedTooltip : hoverSeries.directTouch); - if (stickToHoverSeries && hoverPoint) { - kdpoint = [hoverPoint]; - - // Handle shared tooltip or cases where a series is not yet hovered - } else { - // Find nearest points on all series - each(series, function (s) { - // Skip hidden series - noSharedTooltip = s.noSharedTooltip && shared; - directTouch = !shared && s.directTouch; - if (s.visible && !noSharedTooltip && !directTouch && pick(s.options.enableMouseTracking, true)) { // #3821 - kdpointT = s.searchPoint(e, !noSharedTooltip && s.kdDimensions === 1); // #3828 - if (kdpointT) { - kdpoints.push(kdpointT); - } - } - }); - // Find absolute nearest point - each(kdpoints, function (p) { - if (p) { - // Store both closest points, using point.dist and point.distX comparisons (#4645): - each(['dist', 'distX'], function (dist, k) { - if (isNumber(p[dist])) { - var - // It is closer than the reference point - isCloser = p[dist] < distance[k], - // It is equally close, but above the reference point (#4679) - isAbove = p[dist] === distance[k] && p.series.group.zIndex >= kdpoint[k].series.group.zIndex; - - if (isCloser || isAbove) { - distance[k] = p[dist]; - kdpoint[k] = p; - } - } - }); - } - }); - } - - // Remove points with different x-positions, required for shared tooltip and crosshairs (#4645): - if (shared) { - i = kdpoints.length; - while (i--) { - if (kdpoints[i].clientX !== kdpoint[1].clientX || kdpoints[i].series.noSharedTooltip) { - kdpoints.splice(i, 1); - } - } - } - - // Refresh tooltip for kdpoint if new hover point or tooltip was hidden // #3926, #4200 - if (kdpoint[0] && (kdpoint[0] !== this.prevKDPoint || (tooltip && tooltip.isHidden))) { - // Draw tooltip if necessary - if (shared && !kdpoint[0].series.noSharedTooltip) { - if (kdpoints.length && tooltip) { - tooltip.refresh(kdpoints, e); - } - - // Do mouseover on all points (#3919, #3985, #4410) - each(kdpoints, function (point) { - point.onMouseOver(e, point !== ((hoverSeries && hoverSeries.directTouch && hoverPoint) || kdpoint[0])); - }); - this.prevKDPoint = kdpoint[1]; - } else { - if (tooltip) { - tooltip.refresh(kdpoint[0], e); - } - if (!hoverSeries || !hoverSeries.directTouch) { // #4448 - kdpoint[0].onMouseOver(e); - } - this.prevKDPoint = kdpoint[0]; - } - - // Update positions (regardless of kdpoint or hoverPoint) - } else { - followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer; - if (tooltip && followPointer && !tooltip.isHidden) { - anchor = tooltip.getAnchor([{}], e); - tooltip.updatePosition({ plotX: anchor[0], plotY: anchor[1] }); - } - } - - // Start the event listener to pick up the tooltip and crosshairs - if (!pointer._onDocumentMouseMove) { - pointer._onDocumentMouseMove = function (e) { - if (charts[hoverChartIndex]) { - charts[hoverChartIndex].pointer.onDocumentMouseMove(e); - } - }; - addEvent(doc, 'mousemove', pointer._onDocumentMouseMove); - } - - // Crosshair. For each hover point, loop over axes and draw cross if that point - // belongs to the axis (#4927). - each(shared ? kdpoints : [pick(hoverPoint, kdpoint[1])], function (point) { // #5269 - each(chart.axes, function (axis) { - // In case of snap = false, point is undefined, and we draw the crosshair anyway (#5066) - if (!point || point.series[axis.coll] === axis) { - axis.drawCrosshair(e, point); - } - }); - }); - }, - - /** - * Reset the tracking by hiding the tooltip, the hover series state and the hover point - * - * @param allowMove {Boolean} Instead of destroying the tooltip altogether, allow moving it if possible - */ - reset: function (allowMove, delay) { - var pointer = this, - chart = pointer.chart, - hoverSeries = chart.hoverSeries, - hoverPoint = chart.hoverPoint, - hoverPoints = chart.hoverPoints, - tooltip = chart.tooltip, - tooltipPoints = tooltip && tooltip.shared ? hoverPoints : hoverPoint; - - // Check if the points have moved outside the plot area (#1003, #4736, #5101) - if (allowMove && tooltipPoints) { - each(splat(tooltipPoints), function (point) { - if (point.series.isCartesian && point.plotX === undefined) { - allowMove = false; - } - }); - } - - // Just move the tooltip, #349 - if (allowMove) { - if (tooltip && tooltipPoints) { - tooltip.refresh(tooltipPoints); - if (hoverPoint) { // #2500 - hoverPoint.setState(hoverPoint.state, true); - each(chart.axes, function (axis) { - if (pick(axis.crosshair && axis.crosshair.snap, true)) { - axis.drawCrosshair(null, hoverPoint); - } else { - axis.hideCrosshair(); - } - }); - - } - } - - // Full reset - } else { - - if (hoverPoint) { - hoverPoint.onMouseOut(); - } - - if (hoverPoints) { - each(hoverPoints, function (point) { - point.setState(); - }); - } - - if (hoverSeries) { - hoverSeries.onMouseOut(); - } - - if (tooltip) { - tooltip.hide(delay); - } - - if (pointer._onDocumentMouseMove) { - removeEvent(doc, 'mousemove', pointer._onDocumentMouseMove); - pointer._onDocumentMouseMove = null; - } - - // Remove crosshairs - each(chart.axes, function (axis) { - axis.hideCrosshair(); - }); - - pointer.hoverX = chart.hoverPoints = chart.hoverPoint = null; - - } - }, - - /** - * Scale series groups to a certain scale and translation - */ - scaleGroups: function (attribs, clip) { - - var chart = this.chart, - seriesAttribs; - - // Scale each series - each(chart.series, function (series) { - seriesAttribs = attribs || series.getPlotBox(); // #1701 - if (series.xAxis && series.xAxis.zoomEnabled) { - series.group.attr(seriesAttribs); - if (series.markerGroup) { - series.markerGroup.attr(seriesAttribs); - series.markerGroup.clip(clip ? chart.clipRect : null); - } - if (series.dataLabelsGroup) { - series.dataLabelsGroup.attr(seriesAttribs); - } - } - }); - - // Clip - chart.clipRect.attr(clip || chart.clipBox); - }, - - /** - * Start a drag operation - */ - dragStart: function (e) { - var chart = this.chart; - - // Record the start position - chart.mouseIsDown = e.type; - chart.cancelClick = false; - chart.mouseDownX = this.mouseDownX = e.chartX; - chart.mouseDownY = this.mouseDownY = e.chartY; - }, - - /** - * Perform a drag operation in response to a mousemove event while the mouse is down - */ - drag: function (e) { - - var chart = this.chart, - chartOptions = chart.options.chart, - chartX = e.chartX, - chartY = e.chartY, - zoomHor = this.zoomHor, - zoomVert = this.zoomVert, - plotLeft = chart.plotLeft, - plotTop = chart.plotTop, - plotWidth = chart.plotWidth, - plotHeight = chart.plotHeight, - clickedInside, - size, - selectionMarker = this.selectionMarker, - mouseDownX = this.mouseDownX, - mouseDownY = this.mouseDownY, - panKey = chartOptions.panKey && e[chartOptions.panKey + 'Key']; - - // If the device supports both touch and mouse (like IE11), and we are touch-dragging - // inside the plot area, don't handle the mouse event. #4339. - if (selectionMarker && selectionMarker.touch) { - return; - } - - // If the mouse is outside the plot area, adjust to cooordinates - // inside to prevent the selection marker from going outside - if (chartX < plotLeft) { - chartX = plotLeft; - } else if (chartX > plotLeft + plotWidth) { - chartX = plotLeft + plotWidth; - } - - if (chartY < plotTop) { - chartY = plotTop; - } else if (chartY > plotTop + plotHeight) { - chartY = plotTop + plotHeight; - } - - // determine if the mouse has moved more than 10px - this.hasDragged = Math.sqrt( - Math.pow(mouseDownX - chartX, 2) + - Math.pow(mouseDownY - chartY, 2) - ); - - if (this.hasDragged > 10) { - clickedInside = chart.isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop); - - // make a selection - if (chart.hasCartesianSeries && (this.zoomX || this.zoomY) && clickedInside && !panKey) { - if (!selectionMarker) { - this.selectionMarker = selectionMarker = chart.renderer.rect( - plotLeft, - plotTop, - zoomHor ? 1 : plotWidth, - zoomVert ? 1 : plotHeight, - 0 - ) - .attr({ - fill: chartOptions.selectionMarkerFill || 'rgba(69,114,167,0.25)', - zIndex: 7 - }) - .add(); - } - } - - // adjust the width of the selection marker - if (selectionMarker && zoomHor) { - size = chartX - mouseDownX; - selectionMarker.attr({ - width: mathAbs(size), - x: (size > 0 ? 0 : size) + mouseDownX - }); - } - // adjust the height of the selection marker - if (selectionMarker && zoomVert) { - size = chartY - mouseDownY; - selectionMarker.attr({ - height: mathAbs(size), - y: (size > 0 ? 0 : size) + mouseDownY - }); - } - - // panning - if (clickedInside && !selectionMarker && chartOptions.panning) { - chart.pan(e, chartOptions.panning); - } - } - }, - - /** - * On mouse up or touch end across the entire document, drop the selection. - */ - drop: function (e) { - var pointer = this, - chart = this.chart, - hasPinched = this.hasPinched; - - if (this.selectionMarker) { - var selectionData = { - originalEvent: e, // #4890 - xAxis: [], - yAxis: [] - }, - selectionBox = this.selectionMarker, - selectionLeft = selectionBox.attr ? selectionBox.attr('x') : selectionBox.x, - selectionTop = selectionBox.attr ? selectionBox.attr('y') : selectionBox.y, - selectionWidth = selectionBox.attr ? selectionBox.attr('width') : selectionBox.width, - selectionHeight = selectionBox.attr ? selectionBox.attr('height') : selectionBox.height, - runZoom; - - // a selection has been made - if (this.hasDragged || hasPinched) { - - // record each axis' min and max - each(chart.axes, function (axis) { - if (axis.zoomEnabled && defined(axis.min) && (hasPinched || pointer[{ xAxis: 'zoomX', yAxis: 'zoomY' }[axis.coll]])) { // #859, #3569 - var horiz = axis.horiz, - minPixelPadding = e.type === 'touchend' ? axis.minPixelPadding : 0, // #1207, #3075 - selectionMin = axis.toValue((horiz ? selectionLeft : selectionTop) + minPixelPadding), - selectionMax = axis.toValue((horiz ? selectionLeft + selectionWidth : selectionTop + selectionHeight) - minPixelPadding); - - selectionData[axis.coll].push({ - axis: axis, - min: mathMin(selectionMin, selectionMax), // for reversed axes - max: mathMax(selectionMin, selectionMax) - }); - runZoom = true; - } - }); - if (runZoom) { - fireEvent(chart, 'selection', selectionData, function (args) { - chart.zoom(extend(args, hasPinched ? { animation: false } : null)); - }); - } - - } - this.selectionMarker = this.selectionMarker.destroy(); - - // Reset scaling preview - if (hasPinched) { - this.scaleGroups(); - } - } - - // Reset all - if (chart) { // it may be destroyed on mouse up - #877 - css(chart.container, { cursor: chart._cursor }); - chart.cancelClick = this.hasDragged > 10; // #370 - chart.mouseIsDown = this.hasDragged = this.hasPinched = false; - this.pinchDown = []; - } - }, - - onContainerMouseDown: function (e) { - - e = this.normalize(e); - - // issue #295, dragging not always working in Firefox - if (e.preventDefault) { - e.preventDefault(); - } - - this.dragStart(e); - }, - - - - onDocumentMouseUp: function (e) { - if (charts[hoverChartIndex]) { - charts[hoverChartIndex].pointer.drop(e); - } - }, - - /** - * Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea. - * Issue #149 workaround. The mouseleave event does not always fire. - */ - onDocumentMouseMove: function (e) { - var chart = this.chart, - chartPosition = this.chartPosition; - - e = this.normalize(e, chartPosition); - - // If we're outside, hide the tooltip - if (chartPosition && !this.inClass(e.target, 'highcharts-tracker') && - !chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) { - this.reset(); - } - }, - - /** - * When mouse leaves the container, hide the tooltip. - */ - onContainerMouseLeave: function (e) { - var chart = charts[hoverChartIndex]; - if (chart && (e.relatedTarget || e.toElement)) { // #4886, MS Touch end fires mouseleave but with no related target - chart.pointer.reset(); - chart.pointer.chartPosition = null; // also reset the chart position, used in #149 fix - } - }, - - // The mousemove, touchmove and touchstart event handler - onContainerMouseMove: function (e) { - - var chart = this.chart; - - if (!defined(hoverChartIndex) || !charts[hoverChartIndex] || !charts[hoverChartIndex].mouseIsDown) { - hoverChartIndex = chart.index; - } - - e = this.normalize(e); - e.returnValue = false; // #2251, #3224 - - if (chart.mouseIsDown === 'mousedown') { - this.drag(e); - } - - // Show the tooltip and run mouse over events (#977) - if ((this.inClass(e.target, 'highcharts-tracker') || - chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) && !chart.openMenu) { - this.runPointActions(e); - } - }, - - /** - * Utility to detect whether an element has, or has a parent with, a specific - * class name. Used on detection of tracker objects and on deciding whether - * hovering the tooltip should cause the active series to mouse out. - */ - inClass: function (element, className) { - var elemClassName; - while (element) { - elemClassName = attr(element, 'class'); - if (elemClassName) { - if (elemClassName.indexOf(className) !== -1) { - return true; - } - if (elemClassName.indexOf(PREFIX + 'container') !== -1) { - return false; - } - } - element = element.parentNode; - } - }, - - onTrackerMouseOut: function (e) { - var series = this.chart.hoverSeries, - relatedTarget = e.relatedTarget || e.toElement; - - if (series && relatedTarget && !series.options.stickyTracking && // #4886 - !this.inClass(relatedTarget, PREFIX + 'tooltip') && - !this.inClass(relatedTarget, PREFIX + 'series-' + series.index)) { // #2499, #4465 - series.onMouseOut(); - } - }, - - onContainerClick: function (e) { - var chart = this.chart, - hoverPoint = chart.hoverPoint, - plotLeft = chart.plotLeft, - plotTop = chart.plotTop; - - e = this.normalize(e); - - if (!chart.cancelClick) { - - // On tracker click, fire the series and point events. #783, #1583 - if (hoverPoint && this.inClass(e.target, PREFIX + 'tracker')) { - - // the series click event - fireEvent(hoverPoint.series, 'click', extend(e, { - point: hoverPoint - })); - - // the point click event - if (chart.hoverPoint) { // it may be destroyed (#1844) - hoverPoint.firePointEvent('click', e); - } - - // When clicking outside a tracker, fire a chart event - } else { - extend(e, this.getCoordinates(e)); - - // fire a click event in the chart - if (chart.isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) { - fireEvent(chart, 'click', e); - } - } - - - } - }, - - /** - * Set the JS DOM events on the container and document. This method should contain - * a one-to-one assignment between methods and their handlers. Any advanced logic should - * be moved to the handler reflecting the event's name. - */ - setDOMEvents: function () { - - var pointer = this, - container = pointer.chart.container; - - container.onmousedown = function (e) { - pointer.onContainerMouseDown(e); - }; - container.onmousemove = function (e) { - pointer.onContainerMouseMove(e); - }; - container.onclick = function (e) { - pointer.onContainerClick(e); - }; - addEvent(container, 'mouseleave', pointer.onContainerMouseLeave); - if (chartCount === 1) { - addEvent(doc, 'mouseup', pointer.onDocumentMouseUp); - } - if (hasTouch) { - container.ontouchstart = function (e) { - pointer.onContainerTouchStart(e); - }; - container.ontouchmove = function (e) { - pointer.onContainerTouchMove(e); - }; - if (chartCount === 1) { - addEvent(doc, 'touchend', pointer.onDocumentTouchEnd); - } - } - - }, - - /** - * Destroys the Pointer object and disconnects DOM events. - */ - destroy: function () { - var prop; - - removeEvent(this.chart.container, 'mouseleave', this.onContainerMouseLeave); - if (!chartCount) { - removeEvent(doc, 'mouseup', this.onDocumentMouseUp); - removeEvent(doc, 'touchend', this.onDocumentTouchEnd); - } - - // memory and CPU leak - clearInterval(this.tooltipTimeout); - - for (prop in this) { - this[prop] = null; - } - } - }; - - - /* Support for touch devices */ - extend(Highcharts.Pointer.prototype, { - - /** - * Run translation operations - */ - pinchTranslate: function (pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) { - if (this.zoomHor || this.pinchHor) { - this.pinchTranslateDirection(true, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch); - } - if (this.zoomVert || this.pinchVert) { - this.pinchTranslateDirection(false, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch); - } - }, - - /** - * Run translation operations for each direction (horizontal and vertical) independently - */ - pinchTranslateDirection: function (horiz, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch, forcedScale) { - var chart = this.chart, - xy = horiz ? 'x' : 'y', - XY = horiz ? 'X' : 'Y', - sChartXY = 'chart' + XY, - wh = horiz ? 'width' : 'height', - plotLeftTop = chart['plot' + (horiz ? 'Left' : 'Top')], - selectionWH, - selectionXY, - clipXY, - scale = forcedScale || 1, - inverted = chart.inverted, - bounds = chart.bounds[horiz ? 'h' : 'v'], - singleTouch = pinchDown.length === 1, - touch0Start = pinchDown[0][sChartXY], - touch0Now = touches[0][sChartXY], - touch1Start = !singleTouch && pinchDown[1][sChartXY], - touch1Now = !singleTouch && touches[1][sChartXY], - outOfBounds, - transformScale, - scaleKey, - setScale = function () { - if (!singleTouch && mathAbs(touch0Start - touch1Start) > 20) { // Don't zoom if fingers are too close on this axis - scale = forcedScale || mathAbs(touch0Now - touch1Now) / mathAbs(touch0Start - touch1Start); - } - - clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start; - selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / scale; - }; - - // Set the scale, first pass - setScale(); - - selectionXY = clipXY; // the clip position (x or y) is altered if out of bounds, the selection position is not - - // Out of bounds - if (selectionXY < bounds.min) { - selectionXY = bounds.min; - outOfBounds = true; - } else if (selectionXY + selectionWH > bounds.max) { - selectionXY = bounds.max - selectionWH; - outOfBounds = true; - } - - // Is the chart dragged off its bounds, determined by dataMin and dataMax? - if (outOfBounds) { - - // Modify the touchNow position in order to create an elastic drag movement. This indicates - // to the user that the chart is responsive but can't be dragged further. - touch0Now -= 0.8 * (touch0Now - lastValidTouch[xy][0]); - if (!singleTouch) { - touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]); - } - - // Set the scale, second pass to adapt to the modified touchNow positions - setScale(); - - } else { - lastValidTouch[xy] = [touch0Now, touch1Now]; - } - - // Set geometry for clipping, selection and transformation - if (!inverted) { - clip[xy] = clipXY - plotLeftTop; - clip[wh] = selectionWH; - } - scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY; - transformScale = inverted ? 1 / scale : scale; - - selectionMarker[wh] = selectionWH; - selectionMarker[xy] = selectionXY; - transform[scaleKey] = scale; - transform['translate' + XY] = (transformScale * plotLeftTop) + (touch0Now - (transformScale * touch0Start)); - }, - - /** - * Handle touch events with two touches - */ - pinch: function (e) { - - var self = this, - chart = self.chart, - pinchDown = self.pinchDown, - touches = e.touches, - touchesLength = touches.length, - lastValidTouch = self.lastValidTouch, - hasZoom = self.hasZoom, - selectionMarker = self.selectionMarker, - transform = {}, - fireClickEvent = touchesLength === 1 && ((self.inClass(e.target, PREFIX + 'tracker') && - chart.runTrackerClick) || self.runChartClick), - clip = {}; - - // Don't initiate panning until the user has pinched. This prevents us from - // blocking page scrolling as users scroll down a long page (#4210). - if (touchesLength > 1) { - self.initiated = true; - } - - // On touch devices, only proceed to trigger click if a handler is defined - if (hasZoom && self.initiated && !fireClickEvent) { - e.preventDefault(); - } - - // Normalize each touch - map(touches, function (e) { - return self.normalize(e); - }); - - // Register the touch start position - if (e.type === 'touchstart') { - each(touches, function (e, i) { - pinchDown[i] = { chartX: e.chartX, chartY: e.chartY }; - }); - lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] && pinchDown[1].chartX]; - lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] && pinchDown[1].chartY]; - - // Identify the data bounds in pixels - each(chart.axes, function (axis) { - if (axis.zoomEnabled) { - var bounds = chart.bounds[axis.horiz ? 'h' : 'v'], - minPixelPadding = axis.minPixelPadding, - min = axis.toPixels(pick(axis.options.min, axis.dataMin)), - max = axis.toPixels(pick(axis.options.max, axis.dataMax)), - absMin = mathMin(min, max), - absMax = mathMax(min, max); - - // Store the bounds for use in the touchmove handler - bounds.min = mathMin(axis.pos, absMin - minPixelPadding); - bounds.max = mathMax(axis.pos + axis.len, absMax + minPixelPadding); - } - }); - self.res = true; // reset on next move - - // Event type is touchmove, handle panning and pinching - } else if (pinchDown.length) { // can be 0 when releasing, if touchend fires first - - - // Set the marker - if (!selectionMarker) { - self.selectionMarker = selectionMarker = extend({ - destroy: noop, - touch: true - }, chart.plotBox); - } - - self.pinchTranslate(pinchDown, touches, transform, selectionMarker, clip, lastValidTouch); - - self.hasPinched = hasZoom; - - // Scale and translate the groups to provide visual feedback during pinching - self.scaleGroups(transform, clip); - - // Optionally move the tooltip on touchmove - if (!hasZoom && self.followTouchMove && touchesLength === 1) { - this.runPointActions(self.normalize(e)); - } else if (self.res) { - self.res = false; - this.reset(false, 0); - } - } - }, - - /** - * General touch handler shared by touchstart and touchmove. - */ - touch: function (e, start) { - var chart = this.chart, - hasMoved, - pinchDown; - - hoverChartIndex = chart.index; - - if (e.touches.length === 1) { - - e = this.normalize(e); - - if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop) && !chart.openMenu) { - - // Run mouse events and display tooltip etc - if (start) { - this.runPointActions(e); - } - - // Android fires touchmove events after the touchstart even if the - // finger hasn't moved, or moved only a pixel or two. In iOS however, - // the touchmove doesn't fire unless the finger moves more than ~4px. - // So we emulate this behaviour in Android by checking how much it - // moved, and cancelling on small distances. #3450. - if (e.type === 'touchmove') { - pinchDown = this.pinchDown; - hasMoved = pinchDown[0] ? Math.sqrt( // #5266 - Math.pow(pinchDown[0].chartX - e.chartX, 2) + - Math.pow(pinchDown[0].chartY - e.chartY, 2) - ) >= 4 : false; - } - - if (pick(hasMoved, true)) { - this.pinch(e); - } - - } else if (start) { - // Hide the tooltip on touching outside the plot area (#1203) - this.reset(); - } - - } else if (e.touches.length === 2) { - this.pinch(e); - } - }, - - onContainerTouchStart: function (e) { - this.touch(e, true); - }, - - onContainerTouchMove: function (e) { - this.touch(e); - }, - - onDocumentTouchEnd: function (e) { - if (charts[hoverChartIndex]) { - charts[hoverChartIndex].pointer.drop(e); - } - } - - }); - if (win.PointerEvent || win.MSPointerEvent) { - - // The touches object keeps track of the points being touched at all times - var touches = {}, - hasPointerEvent = !!win.PointerEvent, - getWebkitTouches = function () { - var key, - fake = []; - fake.item = function (i) { - return this[i]; - }; - for (key in touches) { - if (touches.hasOwnProperty(key)) { - fake.push({ - pageX: touches[key].pageX, - pageY: touches[key].pageY, - target: touches[key].target - }); - } - } - return fake; - }, - translateMSPointer = function (e, method, wktype, func) { - var p; - if ((e.pointerType === 'touch' || e.pointerType === e.MSPOINTER_TYPE_TOUCH) && charts[hoverChartIndex]) { - func(e); - p = charts[hoverChartIndex].pointer; - p[method]({ - type: wktype, - target: e.currentTarget, - preventDefault: noop, - touches: getWebkitTouches() - }); - } - }; - - /** - * Extend the Pointer prototype with methods for each event handler and more - */ - extend(Pointer.prototype, { - onContainerPointerDown: function (e) { - translateMSPointer(e, 'onContainerTouchStart', 'touchstart', function (e) { - touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY, target: e.currentTarget }; - }); - }, - onContainerPointerMove: function (e) { - translateMSPointer(e, 'onContainerTouchMove', 'touchmove', function (e) { - touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY }; - if (!touches[e.pointerId].target) { - touches[e.pointerId].target = e.currentTarget; - } - }); - }, - onDocumentPointerUp: function (e) { - translateMSPointer(e, 'onDocumentTouchEnd', 'touchend', function (e) { - delete touches[e.pointerId]; - }); - }, - - /** - * Add or remove the MS Pointer specific events - */ - batchMSEvents: function (fn) { - fn(this.chart.container, hasPointerEvent ? 'pointerdown' : 'MSPointerDown', this.onContainerPointerDown); - fn(this.chart.container, hasPointerEvent ? 'pointermove' : 'MSPointerMove', this.onContainerPointerMove); - fn(doc, hasPointerEvent ? 'pointerup' : 'MSPointerUp', this.onDocumentPointerUp); - } - }); - - // Disable default IE actions for pinch and such on chart element - wrap(Pointer.prototype, 'init', function (proceed, chart, options) { - proceed.call(this, chart, options); - if (this.hasZoom) { // #4014 - css(chart.container, { - '-ms-touch-action': NONE, - 'touch-action': NONE - }); - } - }); - - // Add IE specific touch events to chart - wrap(Pointer.prototype, 'setDOMEvents', function (proceed) { - proceed.apply(this); - if (this.hasZoom || this.followTouchMove) { - this.batchMSEvents(addEvent); - } - }); - // Destroy MS events also - wrap(Pointer.prototype, 'destroy', function (proceed) { - this.batchMSEvents(removeEvent); - proceed.call(this); - }); - } - /** - * The overview of the chart's series - */ - var Legend = Highcharts.Legend = function (chart, options) { - this.init(chart, options); - }; - - Legend.prototype = { - - /** - * Initialize the legend - */ - init: function (chart, options) { - - var legend = this, - itemStyle = options.itemStyle, - padding, - itemMarginTop = options.itemMarginTop || 0; - - this.options = options; - - if (!options.enabled) { - return; - } - - legend.itemStyle = itemStyle; - legend.itemHiddenStyle = merge(itemStyle, options.itemHiddenStyle); - legend.itemMarginTop = itemMarginTop; - legend.padding = padding = pick(options.padding, 8); - legend.initialItemX = padding; - legend.initialItemY = padding - 5; // 5 is the number of pixels above the text - legend.maxItemWidth = 0; - legend.chart = chart; - legend.itemHeight = 0; - legend.symbolWidth = pick(options.symbolWidth, 16); - legend.pages = []; - - - // Render it - legend.render(); - - // move checkboxes - addEvent(legend.chart, 'endResize', function () { - legend.positionCheckboxes(); - }); - - }, - - /** - * Set the colors for the legend item - * @param {Object} item A Series or Point instance - * @param {Object} visible Dimmed or colored - */ - colorizeItem: function (item, visible) { - var legend = this, - options = legend.options, - legendItem = item.legendItem, - legendLine = item.legendLine, - legendSymbol = item.legendSymbol, - hiddenColor = legend.itemHiddenStyle.color, - textColor = visible ? options.itemStyle.color : hiddenColor, - symbolColor = visible ? (item.legendColor || item.color || '#CCC') : hiddenColor, - markerOptions = item.options && item.options.marker, - symbolAttr = { fill: symbolColor }, - key, - val; - - if (legendItem) { - legendItem.css({ fill: textColor, color: textColor }); // color for #1553, oldIE - } - if (legendLine) { - legendLine.attr({ stroke: symbolColor }); - } - - if (legendSymbol) { - - // Apply marker options - if (markerOptions && legendSymbol.isMarker) { // #585 - symbolAttr.stroke = symbolColor; - markerOptions = item.convertAttribs(markerOptions); - for (key in markerOptions) { - val = markerOptions[key]; - if (val !== UNDEFINED) { - symbolAttr[key] = val; - } - } - } - - legendSymbol.attr(symbolAttr); - } - }, - - /** - * Position the legend item - * @param {Object} item A Series or Point instance - */ - positionItem: function (item) { - var legend = this, - options = legend.options, - symbolPadding = options.symbolPadding, - ltr = !options.rtl, - legendItemPos = item._legendItemPos, - itemX = legendItemPos[0], - itemY = legendItemPos[1], - checkbox = item.checkbox, - legendGroup = item.legendGroup; - - if (legendGroup && legendGroup.element) { - legendGroup.translate( - ltr ? itemX : legend.legendWidth - itemX - 2 * symbolPadding - 4, - itemY - ); - } - - if (checkbox) { - checkbox.x = itemX; - checkbox.y = itemY; - } - }, - - /** - * Destroy a single legend item - * @param {Object} item The series or point - */ - destroyItem: function (item) { - var checkbox = item.checkbox; - - // destroy SVG elements - each(['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'], function (key) { - if (item[key]) { - item[key] = item[key].destroy(); - } - }); - - if (checkbox) { - discardElement(item.checkbox); - } - }, - - /** - * Destroys the legend. - */ - destroy: function () { - var legend = this, - legendGroup = legend.group, - box = legend.box; - - if (box) { - legend.box = box.destroy(); - } - - if (legendGroup) { - legend.group = legendGroup.destroy(); - } - }, - - /** - * Position the checkboxes after the width is determined - */ - positionCheckboxes: function (scrollOffset) { - var alignAttr = this.group.alignAttr, - translateY, - clipHeight = this.clipHeight || this.legendHeight, - titleHeight = this.titleHeight; - - if (alignAttr) { - translateY = alignAttr.translateY; - each(this.allItems, function (item) { - var checkbox = item.checkbox, - top; - - if (checkbox) { - top = translateY + titleHeight + checkbox.y + (scrollOffset || 0) + 3; - css(checkbox, { - left: (alignAttr.translateX + item.checkboxOffset + checkbox.x - 20) + PX, - top: top + PX, - display: top > translateY - 6 && top < translateY + clipHeight - 6 ? '' : NONE - }); - } - }); - } - }, - - /** - * Render the legend title on top of the legend - */ - renderTitle: function () { - var options = this.options, - padding = this.padding, - titleOptions = options.title, - titleHeight = 0, - bBox; - - if (titleOptions.text) { - if (!this.title) { - this.title = this.chart.renderer.label(titleOptions.text, padding - 3, padding - 4, null, null, null, null, null, 'legend-title') - .attr({ zIndex: 1 }) - .css(titleOptions.style) - .add(this.group); - } - bBox = this.title.getBBox(); - titleHeight = bBox.height; - this.offsetWidth = bBox.width; // #1717 - this.contentGroup.attr({ translateY: titleHeight }); - } - this.titleHeight = titleHeight; - }, - - /** - * Set the legend item text - */ - setText: function (item) { - var options = this.options; - item.legendItem.attr({ - text: options.labelFormat ? format(options.labelFormat, item) : options.labelFormatter.call(item) - }); - }, - - /** - * Render a single specific legend item - * @param {Object} item A series or point - */ - renderItem: function (item) { - var legend = this, - chart = legend.chart, - renderer = chart.renderer, - options = legend.options, - horizontal = options.layout === 'horizontal', - symbolWidth = legend.symbolWidth, - symbolPadding = options.symbolPadding, - itemStyle = legend.itemStyle, - itemHiddenStyle = legend.itemHiddenStyle, - padding = legend.padding, - itemDistance = horizontal ? pick(options.itemDistance, 20) : 0, - ltr = !options.rtl, - itemHeight, - widthOption = options.width, - itemMarginBottom = options.itemMarginBottom || 0, - itemMarginTop = legend.itemMarginTop, - initialItemX = legend.initialItemX, - bBox, - itemWidth, - li = item.legendItem, - series = item.series && item.series.drawLegendSymbol ? item.series : item, - seriesOptions = series.options, - showCheckbox = legend.createCheckboxForItem && seriesOptions && seriesOptions.showCheckbox, - useHTML = options.useHTML; - - if (!li) { // generate it once, later move it - - // Generate the group box - // A group to hold the symbol and text. Text is to be appended in Legend class. - item.legendGroup = renderer.g('legend-item') - .attr({ zIndex: 1 }) - .add(legend.scrollGroup); - - // Generate the list item text and add it to the group - item.legendItem = li = renderer.text( - '', - ltr ? symbolWidth + symbolPadding : -symbolPadding, - legend.baseline || 0, - useHTML - ) - .css(merge(item.visible ? itemStyle : itemHiddenStyle)) // merge to prevent modifying original (#1021) - .attr({ - align: ltr ? 'left' : 'right', - zIndex: 2 - }) - .add(item.legendGroup); - - // Get the baseline for the first item - the font size is equal for all - if (!legend.baseline) { - legend.fontMetrics = renderer.fontMetrics(itemStyle.fontSize, li); - legend.baseline = legend.fontMetrics.f + 3 + itemMarginTop; - li.attr('y', legend.baseline); - } - - // Draw the legend symbol inside the group box - series.drawLegendSymbol(legend, item); - - if (legend.setItemEvents) { - legend.setItemEvents(item, li, useHTML, itemStyle, itemHiddenStyle); - } - - // add the HTML checkbox on top - if (showCheckbox) { - legend.createCheckboxForItem(item); - } - } - - // Colorize the items - legend.colorizeItem(item, item.visible); - - // Always update the text - legend.setText(item); - - // calculate the positions for the next line - bBox = li.getBBox(); - - itemWidth = item.checkboxOffset = - options.itemWidth || - item.legendItemWidth || - symbolWidth + symbolPadding + bBox.width + itemDistance + (showCheckbox ? 20 : 0); - legend.itemHeight = itemHeight = mathRound(item.legendItemHeight || bBox.height); - - // if the item exceeds the width, start a new line - if (horizontal && legend.itemX - initialItemX + itemWidth > - (widthOption || (chart.chartWidth - 2 * padding - initialItemX - options.x))) { - legend.itemX = initialItemX; - legend.itemY += itemMarginTop + legend.lastLineHeight + itemMarginBottom; - legend.lastLineHeight = 0; // reset for next line (#915, #3976) - } - - // If the item exceeds the height, start a new column - /*if (!horizontal && legend.itemY + options.y + itemHeight > chart.chartHeight - spacingTop - spacingBottom) { - legend.itemY = legend.initialItemY; - legend.itemX += legend.maxItemWidth; - legend.maxItemWidth = 0; - }*/ - - // Set the edge positions - legend.maxItemWidth = mathMax(legend.maxItemWidth, itemWidth); - legend.lastItemY = itemMarginTop + legend.itemY + itemMarginBottom; - legend.lastLineHeight = mathMax(itemHeight, legend.lastLineHeight); // #915 - - // cache the position of the newly generated or reordered items - item._legendItemPos = [legend.itemX, legend.itemY]; - - // advance - if (horizontal) { - legend.itemX += itemWidth; - - } else { - legend.itemY += itemMarginTop + itemHeight + itemMarginBottom; - legend.lastLineHeight = itemHeight; - } - - // the width of the widest item - legend.offsetWidth = widthOption || mathMax( - (horizontal ? legend.itemX - initialItemX - itemDistance : itemWidth) + padding, - legend.offsetWidth - ); - }, - - /** - * Get all items, which is one item per series for normal series and one item per point - * for pie series. - */ - getAllItems: function () { - var allItems = []; - each(this.chart.series, function (series) { - var seriesOptions = series.options; - - // Handle showInLegend. If the series is linked to another series, defaults to false. - if (!pick(seriesOptions.showInLegend, !defined(seriesOptions.linkedTo) ? UNDEFINED : false, true)) { - return; - } - - // use points or series for the legend item depending on legendType - allItems = allItems.concat( - series.legendItems || - (seriesOptions.legendType === 'point' ? - series.data : - series) - ); - }); - return allItems; - }, - - /** - * Adjust the chart margins by reserving space for the legend on only one side - * of the chart. If the position is set to a corner, top or bottom is reserved - * for horizontal legends and left or right for vertical ones. - */ - adjustMargins: function (margin, spacing) { - var chart = this.chart, - options = this.options, - // Use the first letter of each alignment option in order to detect the side - alignment = options.align.charAt(0) + options.verticalAlign.charAt(0) + options.layout.charAt(0); // #4189 - use charAt(x) notation instead of [x] for IE7 - - if (this.display && !options.floating) { - - each([ - /(lth|ct|rth)/, - /(rtv|rm|rbv)/, - /(rbh|cb|lbh)/, - /(lbv|lm|ltv)/ - ], function (alignments, side) { - if (alignments.test(alignment) && !defined(margin[side])) { - // Now we have detected on which side of the chart we should reserve space for the legend - chart[marginNames[side]] = mathMax( - chart[marginNames[side]], - chart.legend[(side + 1) % 2 ? 'legendHeight' : 'legendWidth'] + - [1, -1, -1, 1][side] * options[(side % 2) ? 'x' : 'y'] + - pick(options.margin, 12) + - spacing[side] - ); - } - }); - } - }, - - /** - * Render the legend. This method can be called both before and after - * chart.render. If called after, it will only rearrange items instead - * of creating new ones. - */ - render: function () { - var legend = this, - chart = legend.chart, - renderer = chart.renderer, - legendGroup = legend.group, - allItems, - display, - legendWidth, - legendHeight, - box = legend.box, - options = legend.options, - padding = legend.padding, - legendBorderWidth = options.borderWidth, - legendBackgroundColor = options.backgroundColor; - - legend.itemX = legend.initialItemX; - legend.itemY = legend.initialItemY; - legend.offsetWidth = 0; - legend.lastItemY = 0; - - if (!legendGroup) { - legend.group = legendGroup = renderer.g('legend') - .attr({ zIndex: 7 }) - .add(); - legend.contentGroup = renderer.g() - .attr({ zIndex: 1 }) // above background - .add(legendGroup); - legend.scrollGroup = renderer.g() - .add(legend.contentGroup); - } - - legend.renderTitle(); - - // add each series or point - allItems = legend.getAllItems(); - - // sort by legendIndex - stableSort(allItems, function (a, b) { - return ((a.options && a.options.legendIndex) || 0) - ((b.options && b.options.legendIndex) || 0); - }); - - // reversed legend - if (options.reversed) { - allItems.reverse(); - } - - legend.allItems = allItems; - legend.display = display = !!allItems.length; - - // render the items - legend.lastLineHeight = 0; - each(allItems, function (item) { - legend.renderItem(item); - }); - - // Get the box - legendWidth = (options.width || legend.offsetWidth) + padding; - legendHeight = legend.lastItemY + legend.lastLineHeight + legend.titleHeight; - legendHeight = legend.handleOverflow(legendHeight); - legendHeight += padding; - - // Draw the border and/or background - if (legendBorderWidth || legendBackgroundColor) { - - if (!box) { - legend.box = box = renderer.rect( - 0, - 0, - legendWidth, - legendHeight, - options.borderRadius, - legendBorderWidth || 0 - ).attr({ - stroke: options.borderColor, - 'stroke-width': legendBorderWidth || 0, - fill: legendBackgroundColor || NONE - }) - .add(legendGroup) - .shadow(options.shadow); - box.isNew = true; - - } else if (legendWidth > 0 && legendHeight > 0) { - box[box.isNew ? 'attr' : 'animate']( - box.crisp({ width: legendWidth, height: legendHeight }) - ); - box.isNew = false; - } - - // hide the border if no items - box[display ? 'show' : 'hide'](); - } - - legend.legendWidth = legendWidth; - legend.legendHeight = legendHeight; - - // Now that the legend width and height are established, put the items in the - // final position - each(allItems, function (item) { - legend.positionItem(item); - }); - - // 1.x compatibility: positioning based on style - /*var props = ['left', 'right', 'top', 'bottom'], - prop, - i = 4; - while (i--) { - prop = props[i]; - if (options.style[prop] && options.style[prop] !== 'auto') { - options[i < 2 ? 'align' : 'verticalAlign'] = prop; - options[i < 2 ? 'x' : 'y'] = pInt(options.style[prop]) * (i % 2 ? -1 : 1); - } - }*/ - - if (display) { - legendGroup.align(extend({ - width: legendWidth, - height: legendHeight - }, options), true, 'spacingBox'); - } - - if (!chart.isResizing) { - this.positionCheckboxes(); - } - }, - - /** - * Set up the overflow handling by adding navigation with up and down arrows below the - * legend. - */ - handleOverflow: function (legendHeight) { - var legend = this, - chart = this.chart, - renderer = chart.renderer, - options = this.options, - optionsY = options.y, - alignTop = options.verticalAlign === 'top', - spaceHeight = chart.spacingBox.height + (alignTop ? -optionsY : optionsY) - this.padding, - maxHeight = options.maxHeight, - clipHeight, - clipRect = this.clipRect, - navOptions = options.navigation, - animation = pick(navOptions.animation, true), - arrowSize = navOptions.arrowSize || 12, - nav = this.nav, - pages = this.pages, - padding = this.padding, - lastY, - allItems = this.allItems, - clipToHeight = function (height) { - clipRect.attr({ - height: height - }); - - // useHTML - if (legend.contentGroup.div) { - legend.contentGroup.div.style.clip = 'rect(' + padding + 'px,9999px,' + (padding + height) + 'px,0)'; - } - }; - - - // Adjust the height - if (options.layout === 'horizontal') { - spaceHeight /= 2; - } - if (maxHeight) { - spaceHeight = mathMin(spaceHeight, maxHeight); - } - - // Reset the legend height and adjust the clipping rectangle - pages.length = 0; - if (legendHeight > spaceHeight && navOptions.enabled !== false) { - - this.clipHeight = clipHeight = mathMax(spaceHeight - 20 - this.titleHeight - padding, 0); - this.currentPage = pick(this.currentPage, 1); - this.fullHeight = legendHeight; - - // Fill pages with Y positions so that the top of each a legend item defines - // the scroll top for each page (#2098) - each(allItems, function (item, i) { - var y = item._legendItemPos[1], - h = mathRound(item.legendItem.getBBox().height), - len = pages.length; - - if (!len || (y - pages[len - 1] > clipHeight && (lastY || y) !== pages[len - 1])) { - pages.push(lastY || y); - len++; - } - - if (i === allItems.length - 1 && y + h - pages[len - 1] > clipHeight) { - pages.push(y); - } - if (y !== lastY) { - lastY = y; - } - }); - - // Only apply clipping if needed. Clipping causes blurred legend in PDF export (#1787) - if (!clipRect) { - clipRect = legend.clipRect = renderer.clipRect(0, padding, 9999, 0); - legend.contentGroup.clip(clipRect); - } - - clipToHeight(clipHeight); - - // Add navigation elements - if (!nav) { - this.nav = nav = renderer.g().attr({ zIndex: 1 }).add(this.group); - this.up = renderer.symbol('triangle', 0, 0, arrowSize, arrowSize) - .on('click', function () { - legend.scroll(-1, animation); - }) - .add(nav); - this.pager = renderer.text('', 15, 10) - .css(navOptions.style) - .add(nav); - this.down = renderer.symbol('triangle-down', 0, 0, arrowSize, arrowSize) - .on('click', function () { - legend.scroll(1, animation); - }) - .add(nav); - } - - // Set initial position - legend.scroll(0); - - legendHeight = spaceHeight; - - } else if (nav) { - clipToHeight(chart.chartHeight); - nav.hide(); - this.scrollGroup.attr({ - translateY: 1 - }); - this.clipHeight = 0; // #1379 - } - - return legendHeight; - }, - - /** - * Scroll the legend by a number of pages - * @param {Object} scrollBy - * @param {Object} animation - */ - scroll: function (scrollBy, animation) { - var pages = this.pages, - pageCount = pages.length, - currentPage = this.currentPage + scrollBy, - clipHeight = this.clipHeight, - navOptions = this.options.navigation, - activeColor = navOptions.activeColor, - inactiveColor = navOptions.inactiveColor, - pager = this.pager, - padding = this.padding, - scrollOffset; - - // When resizing while looking at the last page - if (currentPage > pageCount) { - currentPage = pageCount; - } - - if (currentPage > 0) { - - if (animation !== UNDEFINED) { - setAnimation(animation, this.chart); - } - - this.nav.attr({ - translateX: padding, - translateY: clipHeight + this.padding + 7 + this.titleHeight, - visibility: VISIBLE - }); - this.up.attr({ - fill: currentPage === 1 ? inactiveColor : activeColor - }) - .css({ - cursor: currentPage === 1 ? 'default' : 'pointer' - }); - pager.attr({ - text: currentPage + '/' + pageCount - }); - this.down.attr({ - x: 18 + this.pager.getBBox().width, // adjust to text width - fill: currentPage === pageCount ? inactiveColor : activeColor - }) - .css({ - cursor: currentPage === pageCount ? 'default' : 'pointer' - }); - - scrollOffset = -pages[currentPage - 1] + this.initialItemY; - - this.scrollGroup.animate({ - translateY: scrollOffset - }); - - this.currentPage = currentPage; - this.positionCheckboxes(scrollOffset); - } - - } - - }; - - /* - * LegendSymbolMixin - */ - - var LegendSymbolMixin = Highcharts.LegendSymbolMixin = { - - /** - * Get the series' symbol in the legend - * - * @param {Object} legend The legend object - * @param {Object} item The series (this) or point - */ - drawRectangle: function (legend, item) { - var symbolHeight = legend.options.symbolHeight || legend.fontMetrics.f; - - item.legendSymbol = this.chart.renderer.rect( - 0, - legend.baseline - symbolHeight + 1, // #3988 - legend.symbolWidth, - symbolHeight, - legend.options.symbolRadius || 0 - ).attr({ - zIndex: 3 - }).add(item.legendGroup); - - }, - - /** - * Get the series' symbol in the legend. This method should be overridable to create custom - * symbols through Highcharts.seriesTypes[type].prototype.drawLegendSymbols. - * - * @param {Object} legend The legend object - */ - drawLineMarker: function (legend) { - - var options = this.options, - markerOptions = options.marker, - radius, - legendSymbol, - symbolWidth = legend.symbolWidth, - renderer = this.chart.renderer, - legendItemGroup = this.legendGroup, - verticalCenter = legend.baseline - mathRound(legend.fontMetrics.b * 0.3), - attr; - - // Draw the line - if (options.lineWidth) { - attr = { - 'stroke-width': options.lineWidth - }; - if (options.dashStyle) { - attr.dashstyle = options.dashStyle; - } - this.legendLine = renderer.path([ - M, - 0, - verticalCenter, - L, - symbolWidth, - verticalCenter - ]) - .attr(attr) - .add(legendItemGroup); - } - - // Draw the marker - if (markerOptions && markerOptions.enabled !== false) { - radius = markerOptions.radius; - this.legendSymbol = legendSymbol = renderer.symbol( - this.symbol, - (symbolWidth / 2) - radius, - verticalCenter - radius, - 2 * radius, - 2 * radius, - markerOptions - ) - .add(legendItemGroup); - legendSymbol.isMarker = true; - } - } - }; - - // Workaround for #2030, horizontal legend items not displaying in IE11 Preview, - // and for #2580, a similar drawing flaw in Firefox 26. - // Explore if there's a general cause for this. The problem may be related - // to nested group elements, as the legend item texts are within 4 group elements. - if (/Trident\/7\.0/.test(userAgent) || isFirefox) { - wrap(Legend.prototype, 'positionItem', function (proceed, item) { - var legend = this, - runPositionItem = function () { // If chart destroyed in sync, this is undefined (#2030) - if (item._legendItemPos) { - proceed.call(legend, item); - } - }; - - // Do it now, for export and to get checkbox placement - runPositionItem(); - - // Do it after to work around the core issue - setTimeout(runPositionItem); - }); - } - /** - * The Chart class - * @param {String|Object} renderTo The DOM element to render to, or its id - * @param {Object} options - * @param {Function} callback Function to run when the chart has loaded - */ - var Chart = Highcharts.Chart = function () { - this.getArgs.apply(this, arguments); - }; - - Highcharts.chart = function (a, b, c) { - return new Chart(a, b, c); - }; - - Chart.prototype = { - - /** - * Hook for modules - */ - callbacks: [], - - /** - * Handle the arguments passed to the constructor - * @returns {Array} Arguments without renderTo - */ - getArgs: function () { - var args = [].slice.call(arguments); - - // Remove the optional first argument, renderTo, and - // set it on this. - if (isString(args[0]) || args[0].nodeName) { - this.renderTo = args.shift(); - } - this.init(args[0], args[1]); - }, - - /** - * Initialize the chart - */ - init: function (userOptions, callback) { - - // Handle regular options - var options, - seriesOptions = userOptions.series; // skip merging data points to increase performance - - userOptions.series = null; - options = merge(defaultOptions, userOptions); // do the merge - options.series = userOptions.series = seriesOptions; // set back the series data - this.userOptions = userOptions; - - var optionsChart = options.chart; - - // Create margin & spacing array - this.margin = this.splashArray('margin', optionsChart); - this.spacing = this.splashArray('spacing', optionsChart); - - var chartEvents = optionsChart.events; - - //this.runChartClick = chartEvents && !!chartEvents.click; - this.bounds = { h: {}, v: {} }; // Pixel data bounds for touch zoom - - this.callback = callback; - this.isResizing = 0; - this.options = options; - //chartTitleOptions = UNDEFINED; - //chartSubtitleOptions = UNDEFINED; - - this.axes = []; - this.series = []; - this.hasCartesianSeries = optionsChart.showAxes; - //this.axisOffset = UNDEFINED; - //this.maxTicks = UNDEFINED; // handle the greatest amount of ticks on grouped axes - //this.inverted = UNDEFINED; - //this.loadingShown = UNDEFINED; - //this.container = UNDEFINED; - //this.chartWidth = UNDEFINED; - //this.chartHeight = UNDEFINED; - //this.marginRight = UNDEFINED; - //this.marginBottom = UNDEFINED; - //this.containerWidth = UNDEFINED; - //this.containerHeight = UNDEFINED; - //this.oldChartWidth = UNDEFINED; - //this.oldChartHeight = UNDEFINED; - - //this.renderTo = UNDEFINED; - //this.renderToClone = UNDEFINED; - - //this.spacingBox = UNDEFINED - - //this.legend = UNDEFINED; - - // Elements - //this.chartBackground = UNDEFINED; - //this.plotBackground = UNDEFINED; - //this.plotBGImage = UNDEFINED; - //this.plotBorder = UNDEFINED; - //this.loadingDiv = UNDEFINED; - //this.loadingSpan = UNDEFINED; - - var chart = this, - eventType; - - // Add the chart to the global lookup - chart.index = charts.length; - charts.push(chart); - chartCount++; - - // Set up auto resize - if (optionsChart.reflow !== false) { - addEvent(chart, 'load', function () { - chart.initReflow(); - }); - } - - // Chart event handlers - if (chartEvents) { - for (eventType in chartEvents) { - addEvent(chart, eventType, chartEvents[eventType]); - } - } - - chart.xAxis = []; - chart.yAxis = []; - - // Expose methods and variables - chart.animation = useCanVG ? false : pick(optionsChart.animation, true); - chart.pointCount = chart.colorCounter = chart.symbolCounter = 0; - - chart.firstRender(); - }, - - /** - * Initialize an individual series, called internally before render time - */ - initSeries: function (options) { - var chart = this, - optionsChart = chart.options.chart, - type = options.type || optionsChart.type || optionsChart.defaultSeriesType, - series, - constr = seriesTypes[type]; - - // No such series type - if (!constr) { - error(17, true); - } - - series = new constr(); - series.init(this, options); - return series; - }, - - /** - * Check whether a given point is within the plot area - * - * @param {Number} plotX Pixel x relative to the plot area - * @param {Number} plotY Pixel y relative to the plot area - * @param {Boolean} inverted Whether the chart is inverted - */ - isInsidePlot: function (plotX, plotY, inverted) { - var x = inverted ? plotY : plotX, - y = inverted ? plotX : plotY; - - return x >= 0 && - x <= this.plotWidth && - y >= 0 && - y <= this.plotHeight; - }, - - /** - * Redraw legend, axes or series based on updated data - * - * @param {Boolean|Object} animation Whether to apply animation, and optionally animation - * configuration - */ - redraw: function (animation) { - var chart = this, - axes = chart.axes, - series = chart.series, - pointer = chart.pointer, - legend = chart.legend, - redrawLegend = chart.isDirtyLegend, - hasStackedSeries, - hasDirtyStacks, - hasCartesianSeries = chart.hasCartesianSeries, - isDirtyBox = chart.isDirtyBox, - seriesLength = series.length, - i = seriesLength, - serie, - renderer = chart.renderer, - isHiddenChart = renderer.isHidden(), - afterRedraw = []; - - setAnimation(animation, chart); - - if (isHiddenChart) { - chart.cloneRenderTo(); - } - - // Adjust title layout (reflow multiline text) - chart.layOutTitles(); - - // link stacked series - while (i--) { - serie = series[i]; - - if (serie.options.stacking) { - hasStackedSeries = true; - - if (serie.isDirty) { - hasDirtyStacks = true; - break; - } - } - } - if (hasDirtyStacks) { // mark others as dirty - i = seriesLength; - while (i--) { - serie = series[i]; - if (serie.options.stacking) { - serie.isDirty = true; - } - } - } - - // Handle updated data in the series - each(series, function (serie) { - if (serie.isDirty) { - if (serie.options.legendType === 'point') { - if (serie.updateTotals) { - serie.updateTotals(); - } - redrawLegend = true; - } - } - if (serie.isDirtyData) { - fireEvent(serie, 'updatedData'); - } - }); - - // handle added or removed series - if (redrawLegend && legend.options.enabled) { // series or pie points are added or removed - // draw legend graphics - legend.render(); - - chart.isDirtyLegend = false; - } - - // reset stacks - if (hasStackedSeries) { - chart.getStacks(); - } - - - if (hasCartesianSeries) { - if (!chart.isResizing) { - - // reset maxTicks - chart.maxTicks = null; - - // set axes scales - each(axes, function (axis) { - axis.setScale(); - }); - } - } - - chart.getMargins(); // #3098 - - if (hasCartesianSeries) { - // If one axis is dirty, all axes must be redrawn (#792, #2169) - each(axes, function (axis) { - if (axis.isDirty) { - isDirtyBox = true; - } - }); - - // redraw axes - each(axes, function (axis) { - - // Fire 'afterSetExtremes' only if extremes are set - var key = axis.min + ',' + axis.max; - if (axis.extKey !== key) { // #821, #4452 - axis.extKey = key; - afterRedraw.push(function () { // prevent a recursive call to chart.redraw() (#1119) - fireEvent(axis, 'afterSetExtremes', extend(axis.eventArgs, axis.getExtremes())); // #747, #751 - delete axis.eventArgs; - }); - } - if (isDirtyBox || hasStackedSeries) { - axis.redraw(); - } - }); - } - - // the plot areas size has changed - if (isDirtyBox) { - chart.drawChartBox(); - } - - - // redraw affected series - each(series, function (serie) { - if (serie.isDirty && serie.visible && - (!serie.isCartesian || serie.xAxis)) { // issue #153 - serie.redraw(); - } - }); - - // move tooltip or reset - if (pointer) { - pointer.reset(true); - } - - // redraw if canvas - renderer.draw(); - - // fire the event - fireEvent(chart, 'redraw'); - - if (isHiddenChart) { - chart.cloneRenderTo(true); - } - - // Fire callbacks that are put on hold until after the redraw - each(afterRedraw, function (callback) { - callback.call(); - }); - }, - - /** - * Get an axis, series or point object by id. - * @param id {String} The id as given in the configuration options - */ - get: function (id) { - var chart = this, - axes = chart.axes, - series = chart.series; - - var i, - j, - points; - - // search axes - for (i = 0; i < axes.length; i++) { - if (axes[i].options.id === id) { - return axes[i]; - } - } - - // search series - for (i = 0; i < series.length; i++) { - if (series[i].options.id === id) { - return series[i]; - } - } - - // search points - for (i = 0; i < series.length; i++) { - points = series[i].points || []; - for (j = 0; j < points.length; j++) { - if (points[j].id === id) { - return points[j]; - } - } - } - return null; - }, - - /** - * Create the Axis instances based on the config options - */ - getAxes: function () { - var chart = this, - options = this.options, - xAxisOptions = options.xAxis = splat(options.xAxis || {}), - yAxisOptions = options.yAxis = splat(options.yAxis || {}), - optionsArray; - - // make sure the options are arrays and add some members - each(xAxisOptions, function (axis, i) { - axis.index = i; - axis.isX = true; - }); - - each(yAxisOptions, function (axis, i) { - axis.index = i; - }); - - // concatenate all axis options into one array - optionsArray = xAxisOptions.concat(yAxisOptions); - - each(optionsArray, function (axisOptions) { - new Axis(chart, axisOptions); // eslint-disable-line no-new - }); - }, - - - /** - * Get the currently selected points from all series - */ - getSelectedPoints: function () { - var points = []; - each(this.series, function (serie) { - points = points.concat(grep(serie.points || [], function (point) { - return point.selected; - })); - }); - return points; - }, - - /** - * Get the currently selected series - */ - getSelectedSeries: function () { - return grep(this.series, function (serie) { - return serie.selected; - }); - }, - - /** - * Show the title and subtitle of the chart - * - * @param titleOptions {Object} New title options - * @param subtitleOptions {Object} New subtitle options - * - */ - setTitle: function (titleOptions, subtitleOptions, redraw) { - var chart = this, - options = chart.options, - chartTitleOptions, - chartSubtitleOptions; - - chartTitleOptions = options.title = merge(options.title, titleOptions); - chartSubtitleOptions = options.subtitle = merge(options.subtitle, subtitleOptions); - - // add title and subtitle - each([ - ['title', titleOptions, chartTitleOptions], - ['subtitle', subtitleOptions, chartSubtitleOptions] - ], function (arr) { - var name = arr[0], - title = chart[name], - titleOptions = arr[1], - chartTitleOptions = arr[2]; - - if (title && titleOptions) { - chart[name] = title = title.destroy(); // remove old - } - - if (chartTitleOptions && chartTitleOptions.text && !title) { - chart[name] = chart.renderer.text( - chartTitleOptions.text, - 0, - 0, - chartTitleOptions.useHTML - ) - .attr({ - align: chartTitleOptions.align, - 'class': PREFIX + name, - zIndex: chartTitleOptions.zIndex || 4 - }) - .css(chartTitleOptions.style) - .add(); - - } - }); - chart.layOutTitles(redraw); - }, - - /** - * Lay out the chart titles and cache the full offset height for use in getMargins - */ - layOutTitles: function (redraw) { - var titleOffset = 0, - title = this.title, - subtitle = this.subtitle, - options = this.options, - titleOptions = options.title, - subtitleOptions = options.subtitle, - requiresDirtyBox, - renderer = this.renderer, - spacingBox = this.spacingBox; - - if (title) { - title - .css({ width: (titleOptions.width || spacingBox.width + titleOptions.widthAdjust) + PX }) - .align(extend({ - y: renderer.fontMetrics(titleOptions.style.fontSize, title).b - 3 - }, titleOptions), false, spacingBox); - - if (!titleOptions.floating && !titleOptions.verticalAlign) { - titleOffset = title.getBBox().height; - } - } - if (subtitle) { - subtitle - .css({ width: (subtitleOptions.width || spacingBox.width + subtitleOptions.widthAdjust) + PX }) - .align(extend({ - y: titleOffset + (titleOptions.margin - 13) + renderer.fontMetrics(subtitleOptions.style.fontSize, title).b - }, subtitleOptions), false, spacingBox); - - if (!subtitleOptions.floating && !subtitleOptions.verticalAlign) { - titleOffset = mathCeil(titleOffset + subtitle.getBBox().height); - } - } - - requiresDirtyBox = this.titleOffset !== titleOffset; - this.titleOffset = titleOffset; // used in getMargins - - if (!this.isDirtyBox && requiresDirtyBox) { - this.isDirtyBox = requiresDirtyBox; - // Redraw if necessary (#2719, #2744) - if (this.hasRendered && pick(redraw, true) && this.isDirtyBox) { - this.redraw(); - } - } - }, - - /** - * Get chart width and height according to options and container size - */ - getChartSize: function () { - var chart = this, - optionsChart = chart.options.chart, - widthOption = optionsChart.width, - heightOption = optionsChart.height, - renderTo = chart.renderToClone || chart.renderTo; - - // Get inner width and height - if (!defined(widthOption)) { - chart.containerWidth = getStyle(renderTo, 'width'); - } - if (!defined(heightOption)) { - chart.containerHeight = getStyle(renderTo, 'height'); - } - - chart.chartWidth = mathMax(0, widthOption || chart.containerWidth || 600); // #1393, 1460 - chart.chartHeight = mathMax(0, pick(heightOption, - // the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7: - chart.containerHeight > 19 ? chart.containerHeight : 400)); - }, - - /** - * Create a clone of the chart's renderTo div and place it outside the viewport to allow - * size computation on chart.render and chart.redraw - */ - cloneRenderTo: function (revert) { - var clone = this.renderToClone, - container = this.container; - - // Destroy the clone and bring the container back to the real renderTo div - if (revert) { - if (clone) { - this.renderTo.appendChild(container); - discardElement(clone); - delete this.renderToClone; - } - - // Set up the clone - } else { - if (container && container.parentNode === this.renderTo) { - this.renderTo.removeChild(container); // do not clone this - } - this.renderToClone = clone = this.renderTo.cloneNode(0); - css(clone, { - position: ABSOLUTE, - top: '-9999px', - display: 'block' // #833 - }); - if (clone.style.setProperty) { // #2631 - clone.style.setProperty('display', 'block', 'important'); - } - doc.body.appendChild(clone); - if (container) { - clone.appendChild(container); - } - } - }, - - /** - * Get the containing element, determine the size and create the inner container - * div to hold the chart - */ - getContainer: function () { - var chart = this, - container, - options = chart.options, - optionsChart = options.chart, - chartWidth, - chartHeight, - renderTo = chart.renderTo, - indexAttrName = 'data-highcharts-chart', - oldChartIndex, - Ren, - containerId = 'highcharts-' + idCounter++; - - if (!renderTo) { - chart.renderTo = renderTo = optionsChart.renderTo; - } - - if (isString(renderTo)) { - chart.renderTo = renderTo = doc.getElementById(renderTo); - } - - // Display an error if the renderTo is wrong - if (!renderTo) { - error(13, true); - } - - // If the container already holds a chart, destroy it. The check for hasRendered is there - // because web pages that are saved to disk from the browser, will preserve the data-highcharts-chart - // attribute and the SVG contents, but not an interactive chart. So in this case, - // charts[oldChartIndex] will point to the wrong chart if any (#2609). - oldChartIndex = pInt(attr(renderTo, indexAttrName)); - if (isNumber(oldChartIndex) && charts[oldChartIndex] && charts[oldChartIndex].hasRendered) { - charts[oldChartIndex].destroy(); - } - - // Make a reference to the chart from the div - attr(renderTo, indexAttrName, chart.index); - - // remove previous chart - renderTo.innerHTML = ''; - - // If the container doesn't have an offsetWidth, it has or is a child of a node - // that has display:none. We need to temporarily move it out to a visible - // state to determine the size, else the legend and tooltips won't render - // properly. The allowClone option is used in sparklines as a micro optimization, - // saving about 1-2 ms each chart. - if (!optionsChart.skipClone && !renderTo.offsetWidth) { - chart.cloneRenderTo(); - } - - // get the width and height - chart.getChartSize(); - chartWidth = chart.chartWidth; - chartHeight = chart.chartHeight; - - // create the inner container - chart.container = container = createElement(DIV, { - className: PREFIX + 'container' + - (optionsChart.className ? ' ' + optionsChart.className : ''), - id: containerId - }, extend({ - position: RELATIVE, - overflow: HIDDEN, // needed for context menu (avoid scrollbars) and - // content overflow in IE - width: chartWidth + PX, - height: chartHeight + PX, - textAlign: 'left', - lineHeight: 'normal', // #427 - zIndex: 0, // #1072 - '-webkit-tap-highlight-color': 'rgba(0,0,0,0)' - }, optionsChart.style), - chart.renderToClone || renderTo - ); - - // cache the cursor (#1650) - chart._cursor = container.style.cursor; - - // Initialize the renderer - Ren = Highcharts[optionsChart.renderer] || Renderer; - chart.renderer = new Ren( - container, - chartWidth, - chartHeight, - optionsChart.style, - optionsChart.forExport, - options.exporting && options.exporting.allowHTML - ); - - if (useCanVG) { - // If we need canvg library, extend and configure the renderer - // to get the tracker for translating mouse events - chart.renderer.create(chart, container, chartWidth, chartHeight); - } - // Add a reference to the charts index - chart.renderer.chartIndex = chart.index; - }, - - /** - * Calculate margins by rendering axis labels in a preliminary position. Title, - * subtitle and legend have already been rendered at this stage, but will be - * moved into their final positions - */ - getMargins: function (skipAxes) { - var chart = this, - spacing = chart.spacing, - margin = chart.margin, - titleOffset = chart.titleOffset; - - chart.resetMargins(); - - // Adjust for title and subtitle - if (titleOffset && !defined(margin[0])) { - chart.plotTop = mathMax(chart.plotTop, titleOffset + chart.options.title.margin + spacing[0]); - } - - // Adjust for legend - chart.legend.adjustMargins(margin, spacing); - - // adjust for scroller - if (chart.extraBottomMargin) { - chart.marginBottom += chart.extraBottomMargin; - } - if (chart.extraTopMargin) { - chart.plotTop += chart.extraTopMargin; - } - if (!skipAxes) { - this.getAxisMargins(); - } - }, - - getAxisMargins: function () { - - var chart = this, - axisOffset = chart.axisOffset = [0, 0, 0, 0], // top, right, bottom, left - margin = chart.margin; - - // pre-render axes to get labels offset width - if (chart.hasCartesianSeries) { - each(chart.axes, function (axis) { - if (axis.visible) { - axis.getOffset(); - } - }); - } - - // Add the axis offsets - each(marginNames, function (m, side) { - if (!defined(margin[side])) { - chart[m] += axisOffset[side]; - } - }); - - chart.setChartSize(); - - }, - - /** - * Resize the chart to its container if size is not explicitly set - */ - reflow: function (e) { - var chart = this, - optionsChart = chart.options.chart, - renderTo = chart.renderTo, - width = optionsChart.width || getStyle(renderTo, 'width'), - height = optionsChart.height || getStyle(renderTo, 'height'), - target = e ? e.target : win; - - // Width and height checks for display:none. Target is doc in IE8 and Opera, - // win in Firefox, Chrome and IE9. - if (!chart.hasUserSize && !chart.isPrinting && width && height && (target === win || target === doc)) { // #1093 - if (width !== chart.containerWidth || height !== chart.containerHeight) { - clearTimeout(chart.reflowTimeout); - // When called from window.resize, e is set, else it's called directly (#2224) - chart.reflowTimeout = syncTimeout(function () { - if (chart.container) { // It may have been destroyed in the meantime (#1257) - chart.setSize(width, height, false); - chart.hasUserSize = null; - } - }, e ? 100 : 0); - } - chart.containerWidth = width; - chart.containerHeight = height; - } - }, - - /** - * Add the event handlers necessary for auto resizing - */ - initReflow: function () { - var chart = this, - reflow = function (e) { - chart.reflow(e); - }; - - - addEvent(win, 'resize', reflow); - addEvent(chart, 'destroy', function () { - removeEvent(win, 'resize', reflow); - }); - }, - - /** - * Resize the chart to a given width and height - * @param {Number} width - * @param {Number} height - * @param {Object|Boolean} animation - */ - setSize: function (width, height, animation) { - var chart = this, - chartWidth, - chartHeight, - renderer = chart.renderer, - globalAnimation; - - // Handle the isResizing counter - chart.isResizing += 1; - - // set the animation for the current process - setAnimation(animation, chart); - - chart.oldChartHeight = chart.chartHeight; - chart.oldChartWidth = chart.chartWidth; - if (defined(width)) { - chart.chartWidth = chartWidth = mathMax(0, mathRound(width)); - chart.hasUserSize = !!chartWidth; - } - if (defined(height)) { - chart.chartHeight = chartHeight = mathMax(0, mathRound(height)); - } - - // Resize the container with the global animation applied if enabled (#2503) - globalAnimation = renderer.globalAnimation; - (globalAnimation ? animate : css)(chart.container, { - width: chartWidth + PX, - height: chartHeight + PX - }, globalAnimation); - - chart.setChartSize(true); - renderer.setSize(chartWidth, chartHeight, animation); - - // handle axes - chart.maxTicks = null; - each(chart.axes, function (axis) { - axis.isDirty = true; - axis.setScale(); - }); - - // make sure non-cartesian series are also handled - each(chart.series, function (serie) { - serie.isDirty = true; - }); - - chart.isDirtyLegend = true; // force legend redraw - chart.isDirtyBox = true; // force redraw of plot and chart border - - chart.layOutTitles(); // #2857 - chart.getMargins(); - - chart.redraw(animation); - - - chart.oldChartHeight = null; - fireEvent(chart, 'resize'); - - // Fire endResize and set isResizing back. If animation is disabled, fire without delay - syncTimeout(function () { - if (chart) { - fireEvent(chart, 'endResize', null, function () { - chart.isResizing -= 1; - }); - } - }, animObject(globalAnimation).duration); - }, - - /** - * Set the public chart properties. This is done before and after the pre-render - * to determine margin sizes - */ - setChartSize: function (skipAxes) { - var chart = this, - inverted = chart.inverted, - renderer = chart.renderer, - chartWidth = chart.chartWidth, - chartHeight = chart.chartHeight, - optionsChart = chart.options.chart, - spacing = chart.spacing, - clipOffset = chart.clipOffset, - clipX, - clipY, - plotLeft, - plotTop, - plotWidth, - plotHeight, - plotBorderWidth; - - chart.plotLeft = plotLeft = mathRound(chart.plotLeft); - chart.plotTop = plotTop = mathRound(chart.plotTop); - chart.plotWidth = plotWidth = mathMax(0, mathRound(chartWidth - plotLeft - chart.marginRight)); - chart.plotHeight = plotHeight = mathMax(0, mathRound(chartHeight - plotTop - chart.marginBottom)); - - chart.plotSizeX = inverted ? plotHeight : plotWidth; - chart.plotSizeY = inverted ? plotWidth : plotHeight; - - chart.plotBorderWidth = optionsChart.plotBorderWidth || 0; - - // Set boxes used for alignment - chart.spacingBox = renderer.spacingBox = { - x: spacing[3], - y: spacing[0], - width: chartWidth - spacing[3] - spacing[1], - height: chartHeight - spacing[0] - spacing[2] - }; - chart.plotBox = renderer.plotBox = { - x: plotLeft, - y: plotTop, - width: plotWidth, - height: plotHeight - }; - - plotBorderWidth = 2 * mathFloor(chart.plotBorderWidth / 2); - clipX = mathCeil(mathMax(plotBorderWidth, clipOffset[3]) / 2); - clipY = mathCeil(mathMax(plotBorderWidth, clipOffset[0]) / 2); - chart.clipBox = { - x: clipX, - y: clipY, - width: mathFloor(chart.plotSizeX - mathMax(plotBorderWidth, clipOffset[1]) / 2 - clipX), - height: mathMax(0, mathFloor(chart.plotSizeY - mathMax(plotBorderWidth, clipOffset[2]) / 2 - clipY)) - }; - - if (!skipAxes) { - each(chart.axes, function (axis) { - axis.setAxisSize(); - axis.setAxisTranslation(); - }); - } - }, - - /** - * Initial margins before auto size margins are applied - */ - resetMargins: function () { - var chart = this; - - each(marginNames, function (m, side) { - chart[m] = pick(chart.margin[side], chart.spacing[side]); - }); - chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left - chart.clipOffset = [0, 0, 0, 0]; - }, - - /** - * Draw the borders and backgrounds for chart and plot area - */ - drawChartBox: function () { - var chart = this, - optionsChart = chart.options.chart, - renderer = chart.renderer, - chartWidth = chart.chartWidth, - chartHeight = chart.chartHeight, - chartBackground = chart.chartBackground, - plotBackground = chart.plotBackground, - plotBorder = chart.plotBorder, - plotBGImage = chart.plotBGImage, - chartBorderWidth = optionsChart.borderWidth || 0, - chartBackgroundColor = optionsChart.backgroundColor, - plotBackgroundColor = optionsChart.plotBackgroundColor, - plotBackgroundImage = optionsChart.plotBackgroundImage, - plotBorderWidth = optionsChart.plotBorderWidth || 0, - mgn, - bgAttr, - plotLeft = chart.plotLeft, - plotTop = chart.plotTop, - plotWidth = chart.plotWidth, - plotHeight = chart.plotHeight, - plotBox = chart.plotBox, - clipRect = chart.clipRect, - clipBox = chart.clipBox; - - // Chart area - mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0); - - if (chartBorderWidth || chartBackgroundColor) { - if (!chartBackground) { - - bgAttr = { - fill: chartBackgroundColor || NONE - }; - if (chartBorderWidth) { // #980 - bgAttr.stroke = optionsChart.borderColor; - bgAttr['stroke-width'] = chartBorderWidth; - } - chart.chartBackground = renderer.rect(mgn / 2, mgn / 2, chartWidth - mgn, chartHeight - mgn, - optionsChart.borderRadius, chartBorderWidth) - .attr(bgAttr) - .addClass(PREFIX + 'background') - .add() - .shadow(optionsChart.shadow); - - } else { // resize - chartBackground.animate( - chartBackground.crisp({ width: chartWidth - mgn, height: chartHeight - mgn }) - ); - } - } - - - // Plot background - if (plotBackgroundColor) { - if (!plotBackground) { - chart.plotBackground = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0) - .attr({ - fill: plotBackgroundColor - }) - .add() - .shadow(optionsChart.plotShadow); - } else { - plotBackground.animate(plotBox); - } - } - if (plotBackgroundImage) { - if (!plotBGImage) { - chart.plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight) - .add(); - } else { - plotBGImage.animate(plotBox); - } - } - - // Plot clip - if (!clipRect) { - chart.clipRect = renderer.clipRect(clipBox); - } else { - clipRect.animate({ - width: clipBox.width, - height: clipBox.height - }); - } - - // Plot area border - if (plotBorderWidth) { - if (!plotBorder) { - chart.plotBorder = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0, -plotBorderWidth) - .attr({ - stroke: optionsChart.plotBorderColor, - 'stroke-width': plotBorderWidth, - fill: NONE, - zIndex: 1 - }) - .add(); - } else { - plotBorder.strokeWidth = -plotBorderWidth; - plotBorder.animate( - plotBorder.crisp({ x: plotLeft, y: plotTop, width: plotWidth, height: plotHeight }) //#3282 plotBorder should be negative - ); - } - } - - // reset - chart.isDirtyBox = false; - }, - - /** - * Detect whether a certain chart property is needed based on inspecting its options - * and series. This mainly applies to the chart.invert property, and in extensions to - * the chart.angular and chart.polar properties. - */ - propFromSeries: function () { - var chart = this, - optionsChart = chart.options.chart, - klass, - seriesOptions = chart.options.series, - i, - value; - - - each(['inverted', 'angular', 'polar'], function (key) { - - // The default series type's class - klass = seriesTypes[optionsChart.type || optionsChart.defaultSeriesType]; - - // Get the value from available chart-wide properties - value = ( - chart[key] || // 1. it is set before - optionsChart[key] || // 2. it is set in the options - (klass && klass.prototype[key]) // 3. it's default series class requires it - ); - - // 4. Check if any the chart's series require it - i = seriesOptions && seriesOptions.length; - while (!value && i--) { - klass = seriesTypes[seriesOptions[i].type]; - if (klass && klass.prototype[key]) { - value = true; - } - } - - // Set the chart property - chart[key] = value; - }); - - }, - - /** - * Link two or more series together. This is done initially from Chart.render, - * and after Chart.addSeries and Series.remove. - */ - linkSeries: function () { - var chart = this, - chartSeries = chart.series; - - // Reset links - each(chartSeries, function (series) { - series.linkedSeries.length = 0; - }); - - // Apply new links - each(chartSeries, function (series) { - var linkedTo = series.options.linkedTo; - if (isString(linkedTo)) { - if (linkedTo === ':previous') { - linkedTo = chart.series[series.index - 1]; - } else { - linkedTo = chart.get(linkedTo); - } - if (linkedTo) { - linkedTo.linkedSeries.push(series); - series.linkedParent = linkedTo; - series.visible = pick(series.options.visible, linkedTo.options.visible, series.visible); // #3879 - } - } - }); - }, - - /** - * Render series for the chart - */ - renderSeries: function () { - each(this.series, function (serie) { - serie.translate(); - serie.render(); - }); - }, - - /** - * Render labels for the chart - */ - renderLabels: function () { - var chart = this, - labels = chart.options.labels; - if (labels.items) { - each(labels.items, function (label) { - var style = extend(labels.style, label.style), - x = pInt(style.left) + chart.plotLeft, - y = pInt(style.top) + chart.plotTop + 12; - - // delete to prevent rewriting in IE - delete style.left; - delete style.top; - - chart.renderer.text( - label.html, - x, - y - ) - .attr({ zIndex: 2 }) - .css(style) - .add(); - - }); - } - }, - - /** - * Render all graphics for the chart - */ - render: function () { - var chart = this, - axes = chart.axes, - renderer = chart.renderer, - options = chart.options, - tempWidth, - tempHeight, - redoHorizontal, - redoVertical; - - // Title - chart.setTitle(); - - - // Legend - chart.legend = new Legend(chart, options.legend); - - // Get stacks - if (chart.getStacks) { - chart.getStacks(); - } - - // Get chart margins - chart.getMargins(true); - chart.setChartSize(); - - // Record preliminary dimensions for later comparison - tempWidth = chart.plotWidth; - tempHeight = chart.plotHeight = chart.plotHeight - 21; // 21 is the most common correction for X axis labels - - // Get margins by pre-rendering axes - each(axes, function (axis) { - axis.setScale(); - }); - chart.getAxisMargins(); - - // If the plot area size has changed significantly, calculate tick positions again - redoHorizontal = tempWidth / chart.plotWidth > 1.1; - redoVertical = tempHeight / chart.plotHeight > 1.05; // Height is more sensitive - - if (redoHorizontal || redoVertical) { - - chart.maxTicks = null; // reset for second pass - each(axes, function (axis) { - if ((axis.horiz && redoHorizontal) || (!axis.horiz && redoVertical)) { - axis.setTickInterval(true); // update to reflect the new margins - } - }); - chart.getMargins(); // second pass to check for new labels - } - - // Draw the borders and backgrounds - chart.drawChartBox(); - - - // Axes - if (chart.hasCartesianSeries) { - each(axes, function (axis) { - if (axis.visible) { - axis.render(); - } - }); - } - - // The series - if (!chart.seriesGroup) { - chart.seriesGroup = renderer.g('series-group') - .attr({ zIndex: 3 }) - .add(); - } - chart.renderSeries(); - - // Labels - chart.renderLabels(); - - // Credits - chart.showCredits(options.credits); - - // Set flag - chart.hasRendered = true; - - }, - - /** - * Show chart credits based on config options - */ - showCredits: function (credits) { - if (credits.enabled && !this.credits) { - this.credits = this.renderer.text( - credits.text, - 0, - 0 - ) - .on('click', function () { - if (credits.href) { - win.location.href = credits.href; - } - }) - .attr({ - align: credits.position.align, - zIndex: 8 - }) - .css(credits.style) - .add() - .align(credits.position); - } - }, - - /** - * Clean up memory usage - */ - destroy: function () { - var chart = this, - axes = chart.axes, - series = chart.series, - container = chart.container, - i, - parentNode = container && container.parentNode; - - // fire the chart.destoy event - fireEvent(chart, 'destroy'); - - // Delete the chart from charts lookup array - charts[chart.index] = UNDEFINED; - chartCount--; - chart.renderTo.removeAttribute('data-highcharts-chart'); - - // remove events - removeEvent(chart); - - // ==== Destroy collections: - // Destroy axes - i = axes.length; - while (i--) { - axes[i] = axes[i].destroy(); - } - - // Destroy each series - i = series.length; - while (i--) { - series[i] = series[i].destroy(); - } - - // ==== Destroy chart properties: - each(['title', 'subtitle', 'chartBackground', 'plotBackground', 'plotBGImage', - 'plotBorder', 'seriesGroup', 'clipRect', 'credits', 'pointer', 'scroller', - 'rangeSelector', 'legend', 'resetZoomButton', 'tooltip', 'renderer'], function (name) { - var prop = chart[name]; - - if (prop && prop.destroy) { - chart[name] = prop.destroy(); - } - }); - - // remove container and all SVG - if (container) { // can break in IE when destroyed before finished loading - container.innerHTML = ''; - removeEvent(container); - if (parentNode) { - discardElement(container); - } - - } - - // clean it all up - for (i in chart) { - delete chart[i]; - } - - }, - - - /** - * VML namespaces can't be added until after complete. Listening - * for Perini's doScroll hack is not enough. - */ - isReadyToRender: function () { - var chart = this; - - // Note: win == win.top is required - if ((!hasSVG && (win == win.top && doc.readyState !== 'complete')) || (useCanVG && !win.canvg)) { // eslint-disable-line eqeqeq - if (useCanVG) { - // Delay rendering until canvg library is downloaded and ready - CanVGController.push(function () { - chart.firstRender(); - }, chart.options.global.canvasToolsURL); - } else { - doc.attachEvent('onreadystatechange', function () { - doc.detachEvent('onreadystatechange', chart.firstRender); - if (doc.readyState === 'complete') { - chart.firstRender(); - } - }); - } - return false; - } - return true; - }, - - /** - * Prepare for first rendering after all data are loaded - */ - firstRender: function () { - var chart = this, - options = chart.options; - - // Check whether the chart is ready to render - if (!chart.isReadyToRender()) { - return; - } - - // Create the container - chart.getContainer(); - - // Run an early event after the container and renderer are established - fireEvent(chart, 'init'); - - - chart.resetMargins(); - chart.setChartSize(); - - // Set the common chart properties (mainly invert) from the given series - chart.propFromSeries(); - - // get axes - chart.getAxes(); - - // Initialize the series - each(options.series || [], function (serieOptions) { - chart.initSeries(serieOptions); - }); - - chart.linkSeries(); - - // Run an event after axes and series are initialized, but before render. At this stage, - // the series data is indexed and cached in the xData and yData arrays, so we can access - // those before rendering. Used in Highstock. - fireEvent(chart, 'beforeRender'); - - // depends on inverted and on margins being set - if (Highcharts.Pointer) { - chart.pointer = new Pointer(chart, options); - } - - chart.render(); - - // add canvas - chart.renderer.draw(); - - // Fire the load event if there are no external images - if (!chart.renderer.imgCount && chart.onload) { - chart.onload(); - } - - // If the chart was rendered outside the top container, put it back in (#3679) - chart.cloneRenderTo(true); - - }, - - /** - * On chart load - */ - onload: function () { - var chart = this; - - // Run callbacks - each([this.callback].concat(this.callbacks), function (fn) { - if (fn && chart.index !== undefined) { // Chart destroyed in its own callback (#3600) - fn.apply(chart, [chart]); - } - }); - - fireEvent(chart, 'load'); - - // Don't run again - this.onload = null; - }, - - /** - * Creates arrays for spacing and margin from given options. - */ - splashArray: function (target, options) { - var oVar = options[target], - tArray = isObject(oVar) ? oVar : [oVar, oVar, oVar, oVar]; - - return [pick(options[target + 'Top'], tArray[0]), - pick(options[target + 'Right'], tArray[1]), - pick(options[target + 'Bottom'], tArray[2]), - pick(options[target + 'Left'], tArray[3])]; - } - }; // end Chart - - var CenteredSeriesMixin = Highcharts.CenteredSeriesMixin = { - /** - * Get the center of the pie based on the size and center options relative to the - * plot area. Borrowed by the polar and gauge series types. - */ - getCenter: function () { - - var options = this.options, - chart = this.chart, - slicingRoom = 2 * (options.slicedOffset || 0), - handleSlicingRoom, - plotWidth = chart.plotWidth - 2 * slicingRoom, - plotHeight = chart.plotHeight - 2 * slicingRoom, - centerOption = options.center, - positions = [pick(centerOption[0], '50%'), pick(centerOption[1], '50%'), options.size || '100%', options.innerSize || 0], - smallestSize = mathMin(plotWidth, plotHeight), - i, - value; - - for (i = 0; i < 4; ++i) { - value = positions[i]; - handleSlicingRoom = i < 2 || (i === 2 && /%$/.test(value)); - - // i == 0: centerX, relative to width - // i == 1: centerY, relative to height - // i == 2: size, relative to smallestSize - // i == 3: innerSize, relative to size - positions[i] = relativeLength(value, [plotWidth, plotHeight, smallestSize, positions[2]][i]) + - (handleSlicingRoom ? slicingRoom : 0); - - } - // innerSize cannot be larger than size (#3632) - if (positions[3] > positions[2]) { - positions[3] = positions[2]; - } - return positions; - } - }; - - /** - * The Point object and prototype. Inheritable and used as base for PiePoint - */ - var Point = function () {}; - Point.prototype = { - - /** - * Initialize the point - * @param {Object} series The series object containing this point - * @param {Object} options The data in either number, array or object format - */ - init: function (series, options, x) { - - var point = this, - colors; - point.series = series; - point.color = series.color; // #3445 - point.applyOptions(options, x); - point.pointAttr = {}; - - if (series.options.colorByPoint) { - colors = series.options.colors || series.chart.options.colors; - point.color = point.color || colors[series.colorCounter++]; - // loop back to zero - if (series.colorCounter === colors.length) { - series.colorCounter = 0; - } - } - - series.chart.pointCount++; - return point; - }, - /** - * Apply the options containing the x and y data and possible some extra properties. - * This is called on point init or from point.update. - * - * @param {Object} options - */ - applyOptions: function (options, x) { - var point = this, - series = point.series, - pointValKey = series.options.pointValKey || series.pointValKey; - - options = Point.prototype.optionsToObject.call(this, options); - - // copy options directly to point - extend(point, options); - point.options = point.options ? extend(point.options, options) : options; - - // For higher dimension series types. For instance, for ranges, point.y is mapped to point.low. - if (pointValKey) { - point.y = point[pointValKey]; - } - point.isNull = point.x === null || point.y === null; - - // If no x is set by now, get auto incremented value. All points must have an - // x value, however the y value can be null to create a gap in the series - if (point.x === undefined && series) { - point.x = x === undefined ? series.autoIncrement() : x; - } - - return point; - }, - - /** - * Transform number or array configs into objects - */ - optionsToObject: function (options) { - var ret = {}, - series = this.series, - keys = series.options.keys, - pointArrayMap = keys || series.pointArrayMap || ['y'], - valueCount = pointArrayMap.length, - firstItemType, - i = 0, - j = 0; - - if (isNumber(options) || options === null) { - ret[pointArrayMap[0]] = options; - - } else if (isArray(options)) { - // with leading x value - if (!keys && options.length > valueCount) { - firstItemType = typeof options[0]; - if (firstItemType === 'string') { - ret.name = options[0]; - } else if (firstItemType === 'number') { - ret.x = options[0]; - } - i++; - } - while (j < valueCount) { - if (!keys || options[i] !== undefined) { // Skip undefined positions for keys - ret[pointArrayMap[j]] = options[i]; - } - i++; - j++; - } - } else if (typeof options === 'object') { - ret = options; - - // This is the fastest way to detect if there are individual point dataLabels that need - // to be considered in drawDataLabels. These can only occur in object configs. - if (options.dataLabels) { - series._hasPointLabels = true; - } - - // Same approach as above for markers - if (options.marker) { - series._hasPointMarkers = true; - } - } - return ret; - }, - - /** - * Destroy a point to clear memory. Its reference still stays in series.data. - */ - destroy: function () { - var point = this, - series = point.series, - chart = series.chart, - hoverPoints = chart.hoverPoints, - prop; - - chart.pointCount--; - - if (hoverPoints) { - point.setState(); - erase(hoverPoints, point); - if (!hoverPoints.length) { - chart.hoverPoints = null; - } - - } - if (point === chart.hoverPoint) { - point.onMouseOut(); - } - - // remove all events - if (point.graphic || point.dataLabel) { // removeEvent and destroyElements are performance expensive - removeEvent(point); - point.destroyElements(); - } - - if (point.legendItem) { // pies have legend items - chart.legend.destroyItem(point); - } - - for (prop in point) { - point[prop] = null; - } - - - }, - - /** - * Destroy SVG elements associated with the point - */ - destroyElements: function () { - var point = this, - props = ['graphic', 'dataLabel', 'dataLabelUpper', 'connector', 'shadowGroup'], - prop, - i = 6; - while (i--) { - prop = props[i]; - if (point[prop]) { - point[prop] = point[prop].destroy(); - } - } - }, - - /** - * Return the configuration hash needed for the data label and tooltip formatters - */ - getLabelConfig: function () { - return { - x: this.category, - y: this.y, - color: this.color, - key: this.name || this.category, - series: this.series, - point: this, - percentage: this.percentage, - total: this.total || this.stackTotal - }; - }, - - /** - * Extendable method for formatting each point's tooltip line - * - * @return {String} A string to be concatenated in to the common tooltip text - */ - tooltipFormatter: function (pointFormat) { - - // Insert options for valueDecimals, valuePrefix, and valueSuffix - var series = this.series, - seriesTooltipOptions = series.tooltipOptions, - valueDecimals = pick(seriesTooltipOptions.valueDecimals, ''), - valuePrefix = seriesTooltipOptions.valuePrefix || '', - valueSuffix = seriesTooltipOptions.valueSuffix || ''; - - // Loop over the point array map and replace unformatted values with sprintf formatting markup - each(series.pointArrayMap || ['y'], function (key) { - key = '{point.' + key; // without the closing bracket - if (valuePrefix || valueSuffix) { - pointFormat = pointFormat.replace(key + '}', valuePrefix + key + '}' + valueSuffix); - } - pointFormat = pointFormat.replace(key + '}', key + ':,.' + valueDecimals + 'f}'); - }); - - return format(pointFormat, { - point: this, - series: this.series - }); - }, - - /** - * Fire an event on the Point object. - * @param {String} eventType - * @param {Object} eventArgs Additional event arguments - * @param {Function} defaultFunction Default event handler - */ - firePointEvent: function (eventType, eventArgs, defaultFunction) { - var point = this, - series = this.series, - seriesOptions = series.options; - - // load event handlers on demand to save time on mouseover/out - if (seriesOptions.point.events[eventType] || (point.options && point.options.events && point.options.events[eventType])) { - this.importEvents(); - } - - // add default handler if in selection mode - if (eventType === 'click' && seriesOptions.allowPointSelect) { - defaultFunction = function (event) { - // Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera - if (point.select) { // Could be destroyed by prior event handlers (#2911) - point.select(null, event.ctrlKey || event.metaKey || event.shiftKey); - } - }; - } - - fireEvent(this, eventType, eventArgs, defaultFunction); - }, - visible: true - };/** - * @classDescription The base function which all other series types inherit from. The data in the series is stored - * in various arrays. - * - * - First, series.options.data contains all the original config options for - * each point whether added by options or methods like series.addPoint. - * - Next, series.data contains those values converted to points, but in case the series data length - * exceeds the cropThreshold, or if the data is grouped, series.data doesn't contain all the points. It - * only contains the points that have been created on demand. - * - Then there's series.points that contains all currently visible point objects. In case of cropping, - * the cropped-away points are not part of this array. The series.points array starts at series.cropStart - * compared to series.data and series.options.data. If however the series data is grouped, these can't - * be correlated one to one. - * - series.xData and series.processedXData contain clean x values, equivalent to series.data and series.points. - * - series.yData and series.processedYData contain clean x values, equivalent to series.data and series.points. - * - * @param {Object} chart - * @param {Object} options - */ - var Series = Highcharts.Series = function () {}; - - Series.prototype = { - - isCartesian: true, - type: 'line', - pointClass: Point, - sorted: true, // requires the data to be sorted - requireSorting: true, - pointAttrToOptions: { // mapping between SVG attributes and the corresponding options - stroke: 'lineColor', - 'stroke-width': 'lineWidth', - fill: 'fillColor', - r: 'radius' - }, - directTouch: false, - axisTypes: ['xAxis', 'yAxis'], - colorCounter: 0, - parallelArrays: ['x', 'y'], // each point's x and y values are stored in this.xData and this.yData - init: function (chart, options) { - var series = this, - eventType, - events, - chartSeries = chart.series, - sortByIndex = function (a, b) { - return pick(a.options.index, a._i) - pick(b.options.index, b._i); - }; - - series.chart = chart; - series.options = options = series.setOptions(options); // merge with plotOptions - series.linkedSeries = []; - - // bind the axes - series.bindAxes(); - - // set some variables - extend(series, { - name: options.name, - state: NORMAL_STATE, - pointAttr: {}, - visible: options.visible !== false, // true by default - selected: options.selected === true // false by default - }); - - // special - if (useCanVG) { - options.animation = false; - } - - // register event listeners - events = options.events; - for (eventType in events) { - addEvent(series, eventType, events[eventType]); - } - if ( - (events && events.click) || - (options.point && options.point.events && options.point.events.click) || - options.allowPointSelect - ) { - chart.runTrackerClick = true; - } - - series.getColor(); - series.getSymbol(); - - // Set the data - each(series.parallelArrays, function (key) { - series[key + 'Data'] = []; - }); - series.setData(options.data, false); - - // Mark cartesian - if (series.isCartesian) { - chart.hasCartesianSeries = true; - } - - // Register it in the chart - chartSeries.push(series); - series._i = chartSeries.length - 1; - - // Sort series according to index option (#248, #1123, #2456) - stableSort(chartSeries, sortByIndex); - if (this.yAxis) { - stableSort(this.yAxis.series, sortByIndex); - } - - each(chartSeries, function (series, i) { - series.index = i; - series.name = series.name || 'Series ' + (i + 1); - }); - - }, - - /** - * Set the xAxis and yAxis properties of cartesian series, and register the series - * in the axis.series array - */ - bindAxes: function () { - var series = this, - seriesOptions = series.options, - chart = series.chart, - axisOptions; - - each(series.axisTypes || [], function (AXIS) { // repeat for xAxis and yAxis - - each(chart[AXIS], function (axis) { // loop through the chart's axis objects - axisOptions = axis.options; - - // apply if the series xAxis or yAxis option mathches the number of the - // axis, or if undefined, use the first axis - if ((seriesOptions[AXIS] === axisOptions.index) || - (seriesOptions[AXIS] !== UNDEFINED && seriesOptions[AXIS] === axisOptions.id) || - (seriesOptions[AXIS] === UNDEFINED && axisOptions.index === 0)) { - - // register this series in the axis.series lookup - axis.series.push(series); - - // set this series.xAxis or series.yAxis reference - series[AXIS] = axis; - - // mark dirty for redraw - axis.isDirty = true; - } - }); - - // The series needs an X and an Y axis - if (!series[AXIS] && series.optionalAxis !== AXIS) { - error(18, true); - } - - }); - }, - - /** - * For simple series types like line and column, the data values are held in arrays like - * xData and yData for quick lookup to find extremes and more. For multidimensional series - * like bubble and map, this can be extended with arrays like zData and valueData by - * adding to the series.parallelArrays array. - */ - updateParallelArrays: function (point, i) { - var series = point.series, - args = arguments, - fn = isNumber(i) ? - // Insert the value in the given position - function (key) { - var val = key === 'y' && series.toYData ? series.toYData(point) : point[key]; - series[key + 'Data'][i] = val; - } : - // Apply the method specified in i with the following arguments as arguments - function (key) { - Array.prototype[i].apply(series[key + 'Data'], Array.prototype.slice.call(args, 2)); - }; - - each(series.parallelArrays, fn); - }, - - /** - * Return an auto incremented x value based on the pointStart and pointInterval options. - * This is only used if an x value is not given for the point that calls autoIncrement. - */ - autoIncrement: function () { - - var options = this.options, - xIncrement = this.xIncrement, - date, - pointInterval, - pointIntervalUnit = options.pointIntervalUnit; - - xIncrement = pick(xIncrement, options.pointStart, 0); - - this.pointInterval = pointInterval = pick(this.pointInterval, options.pointInterval, 1); - - // Added code for pointInterval strings - if (pointIntervalUnit) { - date = new Date(xIncrement); - - if (pointIntervalUnit === 'day') { - date = +date[setDate](date[getDate]() + pointInterval); - } else if (pointIntervalUnit === 'month') { - date = +date[setMonth](date[getMonth]() + pointInterval); - } else if (pointIntervalUnit === 'year') { - date = +date[setFullYear](date[getFullYear]() + pointInterval); - } - pointInterval = date - xIncrement; - } - - this.xIncrement = xIncrement + pointInterval; - return xIncrement; - }, - - /** - * Set the series options by merging from the options tree - * @param {Object} itemOptions - */ - setOptions: function (itemOptions) { - var chart = this.chart, - chartOptions = chart.options, - plotOptions = chartOptions.plotOptions, - userOptions = chart.userOptions || {}, - userPlotOptions = userOptions.plotOptions || {}, - typeOptions = plotOptions[this.type], - options, - zones; - - this.userOptions = itemOptions; - - // General series options take precedence over type options because otherwise, default - // type options like column.animation would be overwritten by the general option. - // But issues have been raised here (#3881), and the solution may be to distinguish - // between default option and userOptions like in the tooltip below. - options = merge( - typeOptions, - plotOptions.series, - itemOptions - ); - - // The tooltip options are merged between global and series specific options - this.tooltipOptions = merge( - defaultOptions.tooltip, - defaultOptions.plotOptions[this.type].tooltip, - userOptions.tooltip, - userPlotOptions.series && userPlotOptions.series.tooltip, - userPlotOptions[this.type] && userPlotOptions[this.type].tooltip, - itemOptions.tooltip - ); - - // Delete marker object if not allowed (#1125) - if (typeOptions.marker === null) { - delete options.marker; - } - - // Handle color zones - this.zoneAxis = options.zoneAxis; - zones = this.zones = (options.zones || []).slice(); - if ((options.negativeColor || options.negativeFillColor) && !options.zones) { - zones.push({ - value: options[this.zoneAxis + 'Threshold'] || options.threshold || 0, - color: options.negativeColor, - fillColor: options.negativeFillColor - }); - } - if (zones.length) { // Push one extra zone for the rest - if (defined(zones[zones.length - 1].value)) { - zones.push({ - color: this.color, - fillColor: this.fillColor - }); - } - } - return options; - }, - - getCyclic: function (prop, value, defaults) { - var i, - userOptions = this.userOptions, - indexName = '_' + prop + 'Index', - counterName = prop + 'Counter'; - - if (!value) { - if (defined(userOptions[indexName])) { // after Series.update() - i = userOptions[indexName]; - } else { - userOptions[indexName] = i = this.chart[counterName] % defaults.length; - this.chart[counterName] += 1; - } - value = defaults[i]; - } - this[prop] = value; - }, - - /** - * Get the series' color - */ - getColor: function () { - if (this.options.colorByPoint) { - this.options.color = null; // #4359, selected slice got series.color even when colorByPoint was set. - } else { - this.getCyclic('color', this.options.color || defaultPlotOptions[this.type].color, this.chart.options.colors); - } - }, - /** - * Get the series' symbol - */ - getSymbol: function () { - var seriesMarkerOption = this.options.marker; - - this.getCyclic('symbol', seriesMarkerOption.symbol, this.chart.options.symbols); - - // don't substract radius in image symbols (#604) - if (/^url/.test(this.symbol)) { - seriesMarkerOption.radius = 0; - } - }, - - drawLegendSymbol: LegendSymbolMixin.drawLineMarker, - - /** - * Replace the series data with a new set of data - * @param {Object} data - * @param {Object} redraw - */ - setData: function (data, redraw, animation, updatePoints) { - var series = this, - oldData = series.points, - oldDataLength = (oldData && oldData.length) || 0, - dataLength, - options = series.options, - chart = series.chart, - firstPoint = null, - xAxis = series.xAxis, - hasCategories = xAxis && !!xAxis.categories, - i, - turboThreshold = options.turboThreshold, - pt, - xData = this.xData, - yData = this.yData, - pointArrayMap = series.pointArrayMap, - valueCount = pointArrayMap && pointArrayMap.length; - - data = data || []; - dataLength = data.length; - redraw = pick(redraw, true); - - // If the point count is the same as is was, just run Point.update which is - // cheaper, allows animation, and keeps references to points. - if (updatePoints !== false && dataLength && oldDataLength === dataLength && !series.cropped && !series.hasGroupedData && series.visible) { - each(data, function (point, i) { - // .update doesn't exist on a linked, hidden series (#3709) - if (oldData[i].update && point !== options.data[i]) { - oldData[i].update(point, false, null, false); - } - }); - - } else { - - // Reset properties - series.xIncrement = null; - - series.colorCounter = 0; // for series with colorByPoint (#1547) - - // Update parallel arrays - each(this.parallelArrays, function (key) { - series[key + 'Data'].length = 0; - }); - - // In turbo mode, only one- or twodimensional arrays of numbers are allowed. The - // first value is tested, and we assume that all the rest are defined the same - // way. Although the 'for' loops are similar, they are repeated inside each - // if-else conditional for max performance. - if (turboThreshold && dataLength > turboThreshold) { - - // find the first non-null point - i = 0; - while (firstPoint === null && i < dataLength) { - firstPoint = data[i]; - i++; - } - - - if (isNumber(firstPoint)) { // assume all points are numbers - var x = pick(options.pointStart, 0), - pointInterval = pick(options.pointInterval, 1); - - for (i = 0; i < dataLength; i++) { - xData[i] = x; - yData[i] = data[i]; - x += pointInterval; - } - series.xIncrement = x; - } else if (isArray(firstPoint)) { // assume all points are arrays - if (valueCount) { // [x, low, high] or [x, o, h, l, c] - for (i = 0; i < dataLength; i++) { - pt = data[i]; - xData[i] = pt[0]; - yData[i] = pt.slice(1, valueCount + 1); - } - } else { // [x, y] - for (i = 0; i < dataLength; i++) { - pt = data[i]; - xData[i] = pt[0]; - yData[i] = pt[1]; - } - } - } else { - error(12); // Highcharts expects configs to be numbers or arrays in turbo mode - } - } else { - for (i = 0; i < dataLength; i++) { - if (data[i] !== UNDEFINED) { // stray commas in oldIE - pt = { series: series }; - series.pointClass.prototype.applyOptions.apply(pt, [data[i]]); - series.updateParallelArrays(pt, i); - if (hasCategories && defined(pt.name)) { // #4401 - xAxis.names[pt.x] = pt.name; // #2046 - } - } - } - } - - // Forgetting to cast strings to numbers is a common caveat when handling CSV or JSON - if (isString(yData[0])) { - error(14, true); - } - - series.data = []; - series.options.data = series.userOptions.data = data; - - // destroy old points - i = oldDataLength; - while (i--) { - if (oldData[i] && oldData[i].destroy) { - oldData[i].destroy(); - } - } - - // reset minRange (#878) - if (xAxis) { - xAxis.minRange = xAxis.userMinRange; - } - - // redraw - series.isDirty = series.isDirtyData = chart.isDirtyBox = true; - animation = false; - } - - // Typically for pie series, points need to be processed and generated - // prior to rendering the legend - if (options.legendType === 'point') { - this.processData(); - this.generatePoints(); - } - - if (redraw) { - chart.redraw(animation); - } - }, - - /** - * Process the data by cropping away unused data points if the series is longer - * than the crop threshold. This saves computing time for lage series. - */ - processData: function (force) { - var series = this, - processedXData = series.xData, // copied during slice operation below - processedYData = series.yData, - dataLength = processedXData.length, - croppedData, - cropStart = 0, - cropped, - distance, - closestPointRange, - xAxis = series.xAxis, - i, // loop variable - options = series.options, - cropThreshold = options.cropThreshold, - getExtremesFromAll = series.getExtremesFromAll || options.getExtremesFromAll, // #4599 - isCartesian = series.isCartesian, - xExtremes, - val2lin = xAxis && xAxis.val2lin, - isLog = xAxis && xAxis.isLog, - min, - max; - - // If the series data or axes haven't changed, don't go through this. Return false to pass - // the message on to override methods like in data grouping. - if (isCartesian && !series.isDirty && !xAxis.isDirty && !series.yAxis.isDirty && !force) { - return false; - } - - if (xAxis) { - xExtremes = xAxis.getExtremes(); // corrected for log axis (#3053) - min = xExtremes.min; - max = xExtremes.max; - } - - // optionally filter out points outside the plot area - if (isCartesian && series.sorted && !getExtremesFromAll && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) { - - // it's outside current extremes - if (processedXData[dataLength - 1] < min || processedXData[0] > max) { - processedXData = []; - processedYData = []; - - // only crop if it's actually spilling out - } else if (processedXData[0] < min || processedXData[dataLength - 1] > max) { - croppedData = this.cropData(series.xData, series.yData, min, max); - processedXData = croppedData.xData; - processedYData = croppedData.yData; - cropStart = croppedData.start; - cropped = true; - } - } - - - // Find the closest distance between processed points - i = processedXData.length || 1; - while (--i) { - distance = isLog ? - val2lin(processedXData[i]) - val2lin(processedXData[i - 1]) : - processedXData[i] - processedXData[i - 1]; - - if (distance > 0 && (closestPointRange === UNDEFINED || distance < closestPointRange)) { - closestPointRange = distance; - - // Unsorted data is not supported by the line tooltip, as well as data grouping and - // navigation in Stock charts (#725) and width calculation of columns (#1900) - } else if (distance < 0 && series.requireSorting) { - error(15); - } - } - - // Record the properties - series.cropped = cropped; // undefined or true - series.cropStart = cropStart; - series.processedXData = processedXData; - series.processedYData = processedYData; - - series.closestPointRange = closestPointRange; - - }, - - /** - * Iterate over xData and crop values between min and max. Returns object containing crop start/end - * cropped xData with corresponding part of yData, dataMin and dataMax within the cropped range - */ - cropData: function (xData, yData, min, max) { - var dataLength = xData.length, - cropStart = 0, - cropEnd = dataLength, - cropShoulder = pick(this.cropShoulder, 1), // line-type series need one point outside - i, - j; - - // iterate up to find slice start - for (i = 0; i < dataLength; i++) { - if (xData[i] >= min) { - cropStart = mathMax(0, i - cropShoulder); - break; - } - } - - // proceed to find slice end - for (j = i; j < dataLength; j++) { - if (xData[j] > max) { - cropEnd = j + cropShoulder; - break; - } - } - - return { - xData: xData.slice(cropStart, cropEnd), - yData: yData.slice(cropStart, cropEnd), - start: cropStart, - end: cropEnd - }; - }, - - - /** - * Generate the data point after the data has been processed by cropping away - * unused points and optionally grouped in Highcharts Stock. - */ - generatePoints: function () { - var series = this, - options = series.options, - dataOptions = options.data, - data = series.data, - dataLength, - processedXData = series.processedXData, - processedYData = series.processedYData, - pointClass = series.pointClass, - processedDataLength = processedXData.length, - cropStart = series.cropStart || 0, - cursor, - hasGroupedData = series.hasGroupedData, - point, - points = [], - i; - - if (!data && !hasGroupedData) { - var arr = []; - arr.length = dataOptions.length; - data = series.data = arr; - } - - for (i = 0; i < processedDataLength; i++) { - cursor = cropStart + i; - if (!hasGroupedData) { - if (data[cursor]) { - point = data[cursor]; - } else if (dataOptions[cursor] !== UNDEFINED) { // #970 - data[cursor] = point = (new pointClass()).init(series, dataOptions[cursor], processedXData[i]); - } - points[i] = point; - } else { - // splat the y data in case of ohlc data array - points[i] = (new pointClass()).init(series, [processedXData[i]].concat(splat(processedYData[i]))); - points[i].dataGroup = series.groupMap[i]; - } - points[i].index = cursor; // For faster access in Point.update - } - - // Hide cropped-away points - this only runs when the number of points is above cropThreshold, or when - // swithching view from non-grouped data to grouped data (#637) - if (data && (processedDataLength !== (dataLength = data.length) || hasGroupedData)) { - for (i = 0; i < dataLength; i++) { - if (i === cropStart && !hasGroupedData) { // when has grouped data, clear all points - i += processedDataLength; - } - if (data[i]) { - data[i].destroyElements(); - data[i].plotX = UNDEFINED; // #1003 - } - } - } - - series.data = data; - series.points = points; - }, - - /** - * Calculate Y extremes for visible data - */ - getExtremes: function (yData) { - var xAxis = this.xAxis, - yAxis = this.yAxis, - xData = this.processedXData, - yDataLength, - activeYData = [], - activeCounter = 0, - xExtremes = xAxis.getExtremes(), // #2117, need to compensate for log X axis - xMin = xExtremes.min, - xMax = xExtremes.max, - validValue, - withinRange, - x, - y, - i, - j; - - yData = yData || this.stackedYData || this.processedYData || []; - yDataLength = yData.length; - - for (i = 0; i < yDataLength; i++) { - - x = xData[i]; - y = yData[i]; - - // For points within the visible range, including the first point outside the - // visible range, consider y extremes - validValue = y !== null && y !== UNDEFINED && (!yAxis.isLog || (y.length || y > 0)); - withinRange = this.getExtremesFromAll || this.options.getExtremesFromAll || this.cropped || - ((xData[i + 1] || x) >= xMin && (xData[i - 1] || x) <= xMax); - - if (validValue && withinRange) { - - j = y.length; - if (j) { // array, like ohlc or range data - while (j--) { - if (y[j] !== null) { - activeYData[activeCounter++] = y[j]; - } - } - } else { - activeYData[activeCounter++] = y; - } - } - } - this.dataMin = arrayMin(activeYData); - this.dataMax = arrayMax(activeYData); - }, - - /** - * Translate data points from raw data values to chart specific positioning data - * needed later in drawPoints, drawGraph and drawTracker. - */ - translate: function () { - if (!this.processedXData) { // hidden series - this.processData(); - } - this.generatePoints(); - var series = this, - options = series.options, - stacking = options.stacking, - xAxis = series.xAxis, - categories = xAxis.categories, - yAxis = series.yAxis, - points = series.points, - dataLength = points.length, - hasModifyValue = !!series.modifyValue, - i, - pointPlacement = options.pointPlacement, - dynamicallyPlaced = pointPlacement === 'between' || isNumber(pointPlacement), - threshold = options.threshold, - stackThreshold = options.startFromThreshold ? threshold : 0, - plotX, - plotY, - lastPlotX, - stackIndicator, - closestPointRangePx = Number.MAX_VALUE; - - // Translate each point - for (i = 0; i < dataLength; i++) { - var point = points[i], - xValue = point.x, - yValue = point.y, - yBottom = point.low, - stack = stacking && yAxis.stacks[(series.negStacks && yValue < (stackThreshold ? 0 : threshold) ? '-' : '') + series.stackKey], - pointStack, - stackValues; - - // Discard disallowed y values for log axes (#3434) - if (yAxis.isLog && yValue !== null && yValue <= 0) { - point.y = yValue = null; - error(10); - } - - // Get the plotX translation - point.plotX = plotX = correctFloat( // #5236 - mathMin(mathMax(-1e5, xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement, this.type === 'flags')), 1e5) // #3923 - ); - - // Calculate the bottom y value for stacked series - if (stacking && series.visible && !point.isNull && stack && stack[xValue]) { - stackIndicator = series.getStackIndicator(stackIndicator, xValue, series.index); - pointStack = stack[xValue]; - stackValues = pointStack.points[stackIndicator.key]; - yBottom = stackValues[0]; - yValue = stackValues[1]; - - if (yBottom === stackThreshold) { - yBottom = pick(threshold, yAxis.min); - } - if (yAxis.isLog && yBottom <= 0) { // #1200, #1232 - yBottom = null; - } - - point.total = point.stackTotal = pointStack.total; - point.percentage = pointStack.total && (point.y / pointStack.total * 100); - point.stackY = yValue; - - // Place the stack label - pointStack.setOffset(series.pointXOffset || 0, series.barW || 0); - - } - - // Set translated yBottom or remove it - point.yBottom = defined(yBottom) ? - yAxis.translate(yBottom, 0, 1, 0, 1) : - null; - - // general hook, used for Highstock compare mode - if (hasModifyValue) { - yValue = series.modifyValue(yValue, point); - } - - // Set the the plotY value, reset it for redraws - point.plotY = plotY = (typeof yValue === 'number' && yValue !== Infinity) ? - mathMin(mathMax(-1e5, yAxis.translate(yValue, 0, 1, 0, 1)), 1e5) : // #3201 - UNDEFINED; - point.isInside = plotY !== UNDEFINED && plotY >= 0 && plotY <= yAxis.len && // #3519 - plotX >= 0 && plotX <= xAxis.len; - - - // Set client related positions for mouse tracking - point.clientX = dynamicallyPlaced ? xAxis.translate(xValue, 0, 0, 0, 1) : plotX; // #1514 - - point.negative = point.y < (threshold || 0); - - // some API data - point.category = categories && categories[point.x] !== UNDEFINED ? - categories[point.x] : point.x; - - // Determine auto enabling of markers (#3635, #5099) - if (!point.isNull) { - if (lastPlotX !== undefined) { - closestPointRangePx = mathMin(closestPointRangePx, mathAbs(plotX - lastPlotX)); - } - lastPlotX = plotX; - } - - } - series.closestPointRangePx = closestPointRangePx; - }, - - /** - * Return the series points with null points filtered out - */ - getValidPoints: function (points, insideOnly) { - var chart = this.chart; - return grep(points || this.points || [], function isValidPoint(point) { // #3916, #5029 - if (insideOnly && !chart.isInsidePlot(point.plotX, point.plotY, chart.inverted)) { // #5085 - return false; - } - return !point.isNull; - }); - }, - - /** - * Set the clipping for the series. For animated series it is called twice, first to initiate - * animating the clip then the second time without the animation to set the final clip. - */ - setClip: function (animation) { - var chart = this.chart, - options = this.options, - renderer = chart.renderer, - inverted = chart.inverted, - seriesClipBox = this.clipBox, - clipBox = seriesClipBox || chart.clipBox, - sharedClipKey = this.sharedClipKey || ['_sharedClip', animation && animation.duration, animation && animation.easing, clipBox.height, options.xAxis, options.yAxis].join(','), // #4526 - clipRect = chart[sharedClipKey], - markerClipRect = chart[sharedClipKey + 'm']; - - // If a clipping rectangle with the same properties is currently present in the chart, use that. - if (!clipRect) { - - // When animation is set, prepare the initial positions - if (animation) { - clipBox.width = 0; - - chart[sharedClipKey + 'm'] = markerClipRect = renderer.clipRect( - -99, // include the width of the first marker - inverted ? -chart.plotLeft : -chart.plotTop, - 99, - inverted ? chart.chartWidth : chart.chartHeight - ); - } - chart[sharedClipKey] = clipRect = renderer.clipRect(clipBox); - - } - if (animation) { - clipRect.count += 1; - } - - if (options.clip !== false) { - this.group.clip(animation || seriesClipBox ? clipRect : chart.clipRect); - this.markerGroup.clip(markerClipRect); - this.sharedClipKey = sharedClipKey; - } - - // Remove the shared clipping rectangle when all series are shown - if (!animation) { - clipRect.count -= 1; - if (clipRect.count <= 0 && sharedClipKey && chart[sharedClipKey]) { - if (!seriesClipBox) { - chart[sharedClipKey] = chart[sharedClipKey].destroy(); - } - if (chart[sharedClipKey + 'm']) { - chart[sharedClipKey + 'm'] = chart[sharedClipKey + 'm'].destroy(); - } - } - } - }, - - /** - * Animate in the series - */ - animate: function (init) { - var series = this, - chart = series.chart, - clipRect, - animation = series.options.animation, - sharedClipKey; - - // Animation option is set to true - if (animation && !isObject(animation)) { - animation = defaultPlotOptions[series.type].animation; - } - - // Initialize the animation. Set up the clipping rectangle. - if (init) { - - series.setClip(animation); - - // Run the animation - } else { - sharedClipKey = this.sharedClipKey; - clipRect = chart[sharedClipKey]; - if (clipRect) { - clipRect.animate({ - width: chart.plotSizeX - }, animation); - } - if (chart[sharedClipKey + 'm']) { - chart[sharedClipKey + 'm'].animate({ - width: chart.plotSizeX + 99 - }, animation); - } - - // Delete this function to allow it only once - series.animate = null; - - } - }, - - /** - * This runs after animation to land on the final plot clipping - */ - afterAnimate: function () { - this.setClip(); - fireEvent(this, 'afterAnimate'); - }, - - /** - * Draw the markers - */ - drawPoints: function () { - var series = this, - pointAttr, - points = series.points, - chart = series.chart, - plotX, - plotY, - i, - point, - radius, - symbol, - isImage, - graphic, - options = series.options, - seriesMarkerOptions = options.marker, - seriesPointAttr = series.pointAttr[''], - pointMarkerOptions, - hasPointMarker, - enabled, - isInside, - markerGroup = series.markerGroup, - xAxis = series.xAxis, - globallyEnabled = pick( - seriesMarkerOptions.enabled, - xAxis.isRadial, - series.closestPointRangePx > 2 * seriesMarkerOptions.radius - ); - - if (seriesMarkerOptions.enabled !== false || series._hasPointMarkers) { - - i = points.length; - while (i--) { - point = points[i]; - plotX = mathFloor(point.plotX); // #1843 - plotY = point.plotY; - graphic = point.graphic; - pointMarkerOptions = point.marker || {}; - hasPointMarker = !!point.marker; - enabled = (globallyEnabled && pointMarkerOptions.enabled === UNDEFINED) || pointMarkerOptions.enabled; - isInside = point.isInside; - - // only draw the point if y is defined - if (enabled && isNumber(plotY) && point.y !== null) { - - // shortcuts - pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE] || seriesPointAttr; - radius = pointAttr.r; - symbol = pick(pointMarkerOptions.symbol, series.symbol); - isImage = symbol.indexOf('url') === 0; - - if (graphic) { // update - graphic[isInside ? 'show' : 'hide'](true) // Since the marker group isn't clipped, each individual marker must be toggled - .attr(pointAttr) // #4759 - .animate(extend({ - x: plotX - radius, - y: plotY - radius - }, graphic.symbolName ? { // don't apply to image symbols #507 - width: 2 * radius, - height: 2 * radius - } : {})); - } else if (isInside && (radius > 0 || isImage)) { - point.graphic = graphic = chart.renderer.symbol( - symbol, - plotX - radius, - plotY - radius, - 2 * radius, - 2 * radius, - hasPointMarker ? pointMarkerOptions : seriesMarkerOptions - ) - .attr(pointAttr) - .add(markerGroup); - } - - } else if (graphic) { - point.graphic = graphic.destroy(); // #1269 - } - } - } - - }, - - /** - * Convert state properties from API naming conventions to SVG attributes - * - * @param {Object} options API options object - * @param {Object} base1 SVG attribute object to inherit from - * @param {Object} base2 Second level SVG attribute object to inherit from - */ - convertAttribs: function (options, base1, base2, base3) { - var conversion = this.pointAttrToOptions, - attr, - option, - obj = {}; - - options = options || {}; - base1 = base1 || {}; - base2 = base2 || {}; - base3 = base3 || {}; - - for (attr in conversion) { - option = conversion[attr]; - obj[attr] = pick(options[option], base1[attr], base2[attr], base3[attr]); - } - return obj; - }, - - /** - * Get the state attributes. Each series type has its own set of attributes - * that are allowed to change on a point's state change. Series wide attributes are stored for - * all series, and additionally point specific attributes are stored for all - * points with individual marker options. If such options are not defined for the point, - * a reference to the series wide attributes is stored in point.pointAttr. - */ - getAttribs: function () { - var series = this, - seriesOptions = series.options, - normalOptions = defaultPlotOptions[series.type].marker ? seriesOptions.marker : seriesOptions, - stateOptions = normalOptions.states, - stateOptionsHover = stateOptions[HOVER_STATE], - pointStateOptionsHover, - seriesColor = series.color, - seriesNegativeColor = series.options.negativeColor, - normalDefaults = { - stroke: seriesColor, - fill: seriesColor - }, - points = series.points || [], // #927 - i, - j, - threshold, - point, - seriesPointAttr = [], - pointAttr, - pointAttrToOptions = series.pointAttrToOptions, - hasPointSpecificOptions = series.hasPointSpecificOptions, - defaultLineColor = normalOptions.lineColor, - defaultFillColor = normalOptions.fillColor, - turboThreshold = seriesOptions.turboThreshold, - zones = series.zones, - zoneAxis = series.zoneAxis || 'y', - zoneColor, - attr, - key; - - // series type specific modifications - if (seriesOptions.marker) { // line, spline, area, areaspline, scatter - - // if no hover radius is given, default to normal radius + 2 - stateOptionsHover.radius = stateOptionsHover.radius || normalOptions.radius + stateOptionsHover.radiusPlus; - stateOptionsHover.lineWidth = stateOptionsHover.lineWidth || normalOptions.lineWidth + stateOptionsHover.lineWidthPlus; - - } else { // column, bar, pie - - // if no hover color is given, brighten the normal color - stateOptionsHover.color = stateOptionsHover.color || - Color(stateOptionsHover.color || seriesColor) - .brighten(stateOptionsHover.brightness).get(); - - // if no hover negativeColor is given, brighten the normal negativeColor - stateOptionsHover.negativeColor = stateOptionsHover.negativeColor || - Color(stateOptionsHover.negativeColor || seriesNegativeColor) - .brighten(stateOptionsHover.brightness).get(); - } - - // general point attributes for the series normal state - seriesPointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, normalDefaults); - - // HOVER_STATE and SELECT_STATE states inherit from normal state except the default radius - each([HOVER_STATE, SELECT_STATE], function (state) { - seriesPointAttr[state] = - series.convertAttribs(stateOptions[state], seriesPointAttr[NORMAL_STATE]); - }); - - // set it - series.pointAttr = seriesPointAttr; - - - // Generate the point-specific attribute collections if specific point - // options are given. If not, create a referance to the series wide point - // attributes - i = points.length; - if (!turboThreshold || i < turboThreshold || hasPointSpecificOptions) { - while (i--) { - point = points[i]; - normalOptions = (point.options && point.options.marker) || point.options; - if (normalOptions && normalOptions.enabled === false) { - normalOptions.radius = 0; - } - - zoneColor = null; - if (zones.length) { - j = 0; - threshold = zones[j]; - while (point[zoneAxis] >= threshold.value) { - threshold = zones[++j]; - } - - point.color = point.fillColor = zoneColor = pick(threshold.color, series.color); // #3636, #4267, #4430 - inherit color from series, when color is undefined - - } - - hasPointSpecificOptions = seriesOptions.colorByPoint || point.color; // #868 - - // check if the point has specific visual options - if (point.options) { - for (key in pointAttrToOptions) { - if (defined(normalOptions[pointAttrToOptions[key]])) { - hasPointSpecificOptions = true; - } - } - } - - // a specific marker config object is defined for the individual point: - // create it's own attribute collection - if (hasPointSpecificOptions) { - normalOptions = normalOptions || {}; - pointAttr = []; - stateOptions = normalOptions.states || {}; // reassign for individual point - pointStateOptionsHover = stateOptions[HOVER_STATE] = stateOptions[HOVER_STATE] || {}; - - // Handle colors for column and pies - if (!seriesOptions.marker || (point.negative && !pointStateOptionsHover.fillColor && !stateOptionsHover.fillColor)) { // column, bar, point or negative threshold for series with markers (#3636) - // If no hover color is given, brighten the normal color. #1619, #2579 - pointStateOptionsHover[series.pointAttrToOptions.fill] = pointStateOptionsHover.color || (!point.options.color && stateOptionsHover[(point.negative && seriesNegativeColor ? 'negativeColor' : 'color')]) || - Color(point.color) - .brighten(pointStateOptionsHover.brightness || stateOptionsHover.brightness) - .get(); - } - - // normal point state inherits series wide normal state - attr = { color: point.color }; // #868 - if (!defaultFillColor) { // Individual point color or negative color markers (#2219) - attr.fillColor = point.color; - } - if (!defaultLineColor) { - attr.lineColor = point.color; // Bubbles take point color, line markers use white - } - // Color is explicitly set to null or undefined (#1288, #4068) - if (normalOptions.hasOwnProperty('color') && !normalOptions.color) { - delete normalOptions.color; - } - - // When zone is set, but series.states.hover.color is not set, apply zone color on hover, #4670: - if (zoneColor && !stateOptionsHover.fillColor) { - pointStateOptionsHover.fillColor = zoneColor; - } - - pointAttr[NORMAL_STATE] = series.convertAttribs(extend(attr, normalOptions), seriesPointAttr[NORMAL_STATE]); - - // inherit from point normal and series hover - pointAttr[HOVER_STATE] = series.convertAttribs( - stateOptions[HOVER_STATE], - seriesPointAttr[HOVER_STATE], - pointAttr[NORMAL_STATE] - ); - - // inherit from point normal and series hover - pointAttr[SELECT_STATE] = series.convertAttribs( - stateOptions[SELECT_STATE], - seriesPointAttr[SELECT_STATE], - pointAttr[NORMAL_STATE] - ); - - - // no marker config object is created: copy a reference to the series-wide - // attribute collection - } else { - pointAttr = seriesPointAttr; - } - - point.pointAttr = pointAttr; - } - } - }, - - /** - * Clear DOM objects and free up memory - */ - destroy: function () { - var series = this, - chart = series.chart, - issue134 = /AppleWebKit\/533/.test(userAgent), - destroy, - i, - data = series.data || [], - point, - prop, - axis; - - // add event hook - fireEvent(series, 'destroy'); - - // remove all events - removeEvent(series); - - // erase from axes - each(series.axisTypes || [], function (AXIS) { - axis = series[AXIS]; - if (axis) { - erase(axis.series, series); - axis.isDirty = axis.forceRedraw = true; - } - }); - - // remove legend items - if (series.legendItem) { - series.chart.legend.destroyItem(series); - } - - // destroy all points with their elements - i = data.length; - while (i--) { - point = data[i]; - if (point && point.destroy) { - point.destroy(); - } - } - series.points = null; - - // Clear the animation timeout if we are destroying the series during initial animation - clearTimeout(series.animationTimeout); - - // Destroy all SVGElements associated to the series - for (prop in series) { - if (series[prop] instanceof SVGElement && !series[prop].survive) { // Survive provides a hook for not destroying - - // issue 134 workaround - destroy = issue134 && prop === 'group' ? - 'hide' : - 'destroy'; - - series[prop][destroy](); - } - } - - // remove from hoverSeries - if (chart.hoverSeries === series) { - chart.hoverSeries = null; - } - erase(chart.series, series); - - // clear all members - for (prop in series) { - delete series[prop]; - } - }, - - /** - * Get the graph path - */ - getGraphPath: function (points, nullsAsZeroes, connectCliffs) { - var series = this, - options = series.options, - step = options.step, - reversed, - graphPath = [], - gap; - - points = points || series.points; - - // Bottom of a stack is reversed - reversed = points.reversed; - if (reversed) { - points.reverse(); - } - // Reverse the steps (#5004) - step = { right: 1, center: 2 }[step] || (step && 3); - if (step && reversed) { - step = 4 - step; - } - - // Remove invalid points, especially in spline (#5015) - if (options.connectNulls && !nullsAsZeroes && !connectCliffs) { - points = this.getValidPoints(points); - } - - // Build the line - each(points, function (point, i) { - - var plotX = point.plotX, - plotY = point.plotY, - lastPoint = points[i - 1], - pathToPoint; // the path to this point from the previous - - if ((point.leftCliff || (lastPoint && lastPoint.rightCliff)) && !connectCliffs) { - gap = true; // ... and continue - } - - // Line series, nullsAsZeroes is not handled - if (point.isNull && !defined(nullsAsZeroes) && i > 0) { - gap = !options.connectNulls; - - // Area series, nullsAsZeroes is set - } else if (point.isNull && !nullsAsZeroes) { - gap = true; - - } else { - - if (i === 0 || gap) { - pathToPoint = [M, point.plotX, point.plotY]; - - } else if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object - - pathToPoint = series.getPointSpline(points, point, i); - - } else if (step) { - - if (step === 1) { // right - pathToPoint = [ - L, - lastPoint.plotX, - plotY - ]; - - } else if (step === 2) { // center - pathToPoint = [ - L, - (lastPoint.plotX + plotX) / 2, - lastPoint.plotY, - L, - (lastPoint.plotX + plotX) / 2, - plotY - ]; - - } else { - pathToPoint = [ - L, - plotX, - lastPoint.plotY - ]; - } - pathToPoint.push(L, plotX, plotY); - - } else { - // normal line to next point - pathToPoint = [ - L, - plotX, - plotY - ]; - } - - - graphPath.push.apply(graphPath, pathToPoint); - gap = false; - } - }); - - series.graphPath = graphPath; - - return graphPath; - - }, - - /** - * Draw the actual graph - */ - drawGraph: function () { - var series = this, - options = this.options, - props = [['graph', options.lineColor || this.color, options.dashStyle]], - lineWidth = options.lineWidth, - roundCap = options.linecap !== 'square', - graphPath = (this.gappedPath || this.getGraphPath).call(this), - fillColor = (this.fillGraph && this.color) || NONE, // polygon series use filled graph - zones = this.zones; - - each(zones, function (threshold, i) { - props.push(['zoneGraph' + i, threshold.color || series.color, threshold.dashStyle || options.dashStyle]); - }); - - // Draw the graph - each(props, function (prop, i) { - var graphKey = prop[0], - graph = series[graphKey], - attribs; - - if (graph) { - graph.animate({ d: graphPath }); - - } else if ((lineWidth || fillColor) && graphPath.length) { // #1487 - attribs = { - stroke: prop[1], - 'stroke-width': lineWidth, - fill: fillColor, - zIndex: 1 // #1069 - }; - if (prop[2]) { - attribs.dashstyle = prop[2]; - } else if (roundCap) { - attribs['stroke-linecap'] = attribs['stroke-linejoin'] = 'round'; - } - - series[graphKey] = series.chart.renderer.path(graphPath) - .attr(attribs) - .add(series.group) - .shadow((i < 2) && options.shadow); // add shadow to normal series (0) or to first zone (1) #3932 - } - }); - }, - - /** - * Clip the graphs into the positive and negative coloured graphs - */ - applyZones: function () { - var series = this, - chart = this.chart, - renderer = chart.renderer, - zones = this.zones, - translatedFrom, - translatedTo, - clips = this.clips || [], - clipAttr, - graph = this.graph, - area = this.area, - chartSizeMax = mathMax(chart.chartWidth, chart.chartHeight), - axis = this[(this.zoneAxis || 'y') + 'Axis'], - extremes, - reversed = axis.reversed, - inverted = chart.inverted, - horiz = axis.horiz, - pxRange, - pxPosMin, - pxPosMax, - ignoreZones = false; - - if (zones.length && (graph || area) && axis.min !== UNDEFINED) { - // The use of the Color Threshold assumes there are no gaps - // so it is safe to hide the original graph and area - if (graph) { - graph.hide(); - } - if (area) { - area.hide(); - } - - // Create the clips - extremes = axis.getExtremes(); - each(zones, function (threshold, i) { - - translatedFrom = reversed ? - (horiz ? chart.plotWidth : 0) : - (horiz ? 0 : axis.toPixels(extremes.min)); - translatedFrom = mathMin(mathMax(pick(translatedTo, translatedFrom), 0), chartSizeMax); - translatedTo = mathMin(mathMax(mathRound(axis.toPixels(pick(threshold.value, extremes.max), true)), 0), chartSizeMax); - - if (ignoreZones) { - translatedFrom = translatedTo = axis.toPixels(extremes.max); - } - - pxRange = Math.abs(translatedFrom - translatedTo); - pxPosMin = mathMin(translatedFrom, translatedTo); - pxPosMax = mathMax(translatedFrom, translatedTo); - if (axis.isXAxis) { - clipAttr = { - x: inverted ? pxPosMax : pxPosMin, - y: 0, - width: pxRange, - height: chartSizeMax - }; - if (!horiz) { - clipAttr.x = chart.plotHeight - clipAttr.x; - } - } else { - clipAttr = { - x: 0, - y: inverted ? pxPosMax : pxPosMin, - width: chartSizeMax, - height: pxRange - }; - if (horiz) { - clipAttr.y = chart.plotWidth - clipAttr.y; - } - } - - /// VML SUPPPORT - if (chart.inverted && renderer.isVML) { - if (axis.isXAxis) { - clipAttr = { - x: 0, - y: reversed ? pxPosMin : pxPosMax, - height: clipAttr.width, - width: chart.chartWidth - }; - } else { - clipAttr = { - x: clipAttr.y - chart.plotLeft - chart.spacingBox.x, - y: 0, - width: clipAttr.height, - height: chart.chartHeight - }; - } - } - /// END OF VML SUPPORT - - if (clips[i]) { - clips[i].animate(clipAttr); - } else { - clips[i] = renderer.clipRect(clipAttr); - - if (graph) { - series['zoneGraph' + i].clip(clips[i]); - } - - if (area) { - series['zoneArea' + i].clip(clips[i]); - } - } - // if this zone extends out of the axis, ignore the others - ignoreZones = threshold.value > extremes.max; - }); - this.clips = clips; - } - }, - - /** - * Initialize and perform group inversion on series.group and series.markerGroup - */ - invertGroups: function () { - var series = this, - chart = series.chart; - - // Pie, go away (#1736) - if (!series.xAxis) { - return; - } - - // A fixed size is needed for inversion to work - function setInvert() { - var size = { - width: series.yAxis.len, - height: series.xAxis.len - }; - - each(['group', 'markerGroup'], function (groupName) { - if (series[groupName]) { - series[groupName].attr(size).invert(); - } - }); - } - - addEvent(chart, 'resize', setInvert); // do it on resize - addEvent(series, 'destroy', function () { - removeEvent(chart, 'resize', setInvert); - }); - - // Do it now - setInvert(); // do it now - - // On subsequent render and redraw, just do setInvert without setting up events again - series.invertGroups = setInvert; - }, - - /** - * General abstraction for creating plot groups like series.group, series.dataLabelsGroup and - * series.markerGroup. On subsequent calls, the group will only be adjusted to the updated plot size. - */ - plotGroup: function (prop, name, visibility, zIndex, parent) { - var group = this[prop], - isNew = !group; - - // Generate it on first call - if (isNew) { - this[prop] = group = this.chart.renderer.g(name) - .attr({ - zIndex: zIndex || 0.1 // IE8 and pointer logic use this - }) - .add(parent); - - group.addClass('highcharts-series-' + this.index); - } - - // Place it on first and subsequent (redraw) calls - group.attr({ visibility: visibility })[isNew ? 'attr' : 'animate'](this.getPlotBox()); - return group; - }, - - /** - * Get the translation and scale for the plot area of this series - */ - getPlotBox: function () { - var chart = this.chart, - xAxis = this.xAxis, - yAxis = this.yAxis; - - // Swap axes for inverted (#2339) - if (chart.inverted) { - xAxis = yAxis; - yAxis = this.xAxis; - } - return { - translateX: xAxis ? xAxis.left : chart.plotLeft, - translateY: yAxis ? yAxis.top : chart.plotTop, - scaleX: 1, // #1623 - scaleY: 1 - }; - }, - - /** - * Render the graph and markers - */ - render: function () { - var series = this, - chart = series.chart, - group, - options = series.options, - // Animation doesn't work in IE8 quirks when the group div is hidden, - // and looks bad in other oldIE - animDuration = !!series.animate && chart.renderer.isSVG && animObject(options.animation).duration, - visibility = series.visible ? 'inherit' : 'hidden', // #2597 - zIndex = options.zIndex, - hasRendered = series.hasRendered, - chartSeriesGroup = chart.seriesGroup; - - // the group - group = series.plotGroup( - 'group', - 'series', - visibility, - zIndex, - chartSeriesGroup - ); - - series.markerGroup = series.plotGroup( - 'markerGroup', - 'markers', - visibility, - zIndex, - chartSeriesGroup - ); - - // initiate the animation - if (animDuration) { - series.animate(true); - } - - // cache attributes for shapes - series.getAttribs(); - - // SVGRenderer needs to know this before drawing elements (#1089, #1795) - group.inverted = series.isCartesian ? chart.inverted : false; - - // draw the graph if any - if (series.drawGraph) { - series.drawGraph(); - series.applyZones(); - } - - each(series.points, function (point) { - if (point.redraw) { - point.redraw(); - } - }); - - // draw the data labels (inn pies they go before the points) - if (series.drawDataLabels) { - series.drawDataLabels(); - } - - // draw the points - if (series.visible) { - series.drawPoints(); - } - - - // draw the mouse tracking area - if (series.drawTracker && series.options.enableMouseTracking !== false) { - series.drawTracker(); - } - - // Handle inverted series and tracker groups - if (chart.inverted) { - series.invertGroups(); - } - - // Initial clipping, must be defined after inverting groups for VML. Applies to columns etc. (#3839). - if (options.clip !== false && !series.sharedClipKey && !hasRendered) { - group.clip(chart.clipRect); - } - - // Run the animation - if (animDuration) { - series.animate(); - } - - // Call the afterAnimate function on animation complete (but don't overwrite the animation.complete option - // which should be available to the user). - if (!hasRendered) { - series.animationTimeout = syncTimeout(function () { - series.afterAnimate(); - }, animDuration); - } - - series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see - // (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see - series.hasRendered = true; - }, - - /** - * Redraw the series after an update in the axes. - */ - redraw: function () { - var series = this, - chart = series.chart, - wasDirty = series.isDirty || series.isDirtyData, // cache it here as it is set to false in render, but used after - group = series.group, - xAxis = series.xAxis, - yAxis = series.yAxis; - - // reposition on resize - if (group) { - if (chart.inverted) { - group.attr({ - width: chart.plotWidth, - height: chart.plotHeight - }); - } - - group.animate({ - translateX: pick(xAxis && xAxis.left, chart.plotLeft), - translateY: pick(yAxis && yAxis.top, chart.plotTop) - }); - } - - series.translate(); - series.render(); - if (wasDirty) { // #3868, #3945 - delete this.kdTree; - } - }, - - /** - * KD Tree && PointSearching Implementation - */ - - kdDimensions: 1, - kdAxisArray: ['clientX', 'plotY'], - - searchPoint: function (e, compareX) { - var series = this, - xAxis = series.xAxis, - yAxis = series.yAxis, - inverted = series.chart.inverted; - - return this.searchKDTree({ - clientX: inverted ? xAxis.len - e.chartY + xAxis.pos : e.chartX - xAxis.pos, - plotY: inverted ? yAxis.len - e.chartX + yAxis.pos : e.chartY - yAxis.pos - }, compareX); - }, - - buildKDTree: function () { - var series = this, - dimensions = series.kdDimensions; - - // Internal function - function _kdtree(points, depth, dimensions) { - var axis, - median, - length = points && points.length; - - if (length) { - - // alternate between the axis - axis = series.kdAxisArray[depth % dimensions]; - - // sort point array - points.sort(function (a, b) { - return a[axis] - b[axis]; - }); - - median = Math.floor(length / 2); - - // build and return nod - return { - point: points[median], - left: _kdtree(points.slice(0, median), depth + 1, dimensions), - right: _kdtree(points.slice(median + 1), depth + 1, dimensions) - }; - - } - } - - // Start the recursive build process with a clone of the points array and null points filtered out (#3873) - function startRecursive() { - series.kdTree = _kdtree( - series.getValidPoints( - null, - !series.directTouch // For line-type series restrict to plot area, but column-type series not (#3916, #4511) - ), - dimensions, - dimensions - ); - } - delete series.kdTree; - - // For testing tooltips, don't build async - syncTimeout(startRecursive, series.options.kdNow ? 0 : 1); - }, - - searchKDTree: function (point, compareX) { - var series = this, - kdX = this.kdAxisArray[0], - kdY = this.kdAxisArray[1], - kdComparer = compareX ? 'distX' : 'dist'; - - // Set the one and two dimensional distance on the point object - function setDistance(p1, p2) { - var x = (defined(p1[kdX]) && defined(p2[kdX])) ? Math.pow(p1[kdX] - p2[kdX], 2) : null, - y = (defined(p1[kdY]) && defined(p2[kdY])) ? Math.pow(p1[kdY] - p2[kdY], 2) : null, - r = (x || 0) + (y || 0); - - p2.dist = defined(r) ? Math.sqrt(r) : Number.MAX_VALUE; - p2.distX = defined(x) ? Math.sqrt(x) : Number.MAX_VALUE; - } - function _search(search, tree, depth, dimensions) { - var point = tree.point, - axis = series.kdAxisArray[depth % dimensions], - tdist, - sideA, - sideB, - ret = point, - nPoint1, - nPoint2; - - setDistance(search, point); - - // Pick side based on distance to splitting point - tdist = search[axis] - point[axis]; - sideA = tdist < 0 ? 'left' : 'right'; - sideB = tdist < 0 ? 'right' : 'left'; - - // End of tree - if (tree[sideA]) { - nPoint1 = _search(search, tree[sideA], depth + 1, dimensions); - - ret = (nPoint1[kdComparer] < ret[kdComparer] ? nPoint1 : point); - } - if (tree[sideB]) { - // compare distance to current best to splitting point to decide wether to check side B or not - if (Math.sqrt(tdist * tdist) < ret[kdComparer]) { - nPoint2 = _search(search, tree[sideB], depth + 1, dimensions); - ret = (nPoint2[kdComparer] < ret[kdComparer] ? nPoint2 : ret); - } - } - - return ret; - } - - if (!this.kdTree) { - this.buildKDTree(); - } - - if (this.kdTree) { - return _search(point, - this.kdTree, this.kdDimensions, this.kdDimensions); - } - } - - }; // end Series prototype - - /** - * The class for stack items - */ - function StackItem(axis, options, isNegative, x, stackOption) { - - var inverted = axis.chart.inverted; - - this.axis = axis; - - // Tells if the stack is negative - this.isNegative = isNegative; - - // Save the options to be able to style the label - this.options = options; - - // Save the x value to be able to position the label later - this.x = x; - - // Initialize total value - this.total = null; - - // This will keep each points' extremes stored by series.index and point index - this.points = {}; - - // Save the stack option on the series configuration object, and whether to treat it as percent - this.stack = stackOption; - this.leftCliff = 0; - this.rightCliff = 0; - - // The align options and text align varies on whether the stack is negative and - // if the chart is inverted or not. - // First test the user supplied value, then use the dynamic. - this.alignOptions = { - align: options.align || (inverted ? (isNegative ? 'left' : 'right') : 'center'), - verticalAlign: options.verticalAlign || (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')), - y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)), - x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0) - }; - - this.textAlign = options.textAlign || (inverted ? (isNegative ? 'right' : 'left') : 'center'); - } - - StackItem.prototype = { - destroy: function () { - destroyObjectProperties(this, this.axis); - }, - - /** - * Renders the stack total label and adds it to the stack label group. - */ - render: function (group) { - var options = this.options, - formatOption = options.format, - str = formatOption ? - format(formatOption, this) : - options.formatter.call(this); // format the text in the label - - // Change the text to reflect the new total and set visibility to hidden in case the serie is hidden - if (this.label) { - this.label.attr({ text: str, visibility: 'hidden' }); - // Create new label - } else { - this.label = - this.axis.chart.renderer.text(str, null, null, options.useHTML) // dummy positions, actual position updated with setOffset method in columnseries - .css(options.style) // apply style - .attr({ - align: this.textAlign, // fix the text-anchor - rotation: options.rotation, // rotation - visibility: HIDDEN // hidden until setOffset is called - }) - .add(group); // add to the labels-group - } - }, - - /** - * Sets the offset that the stack has from the x value and repositions the label. - */ - setOffset: function (xOffset, xWidth) { - var stackItem = this, - axis = stackItem.axis, - chart = axis.chart, - inverted = chart.inverted, - reversed = axis.reversed, - neg = (this.isNegative && !reversed) || (!this.isNegative && reversed), // #4056 - y = axis.translate(axis.usePercentage ? 100 : this.total, 0, 0, 0, 1), // stack value translated mapped to chart coordinates - yZero = axis.translate(0), // stack origin - h = mathAbs(y - yZero), // stack height - x = chart.xAxis[0].translate(this.x) + xOffset, // stack x position - plotHeight = chart.plotHeight, - stackBox = { // this is the box for the complete stack - x: inverted ? (neg ? y : y - h) : x, - y: inverted ? plotHeight - x - xWidth : (neg ? (plotHeight - y - h) : plotHeight - y), - width: inverted ? h : xWidth, - height: inverted ? xWidth : h - }, - label = this.label, - alignAttr; - - if (label) { - label.align(this.alignOptions, null, stackBox); // align the label to the box - - // Set visibility (#678) - alignAttr = label.alignAttr; - label[this.options.crop === false || chart.isInsidePlot(alignAttr.x, alignAttr.y) ? 'show' : 'hide'](true); - } - } - }; - - /** - * Generate stacks for each series and calculate stacks total values - */ - Chart.prototype.getStacks = function () { - var chart = this; - - // reset stacks for each yAxis - each(chart.yAxis, function (axis) { - if (axis.stacks && axis.hasVisibleSeries) { - axis.oldStacks = axis.stacks; - } - }); - - each(chart.series, function (series) { - if (series.options.stacking && (series.visible === true || chart.options.chart.ignoreHiddenSeries === false)) { - series.stackKey = series.type + pick(series.options.stack, ''); - } - }); - }; - - - // Stacking methods defined on the Axis prototype - - /** - * Build the stacks from top down - */ - Axis.prototype.buildStacks = function () { - var axisSeries = this.series, - series, - reversedStacks = pick(this.options.reversedStacks, true), - len = axisSeries.length, - i; - if (!this.isXAxis) { - this.usePercentage = false; - i = len; - while (i--) { - axisSeries[reversedStacks ? i : len - i - 1].setStackedPoints(); - } - - i = len; - while (i--) { - series = axisSeries[reversedStacks ? i : len - i - 1]; - if (series.setStackCliffs) { - series.setStackCliffs(); - } - } - // Loop up again to compute percent stack - if (this.usePercentage) { - for (i = 0; i < len; i++) { - axisSeries[i].setPercentStacks(); - } - } - } - }; - - Axis.prototype.renderStackTotals = function () { - var axis = this, - chart = axis.chart, - renderer = chart.renderer, - stacks = axis.stacks, - stackKey, - oneStack, - stackCategory, - stackTotalGroup = axis.stackTotalGroup; - - // Create a separate group for the stack total labels - if (!stackTotalGroup) { - axis.stackTotalGroup = stackTotalGroup = - renderer.g('stack-labels') - .attr({ - visibility: VISIBLE, - zIndex: 6 - }) - .add(); - } - - // plotLeft/Top will change when y axis gets wider so we need to translate the - // stackTotalGroup at every render call. See bug #506 and #516 - stackTotalGroup.translate(chart.plotLeft, chart.plotTop); - - // Render each stack total - for (stackKey in stacks) { - oneStack = stacks[stackKey]; - for (stackCategory in oneStack) { - oneStack[stackCategory].render(stackTotalGroup); - } - } - }; - - /** - * Set all the stacks to initial states and destroy unused ones. - */ - Axis.prototype.resetStacks = function () { - var stacks = this.stacks, - type, - i; - if (!this.isXAxis) { - for (type in stacks) { - for (i in stacks[type]) { - - // Clean up memory after point deletion (#1044, #4320) - if (stacks[type][i].touched < this.stacksTouched) { - stacks[type][i].destroy(); - delete stacks[type][i]; - - // Reset stacks - } else { - stacks[type][i].total = null; - stacks[type][i].cum = 0; - } - } - } - } - }; - - Axis.prototype.cleanStacks = function () { - var stacks, type, i; - - if (!this.isXAxis) { - if (this.oldStacks) { - stacks = this.stacks = this.oldStacks; - } - - // reset stacks - for (type in stacks) { - for (i in stacks[type]) { - stacks[type][i].cum = stacks[type][i].total; - } - } - } - }; - - - // Stacking methods defnied for Series prototype - - /** - * Adds series' points value to corresponding stack - */ - Series.prototype.setStackedPoints = function () { - if (!this.options.stacking || (this.visible !== true && this.chart.options.chart.ignoreHiddenSeries !== false)) { - return; - } - - var series = this, - xData = series.processedXData, - yData = series.processedYData, - stackedYData = [], - yDataLength = yData.length, - seriesOptions = series.options, - threshold = seriesOptions.threshold, - stackThreshold = seriesOptions.startFromThreshold ? threshold : 0, - stackOption = seriesOptions.stack, - stacking = seriesOptions.stacking, - stackKey = series.stackKey, - negKey = '-' + stackKey, - negStacks = series.negStacks, - yAxis = series.yAxis, - stacks = yAxis.stacks, - oldStacks = yAxis.oldStacks, - stackIndicator, - isNegative, - stack, - other, - key, - pointKey, - i, - x, - y; - - - yAxis.stacksTouched += 1; - - // loop over the non-null y values and read them into a local array - for (i = 0; i < yDataLength; i++) { - x = xData[i]; - y = yData[i]; - stackIndicator = series.getStackIndicator(stackIndicator, x, series.index); - pointKey = stackIndicator.key; - // Read stacked values into a stack based on the x value, - // the sign of y and the stack key. Stacking is also handled for null values (#739) - isNegative = negStacks && y < (stackThreshold ? 0 : threshold); - key = isNegative ? negKey : stackKey; - - // Create empty object for this stack if it doesn't exist yet - if (!stacks[key]) { - stacks[key] = {}; - } - - // Initialize StackItem for this x - if (!stacks[key][x]) { - if (oldStacks[key] && oldStacks[key][x]) { - stacks[key][x] = oldStacks[key][x]; - stacks[key][x].total = null; - } else { - stacks[key][x] = new StackItem(yAxis, yAxis.options.stackLabels, isNegative, x, stackOption); - } - } - - // If the StackItem doesn't exist, create it first - stack = stacks[key][x]; - if (y !== null) { - stack.points[pointKey] = stack.points[series.index] = [pick(stack.cum, stackThreshold)]; - stack.touched = yAxis.stacksTouched; - - - // In area charts, if there are multiple points on the same X value, let the - // area fill the full span of those points - if (stackIndicator.index > 0 && series.singleStacks === false) { - stack.points[pointKey][0] = stack.points[series.index + ',' + x + ',0'][0]; - } - } - - // Add value to the stack total - if (stacking === 'percent') { - - // Percent stacked column, totals are the same for the positive and negative stacks - other = isNegative ? stackKey : negKey; - if (negStacks && stacks[other] && stacks[other][x]) { - other = stacks[other][x]; - stack.total = other.total = mathMax(other.total, stack.total) + mathAbs(y) || 0; - - // Percent stacked areas - } else { - stack.total = correctFloat(stack.total + (mathAbs(y) || 0)); - } - } else { - stack.total = correctFloat(stack.total + (y || 0)); - } - - stack.cum = pick(stack.cum, stackThreshold) + (y || 0); - - if (y !== null) { - stack.points[pointKey].push(stack.cum); - stackedYData[i] = stack.cum; - } - - } - - if (stacking === 'percent') { - yAxis.usePercentage = true; - } - - this.stackedYData = stackedYData; // To be used in getExtremes - - // Reset old stacks - yAxis.oldStacks = {}; - }; - - /** - * Iterate over all stacks and compute the absolute values to percent - */ - Series.prototype.setPercentStacks = function () { - var series = this, - stackKey = series.stackKey, - stacks = series.yAxis.stacks, - processedXData = series.processedXData, - stackIndicator; - - each([stackKey, '-' + stackKey], function (key) { - var i = processedXData.length, - x, - stack, - pointExtremes, - totalFactor; - - while (i--) { - x = processedXData[i]; - stackIndicator = series.getStackIndicator(stackIndicator, x, series.index); - stack = stacks[key] && stacks[key][x]; - pointExtremes = stack && stack.points[stackIndicator.key]; - if (pointExtremes) { - totalFactor = stack.total ? 100 / stack.total : 0; - pointExtremes[0] = correctFloat(pointExtremes[0] * totalFactor); // Y bottom value - pointExtremes[1] = correctFloat(pointExtremes[1] * totalFactor); // Y value - series.stackedYData[i] = pointExtremes[1]; - } - } - }); - }; - - /** - * Get stack indicator, according to it's x-value, to determine points with the same x-value - */ - Series.prototype.getStackIndicator = function (stackIndicator, x, index) { - if (!defined(stackIndicator) || stackIndicator.x !== x) { - stackIndicator = { - x: x, - index: 0 - }; - } else { - stackIndicator.index++; - } - - stackIndicator.key = [index, x, stackIndicator.index].join(','); - - return stackIndicator; - }; - - // Extend the Chart prototype for dynamic methods - extend(Chart.prototype, { - - /** - * Add a series dynamically after time - * - * @param {Object} options The config options - * @param {Boolean} redraw Whether to redraw the chart after adding. Defaults to true. - * @param {Boolean|Object} animation Whether to apply animation, and optionally animation - * configuration - * - * @return {Object} series The newly created series object - */ - addSeries: function (options, redraw, animation) { - var series, - chart = this; - - if (options) { - redraw = pick(redraw, true); // defaults to true - - fireEvent(chart, 'addSeries', { options: options }, function () { - series = chart.initSeries(options); - - chart.isDirtyLegend = true; // the series array is out of sync with the display - chart.linkSeries(); - if (redraw) { - chart.redraw(animation); - } - }); - } - - return series; - }, - - /** - * Add an axis to the chart - * @param {Object} options The axis option - * @param {Boolean} isX Whether it is an X axis or a value axis - */ - addAxis: function (options, isX, redraw, animation) { - var key = isX ? 'xAxis' : 'yAxis', - chartOptions = this.options, - userOptions = merge(options, { - index: this[key].length, - isX: isX - }); - - new Axis(this, userOptions); // eslint-disable-line no-new - - // Push the new axis options to the chart options - chartOptions[key] = splat(chartOptions[key] || {}); - chartOptions[key].push(userOptions); - - if (pick(redraw, true)) { - this.redraw(animation); - } - }, - - /** - * Dim the chart and show a loading text or symbol - * @param {String} str An optional text to show in the loading label instead of the default one - */ - showLoading: function (str) { - var chart = this, - options = chart.options, - loadingDiv = chart.loadingDiv, - loadingOptions = options.loading, - setLoadingSize = function () { - if (loadingDiv) { - css(loadingDiv, { - left: chart.plotLeft + PX, - top: chart.plotTop + PX, - width: chart.plotWidth + PX, - height: chart.plotHeight + PX - }); - } - }; - - // create the layer at the first call - if (!loadingDiv) { - chart.loadingDiv = loadingDiv = createElement(DIV, { - className: PREFIX + 'loading' - }, extend(loadingOptions.style, { - zIndex: 10, - display: NONE - }), chart.container); - - chart.loadingSpan = createElement( - 'span', - null, - loadingOptions.labelStyle, - loadingDiv - ); - addEvent(chart, 'redraw', setLoadingSize); // #1080 - } - - // update text - chart.loadingSpan.innerHTML = str || options.lang.loading; - - // show it - if (!chart.loadingShown) { - css(loadingDiv, { - opacity: 0, - display: '' - }); - animate(loadingDiv, { - opacity: loadingOptions.style.opacity - }, { - duration: loadingOptions.showDuration || 0 - }); - chart.loadingShown = true; - } - setLoadingSize(); - }, - - /** - * Hide the loading layer - */ - hideLoading: function () { - var options = this.options, - loadingDiv = this.loadingDiv; - - if (loadingDiv) { - animate(loadingDiv, { - opacity: 0 - }, { - duration: options.loading.hideDuration || 100, - complete: function () { - css(loadingDiv, { display: NONE }); - } - }); - } - this.loadingShown = false; - } - }); - - // extend the Point prototype for dynamic methods - extend(Point.prototype, { - /** - * Update the point with new options (typically x/y data) and optionally redraw the series. - * - * @param {Object} options Point options as defined in the series.data array - * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call - * @param {Boolean|Object} animation Whether to apply animation, and optionally animation - * configuration - * - */ - update: function (options, redraw, animation, runEvent) { - var point = this, - series = point.series, - graphic = point.graphic, - i, - chart = series.chart, - seriesOptions = series.options, - names = series.xAxis && series.xAxis.names; - - redraw = pick(redraw, true); - - function update() { - - point.applyOptions(options); - - // Update visuals - if (point.y === null && graphic) { // #4146 - point.graphic = graphic.destroy(); - } - if (isObject(options) && !isArray(options)) { - // Defer the actual redraw until getAttribs has been called (#3260) - point.redraw = function () { - if (graphic && graphic.element) { - if (options && options.marker && options.marker.symbol) { - point.graphic = graphic.destroy(); - } - } - if (options && options.dataLabels && point.dataLabel) { // #2468 - point.dataLabel = point.dataLabel.destroy(); - } - point.redraw = null; - }; - } - - // record changes in the parallel arrays - i = point.index; - series.updateParallelArrays(point, i); - if (names && point.name) { - names[point.x] = point.name; - } - - // Record the options to options.data. If there is an object from before, - // use point options, otherwise use raw options. (#4701) - seriesOptions.data[i] = (isObject(seriesOptions.data[i]) && !isArray(seriesOptions.data[i])) ? point.options : options; - - // redraw - series.isDirty = series.isDirtyData = true; - if (!series.fixedBox && series.hasCartesianSeries) { // #1906, #2320 - chart.isDirtyBox = true; - } - - if (seriesOptions.legendType === 'point') { // #1831, #1885 - chart.isDirtyLegend = true; - } - if (redraw) { - chart.redraw(animation); - } - } - - // Fire the event with a default handler of doing the update - if (runEvent === false) { // When called from setData - update(); - } else { - point.firePointEvent('update', { options: options }, update); - } - }, - - /** - * Remove a point and optionally redraw the series and if necessary the axes - * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call - * @param {Boolean|Object} animation Whether to apply animation, and optionally animation - * configuration - */ - remove: function (redraw, animation) { - this.series.removePoint(inArray(this, this.series.data), redraw, animation); - } - }); - - // Extend the series prototype for dynamic methods - extend(Series.prototype, { - /** - * Add a point dynamically after chart load time - * @param {Object} options Point options as given in series.data - * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call - * @param {Boolean} shift If shift is true, a point is shifted off the start - * of the series as one is appended to the end. - * @param {Boolean|Object} animation Whether to apply animation, and optionally animation - * configuration - */ - addPoint: function (options, redraw, shift, animation) { - var series = this, - seriesOptions = series.options, - data = series.data, - graph = series.graph, - area = series.area, - chart = series.chart, - names = series.xAxis && series.xAxis.names, - currentShift = (graph && graph.shift) || 0, - shiftShapes = ['graph', 'area'], - dataOptions = seriesOptions.data, - point, - isInTheMiddle, - xData = series.xData, - i, - x; - - setAnimation(animation, chart); - - // Make graph animate sideways - if (shift) { - i = series.zones.length; - while (i--) { - shiftShapes.push('zoneGraph' + i, 'zoneArea' + i); - } - each(shiftShapes, function (shape) { - if (series[shape]) { - series[shape].shift = currentShift + (seriesOptions.step ? 2 : 1); - } - }); - } - if (area) { - area.isArea = true; // needed in animation, both with and without shift - } - - // Optional redraw, defaults to true - redraw = pick(redraw, true); - - // Get options and push the point to xData, yData and series.options. In series.generatePoints - // the Point instance will be created on demand and pushed to the series.data array. - point = { series: series }; - series.pointClass.prototype.applyOptions.apply(point, [options]); - x = point.x; - - // Get the insertion point - i = xData.length; - if (series.requireSorting && x < xData[i - 1]) { - isInTheMiddle = true; - while (i && xData[i - 1] > x) { - i--; - } - } - - series.updateParallelArrays(point, 'splice', i, 0, 0); // insert undefined item - series.updateParallelArrays(point, i); // update it - - if (names && point.name) { - names[x] = point.name; - } - dataOptions.splice(i, 0, options); - - if (isInTheMiddle) { - series.data.splice(i, 0, null); - series.processData(); - } - - // Generate points to be added to the legend (#1329) - if (seriesOptions.legendType === 'point') { - series.generatePoints(); - } - - // Shift the first point off the parallel arrays - if (shift) { - if (data[0] && data[0].remove) { - data[0].remove(false); - } else { - data.shift(); - series.updateParallelArrays(point, 'shift'); - - dataOptions.shift(); - } - } - - // redraw - series.isDirty = true; - series.isDirtyData = true; - if (redraw) { - series.getAttribs(); // #1937 - chart.redraw(); - } - }, - - /** - * Remove a point (rendered or not), by index - */ - removePoint: function (i, redraw, animation) { - - var series = this, - data = series.data, - point = data[i], - points = series.points, - chart = series.chart, - remove = function () { - - if (points && points.length === data.length) { // #4935 - points.splice(i, 1); - } - data.splice(i, 1); - series.options.data.splice(i, 1); - series.updateParallelArrays(point || { series: series }, 'splice', i, 1); - - if (point) { - point.destroy(); - } - - // redraw - series.isDirty = true; - series.isDirtyData = true; - if (redraw) { - chart.redraw(); - } - }; - - setAnimation(animation, chart); - redraw = pick(redraw, true); - - // Fire the event with a default handler of removing the point - if (point) { - point.firePointEvent('remove', null, remove); - } else { - remove(); - } - }, - - /** - * Remove a series and optionally redraw the chart - * - * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call - * @param {Boolean|Object} animation Whether to apply animation, and optionally animation - * configuration - */ - remove: function (redraw, animation) { - var series = this, - chart = series.chart; - - // Fire the event with a default handler of removing the point - fireEvent(series, 'remove', null, function () { - - // Destroy elements - series.destroy(); - - // Redraw - chart.isDirtyLegend = chart.isDirtyBox = true; - chart.linkSeries(); - - if (pick(redraw, true)) { - chart.redraw(animation); - } - }); - }, - - /** - * Update the series with a new set of options - */ - update: function (newOptions, redraw) { - var series = this, - chart = this.chart, - // must use user options when changing type because this.options is merged - // in with type specific plotOptions - oldOptions = this.userOptions, - oldType = this.type, - proto = seriesTypes[oldType].prototype, - preserve = ['group', 'markerGroup', 'dataLabelsGroup'], - n; - - // If we're changing type or zIndex, create new groups (#3380, #3404) - if ((newOptions.type && newOptions.type !== oldType) || newOptions.zIndex !== undefined) { - preserve.length = 0; - } - - // Make sure groups are not destroyed (#3094) - each(preserve, function (prop) { - preserve[prop] = series[prop]; - delete series[prop]; - }); - - // Do the merge, with some forced options - newOptions = merge(oldOptions, { - animation: false, - index: this.index, - pointStart: this.xData[0] // when updating after addPoint - }, { data: this.options.data }, newOptions); - - // Destroy the series and delete all properties. Reinsert all methods - // and properties from the new type prototype (#2270, #3719) - this.remove(false); - for (n in proto) { - this[n] = UNDEFINED; - } - extend(this, seriesTypes[newOptions.type || oldType].prototype); - - // Re-register groups (#3094) - each(preserve, function (prop) { - series[prop] = preserve[prop]; - }); - - this.init(chart, newOptions); - chart.linkSeries(); // Links are lost in this.remove (#3028) - if (pick(redraw, true)) { - chart.redraw(false); - } - } - }); - - // Extend the Axis.prototype for dynamic methods - extend(Axis.prototype, { - - /** - * Update the axis with a new options structure - */ - update: function (newOptions, redraw) { - var chart = this.chart; - - newOptions = chart.options[this.coll][this.options.index] = merge(this.userOptions, newOptions); - - this.destroy(true); - this._addedPlotLB = this.chart._labelPanes = UNDEFINED; // #1611, #2887, #4314 - - this.init(chart, extend(newOptions, { events: UNDEFINED })); - - chart.isDirtyBox = true; - if (pick(redraw, true)) { - chart.redraw(); - } - }, - - /** - * Remove the axis from the chart - */ - remove: function (redraw) { - var chart = this.chart, - key = this.coll, // xAxis or yAxis - axisSeries = this.series, - i = axisSeries.length; - - // Remove associated series (#2687) - while (i--) { - if (axisSeries[i]) { - axisSeries[i].remove(false); - } - } - - // Remove the axis - erase(chart.axes, this); - erase(chart[key], this); - chart.options[key].splice(this.options.index, 1); - each(chart[key], function (axis, i) { // Re-index, #1706 - axis.options.index = i; - }); - this.destroy(); - chart.isDirtyBox = true; - - if (pick(redraw, true)) { - chart.redraw(); - } - }, - - /** - * Update the axis title by options - */ - setTitle: function (newTitleOptions, redraw) { - this.update({ title: newTitleOptions }, redraw); - }, - - /** - * Set new axis categories and optionally redraw - * @param {Array} categories - * @param {Boolean} redraw - */ - setCategories: function (categories, redraw) { - this.update({ categories: categories }, redraw); - } - - }); - - - /** - * LineSeries object - */ - var LineSeries = extendClass(Series); - seriesTypes.line = LineSeries; - - /** - * Set the default options for area - */ - defaultPlotOptions.area = merge(defaultSeriesOptions, { - softThreshold: false, - threshold: 0 - // trackByArea: false, - // lineColor: null, // overrides color, but lets fillColor be unaltered - // fillOpacity: 0.75, - // fillColor: null - }); - - /** - * AreaSeries object - */ - var AreaSeries = extendClass(Series, { - type: 'area', - singleStacks: false, - /** - * Return an array of stacked points, where null and missing points are replaced by - * dummy points in order for gaps to be drawn correctly in stacks. - */ - getStackPoints: function () { - var series = this, - segment = [], - keys = [], - xAxis = this.xAxis, - yAxis = this.yAxis, - stack = yAxis.stacks[this.stackKey], - pointMap = {}, - points = this.points, - seriesIndex = series.index, - yAxisSeries = yAxis.series, - seriesLength = yAxisSeries.length, - visibleSeries, - upOrDown = pick(yAxis.options.reversedStacks, true) ? 1 : -1, - i, - x; - - if (this.options.stacking) { - // Create a map where we can quickly look up the points by their X value. - for (i = 0; i < points.length; i++) { - pointMap[points[i].x] = points[i]; - } - - // Sort the keys (#1651) - for (x in stack) { - if (stack[x].total !== null) { // nulled after switching between grouping and not (#1651, #2336) - keys.push(x); - } - } - keys.sort(function (a, b) { - return a - b; - }); - - visibleSeries = map(yAxisSeries, function () { - return this.visible; - }); - - each(keys, function (x, idx) { - var y = 0, - stackPoint, - stackedValues; - - if (pointMap[x] && !pointMap[x].isNull) { - segment.push(pointMap[x]); - - // Find left and right cliff. -1 goes left, 1 goes right. - each([-1, 1], function (direction) { - var nullName = direction === 1 ? 'rightNull' : 'leftNull', - cliffName = direction === 1 ? 'rightCliff' : 'leftCliff', - cliff = 0, - otherStack = stack[keys[idx + direction]]; - - // If there is a stack next to this one, to the left or to the right... - if (otherStack) { - i = seriesIndex; - while (i >= 0 && i < seriesLength) { // Can go either up or down, depending on reversedStacks - stackPoint = otherStack.points[i]; - if (!stackPoint) { - // If the next point in this series is missing, mark the point - // with point.leftNull or point.rightNull = true. - if (i === seriesIndex) { - pointMap[x][nullName] = true; - - // If there are missing points in the next stack in any of the - // series below this one, we need to substract the missing values - // and add a hiatus to the left or right. - } else if (visibleSeries[i]) { - stackedValues = stack[x].points[i]; - if (stackedValues) { - cliff -= stackedValues[1] - stackedValues[0]; - } - } - } - // When reversedStacks is true, loop up, else loop down - i += upOrDown; - } - } - pointMap[x][cliffName] = cliff; - }); - - - // There is no point for this X value in this series, so we - // insert a dummy point in order for the areas to be drawn - // correctly. - } else { - - // Loop down the stack to find the series below this one that has - // a value (#1991) - i = seriesIndex; - while (i >= 0 && i < seriesLength) { - stackPoint = stack[x].points[i]; - if (stackPoint) { - y = stackPoint[1]; - break; - } - // When reversedStacks is true, loop up, else loop down - i += upOrDown; - } - - y = yAxis.toPixels(y, true); - segment.push({ - isNull: true, - plotX: xAxis.toPixels(x, true), - plotY: y, - yBottom: y - }); - } - }); - - } - - return segment; - }, - - getGraphPath: function (points) { - var getGraphPath = Series.prototype.getGraphPath, - graphPath, - options = this.options, - stacking = options.stacking, - yAxis = this.yAxis, - topPath, - //topPoints = [], - bottomPath, - bottomPoints = [], - graphPoints = [], - seriesIndex = this.index, - i, - areaPath, - plotX, - stacks = yAxis.stacks[this.stackKey], - threshold = options.threshold, - translatedThreshold = yAxis.getThreshold(options.threshold), - isNull, - yBottom, - connectNulls = options.connectNulls || stacking === 'percent', - /** - * To display null points in underlying stacked series, this series graph must be - * broken, and the area also fall down to fill the gap left by the null point. #2069 - */ - addDummyPoints = function (i, otherI, side) { - var point = points[i], - stackedValues = stacking && stacks[point.x].points[seriesIndex], - nullVal = point[side + 'Null'] || 0, - cliffVal = point[side + 'Cliff'] || 0, - top, - bottom, - isNull = true; - - if (cliffVal || nullVal) { - - top = (nullVal ? stackedValues[0] : stackedValues[1]) + cliffVal; - bottom = stackedValues[0] + cliffVal; - isNull = !!nullVal; - - } else if (!stacking && points[otherI] && points[otherI].isNull) { - top = bottom = threshold; - } - - // Add to the top and bottom line of the area - if (top !== undefined) { - graphPoints.push({ - plotX: plotX, - plotY: top === null ? translatedThreshold : yAxis.getThreshold(top), - isNull: isNull - }); - bottomPoints.push({ - plotX: plotX, - plotY: bottom === null ? translatedThreshold : yAxis.getThreshold(bottom) - }); - } - }; - - // Find what points to use - points = points || this.points; - - - // Fill in missing points - if (stacking) { - points = this.getStackPoints(); - } - - for (i = 0; i < points.length; i++) { - isNull = points[i].isNull; - plotX = pick(points[i].rectPlotX, points[i].plotX); - yBottom = pick(points[i].yBottom, translatedThreshold); - - if (!isNull || connectNulls) { - - if (!connectNulls) { - addDummyPoints(i, i - 1, 'left'); - } - - if (!(isNull && !stacking && connectNulls)) { // Skip null point when stacking is false and connectNulls true - graphPoints.push(points[i]); - bottomPoints.push({ - x: i, - plotX: plotX, - plotY: yBottom - }); - } - - if (!connectNulls) { - addDummyPoints(i, i + 1, 'right'); - } - } - } - - topPath = getGraphPath.call(this, graphPoints, true, true); - - bottomPoints.reversed = true; - bottomPath = getGraphPath.call(this, bottomPoints, true, true); - if (bottomPath.length) { - bottomPath[0] = L; - } - - areaPath = topPath.concat(bottomPath); - graphPath = getGraphPath.call(this, graphPoints, false, connectNulls); // TODO: don't set leftCliff and rightCliff when connectNulls? - - this.areaPath = areaPath; - return graphPath; - }, - - /** - * Draw the graph and the underlying area. This method calls the Series base - * function and adds the area. The areaPath is calculated in the getSegmentPath - * method called from Series.prototype.drawGraph. - */ - drawGraph: function () { - - // Define or reset areaPath - this.areaPath = []; - - // Call the base method - Series.prototype.drawGraph.apply(this); - - // Define local variables - var series = this, - areaPath = this.areaPath, - options = this.options, - zones = this.zones, - props = [['area', this.color, options.fillColor]]; // area name, main color, fill color - - each(zones, function (threshold, i) { - props.push(['zoneArea' + i, threshold.color || series.color, threshold.fillColor || options.fillColor]); - }); - each(props, function (prop) { - var areaKey = prop[0], - area = series[areaKey], - attr; - - // Create or update the area - if (area) { // update - area.animate({ d: areaPath }); - - } else { // create - attr = { - fill: prop[2] || prop[1], - zIndex: 0 // #1069 - }; - if (!prop[2]) { - attr['fill-opacity'] = pick(options.fillOpacity, 0.75); - } - series[areaKey] = series.chart.renderer.path(areaPath) - .attr(attr) - .add(series.group); - } - }); - }, - - drawLegendSymbol: LegendSymbolMixin.drawRectangle - }); - - seriesTypes.area = AreaSeries; - /** - * Set the default options for spline - */ - defaultPlotOptions.spline = merge(defaultSeriesOptions); - - /** - * SplineSeries object - */ - var SplineSeries = extendClass(Series, { - type: 'spline', - - /** - * Get the spline segment from a given point's previous neighbour to the given point - */ - getPointSpline: function (points, point, i) { - var smoothing = 1.5, // 1 means control points midway between points, 2 means 1/3 from the point, 3 is 1/4 etc - denom = smoothing + 1, - plotX = point.plotX, - plotY = point.plotY, - lastPoint = points[i - 1], - nextPoint = points[i + 1], - leftContX, - leftContY, - rightContX, - rightContY, - ret; - - // Find control points - if (lastPoint && !lastPoint.isNull && nextPoint && !nextPoint.isNull) { - var lastX = lastPoint.plotX, - lastY = lastPoint.plotY, - nextX = nextPoint.plotX, - nextY = nextPoint.plotY, - correction = 0; - - leftContX = (smoothing * plotX + lastX) / denom; - leftContY = (smoothing * plotY + lastY) / denom; - rightContX = (smoothing * plotX + nextX) / denom; - rightContY = (smoothing * plotY + nextY) / denom; - - // Have the two control points make a straight line through main point - if (rightContX !== leftContX) { // #5016, division by zero - correction = ((rightContY - leftContY) * (rightContX - plotX)) / - (rightContX - leftContX) + plotY - rightContY; - } - - leftContY += correction; - rightContY += correction; - - // to prevent false extremes, check that control points are between - // neighbouring points' y values - if (leftContY > lastY && leftContY > plotY) { - leftContY = mathMax(lastY, plotY); - rightContY = 2 * plotY - leftContY; // mirror of left control point - } else if (leftContY < lastY && leftContY < plotY) { - leftContY = mathMin(lastY, plotY); - rightContY = 2 * plotY - leftContY; - } - if (rightContY > nextY && rightContY > plotY) { - rightContY = mathMax(nextY, plotY); - leftContY = 2 * plotY - rightContY; - } else if (rightContY < nextY && rightContY < plotY) { - rightContY = mathMin(nextY, plotY); - leftContY = 2 * plotY - rightContY; - } - - // record for drawing in next point - point.rightContX = rightContX; - point.rightContY = rightContY; - - - } - - // Visualize control points for debugging - /* - if (leftContX) { - this.chart.renderer.circle(leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop, 2) - .attr({ - stroke: 'red', - 'stroke-width': 1, - fill: 'none' - }) - .add(); - this.chart.renderer.path(['M', leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop, - 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop]) - .attr({ - stroke: 'red', - 'stroke-width': 1 - }) - .add(); - this.chart.renderer.circle(rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop, 2) - .attr({ - stroke: 'green', - 'stroke-width': 1, - fill: 'none' - }) - .add(); - this.chart.renderer.path(['M', rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop, - 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop]) - .attr({ - stroke: 'green', - 'stroke-width': 1 - }) - .add(); - } - // */ - ret = [ - 'C', - pick(lastPoint.rightContX, lastPoint.plotX), - pick(lastPoint.rightContY, lastPoint.plotY), - pick(leftContX, plotX), - pick(leftContY, plotY), - plotX, - plotY - ]; - lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later - return ret; - } - }); - seriesTypes.spline = SplineSeries; - - /** - * Set the default options for areaspline - */ - defaultPlotOptions.areaspline = merge(defaultPlotOptions.area); - - /** - * AreaSplineSeries object - */ - var areaProto = AreaSeries.prototype, - AreaSplineSeries = extendClass(SplineSeries, { - type: 'areaspline', - getStackPoints: areaProto.getStackPoints, - getGraphPath: areaProto.getGraphPath, - setStackCliffs: areaProto.setStackCliffs, - drawGraph: areaProto.drawGraph, - drawLegendSymbol: LegendSymbolMixin.drawRectangle - }); - - seriesTypes.areaspline = AreaSplineSeries; - - /** - * Set the default options for column - */ - defaultPlotOptions.column = merge(defaultSeriesOptions, { - borderColor: '#FFFFFF', - //borderWidth: 1, - borderRadius: 0, - //colorByPoint: undefined, - groupPadding: 0.2, - //grouping: true, - marker: null, // point options are specified in the base options - pointPadding: 0.1, - //pointWidth: null, - minPointLength: 0, - cropThreshold: 50, // when there are more points, they will not animate out of the chart on xAxis.setExtremes - pointRange: null, // null means auto, meaning 1 in a categorized axis and least distance between points if not categories - states: { - hover: { - brightness: 0.1, - shadow: false, - halo: false - }, - select: { - color: '#C0C0C0', - borderColor: '#000000', - shadow: false - } - }, - dataLabels: { - align: null, // auto - verticalAlign: null, // auto - y: null - }, - softThreshold: false, - startFromThreshold: true, // false doesn't work well: http://jsfiddle.net/highcharts/hz8fopan/14/ - stickyTracking: false, - tooltip: { - distance: 6 - }, - threshold: 0 - }); - - /** - * ColumnSeries object - */ - var ColumnSeries = extendClass(Series, { - type: 'column', - pointAttrToOptions: { // mapping between SVG attributes and the corresponding options - stroke: 'borderColor', - fill: 'color', - r: 'borderRadius' - }, - cropShoulder: 0, - directTouch: true, // When tooltip is not shared, this series (and derivatives) requires direct touch/hover. KD-tree does not apply. - trackerGroups: ['group', 'dataLabelsGroup'], - negStacks: true, // use separate negative stacks, unlike area stacks where a negative - // point is substracted from previous (#1910) - - /** - * Initialize the series - */ - init: function () { - Series.prototype.init.apply(this, arguments); - - var series = this, - chart = series.chart; - - // if the series is added dynamically, force redraw of other - // series affected by a new column - if (chart.hasRendered) { - each(chart.series, function (otherSeries) { - if (otherSeries.type === series.type) { - otherSeries.isDirty = true; - } - }); - } - }, - - /** - * Return the width and x offset of the columns adjusted for grouping, groupPadding, pointPadding, - * pointWidth etc. - */ - getColumnMetrics: function () { - - var series = this, - options = series.options, - xAxis = series.xAxis, - yAxis = series.yAxis, - reversedXAxis = xAxis.reversed, - stackKey, - stackGroups = {}, - columnCount = 0; - - // Get the total number of column type series. - // This is called on every series. Consider moving this logic to a - // chart.orderStacks() function and call it on init, addSeries and removeSeries - if (options.grouping === false) { - columnCount = 1; - } else { - each(series.chart.series, function (otherSeries) { - var otherOptions = otherSeries.options, - otherYAxis = otherSeries.yAxis, - columnIndex; - if (otherSeries.type === series.type && otherSeries.visible && - yAxis.len === otherYAxis.len && yAxis.pos === otherYAxis.pos) { // #642, #2086 - if (otherOptions.stacking) { - stackKey = otherSeries.stackKey; - if (stackGroups[stackKey] === UNDEFINED) { - stackGroups[stackKey] = columnCount++; - } - columnIndex = stackGroups[stackKey]; - } else if (otherOptions.grouping !== false) { // #1162 - columnIndex = columnCount++; - } - otherSeries.columnIndex = columnIndex; - } - }); - } - - var categoryWidth = mathMin( - mathAbs(xAxis.transA) * (xAxis.ordinalSlope || options.pointRange || xAxis.closestPointRange || xAxis.tickInterval || 1), // #2610 - xAxis.len // #1535 - ), - groupPadding = categoryWidth * options.groupPadding, - groupWidth = categoryWidth - 2 * groupPadding, - pointOffsetWidth = groupWidth / columnCount, - pointWidth = mathMin( - options.maxPointWidth || xAxis.len, - pick(options.pointWidth, pointOffsetWidth * (1 - 2 * options.pointPadding)) - ), - pointPadding = (pointOffsetWidth - pointWidth) / 2, - colIndex = (series.columnIndex || 0) + (reversedXAxis ? 1 : 0), // #1251, #3737 - pointXOffset = pointPadding + (groupPadding + colIndex * - pointOffsetWidth - (categoryWidth / 2)) * - (reversedXAxis ? -1 : 1); - - // Save it for reading in linked series (Error bars particularly) - series.columnMetrics = { - width: pointWidth, - offset: pointXOffset - }; - return series.columnMetrics; - - }, - - /** - * Make the columns crisp. The edges are rounded to the nearest full pixel. - */ - crispCol: function (x, y, w, h) { - var chart = this.chart, - borderWidth = this.borderWidth, - xCrisp = -(borderWidth % 2 ? 0.5 : 0), - yCrisp = borderWidth % 2 ? 0.5 : 1, - right, - bottom, - fromTop; - - if (chart.inverted && chart.renderer.isVML) { - yCrisp += 1; - } - - // Horizontal. We need to first compute the exact right edge, then round it - // and compute the width from there. - right = Math.round(x + w) + xCrisp; - x = Math.round(x) + xCrisp; - w = right - x; - - // Vertical - bottom = Math.round(y + h) + yCrisp; - fromTop = mathAbs(y) <= 0.5 && bottom > 0.5; // #4504, #4656 - y = Math.round(y) + yCrisp; - h = bottom - y; - - // Top edges are exceptions - if (fromTop && h) { // #5146 - y -= 1; - h += 1; - } - - return { - x: x, - y: y, - width: w, - height: h - }; - }, - - /** - * Translate each point to the plot area coordinate system and find shape positions - */ - translate: function () { - var series = this, - chart = series.chart, - options = series.options, - borderWidth = series.borderWidth = pick( - options.borderWidth, - series.closestPointRange * series.xAxis.transA < 2 ? 0 : 1 // #3635 - ), - yAxis = series.yAxis, - threshold = options.threshold, - translatedThreshold = series.translatedThreshold = yAxis.getThreshold(threshold), - minPointLength = pick(options.minPointLength, 5), - metrics = series.getColumnMetrics(), - pointWidth = metrics.width, - seriesBarW = series.barW = mathMax(pointWidth, 1 + 2 * borderWidth), // postprocessed for border width - pointXOffset = series.pointXOffset = metrics.offset; - - if (chart.inverted) { - translatedThreshold -= 0.5; // #3355 - } - - // When the pointPadding is 0, we want the columns to be packed tightly, so we allow individual - // columns to have individual sizes. When pointPadding is greater, we strive for equal-width - // columns (#2694). - if (options.pointPadding) { - seriesBarW = mathCeil(seriesBarW); - } - - Series.prototype.translate.apply(series); - - // Record the new values - each(series.points, function (point) { - var yBottom = mathMin(pick(point.yBottom, translatedThreshold), 9e4), // #3575 - safeDistance = 999 + mathAbs(yBottom), - plotY = mathMin(mathMax(-safeDistance, point.plotY), yAxis.len + safeDistance), // Don't draw too far outside plot area (#1303, #2241, #4264) - barX = point.plotX + pointXOffset, - barW = seriesBarW, - barY = mathMin(plotY, yBottom), - up, - barH = mathMax(plotY, yBottom) - barY; - - // Handle options.minPointLength - if (mathAbs(barH) < minPointLength) { - if (minPointLength) { - barH = minPointLength; - up = (!yAxis.reversed && !point.negative) || (yAxis.reversed && point.negative); - barY = mathAbs(barY - translatedThreshold) > minPointLength ? // stacked - yBottom - minPointLength : // keep position - translatedThreshold - (up ? minPointLength : 0); // #1485, #4051 - } - } - - // Cache for access in polar - point.barX = barX; - point.pointWidth = pointWidth; - - // Fix the tooltip on center of grouped columns (#1216, #424, #3648) - point.tooltipPos = chart.inverted ? - [yAxis.len + yAxis.pos - chart.plotLeft - plotY, series.xAxis.len - barX - barW / 2, barH] : - [barX + barW / 2, plotY + yAxis.pos - chart.plotTop, barH]; - - // Register shape type and arguments to be used in drawPoints - point.shapeType = 'rect'; - point.shapeArgs = series.crispCol(barX, barY, barW, barH); - }); - - }, - - getSymbol: noop, - - /** - * Use a solid rectangle like the area series types - */ - drawLegendSymbol: LegendSymbolMixin.drawRectangle, - - - /** - * Columns have no graph - */ - drawGraph: noop, - - /** - * Draw the columns. For bars, the series.group is rotated, so the same coordinates - * apply for columns and bars. This method is inherited by scatter series. - * - */ - drawPoints: function () { - var series = this, - chart = this.chart, - options = series.options, - renderer = chart.renderer, - animationLimit = options.animationLimit || 250, - shapeArgs, - pointAttr; - - // draw the columns - each(series.points, function (point) { - var plotY = point.plotY, - graphic = point.graphic, - borderAttr; - - if (isNumber(plotY) && point.y !== null) { - shapeArgs = point.shapeArgs; - - borderAttr = defined(series.borderWidth) ? { - 'stroke-width': series.borderWidth - } : {}; - - pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE] || series.pointAttr[NORMAL_STATE]; - - if (graphic) { // update - stop(graphic); - graphic.attr(borderAttr).attr(pointAttr)[chart.pointCount < animationLimit ? 'animate' : 'attr'](merge(shapeArgs)); // #4267 - - } else { - point.graphic = graphic = renderer[point.shapeType](shapeArgs) - .attr(borderAttr) - .attr(pointAttr) - .add(point.group || series.group) - .shadow(options.shadow, null, options.stacking && !options.borderRadius); - } - - } else if (graphic) { - point.graphic = graphic.destroy(); // #1269 - } - }); - }, - - /** - * Animate the column heights one by one from zero - * @param {Boolean} init Whether to initialize the animation or run it - */ - animate: function (init) { - var series = this, - yAxis = this.yAxis, - options = series.options, - inverted = this.chart.inverted, - attr = {}, - translatedThreshold; - - if (hasSVG) { // VML is too slow anyway - if (init) { - attr.scaleY = 0.001; - translatedThreshold = mathMin(yAxis.pos + yAxis.len, mathMax(yAxis.pos, yAxis.toPixels(options.threshold))); - if (inverted) { - attr.translateX = translatedThreshold - yAxis.len; - } else { - attr.translateY = translatedThreshold; - } - series.group.attr(attr); - - } else { // run the animation - - attr[inverted ? 'translateX' : 'translateY'] = yAxis.pos; - series.group.animate(attr, extend(animObject(series.options.animation), { - // Do the scale synchronously to ensure smooth updating (#5030) - step: function (val, fx) { - series.group.attr({ - scaleY: mathMax(0.001, fx.pos) // #5250 - }); - } - })); - - // delete this function to allow it only once - series.animate = null; - } - } - }, - - /** - * Remove this series from the chart - */ - remove: function () { - var series = this, - chart = series.chart; - - // column and bar series affects other series of the same type - // as they are either stacked or grouped - if (chart.hasRendered) { - each(chart.series, function (otherSeries) { - if (otherSeries.type === series.type) { - otherSeries.isDirty = true; - } - }); - } - - Series.prototype.remove.apply(series, arguments); - } - }); - seriesTypes.column = ColumnSeries; - /** - * Set the default options for bar - */ - defaultPlotOptions.bar = merge(defaultPlotOptions.column); - /** - * The Bar series class - */ - var BarSeries = extendClass(ColumnSeries, { - type: 'bar', - inverted: true - }); - seriesTypes.bar = BarSeries; - - /** - * Set the default options for scatter - */ - defaultPlotOptions.scatter = merge(defaultSeriesOptions, { - lineWidth: 0, - marker: { - enabled: true // Overrides auto-enabling in line series (#3647) - }, - tooltip: { - headerFormat: '\u25CF {series.name}
', - pointFormat: 'x: {point.x}
y: {point.y}
' - } - }); - - /** - * The scatter series class - */ - var ScatterSeries = extendClass(Series, { - type: 'scatter', - sorted: false, - requireSorting: false, - noSharedTooltip: true, - trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'], - takeOrdinalPosition: false, // #2342 - kdDimensions: 2, - drawGraph: function () { - if (this.options.lineWidth) { - Series.prototype.drawGraph.call(this); - } - } - }); - - seriesTypes.scatter = ScatterSeries; - - /** - * Set the default options for pie - */ - defaultPlotOptions.pie = merge(defaultSeriesOptions, { - borderColor: '#FFFFFF', - borderWidth: 1, - center: [null, null], - clip: false, - colorByPoint: true, // always true for pies - dataLabels: { - // align: null, - // connectorWidth: 1, - // connectorColor: point.color, - // connectorPadding: 5, - distance: 30, - enabled: true, - formatter: function () { // #2945 - return this.y === null ? undefined : this.point.name; - }, - // softConnector: true, - x: 0 - // y: 0 - }, - ignoreHiddenPoint: true, - //innerSize: 0, - legendType: 'point', - marker: null, // point options are specified in the base options - size: null, - showInLegend: false, - slicedOffset: 10, - states: { - hover: { - brightness: 0.1, - shadow: false - } - }, - stickyTracking: false, - tooltip: { - followPointer: true - } - }); - - /** - * Extended point object for pies - */ - var PiePoint = extendClass(Point, { - /** - * Initiate the pie slice - */ - init: function () { - - Point.prototype.init.apply(this, arguments); - - var point = this, - toggleSlice; - - point.name = pick(point.name, 'Slice'); - - // add event listener for select - toggleSlice = function (e) { - point.slice(e.type === 'select'); - }; - addEvent(point, 'select', toggleSlice); - addEvent(point, 'unselect', toggleSlice); - - return point; - }, - - /** - * Toggle the visibility of the pie slice - * @param {Boolean} vis Whether to show the slice or not. If undefined, the - * visibility is toggled - */ - setVisible: function (vis, redraw) { - var point = this, - series = point.series, - chart = series.chart, - ignoreHiddenPoint = series.options.ignoreHiddenPoint; - - redraw = pick(redraw, ignoreHiddenPoint); - - if (vis !== point.visible) { - - // If called without an argument, toggle visibility - point.visible = point.options.visible = vis = vis === UNDEFINED ? !point.visible : vis; - series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data - - // Show and hide associated elements. This is performed regardless of redraw or not, - // because chart.redraw only handles full series. - each(['graphic', 'dataLabel', 'connector', 'shadowGroup'], function (key) { - if (point[key]) { - point[key][vis ? 'show' : 'hide'](true); - } - }); - - if (point.legendItem) { - chart.legend.colorizeItem(point, vis); - } - - // #4170, hide halo after hiding point - if (!vis && point.state === 'hover') { - point.setState(''); - } - - // Handle ignore hidden slices - if (ignoreHiddenPoint) { - series.isDirty = true; - } - - if (redraw) { - chart.redraw(); - } - } - }, - - /** - * Set or toggle whether the slice is cut out from the pie - * @param {Boolean} sliced When undefined, the slice state is toggled - * @param {Boolean} redraw Whether to redraw the chart. True by default. - */ - slice: function (sliced, redraw, animation) { - var point = this, - series = point.series, - chart = series.chart, - translation; - - setAnimation(animation, chart); - - // redraw is true by default - redraw = pick(redraw, true); - - // if called without an argument, toggle - point.sliced = point.options.sliced = sliced = defined(sliced) ? sliced : !point.sliced; - series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data - - translation = sliced ? point.slicedTranslation : { - translateX: 0, - translateY: 0 - }; - - point.graphic.animate(translation); - - if (point.shadowGroup) { - point.shadowGroup.animate(translation); - } - - }, - - haloPath: function (size) { - var shapeArgs = this.shapeArgs, - chart = this.series.chart; - - return this.sliced || !this.visible ? [] : this.series.chart.renderer.symbols.arc(chart.plotLeft + shapeArgs.x, chart.plotTop + shapeArgs.y, shapeArgs.r + size, shapeArgs.r + size, { - innerR: this.shapeArgs.r, - start: shapeArgs.start, - end: shapeArgs.end - }); - } - }); - - /** - * The Pie series class - */ - var PieSeries = { - type: 'pie', - isCartesian: false, - pointClass: PiePoint, - requireSorting: false, - directTouch: true, - noSharedTooltip: true, - trackerGroups: ['group', 'dataLabelsGroup'], - axisTypes: [], - pointAttrToOptions: { // mapping between SVG attributes and the corresponding options - stroke: 'borderColor', - 'stroke-width': 'borderWidth', - fill: 'color' - }, - - /** - * Animate the pies in - */ - animate: function (init) { - var series = this, - points = series.points, - startAngleRad = series.startAngleRad; - - if (!init) { - each(points, function (point) { - var graphic = point.graphic, - args = point.shapeArgs; - - if (graphic) { - // start values - graphic.attr({ - r: point.startR || (series.center[3] / 2), // animate from inner radius (#779) - start: startAngleRad, - end: startAngleRad - }); - - // animate - graphic.animate({ - r: args.r, - start: args.start, - end: args.end - }, series.options.animation); - } - }); - - // delete this function to allow it only once - series.animate = null; - } - }, - - /** - * Recompute total chart sum and update percentages of points. - */ - updateTotals: function () { - var i, - total = 0, - points = this.points, - len = points.length, - point, - ignoreHiddenPoint = this.options.ignoreHiddenPoint; - - // Get the total sum - for (i = 0; i < len; i++) { - point = points[i]; - total += (ignoreHiddenPoint && !point.visible) ? 0 : point.y; - } - this.total = total; - - // Set each point's properties - for (i = 0; i < len; i++) { - point = points[i]; - point.percentage = (total > 0 && (point.visible || !ignoreHiddenPoint)) ? point.y / total * 100 : 0; - point.total = total; - } - }, - - /** - * Extend the generatePoints method by adding total and percentage properties to each point - */ - generatePoints: function () { - Series.prototype.generatePoints.call(this); - this.updateTotals(); - }, - - /** - * Do translation for pie slices - */ - translate: function (positions) { - this.generatePoints(); - - var series = this, - cumulative = 0, - precision = 1000, // issue #172 - options = series.options, - slicedOffset = options.slicedOffset, - connectorOffset = slicedOffset + options.borderWidth, - start, - end, - angle, - startAngle = options.startAngle || 0, - startAngleRad = series.startAngleRad = mathPI / 180 * (startAngle - 90), - endAngleRad = series.endAngleRad = mathPI / 180 * ((pick(options.endAngle, startAngle + 360)) - 90), - circ = endAngleRad - startAngleRad, //2 * mathPI, - points = series.points, - radiusX, // the x component of the radius vector for a given point - radiusY, - labelDistance = options.dataLabels.distance, - ignoreHiddenPoint = options.ignoreHiddenPoint, - i, - len = points.length, - point; - - // Get positions - either an integer or a percentage string must be given. - // If positions are passed as a parameter, we're in a recursive loop for adjusting - // space for data labels. - if (!positions) { - series.center = positions = series.getCenter(); - } - - // utility for getting the x value from a given y, used for anticollision logic in data labels - series.getX = function (y, left) { - - angle = math.asin(mathMin((y - positions[1]) / (positions[2] / 2 + labelDistance), 1)); - - return positions[0] + - (left ? -1 : 1) * - (mathCos(angle) * (positions[2] / 2 + labelDistance)); - }; - - // Calculate the geometry for each point - for (i = 0; i < len; i++) { - - point = points[i]; - - // set start and end angle - start = startAngleRad + (cumulative * circ); - if (!ignoreHiddenPoint || point.visible) { - cumulative += point.percentage / 100; - } - end = startAngleRad + (cumulative * circ); - - // set the shape - point.shapeType = 'arc'; - point.shapeArgs = { - x: positions[0], - y: positions[1], - r: positions[2] / 2, - innerR: positions[3] / 2, - start: mathRound(start * precision) / precision, - end: mathRound(end * precision) / precision - }; - - // The angle must stay within -90 and 270 (#2645) - angle = (end + start) / 2; - if (angle > 1.5 * mathPI) { - angle -= 2 * mathPI; - } else if (angle < -mathPI / 2) { - angle += 2 * mathPI; - } - - // Center for the sliced out slice - point.slicedTranslation = { - translateX: mathRound(mathCos(angle) * slicedOffset), - translateY: mathRound(mathSin(angle) * slicedOffset) - }; - - // set the anchor point for tooltips - radiusX = mathCos(angle) * positions[2] / 2; - radiusY = mathSin(angle) * positions[2] / 2; - point.tooltipPos = [ - positions[0] + radiusX * 0.7, - positions[1] + radiusY * 0.7 - ]; - - point.half = angle < -mathPI / 2 || angle > mathPI / 2 ? 1 : 0; - point.angle = angle; - - // set the anchor point for data labels - connectorOffset = mathMin(connectorOffset, labelDistance / 2); // #1678 - point.labelPos = [ - positions[0] + radiusX + mathCos(angle) * labelDistance, // first break of connector - positions[1] + radiusY + mathSin(angle) * labelDistance, // a/a - positions[0] + radiusX + mathCos(angle) * connectorOffset, // second break, right outside pie - positions[1] + radiusY + mathSin(angle) * connectorOffset, // a/a - positions[0] + radiusX, // landing point for connector - positions[1] + radiusY, // a/a - labelDistance < 0 ? // alignment - 'center' : - point.half ? 'right' : 'left', // alignment - angle // center angle - ]; - - } - }, - - drawGraph: null, - - /** - * Draw the data points - */ - drawPoints: function () { - var series = this, - chart = series.chart, - renderer = chart.renderer, - groupTranslation, - //center, - graphic, - //group, - shadow = series.options.shadow, - shadowGroup, - pointAttr, - shapeArgs, - attr; - - if (shadow && !series.shadowGroup) { - series.shadowGroup = renderer.g('shadow') - .add(series.group); - } - - // draw the slices - each(series.points, function (point) { - if (point.y !== null) { - graphic = point.graphic; - shapeArgs = point.shapeArgs; - shadowGroup = point.shadowGroup; - pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]; - if (!pointAttr.stroke) { - pointAttr.stroke = pointAttr.fill; - } - - // put the shadow behind all points - if (shadow && !shadowGroup) { - shadowGroup = point.shadowGroup = renderer.g('shadow') - .add(series.shadowGroup); - } - - // if the point is sliced, use special translation, else use plot area traslation - groupTranslation = point.sliced ? point.slicedTranslation : { - translateX: 0, - translateY: 0 - }; - - //group.translate(groupTranslation[0], groupTranslation[1]); - if (shadowGroup) { - shadowGroup.attr(groupTranslation); - } - - // draw the slice - if (graphic) { - graphic - .setRadialReference(series.center) - .attr(pointAttr) - .animate(extend(shapeArgs, groupTranslation)); - } else { - attr = { 'stroke-linejoin': 'round' }; - if (!point.visible) { - attr.visibility = 'hidden'; - } - - point.graphic = graphic = renderer[point.shapeType](shapeArgs) - .setRadialReference(series.center) - .attr(pointAttr) - .attr(attr) - .attr(groupTranslation) - .add(series.group) - .shadow(shadow, shadowGroup); - } - } - }); - - }, - - - searchPoint: noop, - - /** - * Utility for sorting data labels - */ - sortByAngle: function (points, sign) { - points.sort(function (a, b) { - return a.angle !== undefined && (b.angle - a.angle) * sign; - }); - }, - - /** - * Use a simple symbol from LegendSymbolMixin - */ - drawLegendSymbol: LegendSymbolMixin.drawRectangle, - - /** - * Use the getCenter method from drawLegendSymbol - */ - getCenter: CenteredSeriesMixin.getCenter, - - /** - * Pies don't have point marker symbols - */ - getSymbol: noop - - }; - PieSeries = extendClass(Series, PieSeries); - seriesTypes.pie = PieSeries; - - /** - * Draw the data labels - */ - Series.prototype.drawDataLabels = function () { - - var series = this, - seriesOptions = series.options, - cursor = seriesOptions.cursor, - options = seriesOptions.dataLabels, - points = series.points, - pointOptions, - generalOptions, - hasRendered = series.hasRendered || 0, - str, - dataLabelsGroup, - defer = pick(options.defer, true), - renderer = series.chart.renderer; - - if (options.enabled || series._hasPointLabels) { - - // Process default alignment of data labels for columns - if (series.dlProcessOptions) { - series.dlProcessOptions(options); - } - - // Create a separate group for the data labels to avoid rotation - dataLabelsGroup = series.plotGroup( - 'dataLabelsGroup', - 'data-labels', - defer && !hasRendered ? 'hidden' : 'visible', // #5133 - options.zIndex || 6 - ); - - if (defer) { - dataLabelsGroup.attr({ opacity: +hasRendered }); // #3300 - if (!hasRendered) { - addEvent(series, 'afterAnimate', function () { - if (series.visible) { // #3023, #3024 - dataLabelsGroup.show(); - } - dataLabelsGroup[seriesOptions.animation ? 'animate' : 'attr']({ opacity: 1 }, { duration: 200 }); - }); - } - } - - // Make the labels for each point - generalOptions = options; - each(points, function (point) { - - var enabled, - dataLabel = point.dataLabel, - labelConfig, - attr, - name, - rotation, - connector = point.connector, - isNew = true, - style, - moreStyle = {}; - - // Determine if each data label is enabled - pointOptions = point.dlOptions || (point.options && point.options.dataLabels); // dlOptions is used in treemaps - enabled = pick(pointOptions && pointOptions.enabled, generalOptions.enabled) && point.y !== null; // #2282, #4641 - - - // If the point is outside the plot area, destroy it. #678, #820 - if (dataLabel && !enabled) { - point.dataLabel = dataLabel.destroy(); - - // Individual labels are disabled if the are explicitly disabled - // in the point options, or if they fall outside the plot area. - } else if (enabled) { - - // Create individual options structure that can be extended without - // affecting others - options = merge(generalOptions, pointOptions); - style = options.style; - - rotation = options.rotation; - - // Get the string - labelConfig = point.getLabelConfig(); - str = options.format ? - format(options.format, labelConfig) : - options.formatter.call(labelConfig, options); - - // Determine the color - style.color = pick(options.color, style.color, series.color, 'black'); - - - // update existing label - if (dataLabel) { - - if (defined(str)) { - dataLabel - .attr({ - text: str - }); - isNew = false; - - } else { // #1437 - the label is shown conditionally - point.dataLabel = dataLabel = dataLabel.destroy(); - if (connector) { - point.connector = connector.destroy(); - } - } - - // create new label - } else if (defined(str)) { - attr = { - //align: align, - fill: options.backgroundColor, - stroke: options.borderColor, - 'stroke-width': options.borderWidth, - r: options.borderRadius || 0, - rotation: rotation, - padding: options.padding, - zIndex: 1 - }; - - // Get automated contrast color - if (style.color === 'contrast') { - moreStyle.color = options.inside || options.distance < 0 || !!seriesOptions.stacking ? - renderer.getContrast(point.color || series.color) : - '#000000'; - } - if (cursor) { - moreStyle.cursor = cursor; - } - - - // Remove unused attributes (#947) - for (name in attr) { - if (attr[name] === UNDEFINED) { - delete attr[name]; - } - } - - dataLabel = point.dataLabel = renderer[rotation ? 'text' : 'label']( // labels don't support rotation - str, - 0, - -9999, - options.shape, - null, - null, - options.useHTML - ) - .attr(attr) - .css(extend(style, moreStyle)) - .add(dataLabelsGroup) - .shadow(options.shadow); - - } - - if (dataLabel) { - // Now the data label is created and placed at 0,0, so we need to align it - series.alignDataLabel(point, dataLabel, options, null, isNew); - } - } - }); - } - }; - - /** - * Align each individual data label - */ - Series.prototype.alignDataLabel = function (point, dataLabel, options, alignTo, isNew) { - var chart = this.chart, - inverted = chart.inverted, - plotX = pick(point.plotX, -9999), - plotY = pick(point.plotY, -9999), - bBox = dataLabel.getBBox(), - baseline = chart.renderer.fontMetrics(options.style.fontSize).b, - rotation = options.rotation, - normRotation, - negRotation, - align = options.align, - rotCorr, // rotation correction - // Math.round for rounding errors (#2683), alignTo to allow column labels (#2700) - visible = this.visible && (point.series.forceDL || chart.isInsidePlot(plotX, mathRound(plotY), inverted) || - (alignTo && chart.isInsidePlot(plotX, inverted ? alignTo.x + 1 : alignTo.y + alignTo.height - 1, inverted))), - alignAttr, // the final position; - justify = pick(options.overflow, 'justify') === 'justify'; - - if (visible) { - - // The alignment box is a singular point - alignTo = extend({ - x: inverted ? chart.plotWidth - plotY : plotX, - y: mathRound(inverted ? chart.plotHeight - plotX : plotY), - width: 0, - height: 0 - }, alignTo); - - // Add the text size for alignment calculation - extend(options, { - width: bBox.width, - height: bBox.height - }); - - // Allow a hook for changing alignment in the last moment, then do the alignment - if (rotation) { - justify = false; // Not supported for rotated text - rotCorr = chart.renderer.rotCorr(baseline, rotation); // #3723 - alignAttr = { - x: alignTo.x + options.x + alignTo.width / 2 + rotCorr.x, - y: alignTo.y + options.y + { top: 0, middle: 0.5, bottom: 1 }[options.verticalAlign] * alignTo.height - }; - dataLabel[isNew ? 'attr' : 'animate'](alignAttr) - .attr({ // #3003 - align: align - }); - - // Compensate for the rotated label sticking out on the sides - normRotation = (rotation + 720) % 360; - negRotation = normRotation > 180 && normRotation < 360; - - if (align === 'left') { - alignAttr.y -= negRotation ? bBox.height : 0; - } else if (align === 'center') { - alignAttr.x -= bBox.width / 2; - alignAttr.y -= bBox.height / 2; - } else if (align === 'right') { - alignAttr.x -= bBox.width; - alignAttr.y -= negRotation ? 0 : bBox.height; - } - - - } else { - dataLabel.align(options, null, alignTo); - alignAttr = dataLabel.alignAttr; - } - - // Handle justify or crop - if (justify) { - this.justifyDataLabel(dataLabel, options, alignAttr, bBox, alignTo, isNew); - - // Now check that the data label is within the plot area - } else if (pick(options.crop, true)) { - visible = chart.isInsidePlot(alignAttr.x, alignAttr.y) && chart.isInsidePlot(alignAttr.x + bBox.width, alignAttr.y + bBox.height); - } - - // When we're using a shape, make it possible with a connector or an arrow pointing to thie point - if (options.shape && !rotation) { - dataLabel.attr({ - anchorX: point.plotX, - anchorY: point.plotY - }); - } - } - - // Show or hide based on the final aligned position - if (!visible) { - stop(dataLabel); - dataLabel.attr({ y: -9999 }); - dataLabel.placed = false; // don't animate back in - } - - }; - - /** - * If data labels fall partly outside the plot area, align them back in, in a way that - * doesn't hide the point. - */ - Series.prototype.justifyDataLabel = function (dataLabel, options, alignAttr, bBox, alignTo, isNew) { - var chart = this.chart, - align = options.align, - verticalAlign = options.verticalAlign, - off, - justified, - padding = dataLabel.box ? 0 : (dataLabel.padding || 0); - - // Off left - off = alignAttr.x + padding; - if (off < 0) { - if (align === 'right') { - options.align = 'left'; - } else { - options.x = -off; - } - justified = true; - } - - // Off right - off = alignAttr.x + bBox.width - padding; - if (off > chart.plotWidth) { - if (align === 'left') { - options.align = 'right'; - } else { - options.x = chart.plotWidth - off; - } - justified = true; - } - - // Off top - off = alignAttr.y + padding; - if (off < 0) { - if (verticalAlign === 'bottom') { - options.verticalAlign = 'top'; - } else { - options.y = -off; - } - justified = true; - } - - // Off bottom - off = alignAttr.y + bBox.height - padding; - if (off > chart.plotHeight) { - if (verticalAlign === 'top') { - options.verticalAlign = 'bottom'; - } else { - options.y = chart.plotHeight - off; - } - justified = true; - } - - if (justified) { - dataLabel.placed = !isNew; - dataLabel.align(options, null, alignTo); - } - }; - - /** - * Override the base drawDataLabels method by pie specific functionality - */ - if (seriesTypes.pie) { - seriesTypes.pie.prototype.drawDataLabels = function () { - var series = this, - data = series.data, - point, - chart = series.chart, - options = series.options.dataLabels, - connectorPadding = pick(options.connectorPadding, 10), - connectorWidth = pick(options.connectorWidth, 1), - plotWidth = chart.plotWidth, - plotHeight = chart.plotHeight, - connector, - connectorPath, - softConnector = pick(options.softConnector, true), - distanceOption = options.distance, - seriesCenter = series.center, - radius = seriesCenter[2] / 2, - centerY = seriesCenter[1], - outside = distanceOption > 0, - dataLabel, - dataLabelWidth, - labelPos, - labelHeight, - halves = [// divide the points into right and left halves for anti collision - [], // right - [] // left - ], - x, - y, - visibility, - rankArr, - i, - j, - overflow = [0, 0, 0, 0], // top, right, bottom, left - sort = function (a, b) { - return b.y - a.y; - }; - - // get out if not enabled - if (!series.visible || (!options.enabled && !series._hasPointLabels)) { - return; - } - - // run parent method - Series.prototype.drawDataLabels.apply(series); - - each(data, function (point) { - if (point.dataLabel && point.visible) { // #407, #2510 - - // Arrange points for detection collision - halves[point.half].push(point); - - // Reset positions (#4905) - point.dataLabel._pos = null; - } - }); - - /* Loop over the points in each half, starting from the top and bottom - * of the pie to detect overlapping labels. - */ - i = 2; - while (i--) { - - var slots = [], - slotsLength, - usedSlots = [], - points = halves[i], - pos, - bottom, - length = points.length, - slotIndex; - - if (!length) { - continue; - } - - // Sort by angle - series.sortByAngle(points, i - 0.5); - - // Assume equal label heights on either hemisphere (#2630) - j = labelHeight = 0; - while (!labelHeight && points[j]) { // #1569 - labelHeight = points[j] && points[j].dataLabel && (points[j].dataLabel.getBBox().height || 21); // 21 is for #968 - j++; - } - - // Only do anti-collision when we are outside the pie and have connectors (#856) - if (distanceOption > 0) { - - // Build the slots - bottom = mathMin(centerY + radius + distanceOption, chart.plotHeight); - for (pos = mathMax(0, centerY - radius - distanceOption); pos <= bottom; pos += labelHeight) { - slots.push(pos); - } - slotsLength = slots.length; - - - /* Visualize the slots - if (!series.slotElements) { - series.slotElements = []; - } - if (i === 1) { - series.slotElements.forEach(function (elem) { - elem.destroy(); - }); - series.slotElements.length = 0; - } - - slots.forEach(function (pos, no) { - var slotX = series.getX(pos, i) + chart.plotLeft - (i ? 100 : 0), - slotY = pos + chart.plotTop; - - if (isNumber(slotX)) { - series.slotElements.push(chart.renderer.rect(slotX, slotY - 7, 100, labelHeight, 1) - .attr({ - 'stroke-width': 1, - stroke: 'silver', - fill: 'rgba(0,0,255,0.1)' - }) - .add()); - series.slotElements.push(chart.renderer.text('Slot '+ no, slotX, slotY + 4) - .attr({ - fill: 'silver' - }).add()); - } - }); - // */ - - // if there are more values than available slots, remove lowest values - if (length > slotsLength) { - // create an array for sorting and ranking the points within each quarter - rankArr = [].concat(points); - rankArr.sort(sort); - j = length; - while (j--) { - rankArr[j].rank = j; - } - j = length; - while (j--) { - if (points[j].rank >= slotsLength) { - points.splice(j, 1); - } - } - length = points.length; - } - - // The label goes to the nearest open slot, but not closer to the edge than - // the label's index. - for (j = 0; j < length; j++) { - - point = points[j]; - labelPos = point.labelPos; - - var closest = 9999, - distance, - slotI; - - // find the closest slot index - for (slotI = 0; slotI < slotsLength; slotI++) { - distance = mathAbs(slots[slotI] - labelPos[1]); - if (distance < closest) { - closest = distance; - slotIndex = slotI; - } - } - - // if that slot index is closer to the edges of the slots, move it - // to the closest appropriate slot - if (slotIndex < j && slots[j] !== null) { // cluster at the top - slotIndex = j; - } else if (slotsLength < length - j + slotIndex && slots[j] !== null) { // cluster at the bottom - slotIndex = slotsLength - length + j; - while (slots[slotIndex] === null) { // make sure it is not taken - slotIndex++; - } - } else { - // Slot is taken, find next free slot below. In the next run, the next slice will find the - // slot above these, because it is the closest one - while (slots[slotIndex] === null) { // make sure it is not taken - slotIndex++; - } - } - - usedSlots.push({ i: slotIndex, y: slots[slotIndex] }); - slots[slotIndex] = null; // mark as taken - } - // sort them in order to fill in from the top - usedSlots.sort(sort); - } - - // now the used slots are sorted, fill them up sequentially - for (j = 0; j < length; j++) { - - var slot, naturalY; - - point = points[j]; - labelPos = point.labelPos; - dataLabel = point.dataLabel; - visibility = point.visible === false ? HIDDEN : 'inherit'; - naturalY = labelPos[1]; - - if (distanceOption > 0) { - slot = usedSlots.pop(); - slotIndex = slot.i; - - // if the slot next to currrent slot is free, the y value is allowed - // to fall back to the natural position - y = slot.y; - if ((naturalY > y && slots[slotIndex + 1] !== null) || - (naturalY < y && slots[slotIndex - 1] !== null)) { - y = mathMin(mathMax(0, naturalY), chart.plotHeight); - } - - } else { - y = naturalY; - } - - // get the x - use the natural x position for first and last slot, to prevent the top - // and botton slice connectors from touching each other on either side - x = options.justify ? - seriesCenter[0] + (i ? -1 : 1) * (radius + distanceOption) : - series.getX(y === centerY - radius - distanceOption || y === centerY + radius + distanceOption ? naturalY : y, i); - - - // Record the placement and visibility - dataLabel._attr = { - visibility: visibility, - align: labelPos[6] - }; - dataLabel._pos = { - x: x + options.x + - ({ left: connectorPadding, right: -connectorPadding }[labelPos[6]] || 0), - y: y + options.y - 10 // 10 is for the baseline (label vs text) - }; - dataLabel.connX = x; - dataLabel.connY = y; - - - // Detect overflowing data labels - if (this.options.size === null) { - dataLabelWidth = dataLabel.width; - // Overflow left - if (x - dataLabelWidth < connectorPadding) { - overflow[3] = mathMax(mathRound(dataLabelWidth - x + connectorPadding), overflow[3]); - - // Overflow right - } else if (x + dataLabelWidth > plotWidth - connectorPadding) { - overflow[1] = mathMax(mathRound(x + dataLabelWidth - plotWidth + connectorPadding), overflow[1]); - } - - // Overflow top - if (y - labelHeight / 2 < 0) { - overflow[0] = mathMax(mathRound(-y + labelHeight / 2), overflow[0]); - - // Overflow left - } else if (y + labelHeight / 2 > plotHeight) { - overflow[2] = mathMax(mathRound(y + labelHeight / 2 - plotHeight), overflow[2]); - } - } - } // for each point - } // for each half - - // Do not apply the final placement and draw the connectors until we have verified - // that labels are not spilling over. - if (arrayMax(overflow) === 0 || this.verifyDataLabelOverflow(overflow)) { - - // Place the labels in the final position - this.placeDataLabels(); - - // Draw the connectors - if (outside && connectorWidth) { - each(this.points, function (point) { - connector = point.connector; - labelPos = point.labelPos; - dataLabel = point.dataLabel; - - if (dataLabel && dataLabel._pos && point.visible) { - visibility = dataLabel._attr.visibility; - x = dataLabel.connX; - y = dataLabel.connY; - connectorPath = softConnector ? [ - M, - x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label - 'C', - x, y, // first break, next to the label - 2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5], - labelPos[2], labelPos[3], // second break - L, - labelPos[4], labelPos[5] // base - ] : [ - M, - x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label - L, - labelPos[2], labelPos[3], // second break - L, - labelPos[4], labelPos[5] // base - ]; - - if (connector) { - connector.animate({ d: connectorPath }); - connector.attr('visibility', visibility); - - } else { - point.connector = connector = series.chart.renderer.path(connectorPath).attr({ - 'stroke-width': connectorWidth, - stroke: options.connectorColor || point.color || '#606060', - visibility: visibility - //zIndex: 0 // #2722 (reversed) - }) - .add(series.dataLabelsGroup); - } - } else if (connector) { - point.connector = connector.destroy(); - } - }); - } - } - }; - /** - * Perform the final placement of the data labels after we have verified that they - * fall within the plot area. - */ - seriesTypes.pie.prototype.placeDataLabels = function () { - each(this.points, function (point) { - var dataLabel = point.dataLabel, - _pos; - - if (dataLabel && point.visible) { - _pos = dataLabel._pos; - if (_pos) { - dataLabel.attr(dataLabel._attr); - dataLabel[dataLabel.moved ? 'animate' : 'attr'](_pos); - dataLabel.moved = true; - } else if (dataLabel) { - dataLabel.attr({ y: -9999 }); - } - } - }); - }; - - seriesTypes.pie.prototype.alignDataLabel = noop; - - /** - * Verify whether the data labels are allowed to draw, or we should run more translation and data - * label positioning to keep them inside the plot area. Returns true when data labels are ready - * to draw. - */ - seriesTypes.pie.prototype.verifyDataLabelOverflow = function (overflow) { - - var center = this.center, - options = this.options, - centerOption = options.center, - minSize = options.minSize || 80, - newSize = minSize, - ret; - - // Handle horizontal size and center - if (centerOption[0] !== null) { // Fixed center - newSize = mathMax(center[2] - mathMax(overflow[1], overflow[3]), minSize); - - } else { // Auto center - newSize = mathMax( - center[2] - overflow[1] - overflow[3], // horizontal overflow - minSize - ); - center[0] += (overflow[3] - overflow[1]) / 2; // horizontal center - } - - // Handle vertical size and center - if (centerOption[1] !== null) { // Fixed center - newSize = mathMax(mathMin(newSize, center[2] - mathMax(overflow[0], overflow[2])), minSize); - - } else { // Auto center - newSize = mathMax( - mathMin( - newSize, - center[2] - overflow[0] - overflow[2] // vertical overflow - ), - minSize - ); - center[1] += (overflow[0] - overflow[2]) / 2; // vertical center - } - - // If the size must be decreased, we need to run translate and drawDataLabels again - if (newSize < center[2]) { - center[2] = newSize; - center[3] = Math.min(relativeLength(options.innerSize || 0, newSize), newSize); // #3632 - this.translate(center); - - if (this.drawDataLabels) { - this.drawDataLabels(); - } - // Else, return true to indicate that the pie and its labels is within the plot area - } else { - ret = true; - } - return ret; - }; - } - - if (seriesTypes.column) { - - /** - * Override the basic data label alignment by adjusting for the position of the column - */ - seriesTypes.column.prototype.alignDataLabel = function (point, dataLabel, options, alignTo, isNew) { - var inverted = this.chart.inverted, - series = point.series, - dlBox = point.dlBox || point.shapeArgs, // data label box for alignment - below = pick(point.below, point.plotY > pick(this.translatedThreshold, series.yAxis.len)), // point.below is used in range series - inside = pick(options.inside, !!this.options.stacking), // draw it inside the box? - overshoot; - - // Align to the column itself, or the top of it - if (dlBox) { // Area range uses this method but not alignTo - alignTo = merge(dlBox); - - if (alignTo.y < 0) { - alignTo.height += alignTo.y; - alignTo.y = 0; - } - overshoot = alignTo.y + alignTo.height - series.yAxis.len; - if (overshoot > 0) { - alignTo.height -= overshoot; - } - - if (inverted) { - alignTo = { - x: series.yAxis.len - alignTo.y - alignTo.height, - y: series.xAxis.len - alignTo.x - alignTo.width, - width: alignTo.height, - height: alignTo.width - }; - } - - // Compute the alignment box - if (!inside) { - if (inverted) { - alignTo.x += below ? 0 : alignTo.width; - alignTo.width = 0; - } else { - alignTo.y += below ? alignTo.height : 0; - alignTo.height = 0; - } - } - } - - - // When alignment is undefined (typically columns and bars), display the individual - // point below or above the point depending on the threshold - options.align = pick( - options.align, - !inverted || inside ? 'center' : below ? 'right' : 'left' - ); - options.verticalAlign = pick( - options.verticalAlign, - inverted || inside ? 'middle' : below ? 'top' : 'bottom' - ); - - // Call the parent method - Series.prototype.alignDataLabel.call(this, point, dataLabel, options, alignTo, isNew); - }; - } - - - - /** - * Highcharts module to hide overlapping data labels. This module is included in Highcharts. - */ - (function (H) { - var Chart = H.Chart, - each = H.each, - pick = H.pick, - addEvent = H.addEvent; - - // Collect potensial overlapping data labels. Stack labels probably don't need to be - // considered because they are usually accompanied by data labels that lie inside the columns. - Chart.prototype.callbacks.push(function (chart) { - function collectAndHide() { - var labels = []; - - each(chart.series, function (series) { - var dlOptions = series.options.dataLabels, - collections = series.dataLabelCollections || ['dataLabel']; // Range series have two collections - if ((dlOptions.enabled || series._hasPointLabels) && !dlOptions.allowOverlap && series.visible) { // #3866 - each(collections, function (coll) { - each(series.points, function (point) { - if (point[coll]) { - point[coll].labelrank = pick(point.labelrank, point.shapeArgs && point.shapeArgs.height); // #4118 - labels.push(point[coll]); - } - }); - }); - } - }); - chart.hideOverlappingLabels(labels); - } - - // Do it now ... - collectAndHide(); - - // ... and after each chart redraw - addEvent(chart, 'redraw', collectAndHide); - - }); - - /** - * Hide overlapping labels. Labels are moved and faded in and out on zoom to provide a smooth - * visual imression. - */ - Chart.prototype.hideOverlappingLabels = function (labels) { - - var len = labels.length, - label, - i, - j, - label1, - label2, - isIntersecting, - pos1, - pos2, - parent1, - parent2, - padding, - intersectRect = function (x1, y1, w1, h1, x2, y2, w2, h2) { - return !( - x2 > x1 + w1 || - x2 + w2 < x1 || - y2 > y1 + h1 || - y2 + h2 < y1 - ); - }; - - // Mark with initial opacity - for (i = 0; i < len; i++) { - label = labels[i]; - if (label) { - label.oldOpacity = label.opacity; - label.newOpacity = 1; - } - } - - // Prevent a situation in a gradually rising slope, that each label - // will hide the previous one because the previous one always has - // lower rank. - labels.sort(function (a, b) { - return (b.labelrank || 0) - (a.labelrank || 0); - }); - - // Detect overlapping labels - for (i = 0; i < len; i++) { - label1 = labels[i]; - - for (j = i + 1; j < len; ++j) { - label2 = labels[j]; - if (label1 && label2 && label1.placed && label2.placed && label1.newOpacity !== 0 && label2.newOpacity !== 0) { - pos1 = label1.alignAttr; - pos2 = label2.alignAttr; - parent1 = label1.parentGroup; // Different panes have different positions - parent2 = label2.parentGroup; - padding = 2 * (label1.box ? 0 : label1.padding); // Substract the padding if no background or border (#4333) - isIntersecting = intersectRect( - pos1.x + parent1.translateX, - pos1.y + parent1.translateY, - label1.width - padding, - label1.height - padding, - pos2.x + parent2.translateX, - pos2.y + parent2.translateY, - label2.width - padding, - label2.height - padding - ); - - if (isIntersecting) { - (label1.labelrank < label2.labelrank ? label1 : label2).newOpacity = 0; - } - } - } - } - - // Hide or show - each(labels, function (label) { - var complete, - newOpacity; - - if (label) { - newOpacity = label.newOpacity; - - if (label.oldOpacity !== newOpacity && label.placed) { - - // Make sure the label is completely hidden to avoid catching clicks (#4362) - if (newOpacity) { - label.show(true); - } else { - complete = function () { - label.hide(); - }; - } - - // Animate or set the opacity - label.alignAttr.opacity = newOpacity; - label[label.isOld ? 'animate' : 'attr'](label.alignAttr, null, complete); - - } - label.isOld = true; - } - }); - }; - }(Highcharts)); - /** - * TrackerMixin for points and graphs - */ - - var TrackerMixin = Highcharts.TrackerMixin = { - - drawTrackerPoint: function () { - var series = this, - chart = series.chart, - pointer = chart.pointer, - cursor = series.options.cursor, - css = cursor && { cursor: cursor }, - onMouseOver = function (e) { - var target = e.target, - point; - - while (target && !point) { - point = target.point; - target = target.parentNode; - } - - if (point !== UNDEFINED && point !== chart.hoverPoint) { // undefined on graph in scatterchart - point.onMouseOver(e); - } - }; - - // Add reference to the point - each(series.points, function (point) { - if (point.graphic) { - point.graphic.element.point = point; - } - if (point.dataLabel) { - point.dataLabel.element.point = point; - } - }); - - // Add the event listeners, we need to do this only once - if (!series._hasTracking) { - each(series.trackerGroups, function (key) { - if (series[key]) { // we don't always have dataLabelsGroup - series[key] - .addClass(PREFIX + 'tracker') - .on('mouseover', onMouseOver) - .on('mouseout', function (e) { - pointer.onTrackerMouseOut(e); - }) - .css(css); - if (hasTouch) { - series[key].on('touchstart', onMouseOver); - } - } - }); - series._hasTracking = true; - } - }, - - /** - * Draw the tracker object that sits above all data labels and markers to - * track mouse events on the graph or points. For the line type charts - * the tracker uses the same graphPath, but with a greater stroke width - * for better control. - */ - drawTrackerGraph: function () { - var series = this, - options = series.options, - trackByArea = options.trackByArea, - trackerPath = [].concat(trackByArea ? series.areaPath : series.graphPath), - trackerPathLength = trackerPath.length, - chart = series.chart, - pointer = chart.pointer, - renderer = chart.renderer, - snap = chart.options.tooltip.snap, - tracker = series.tracker, - cursor = options.cursor, - css = cursor && { cursor: cursor }, - i, - onMouseOver = function () { - if (chart.hoverSeries !== series) { - series.onMouseOver(); - } - }, - /* - * Empirical lowest possible opacities for TRACKER_FILL for an element to stay invisible but clickable - * IE6: 0.002 - * IE7: 0.002 - * IE8: 0.002 - * IE9: 0.00000000001 (unlimited) - * IE10: 0.0001 (exporting only) - * FF: 0.00000000001 (unlimited) - * Chrome: 0.000001 - * Safari: 0.000001 - * Opera: 0.00000000001 (unlimited) - */ - TRACKER_FILL = 'rgba(192,192,192,' + (hasSVG ? 0.0001 : 0.002) + ')'; - - // Extend end points. A better way would be to use round linecaps, - // but those are not clickable in VML. - if (trackerPathLength && !trackByArea) { - i = trackerPathLength + 1; - while (i--) { - if (trackerPath[i] === M) { // extend left side - trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], L); - } - if ((i && trackerPath[i] === M) || i === trackerPathLength) { // extend right side - trackerPath.splice(i, 0, L, trackerPath[i - 2] + snap, trackerPath[i - 1]); - } - } - } - - // handle single points - /*for (i = 0; i < singlePoints.length; i++) { - singlePoint = singlePoints[i]; - trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY, - L, singlePoint.plotX + snap, singlePoint.plotY); - }*/ - - // draw the tracker - if (tracker) { - tracker.attr({ d: trackerPath }); - } else { // create - - series.tracker = renderer.path(trackerPath) - .attr({ - 'stroke-linejoin': 'round', // #1225 - visibility: series.visible ? VISIBLE : HIDDEN, - stroke: TRACKER_FILL, - fill: trackByArea ? TRACKER_FILL : NONE, - 'stroke-width': options.lineWidth + (trackByArea ? 0 : 2 * snap), - zIndex: 2 - }) - .add(series.group); - - // The tracker is added to the series group, which is clipped, but is covered - // by the marker group. So the marker group also needs to capture events. - each([series.tracker, series.markerGroup], function (tracker) { - tracker.addClass(PREFIX + 'tracker') - .on('mouseover', onMouseOver) - .on('mouseout', function (e) { - pointer.onTrackerMouseOut(e); - }) - .css(css); - - if (hasTouch) { - tracker.on('touchstart', onMouseOver); - } - }); - } - } - }; - /* End TrackerMixin */ - - - /** - * Add tracking event listener to the series group, so the point graphics - * themselves act as trackers - */ - - if (seriesTypes.column) { - ColumnSeries.prototype.drawTracker = TrackerMixin.drawTrackerPoint; - } - - if (seriesTypes.pie) { - seriesTypes.pie.prototype.drawTracker = TrackerMixin.drawTrackerPoint; - } - - if (seriesTypes.scatter) { - ScatterSeries.prototype.drawTracker = TrackerMixin.drawTrackerPoint; - } - - /* - * Extend Legend for item events - */ - extend(Legend.prototype, { - - setItemEvents: function (item, legendItem, useHTML, itemStyle, itemHiddenStyle) { - var legend = this; - // Set the events on the item group, or in case of useHTML, the item itself (#1249) - (useHTML ? legendItem : item.legendGroup).on('mouseover', function () { - item.setState(HOVER_STATE); - legendItem.css(legend.options.itemHoverStyle); - }) - .on('mouseout', function () { - legendItem.css(item.visible ? itemStyle : itemHiddenStyle); - item.setState(); - }) - .on('click', function (event) { - var strLegendItemClick = 'legendItemClick', - fnLegendItemClick = function () { - if (item.setVisible) { - item.setVisible(); - } - }; - - // Pass over the click/touch event. #4. - event = { - browserEvent: event - }; - - // click the name or symbol - if (item.firePointEvent) { // point - item.firePointEvent(strLegendItemClick, event, fnLegendItemClick); - } else { - fireEvent(item, strLegendItemClick, event, fnLegendItemClick); - } - }); - }, - - createCheckboxForItem: function (item) { - var legend = this; - - item.checkbox = createElement('input', { - type: 'checkbox', - checked: item.selected, - defaultChecked: item.selected // required by IE7 - }, legend.options.itemCheckboxStyle, legend.chart.container); - - addEvent(item.checkbox, 'click', function (event) { - var target = event.target; - fireEvent( - item.series || item, - 'checkboxClick', - { // #3712 - checked: target.checked, - item: item - }, - function () { - item.select(); - } - ); - }); - } - }); - - /* - * Add pointer cursor to legend itemstyle in defaultOptions - */ - defaultOptions.legend.itemStyle.cursor = 'pointer'; - - - /* - * Extend the Chart object with interaction - */ - - extend(Chart.prototype, { - /** - * Display the zoom button - */ - showResetZoom: function () { - var chart = this, - lang = defaultOptions.lang, - btnOptions = chart.options.chart.resetZoomButton, - theme = btnOptions.theme, - states = theme.states, - alignTo = btnOptions.relativeTo === 'chart' ? null : 'plotBox'; - - function zoomOut() { - chart.zoomOut(); - } - - this.resetZoomButton = chart.renderer.button(lang.resetZoom, null, null, zoomOut, theme, states && states.hover) - .attr({ - align: btnOptions.position.align, - title: lang.resetZoomTitle - }) - .add() - .align(btnOptions.position, false, alignTo); - - }, - - /** - * Zoom out to 1:1 - */ - zoomOut: function () { - var chart = this; - fireEvent(chart, 'selection', { resetSelection: true }, function () { - chart.zoom(); - }); - }, - - /** - * Zoom into a given portion of the chart given by axis coordinates - * @param {Object} event - */ - zoom: function (event) { - var chart = this, - hasZoomed, - pointer = chart.pointer, - displayButton = false, - resetZoomButton; - - // If zoom is called with no arguments, reset the axes - if (!event || event.resetSelection) { - each(chart.axes, function (axis) { - hasZoomed = axis.zoom(); - }); - } else { // else, zoom in on all axes - each(event.xAxis.concat(event.yAxis), function (axisData) { - var axis = axisData.axis, - isXAxis = axis.isXAxis; - - // don't zoom more than minRange - if (pointer[isXAxis ? 'zoomX' : 'zoomY'] || pointer[isXAxis ? 'pinchX' : 'pinchY']) { - hasZoomed = axis.zoom(axisData.min, axisData.max); - if (axis.displayBtn) { - displayButton = true; - } - } - }); - } - - // Show or hide the Reset zoom button - resetZoomButton = chart.resetZoomButton; - if (displayButton && !resetZoomButton) { - chart.showResetZoom(); - } else if (!displayButton && isObject(resetZoomButton)) { - chart.resetZoomButton = resetZoomButton.destroy(); - } - - - // Redraw - if (hasZoomed) { - chart.redraw( - pick(chart.options.chart.animation, event && event.animation, chart.pointCount < 100) // animation - ); - } - }, - - /** - * Pan the chart by dragging the mouse across the pane. This function is called - * on mouse move, and the distance to pan is computed from chartX compared to - * the first chartX position in the dragging operation. - */ - pan: function (e, panning) { - - var chart = this, - hoverPoints = chart.hoverPoints, - doRedraw; - - // remove active points for shared tooltip - if (hoverPoints) { - each(hoverPoints, function (point) { - point.setState(); - }); - } - - each(panning === 'xy' ? [1, 0] : [1], function (isX) { // xy is used in maps - var axis = chart[isX ? 'xAxis' : 'yAxis'][0], - horiz = axis.horiz, - mousePos = e[horiz ? 'chartX' : 'chartY'], - mouseDown = horiz ? 'mouseDownX' : 'mouseDownY', - startPos = chart[mouseDown], - halfPointRange = (axis.pointRange || 0) / 2, - extremes = axis.getExtremes(), - newMin = axis.toValue(startPos - mousePos, true) + halfPointRange, - newMax = axis.toValue(startPos + axis.len - mousePos, true) - halfPointRange, - goingLeft = startPos > mousePos; // #3613 - - if (axis.series.length && - (goingLeft || newMin > mathMin(extremes.dataMin, extremes.min)) && - (!goingLeft || newMax < mathMax(extremes.dataMax, extremes.max))) { - axis.setExtremes(newMin, newMax, false, false, { trigger: 'pan' }); - doRedraw = true; - } - - chart[mouseDown] = mousePos; // set new reference for next run - }); - - if (doRedraw) { - chart.redraw(false); - } - css(chart.container, { cursor: 'move' }); - } - }); - - /* - * Extend the Point object with interaction - */ - extend(Point.prototype, { - /** - * Toggle the selection status of a point - * @param {Boolean} selected Whether to select or unselect the point. - * @param {Boolean} accumulate Whether to add to the previous selection. By default, - * this happens if the control key (Cmd on Mac) was pressed during clicking. - */ - select: function (selected, accumulate) { - var point = this, - series = point.series, - chart = series.chart; - - selected = pick(selected, !point.selected); - - // fire the event with the default handler - point.firePointEvent(selected ? 'select' : 'unselect', { accumulate: accumulate }, function () { - point.selected = point.options.selected = selected; - series.options.data[inArray(point, series.data)] = point.options; - - point.setState(selected && SELECT_STATE); - - // unselect all other points unless Ctrl or Cmd + click - if (!accumulate) { - each(chart.getSelectedPoints(), function (loopPoint) { - if (loopPoint.selected && loopPoint !== point) { - loopPoint.selected = loopPoint.options.selected = false; - series.options.data[inArray(loopPoint, series.data)] = loopPoint.options; - loopPoint.setState(NORMAL_STATE); - loopPoint.firePointEvent('unselect'); - } - }); - } - }); - }, - - /** - * Runs on mouse over the point - * - * @param {Object} e The event arguments - * @param {Boolean} byProximity Falsy for kd points that are closest to the mouse, or to - * actually hovered points. True for other points in shared tooltip. - */ - onMouseOver: function (e, byProximity) { - var point = this, - series = point.series, - chart = series.chart, - tooltip = chart.tooltip, - hoverPoint = chart.hoverPoint; - - if (chart.hoverSeries !== series) { - series.onMouseOver(); - } - - // set normal state to previous series - if (hoverPoint && hoverPoint !== point) { - hoverPoint.onMouseOut(); - } - - if (point.series) { // It may have been destroyed, #4130 - - // trigger the event - point.firePointEvent('mouseOver'); - - // update the tooltip - if (tooltip && (!tooltip.shared || series.noSharedTooltip)) { - tooltip.refresh(point, e); - } - - // hover this - point.setState(HOVER_STATE); - if (!byProximity) { - chart.hoverPoint = point; - } - } - }, - - /** - * Runs on mouse out from the point - */ - onMouseOut: function () { - var chart = this.series.chart, - hoverPoints = chart.hoverPoints; - - this.firePointEvent('mouseOut'); - - if (!hoverPoints || inArray(this, hoverPoints) === -1) { // #887, #2240 - this.setState(); - chart.hoverPoint = null; - } - }, - - /** - * Import events from the series' and point's options. Only do it on - * demand, to save processing time on hovering. - */ - importEvents: function () { - if (!this.hasImportedEvents) { - var point = this, - options = merge(point.series.options.point, point.options), - events = options.events, - eventType; - - point.events = events; - - for (eventType in events) { - addEvent(point, eventType, events[eventType]); - } - this.hasImportedEvents = true; - - } - }, - - /** - * Set the point's state - * @param {String} state - */ - setState: function (state, move) { - var point = this, - plotX = mathFloor(point.plotX), // #4586 - plotY = point.plotY, - series = point.series, - stateOptions = series.options.states, - markerOptions = defaultPlotOptions[series.type].marker && series.options.marker, - normalDisabled = markerOptions && !markerOptions.enabled, - markerStateOptions = markerOptions && markerOptions.states[state], - stateDisabled = markerStateOptions && markerStateOptions.enabled === false, - stateMarkerGraphic = series.stateMarkerGraphic, - pointMarker = point.marker || {}, - chart = series.chart, - radius, - halo = series.halo, - haloOptions, - newSymbol, - pointAttr; - - state = state || NORMAL_STATE; // empty string - pointAttr = point.pointAttr[state] || series.pointAttr[state]; - - if ( - // already has this state - (state === point.state && !move) || - // selected points don't respond to hover - (point.selected && state !== SELECT_STATE) || - // series' state options is disabled - (stateOptions[state] && stateOptions[state].enabled === false) || - // general point marker's state options is disabled - (state && (stateDisabled || (normalDisabled && markerStateOptions.enabled === false))) || - // individual point marker's state options is disabled - (state && pointMarker.states && pointMarker.states[state] && pointMarker.states[state].enabled === false) // #1610 - - ) { - return; - } - - // apply hover styles to the existing point - if (point.graphic) { - radius = markerOptions && point.graphic.symbolName && pointAttr.r; - point.graphic.attr(merge( - pointAttr, - radius ? { // new symbol attributes (#507, #612) - x: plotX - radius, - y: plotY - radius, - width: 2 * radius, - height: 2 * radius - } : {} - )); - - // Zooming in from a range with no markers to a range with markers - if (stateMarkerGraphic) { - stateMarkerGraphic.hide(); - } - } else { - // if a graphic is not applied to each point in the normal state, create a shared - // graphic for the hover state - if (state && markerStateOptions) { - radius = markerStateOptions.radius; - newSymbol = pointMarker.symbol || series.symbol; - - // If the point has another symbol than the previous one, throw away the - // state marker graphic and force a new one (#1459) - if (stateMarkerGraphic && stateMarkerGraphic.currentSymbol !== newSymbol) { - stateMarkerGraphic = stateMarkerGraphic.destroy(); - } - - // Add a new state marker graphic - if (!stateMarkerGraphic) { - if (newSymbol) { - series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol( - newSymbol, - plotX - radius, - plotY - radius, - 2 * radius, - 2 * radius - ) - .attr(pointAttr) - .add(series.markerGroup); - stateMarkerGraphic.currentSymbol = newSymbol; - } - - // Move the existing graphic - } else { - stateMarkerGraphic[move ? 'animate' : 'attr']({ // #1054 - x: plotX - radius, - y: plotY - radius - }); - } - } - - if (stateMarkerGraphic) { - stateMarkerGraphic[state && chart.isInsidePlot(plotX, plotY, chart.inverted) ? 'show' : 'hide'](); // #2450 - stateMarkerGraphic.element.point = point; // #4310 - } - } - - // Show me your halo - haloOptions = stateOptions[state] && stateOptions[state].halo; - if (haloOptions && haloOptions.size) { - if (!halo) { - series.halo = halo = chart.renderer.path() - .add(chart.seriesGroup); - } - halo.attr(extend({ - 'fill': point.color || series.color, - 'fill-opacity': haloOptions.opacity, - 'zIndex': -1 // #4929, IE8 added halo above everything - }, - haloOptions.attributes))[move ? 'animate' : 'attr']({ - d: point.haloPath(haloOptions.size) - }); - } else if (halo) { - halo.attr({ d: [] }); - } - - point.state = state; - }, - - /** - * Get the circular path definition for the halo - * @param {Number} size The radius of the circular halo - * @returns {Array} The path definition - */ - haloPath: function (size) { - var series = this.series, - chart = series.chart, - plotBox = series.getPlotBox(), - inverted = chart.inverted, - plotX = Math.floor(this.plotX); - - return chart.renderer.symbols.circle( - plotBox.translateX + (inverted ? series.yAxis.len - this.plotY : plotX) - size, - plotBox.translateY + (inverted ? series.xAxis.len - plotX : this.plotY) - size, - size * 2, - size * 2 - ); - } - }); - - /* - * Extend the Series object with interaction - */ - - extend(Series.prototype, { - /** - * Series mouse over handler - */ - onMouseOver: function () { - var series = this, - chart = series.chart, - hoverSeries = chart.hoverSeries; - - // set normal state to previous series - if (hoverSeries && hoverSeries !== series) { - hoverSeries.onMouseOut(); - } - - // trigger the event, but to save processing time, - // only if defined - if (series.options.events.mouseOver) { - fireEvent(series, 'mouseOver'); - } - - // hover this - series.setState(HOVER_STATE); - chart.hoverSeries = series; - }, - - /** - * Series mouse out handler - */ - onMouseOut: function () { - // trigger the event only if listeners exist - var series = this, - options = series.options, - chart = series.chart, - tooltip = chart.tooltip, - hoverPoint = chart.hoverPoint; - - chart.hoverSeries = null; // #182, set to null before the mouseOut event fires - - // trigger mouse out on the point, which must be in this series - if (hoverPoint) { - hoverPoint.onMouseOut(); - } - - // fire the mouse out event - if (series && options.events.mouseOut) { - fireEvent(series, 'mouseOut'); - } - - - // hide the tooltip - if (tooltip && !options.stickyTracking && (!tooltip.shared || series.noSharedTooltip)) { - tooltip.hide(); - } - - // set normal state - series.setState(); - }, - - /** - * Set the state of the graph - */ - setState: function (state) { - var series = this, - options = series.options, - graph = series.graph, - stateOptions = options.states, - lineWidth = options.lineWidth, - attribs, - i = 0; - - state = state || NORMAL_STATE; - - if (series.state !== state) { - series.state = state; - - if (stateOptions[state] && stateOptions[state].enabled === false) { - return; - } - - if (state) { - lineWidth = stateOptions[state].lineWidth || lineWidth + (stateOptions[state].lineWidthPlus || 0); // #4035 - } - - if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML - attribs = { - 'stroke-width': lineWidth - }; - // use attr because animate will cause any other animation on the graph to stop - graph.attr(attribs); - while (series['zoneGraph' + i]) { - series['zoneGraph' + i].attr(attribs); - i = i + 1; - } - } - } - }, - - /** - * Set the visibility of the graph - * - * @param vis {Boolean} True to show the series, false to hide. If UNDEFINED, - * the visibility is toggled. - */ - setVisible: function (vis, redraw) { - var series = this, - chart = series.chart, - legendItem = series.legendItem, - showOrHide, - ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries, - oldVisibility = series.visible; - - // if called without an argument, toggle visibility - series.visible = vis = series.userOptions.visible = vis === UNDEFINED ? !oldVisibility : vis; - showOrHide = vis ? 'show' : 'hide'; - - // show or hide elements - each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker'], function (key) { - if (series[key]) { - series[key][showOrHide](); - } - }); - - - // hide tooltip (#1361) - if (chart.hoverSeries === series || (chart.hoverPoint && chart.hoverPoint.series) === series) { - series.onMouseOut(); - } - - - if (legendItem) { - chart.legend.colorizeItem(series, vis); - } - - - // rescale or adapt to resized chart - series.isDirty = true; - // in a stack, all other series are affected - if (series.options.stacking) { - each(chart.series, function (otherSeries) { - if (otherSeries.options.stacking && otherSeries.visible) { - otherSeries.isDirty = true; - } - }); - } - - // show or hide linked series - each(series.linkedSeries, function (otherSeries) { - otherSeries.setVisible(vis, false); - }); - - if (ignoreHiddenSeries) { - chart.isDirtyBox = true; - } - if (redraw !== false) { - chart.redraw(); - } - - fireEvent(series, showOrHide); - }, - - /** - * Show the graph - */ - show: function () { - this.setVisible(true); - }, - - /** - * Hide the graph - */ - hide: function () { - this.setVisible(false); - }, - - - /** - * Set the selected state of the graph - * - * @param selected {Boolean} True to select the series, false to unselect. If - * UNDEFINED, the selection state is toggled. - */ - select: function (selected) { - var series = this; - // if called without an argument, toggle - series.selected = selected = (selected === UNDEFINED) ? !series.selected : selected; - - if (series.checkbox) { - series.checkbox.checked = selected; - } - - fireEvent(series, selected ? 'select' : 'unselect'); - }, - - drawTracker: TrackerMixin.drawTrackerGraph - }); - - // global variables - extend(Highcharts, { - - // Constructors - Color: Color, - Point: Point, - Tick: Tick, - Renderer: Renderer, - SVGElement: SVGElement, - SVGRenderer: SVGRenderer, - - // Various - arrayMin: arrayMin, - arrayMax: arrayMax, - charts: charts, - correctFloat: correctFloat, - dateFormat: dateFormat, - error: error, - format: format, - pathAnim: pathAnim, - getOptions: getOptions, - hasBidiBug: hasBidiBug, - isTouchDevice: isTouchDevice, - setOptions: setOptions, - addEvent: addEvent, - removeEvent: removeEvent, - createElement: createElement, - discardElement: discardElement, - css: css, - each: each, - map: map, - merge: merge, - splat: splat, - stableSort: stableSort, - extendClass: extendClass, - pInt: pInt, - svg: hasSVG, - canvas: useCanVG, - vml: !hasSVG && !useCanVG, - product: PRODUCT, - version: VERSION - }); - - return Highcharts; -})); diff --git a/public/vendor/highcharts-4.2.5/highmaps.js b/public/vendor/highcharts-4.2.5/highmaps.js deleted file mode 100644 index 63d90b2b94e3b..0000000000000 --- a/public/vendor/highcharts-4.2.5/highmaps.js +++ /dev/null @@ -1,370 +0,0 @@ -/* - Highmaps JS v4.2.5 (2016-05-06) - - (c) 2011-2016 Torstein Honsi - - License: www.highcharts.com/license -*/ -(function(D,V){typeof module==="object"&&module.exports?module.exports=D.document?V(D):V:D.Highcharts=V(D)})(typeof window!=="undefined"?window:this,function(D){function V(a,b){var c="Highcharts error #"+a+": www.highcharts.com/errors/"+a;if(b)throw Error(c);D.console&&console.log(c)}function kb(a,b,c){this.options=b;this.elem=a;this.prop=c}function C(){var a,b=arguments,c,d={},e=function(a,b){var c,d;typeof a!=="object"&&(a={});for(d in b)b.hasOwnProperty(d)&&(c=b[d],a[d]=c&&typeof c==="object"&& -Object.prototype.toString.call(c)!=="[object Array]"&&d!=="renderTo"&&typeof c.nodeType!=="number"?e(a[d]||{},c):b[d]);return a};b[0]===!0&&(d=b[1],b=Array.prototype.slice.call(b,2));c=b.length;for(a=0;a-1?h.thousandsSep:""))):e=Oa(f,e)}k.push(e);a=a.slice(c+1);c=(d=!d)?"}":"{"}k.push(a);return k.join("")}function ub(a,b,c,d,e){var f,g=a,c=o(c,1);f=a/c;b||(b=[1,2,2.5,5,10],d===!1&&(c===1?b=[1,2,5,10]:c<=0.1&&(b=[1/c])));for(d=0;d=a||!e&&f<=(b[d]+(b[d+1]||b[d]))/2)break;g*=c;return g}function cb(a,b){var c=a.length,d,e;for(e=0;ec&&(c=a[b]);return c}function db(a,b){for(var c in a)a[c]&&a[c]!==b&&a[c].destroy&&a[c].destroy(),delete a[c]}function Qa(a){eb||(eb=da(Ja));a&&eb.appendChild(a);eb.innerHTML=""}function pa(a,b){return parseFloat(a.toPrecision(b||14))}function Va(a,b){b.renderer.globalAnimation=o(a,b.animation)}function Ra(a){return aa(a)?C(a):{duration:a?500:0}}function vb(){var a=L.global,b=a.useUTC,c=b?"getUTC":"get", -d=b?"setUTC":"set";sa=a.Date||D.Date;wb=b&&a.timezoneOffset;lb=b&&a.getTimezoneOffset;xb=c+"Minutes";yb=c+"Hours";zb=c+"Day";mb=c+"Date";nb=c+"Month";ob=c+"FullYear";Ab=d+"Date";Bb=d+"Month";Cb=d+"FullYear"}function T(a){if(!(this instanceof T))return new T(a);this.init(a)}function Q(){}function Sa(a,b,c,d){this.axis=a;this.pos=b;this.type=c||"";this.isNew=!0;!c&&!d&&this.addLabel()}function Db(a,b){var c,d,e,f,g=!1,h=a.x,i=a.y;for(c=0,d=b.length-1;ci,f=b[d][1]>i,e!==f&& -h<(b[d][0]-b[c][0])*(i-b[c][1])/(b[d][1]-b[c][1])+b[c][0]&&(g=!g);return g}function Eb(a,b,c,d,e,f,g,h){return["M",a+e,b,"L",a+c-f,b,"C",a+c-f/2,b,a+c,b+f/2,a+c,b+f,"L",a+c,b+d-g,"C",a+c,b+d-g/2,a+c-g/2,b+d,a+c-g,b+d,"L",a+h,b+d,"C",a+h/2,b+d,a,b+d-h/2,a,b+d-h,"L",a,b+e,"C",a,b+e/2,a+e/2,b,a+e,b,"Z"]}var w,z=D.document,H=Math,x=H.round,fa=H.floor,ta=H.ceil,u=H.max,I=H.min,S=H.abs,ka=H.cos,qa=H.sin,pb=H.PI,la=pb*2/360,ya=D.navigator&&D.navigator.userAgent||"",Fb=D.opera,xa=/(msie|trident|edge)/i.test(ya)&& -!Fb,fb=z&&z.documentMode===8,gb=!xa&&/AppleWebKit/.test(ya),Ka=/Firefox/.test(ya),Gb=/(Mobile|Android|Windows Phone)/.test(ya),Fa="http://www.w3.org/2000/svg",ea=z&&z.createElementNS&&!!z.createElementNS(Fa,"svg").createSVGRect,Mb=Ka&&parseInt(ya.split("Firefox/")[1],10)<4,ma=z&&!ea&&!xa&&!!z.createElement("canvas").getContext,Ta,Wa,Hb={},qb=0,eb,L,Oa,hb,W=function(){},O=[],Xa=0,Ja="div",Nb=/^[0-9]+$/,ib=["plotTop","marginRight","marginBottom","plotLeft"],sa,wb,lb,xb,yb,zb,mb,nb,ob,Ab,Bb,Cb,y={}, -s;s=D.Highcharts?V(16,!0):{win:D};s.seriesTypes=y;var Ga=[],na,za,n,Ha,rb,Aa,N,Y,G,Ua,La;kb.prototype={dSetter:function(){var a=this.paths[0],b=this.paths[1],c=[],d=this.now,e=a.length,f;if(d===1)c=this.toD;else if(e===b.length&&d<1)for(;e--;)f=parseFloat(a[e]),c[e]=isNaN(f)?a[e]:d*parseFloat(b[e]-f)+f;else c=b;this.elem.attr("d",c)},update:function(){var a=this.elem,b=this.prop,c=this.now,d=this.options.step;if(this[b+"Setter"])this[b+"Setter"]();else a.attr?a.element&&a.attr(b,c):a.style[b]=c+this.unit; -d&&d.call(a,c,this)},run:function(a,b,c){var d=this,e=function(a){return e.stopped?!1:d.step(a)},f;this.startTime=+new sa;this.start=a;this.end=b;this.unit=c;this.now=this.start;this.pos=0;e.elem=this.elem;if(e()&&Ga.push(e)===1)e.timerId=setInterval(function(){for(f=0;f=f+ -this.startTime){this.now=this.end;this.pos=1;this.update();a=g[this.prop]=!0;for(h in g)g[h]!==!0&&(a=!1);a&&e&&e.call(c);c=!1}else this.pos=d.easing((b-this.startTime)/f),this.now=this.start+(this.end-this.start)*this.pos,this.update(),c=!0;return c},initPath:function(a,b,c){var b=b||"",d=a.shift,e=b.indexOf("C")>-1,f=e?7:3,g,b=b.split(" "),c=[].concat(c),h=a.isArea,i=h?2:1,k=function(a){for(g=a.length;g--;)(a[g]==="M"||a[g]==="L")&&a.splice(g+1,0,a[g+1],a[g+2],a[g+1],a[g+2])};e&&(k(b),k(c));if(d<= -c.length/f&&b.length===c.length)for(;d--;)c=c.slice(0,f).concat(c),h&&(c=c.concat(c.slice(c.length-f)));a.shift=0;if(b.length)for(a=c.length;b.length3?g.length%3:0;c=o(c,e.decimalPoint);d=o(d,e.thousandsSep);a=a<0?"-":"";a+=h?g.substr(0,h)+d:"";a+=g.substr(h).replace(/(\d{3})(?=\d)/g,"$1"+d);b&&(d=Math.abs(i-g+Math.pow(10,-Math.max(b,f)-1)),a+=c+d.toFixed(b).slice(2));return a};Math.easeInOutSine=function(a){return-0.5*(Math.cos(Math.PI*a)-1)};na=function(a,b){var c;if(b==="width")return Math.min(a.offsetWidth,a.scrollWidth)-na(a,"padding-left")-na(a,"padding-right"); -else if(b==="height")return Math.min(a.offsetHeight,a.scrollHeight)-na(a,"padding-top")-na(a,"padding-bottom");return(c=D.getComputedStyle(a,void 0))&&E(c.getPropertyValue(b))};za=function(a,b){return b.indexOf?b.indexOf(a):[].indexOf.call(b,a)};Ha=function(a,b){return[].filter.call(a,b)};Aa=function(a,b){for(var c=[],d=0,e=a.length;d-1&&(f.splice(h,1),g[b]=f),d(b,c)):(e(),g[b]=[])):(e(),a.hcEvents={})};G=function(a,b,c,d){var e;e=a.hcEvents;var f,g,c=c||{};if(z.createEvent&&(a.dispatchEvent||a.fireEvent))e=z.createEvent("Events"), -e.initEvent(b,!0,!0),e.target=a,r(e,c),a.dispatchEvent?a.dispatchEvent(e):a.fireEvent(b,e);else if(e){e=e[b]||[];f=e.length;if(!c.preventDefault)c.preventDefault=function(){c.defaultPrevented=!0};c.target=a;if(!c.type)c.type=b;for(b=0;b{point.key}
',pointFormat:'\u25cf {series.name}: {point.y}
',shadow:!0,snap:Gb?25:10,style:{color:"#333333",cursor:"default",fontSize:"12px",padding:"8px",pointerEvents:"none",whiteSpace:"nowrap"}},credits:{enabled:!0,text:"Highcharts.com", -href:"http://www.highcharts.com",position:{align:"right",x:-10,verticalAlign:"bottom",y:-5},style:{cursor:"pointer",color:"#909090",fontSize:"9px"}}};var Z=L.plotOptions,Ya=Z.line;vb();T.prototype={parsers:[{regex:/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/,parse:function(a){return[E(a[1]),E(a[2]),E(a[3]),parseFloat(a[4],10)]}},{regex:/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/,parse:function(a){return[E(a[1],16),E(a[2],16),E(a[3],16),1]}}, -{regex:/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/,parse:function(a){return[E(a[1]),E(a[2]),E(a[3]),1]}}],init:function(a){var b,c,d,e;if((this.input=a)&&a.stops)this.stops=Aa(a.stops,function(a){return new T(a[1])});else for(d=this.parsers.length;d--&&!c;)e=this.parsers[d],(b=e.regex.exec(a))&&(c=e.parse(b));this.rgba=c||[]},get:function(a){var b=this.input,c=this.rgba,d;this.stops?(d=C(b),d.stops=[].concat(d.stops),n(this.stops,function(b,c){d.stops[c]=[d.stops[c][0],b.get(a)]})): -d=c&&F(c[0])?a==="rgb"||!a&&c[3]===1?"rgb("+c[0]+","+c[1]+","+c[2]+")":a==="a"?c[3]:"rgba("+c.join(",")+")":b;return d},brighten:function(a){var b,c=this.rgba;if(this.stops)n(this.stops,function(b){b.brighten(a)});else if(F(a)&&a!==0)for(b=0;b<3;b++)c[b]+=E(a*255),c[b]<0&&(c[b]=0),c[b]>255&&(c[b]=255);return this},setOpacity:function(a){this.rgba[3]=a;return this}};Q.prototype={opacity:1,textProps:"direction,fontSize,fontWeight,fontFamily,fontStyle,color,lineHeight,width,textDecoration,textOverflow,textShadow".split(","), -init:function(a,b){this.element=b==="span"?da(b):z.createElementNS(Fa,b);this.renderer=a},animate:function(a,b,c){b=o(b,this.renderer.globalAnimation,!0);La(this);if(b){if(c)b.complete=c;Ua(this,a,b)}else this.attr(a,null,c);return this},colorGradient:function(a,b,c){var d=this.renderer,e,f,g,h,i,k,j,l,m,p,q,A=[],o;a.linearGradient?f="linearGradient":a.radialGradient&&(f="radialGradient");if(f){g=a[f];i=d.gradients;j=a.stops;p=c.radialReference;Ca(g)&&(a[f]=g={x1:g[0],y1:g[1],x2:g[2],y2:g[3],gradientUnits:"userSpaceOnUse"}); -f==="radialGradient"&&p&&!t(g.gradientUnits)&&(h=g,g=C(g,d.getRadialAttr(p,h),{gradientUnits:"userSpaceOnUse"}));for(q in g)q!=="id"&&A.push(q,g[q]);for(q in j)A.push(j[q]);A=A.join(",");i[A]?p=i[A].attr("id"):(g.id=p="highcharts-"+qb++,i[A]=k=d.createElement(f).attr(g).add(d.defs),k.radAttr=h,k.stops=[],n(j,function(a){a[1].indexOf("rgba")===0?(e=T(a[1]),l=e.get("rgb"),m=e.get("a")):(l=a[1],m=1);a=d.createElement("stop").attr({offset:a[0],"stop-color":l,"stop-opacity":m}).add(k);k.stops.push(a)})); -o="url("+d.url+"#"+p+")";c.setAttribute(b,o);c.gradient=A;a.toString=function(){return o}}},applyTextShadow:function(a){var b=this.element,c,d=a.indexOf("contrast")!==-1,e={},f=this.renderer.forExport,g=f||b.style.textShadow!==w&&!xa;if(d)e.textShadow=a=a.replace(/contrast/g,this.renderer.getContrast(b.style.fill));if(gb||f)e.textRendering="geometricPrecision";g?this.css(e):(this.fakeTS=!0,this.ySetter=this.xSetter,c=[].slice.call(b.getElementsByTagName("tspan")),n(a.split(/\s?,\s?/g),function(a){var d= -b.firstChild,e,f,a=a.split(" ");e=a[a.length-1];(f=a[a.length-2])&&n(c,function(a,c){var g;c===0&&(a.setAttribute("x",b.getAttribute("x")),c=b.getAttribute("y"),a.setAttribute("y",c||0),c===null&&b.setAttribute("y",0));g=a.cloneNode(1);K(g,{"class":"highcharts-text-shadow",fill:e,stroke:e,"stroke-opacity":1/u(E(f),3),"stroke-width":f,"stroke-linejoin":"round"});b.insertBefore(g,d)})}))},attr:function(a,b,c){var d,e=this.element,f,g=this,h;typeof a==="string"&&b!==w&&(d=a,a={},a[d]=b);if(typeof a=== -"string")g=(this[a+"Getter"]||this._defaultGetter).call(this,a,e);else{for(d in a){b=a[d];h=!1;this.symbolName&&/^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)/.test(d)&&(f||(this.symbolAttr(a),f=!0),h=!0);if(this.rotation&&(d==="x"||d==="y"))this.doTransform=!0;h||(h=this[d+"Setter"]||this._defaultSetter,h.call(this,b,d,e),this.shadows&&/^(width|height|visibility|x|y|d|transform|cx|cy|r)$/.test(d)&&this.updateShadows(d,b,h))}if(this.doTransform)this.updateTransform(),this.doTransform=!1}c&& -c();return g},updateShadows:function(a,b,c){for(var d=this.shadows,e=d.length;e--;)c.call(d[e],a==="height"?Math.max(b-(d[e].cutHeight||0),0):a==="d"?this.d:b,a,d[e])},addClass:function(a){var b=this.element,c=K(b,"class")||"";c.indexOf(a)===-1&&K(b,"class",c+" "+a);return this},symbolAttr:function(a){var b=this;n("x,y,r,start,end,width,height,innerR,anchorX,anchorY".split(","),function(c){b[c]=o(a[c],b[c])});b.attr({d:b.renderer.symbols[b.symbolName](b.x,b.y,b.width,b.height,b)})},clip:function(a){return this.attr("clip-path", -a?"url("+this.renderer.url+"#"+a.id+")":"none")},crisp:function(a){var b,c={},d,e=this.strokeWidth||0;d=x(e)%2/2;a.x=fa(a.x||this.x||0)+d;a.y=fa(a.y||this.y||0)+d;a.width=fa((a.width||this.width||0)-2*d);a.height=fa((a.height||this.height||0)-2*d);a.strokeWidth=e;for(b in a)this[b]!==a[b]&&(this[b]=c[b]=a[b]);return c},css:function(a){var b=this.styles,c={},d=this.element,e,f,g="";e=!b;if(a&&a.color)a.fill=a.color;if(b)for(f in a)a[f]!==b[f]&&(c[f]=a[f],e=!0);if(e){e=this.textWidth=a&&a.width&&d.nodeName.toLowerCase()=== -"text"&&E(a.width)||this.textWidth;b&&(a=r(b,c));this.styles=a;e&&(ma||!ea&&this.renderer.forExport)&&delete a.width;if(xa&&!ea)M(this.element,a);else{b=function(a,b){return"-"+b.toLowerCase()};for(f in a)g+=f.replace(/([A-Z])/g,b)+":"+a[f]+";";K(d,"style",g)}e&&this.added&&this.renderer.buildText(this)}return this},on:function(a,b){var c=this,d=c.element;Wa&&a==="click"?(d.ontouchstart=function(a){c.touchEventFired=sa.now();a.preventDefault();b.call(d,a)},d.onclick=function(a){(ya.indexOf("Android")=== --1||sa.now()-(c.touchEventFired||0)>1100)&&b.call(d,a)}):d["on"+a]=b;return this},setRadialReference:function(a){var b=this.renderer.gradients[this.element.gradient];this.element.radialReference=a;b&&b.radAttr&&b.animate(this.renderer.getRadialAttr(a,b.radAttr));return this},translate:function(a,b){return this.attr({translateX:a,translateY:b})},invert:function(){this.inverted=!0;this.updateTransform();return this},updateTransform:function(){var a=this.translateX||0,b=this.translateY||0,c=this.scaleX, -d=this.scaleY,e=this.inverted,f=this.rotation,g=this.element;e&&(a+=this.attr("width"),b+=this.attr("height"));a=["translate("+a+","+b+")"];e?a.push("rotate(90) scale(-1,1)"):f&&a.push("rotate("+f+" "+(g.getAttribute("x")||0)+" "+(g.getAttribute("y")||0)+")");(t(c)||t(d))&&a.push("scale("+o(c,1)+" "+o(d,1)+")");a.length&&g.setAttribute("transform",a.join(" "))},toFront:function(){var a=this.element;a.parentNode.appendChild(a);return this},align:function(a,b,c){var d,e,f,g,h={};e=this.renderer;f=e.alignedObjects; -if(a){if(this.alignOptions=a,this.alignByTranslate=b,!c||va(c))this.alignTo=d=c||"renderer",wa(f,this),f.push(this),c=null}else a=this.alignOptions,b=this.alignByTranslate,d=this.alignTo;c=o(c,e[d],e);d=a.align;e=a.verticalAlign;f=(c.x||0)+(a.x||0);g=(c.y||0)+(a.y||0);if(d==="right"||d==="center")f+=(c.width-(a.width||0))/{right:1,center:2}[d];h[b?"translateX":"x"]=x(f);if(e==="bottom"||e==="middle")g+=(c.height-(a.height||0))/({bottom:1,middle:2}[e]||1);h[b?"translateY":"y"]=x(g);this[this.placed? -"animate":"attr"](h);this.placed=!0;this.alignAttr=h;return this},getBBox:function(a,b){var c,d=this.renderer,e,f,g,h=this.element,i=this.styles;e=this.textStr;var k,j=h.style,l,m=d.cache,p=d.cacheKeys,q;f=o(b,this.rotation);g=f*la;e!==w&&(q=["",f||0,i&&i.fontSize,h.style.width].join(","),q=e===""||Nb.test(e)?"num:"+e.toString().length+q:e+q);q&&!a&&(c=m[q]);if(!c){if(h.namespaceURI===Fa||d.forExport){try{l=this.fakeTS&&function(a){n(h.querySelectorAll(".highcharts-text-shadow"),function(b){b.style.display= -a})},Ka&&j.textShadow?(k=j.textShadow,j.textShadow=""):l&&l("none"),c=h.getBBox?r({},h.getBBox()):{width:h.offsetWidth,height:h.offsetHeight},k?j.textShadow=k:l&&l("")}catch(A){}if(!c||c.width<0)c={width:0,height:0}}else c=this.htmlGetBBox();if(d.isSVG){d=c.width;e=c.height;if(xa&&i&&i.fontSize==="11px"&&e.toPrecision(3)==="16.9")c.height=e=14;if(f)c.width=S(e*qa(g))+S(d*ka(g)),c.height=S(e*ka(g))+S(d*qa(g))}if(q){for(;p.length>250;)delete m[p.shift()];m[q]||p.push(q);m[q]=c}}return c},show:function(a){return this.attr({visibility:a? -"inherit":"visible"})},hide:function(){return this.attr({visibility:"hidden"})},fadeOut:function(a){var b=this;b.animate({opacity:0},{duration:a||150,complete:function(){b.attr({y:-9999})}})},add:function(a){var b=this.renderer,c=this.element,d;if(a)this.parentGroup=a;this.parentInverted=a&&a.inverted;this.textStr!==void 0&&b.buildText(this);this.added=!0;if(!a||a.handleZ||this.zIndex)d=this.zIndexSetter();d||(a?a.element:b.box).appendChild(c);if(this.onAdd)this.onAdd();return this},safeRemoveChild:function(a){var b= -a.parentNode;b&&b.removeChild(a)},destroy:function(){var a=this,b=a.element||{},c=a.shadows,d=a.renderer.isSVG&&b.nodeName==="SPAN"&&a.parentGroup,e,f;b.onclick=b.onmouseout=b.onmouseover=b.onmousemove=b.point=null;La(a);if(a.clipPath)a.clipPath=a.clipPath.destroy();if(a.stops){for(f=0;f]*>/g,"")))},textSetter:function(a){if(a!==this.textStr)delete this.bBox,this.textStr=a,this.added&&this.renderer.buildText(this)},fillSetter:function(a,b,c){typeof a==="string"?c.setAttribute(b,a): -a&&this.colorGradient(a,b,c)},visibilitySetter:function(a,b,c){a==="inherit"?c.removeAttribute(b):c.setAttribute(b,a)},zIndexSetter:function(a,b){var c=this.renderer,d=this.parentGroup,c=(d||c).element||c.box,e,f,g=this.element,h;e=this.added;var i;if(t(a))g.zIndex=a,a=+a,this[b]===a&&(e=!1),this[b]=a;if(e){if((a=this.zIndex)&&d)d.handleZ=!0;d=c.childNodes;for(i=0;ia||!t(a)&&t(f)))c.insertBefore(g,e),h=!0;h||c.appendChild(g)}return h},_defaultSetter:function(a, -b,c){c.setAttribute(b,a)}};Q.prototype.yGetter=Q.prototype.xGetter;Q.prototype.translateXSetter=Q.prototype.translateYSetter=Q.prototype.rotationSetter=Q.prototype.verticalAlignSetter=Q.prototype.scaleXSetter=Q.prototype.scaleYSetter=function(a,b){this[b]=a;this.doTransform=!0};Q.prototype["stroke-widthSetter"]=Q.prototype.strokeSetter=function(a,b,c){this[b]=a;if(this.stroke&&this["stroke-width"])this.strokeWidth=this["stroke-width"],Q.prototype.fillSetter.call(this,this.stroke,"stroke",c),c.setAttribute("stroke-width", -this["stroke-width"]),this.hasStroke=!0;else if(b==="stroke-width"&&a===0&&this.hasStroke)c.removeAttribute("stroke"),this.hasStroke=!1};var oa=function(){this.init.apply(this,arguments)};oa.prototype={Element:Q,init:function(a,b,c,d,e,f){var g,d=this.createElement("svg").attr({version:"1.1"}).css(this.getStyle(d));g=d.element;a.appendChild(g);a.innerHTML.indexOf("xmlns")===-1&&K(g,"xmlns",Fa);this.isSVG=!0;this.box=g;this.boxWrapper=d;this.alignedObjects=[];this.url=(Ka||gb)&&z.getElementsByTagName("base").length? -D.location.href.replace(/#.*?$/,"").replace(/([\('\)])/g,"\\$1").replace(/ /g,"%20"):"";this.createElement("desc").add().element.appendChild(z.createTextNode("Created with Highmaps 4.2.5"));this.defs=this.createElement("defs").add();this.allowHTML=f;this.forExport=e;this.gradients={};this.cache={};this.cacheKeys=[];this.imgCount=0;this.setSize(b,c,!1);var h;if(Ka&&a.getBoundingClientRect)this.subPixelFix=b=function(){M(a,{left:0,top:0});h=a.getBoundingClientRect();M(a,{left:ta(h.left)-h.left+"px", -top:ta(h.top)-h.top+"px"})},b(),N(D,"resize",b)},getStyle:function(a){return this.style=r({fontFamily:'"Lucida Grande", "Lucida Sans Unicode", Arial, Helvetica, sans-serif',fontSize:"12px"},a)},isHidden:function(){return!this.boxWrapper.getBBox().width},destroy:function(){var a=this.defs;this.box=null;this.boxWrapper=this.boxWrapper.destroy();db(this.gradients||{});this.gradients=null;if(a)this.defs=a.destroy();this.subPixelFix&&Y(D,"resize",this.subPixelFix);return this.alignedObjects=null},createElement:function(a){var b= -new this.Element;b.init(this,a);return b},draw:function(){},getRadialAttr:function(a,b){return{cx:a[0]-a[2]/2+b.cx*a[2],cy:a[1]-a[2]/2+b.cy*a[2],r:b.r*a[2]}},buildText:function(a){for(var b=a.element,c=this,d=c.forExport,e=o(a.textStr,"").toString(),f=e.indexOf("<")!==-1,g=b.childNodes,h,i,k,j=K(b,"x"),l=a.styles,m=a.textWidth,p=l&&l.lineHeight,q=l&&l.textShadow,A=l&&l.textOverflow==="ellipsis",ha=g.length,v=m&&!a.added&&this.box,B=function(a){return p?E(p):c.fontMetrics(/(px|em)$/.test(a&&a.style.fontSize)? -a.style.fontSize:l&&l.fontSize||c.style.fontSize||12,a).h},P=function(a){return a.replace(/</g,"<").replace(/>/g,">")};ha--;)b.removeChild(g[ha]);!f&&!q&&!A&&e.indexOf(" ")===-1?b.appendChild(z.createTextNode(P(e))):(h=/<.*style="([^"]+)".*>/,i=/<.*href="(http[^"]+)".*>/,v&&v.appendChild(b),e=f?e.replace(/<(b|strong)>/g,'').replace(/<(i|em)>/g,'').replace(/
/g,"").split(//g): -[e],e=Ha(e,function(a){return a!==""}),n(e,function(e,f){var g,p=0,e=e.replace(/^\s+|\s+$/g,"").replace(//g,"|||");g=e.split("|||");n(g,function(e){if(e!==""||g.length===1){var q={},n=z.createElementNS(Fa,"tspan"),o;h.test(e)&&(o=e.match(h)[1].replace(/(;| |^)color([ :])/,"$1fill$2"),K(n,"style",o));i.test(e)&&!d&&(K(n,"onclick",'location.href="'+e.match(i)[1]+'"'),M(n,{cursor:"pointer"}));e=P(e.replace(/<(.|\n)*?>/g,"")||" ");if(e!==" "){n.appendChild(z.createTextNode(e)); -if(p)q.dx=0;else if(f&&j!==null)q.x=j;K(n,q);b.appendChild(n);!p&&f&&(!ea&&d&&M(n,{display:"block"}),K(n,"dy",B(n)));if(m){for(var q=e.replace(/([^\^])-/g,"$1- ").split(" "),ha=g.length>1||f||q.length>1&&l.whiteSpace!=="nowrap",v,t,r=[],u=B(n),Ob=1,w=a.rotation,s=e,x=s.length;(ha||A)&&(q.length||r.length);)a.rotation=0,v=a.getBBox(!0),t=v.width,!ea&&c.forExport&&(t=c.measureSpanWidth(n.firstChild.data,a.styles)),v=t>m,k===void 0&&(k=v),A&&k?(x/=2,s===""||!v&&x<0.5?q=[]:(s=e.substring(0,s.length+(v? --1:1)*ta(x)),q=[s+(m>3?"\u2026":"")],n.removeChild(n.firstChild))):!v||q.length===1?(q=r,r=[],q.length&&(Ob++,n=z.createElementNS(Fa,"tspan"),K(n,{dy:u,x:j}),o&&K(n,"style",o),b.appendChild(n)),t>m&&(m=t)):(n.removeChild(n.firstChild),r.unshift(q.pop())),q.length&&n.appendChild(z.createTextNode(q.join(" ").replace(/- /g,"-")));a.rotation=w}p++}}})}),k&&a.attr("title",a.textStr),v&&v.removeChild(b),q&&a.applyTextShadow&&a.applyTextShadow(q))},getContrast:function(a){a=T(a).rgba;return a[0]+a[1]+a[2]> -384?"#000000":"#FFFFFF"},button:function(a,b,c,d,e,f,g,h,i){var k=this.label(a,b,c,i,null,null,null,null,"button"),j=0,l,m,p,q,A,n,a={x1:0,y1:0,x2:0,y2:1},e=C({"stroke-width":1,stroke:"#CCCCCC",fill:{linearGradient:a,stops:[[0,"#FEFEFE"],[1,"#F6F6F6"]]},r:2,padding:5,style:{color:"black"}},e);p=e.style;delete e.style;f=C(e,{stroke:"#68A",fill:{linearGradient:a,stops:[[0,"#FFF"],[1,"#ACF"]]}},f);q=f.style;delete f.style;g=C(e,{stroke:"#68A",fill:{linearGradient:a,stops:[[0,"#9BD"],[1,"#CDF"]]}},g); -A=g.style;delete g.style;h=C(e,{style:{color:"#CCC"}},h);n=h.style;delete h.style;N(k.element,xa?"mouseover":"mouseenter",function(){j!==3&&k.attr(f).css(q)});N(k.element,xa?"mouseout":"mouseleave",function(){j!==3&&(l=[e,f,g][j],m=[p,q,A][j],k.attr(l).css(m))});k.setState=function(a){(k.state=j=a)?a===2?k.attr(g).css(A):a===3&&k.attr(h).css(n):k.attr(e).css(p)};return k.on("click",function(a){j!==3&&d.call(k,a)}).attr(e).css(r({cursor:"default"},p))},crispLine:function(a,b){a[1]===a[4]&&(a[1]=a[4]= -x(a[1])-b%2/2);a[2]===a[5]&&(a[2]=a[5]=x(a[2])+b%2/2);return a},path:function(a){var b={fill:"none"};Ca(a)?b.d=a:aa(a)&&r(b,a);return this.createElement("path").attr(b)},circle:function(a,b,c){a=aa(a)?a:{x:a,y:b,r:c};b=this.createElement("circle");b.xSetter=b.ySetter=function(a,b,c){c.setAttribute("c"+b,a)};return b.attr(a)},arc:function(a,b,c,d,e,f){if(aa(a))b=a.y,c=a.r,d=a.innerR,e=a.start,f=a.end,a=a.x;a=this.symbol("arc",a||0,b||0,c||0,c||0,{innerR:d||0,start:e||0,end:f||0});a.r=c;return a},rect:function(a, -b,c,d,e,f){var e=aa(a)?a.r:e,g=this.createElement("rect"),a=aa(a)?a:a===w?{}:{x:a,y:b,width:u(c,0),height:u(d,0)};if(f!==w)g.strokeWidth=f,a=g.crisp(a);if(e)a.r=e;g.rSetter=function(a,b,c){K(c,{rx:a,ry:a})};return g.attr(a)},setSize:function(a,b,c){var d=this.alignedObjects,e=d.length;this.width=a;this.height=b;for(this.boxWrapper[o(c,!0)?"animate":"attr"]({width:a,height:b});e--;)d[e].align()},g:function(a){var b=this.createElement("g");return t(a)?b.attr({"class":"highcharts-"+a}):b},image:function(a, -b,c,d,e){var f={preserveAspectRatio:"none"};arguments.length>1&&r(f,{x:b,y:c,width:d,height:e});f=this.createElement("image").attr(f);f.element.setAttributeNS?f.element.setAttributeNS("http://www.w3.org/1999/xlink","href",a):f.element.setAttribute("hc-svg-href",a);return f},symbol:function(a,b,c,d,e,f){var g=this,h,i=this.symbols[a],i=i&&i(x(b),x(c),d,e,f),k=/^url\((.*?)\)$/,j,l;if(i)h=this.path(i),r(h,{symbolName:a,x:b,y:c,width:d,height:e}),f&&r(h,f);else if(k.test(a))l=function(a,b){a.element&& -(a.attr({width:b[0],height:b[1]}),a.alignByTranslate||a.translate(x((d-b[0])/2),x((e-b[1])/2)))},j=a.match(k)[1],a=Hb[j]||f&&f.width&&f.height&&[f.width,f.height],h=this.image(j).attr({x:b,y:c}),h.isImg=!0,a?l(h,a):(h.attr({width:0,height:0}),da("img",{onload:function(){this.width===0&&(M(this,{position:"absolute",top:"-999em"}),z.body.appendChild(this));l(h,Hb[j]=[this.width,this.height]);this.parentNode&&this.parentNode.removeChild(this);g.imgCount--;if(!g.imgCount&&O[g.chartIndex].onload)O[g.chartIndex].onload()}, -src:j}),this.imgCount++);return h},symbols:{circle:function(a,b,c,d){var e=0.166*c;return["M",a+c/2,b,"C",a+c+e,b,a+c+e,b+d,a+c/2,b+d,"C",a-e,b+d,a-e,b,a+c/2,b,"Z"]},square:function(a,b,c,d){return["M",a,b,"L",a+c,b,a+c,b+d,a,b+d,"Z"]},triangle:function(a,b,c,d){return["M",a+c/2,b,"L",a+c,b+d,a,b+d,"Z"]},"triangle-down":function(a,b,c,d){return["M",a,b,"L",a+c,b,a+c/2,b+d,"Z"]},diamond:function(a,b,c,d){return["M",a+c/2,b,"L",a+c,b+d/2,a+c/2,b+d,a,b+d/2,"Z"]},arc:function(a,b,c,d,e){var f=e.start, -c=e.r||c||d,g=e.end-0.001,d=e.innerR,h=e.open,i=ka(f),k=qa(f),j=ka(g),g=qa(g),e=e.end-fc&&e>b+g&&eb+g&&ed&&h>a+g&&ha+g&&hj&&/[ \-]/.test(b.textContent||b.innerText))M(b,{width:j+"px",display:"block",whiteSpace:l||"normal"}),this.hasTextWidth=!0;else if(this.hasTextWidth)M(b,{width:"",display:"",whiteSpace:l|| -"nowrap"}),this.hasTextWidth=!1;this.getSpanCorrection(this.hasTextWidth?j:b.offsetWidth,k,h,i,g)}M(b,{left:e+(this.xCorr||0)+"px",top:f+(this.yCorr||0)+"px"});if(gb)k=b.offsetHeight;this.cTT=m}}else this.alignOnAdd=!0},setSpanRotation:function(a,b,c){var d={},e=xa?"-ms-transform":gb?"-webkit-transform":Ka?"MozTransform":Fb?"-o-transform":"";d[e]=d.transform="rotate("+a+"deg)";d[e+(Ka?"Origin":"-origin")]=d.transformOrigin=b*100+"% "+c+"px";M(this.element,d)},getSpanCorrection:function(a,b,c){this.xCorr= --a*c;this.yCorr=-b}});r(oa.prototype,{html:function(a,b,c){var d=this.createElement("span"),e=d.element,f=d.renderer,g=f.isSVG,h=function(a,b){n(["opacity","visibility"],function(c){ga(a,c+"Setter",function(a,c,d,e){a.call(this,c,d,e);b[d]=c})})};d.textSetter=function(a){a!==e.innerHTML&&delete this.bBox;e.innerHTML=this.textStr=a;d.htmlUpdateTransform()};g&&h(d,d.element.style);d.xSetter=d.ySetter=d.alignSetter=d.rotationSetter=function(a,b){b==="align"&&(b="textAlign");d[b]=a;d.htmlUpdateTransform()}; -d.attr({text:a,x:x(b),y:x(c)}).css({position:"absolute",fontFamily:this.style.fontFamily,fontSize:this.style.fontSize});e.style.whiteSpace="nowrap";d.css=d.htmlCss;if(g)d.add=function(a){var b,c=f.box.parentNode,g=[];if(this.parentGroup=a){if(b=a.div,!b){for(;a;)g.push(a),a=a.parentGroup;n(g.reverse(),function(a){var d,e=K(a.element,"class");e&&(e={className:e});b=a.div=a.div||da(Ja,e,{position:"absolute",left:(a.translateX||0)+"px",top:(a.translateY||0)+"px",opacity:a.opacity},b||c);d=b.style;r(a, -{translateXSetter:function(b,c){d.left=b+"px";a[c]=b;a.doTransform=!0},translateYSetter:function(b,c){d.top=b+"px";a[c]=b;a.doTransform=!0}});h(a,d)})}}else b=c;b.appendChild(e);d.added=!0;d.alignOnAdd&&d.htmlUpdateTransform();return d};return d}});var Za,U;if(!ea&&!ma)U={init:function(a,b){var c=["<",b,' filled="f" stroked="f"'],d=["position: ","absolute",";"],e=b===Ja;(b==="shape"||e)&&d.push("left:0;top:0;width:1px;height:1px;");d.push("visibility: ",e?"hidden":"visible");c.push(' style="',d.join(""), -'"/>');if(b)c=e||b==="span"||b==="img"?c.join(""):a.prepVML(c),this.element=da(c);this.renderer=a},add:function(a){var b=this.renderer,c=this.element,d=b.box,e=a&&a.inverted,d=a?a.element||a:d;if(a)this.parentGroup=a;e&&b.invertChild(c,d);d.appendChild(c);this.added=!0;this.alignOnAdd&&!this.deferUpdateTransform&&this.updateTransform();if(this.onAdd)this.onAdd();return this},updateTransform:Q.prototype.htmlUpdateTransform,setSpanRotation:function(){var a=this.rotation,b=ka(a*la),c=qa(a*la);M(this.element, -{filter:a?["progid:DXImageTransform.Microsoft.Matrix(M11=",b,", M12=",-c,", M21=",c,", M22=",b,", sizingMethod='auto expand')"].join(""):"none"})},getSpanCorrection:function(a,b,c,d,e){var f=d?ka(d*la):1,g=d?qa(d*la):0,h=o(this.elemHeight,this.element.offsetHeight),i;this.xCorr=f<0&&-a;this.yCorr=g<0&&-h;i=f*g<0;this.xCorr+=g*b*(i?1-c:c);this.yCorr-=f*b*(d?i?c:1-c:1);e&&e!=="left"&&(this.xCorr-=a*c*(f<0?-1:1),d&&(this.yCorr-=h*c*(g<0?-1:1)),M(this.element,{textAlign:e}))},pathToVML:function(a){for(var b= -a.length,c=[];b--;)if(F(a[b]))c[b]=x(a[b]*10)-5;else if(a[b]==="Z")c[b]="x";else if(c[b]=a[b],a.isArc&&(a[b]==="wa"||a[b]==="at"))c[b+5]===c[b+7]&&(c[b+7]+=a[b+7]>a[b+5]?1:-1),c[b+6]===c[b+8]&&(c[b+8]+=a[b+8]>a[b+6]?1:-1);return c.join(" ")||"x"},clip:function(a){var b=this,c;a?(c=a.members,wa(c,b),c.push(b),b.destroyClip=function(){wa(c,b)},a=a.getCSS(b)):(b.destroyClip&&b.destroyClip(),a={clip:fb?"inherit":"rect(auto)"});return b.css(a)},css:Q.prototype.htmlCss,safeRemoveChild:function(a){a.parentNode&& -Qa(a)},destroy:function(){this.destroyClip&&this.destroyClip();return Q.prototype.destroy.apply(this)},on:function(a,b){this.element["on"+a]=function(){var a=D.event;a.target=a.srcElement;b(a)};return this},cutOffPath:function(a,b){var c,a=a.split(/[ ,]/);c=a.length;if(c===9||c===11)a[c-4]=a[c-2]=E(a[c-2])-10*b;return a.join(" ")},shadow:function(a,b,c){var d=[],e,f=this.element,g=this.renderer,h,i=f.style,k,j=f.path,l,m,p,q;j&&typeof j.value!=="string"&&(j="x");m=j;if(a){p=o(a.width,3);q=(a.opacity|| -0.15)/p;for(e=1;e<=3;e++){l=p*2+1-2*e;c&&(m=this.cutOffPath(j.value,l+0.5));k=[''];h=da(g.prepVML(k),null,{left:E(i.left)+o(a.offsetX,1),top:E(i.top)+o(a.offsetY,1)});if(c)h.cutOff=l+1;k=[''];da(g.prepVML(k),null,null,h);b?b.element.appendChild(h):f.parentNode.insertBefore(h,f);d.push(h)}this.shadows=d}return this},updateShadows:W, -setAttr:function(a,b){fb?this.element[a]=b:this.element.setAttribute(a,b)},classSetter:function(a){this.element.className=a},dashstyleSetter:function(a,b,c){(c.getElementsByTagName("stroke")[0]||da(this.renderer.prepVML([""]),null,null,c))[b]=a||"solid";this[b]=a},dSetter:function(a,b,c){var d=this.shadows,a=a||[];this.d=a.join&&a.join(" ");c.path=a=this.pathToVML(a);if(d)for(c=d.length;c--;)d[c].path=d[c].cutOff?this.cutOffPath(a,d[c].cutOff):a;this.setAttr(b,a)},fillSetter:function(a,b, -c){var d=c.nodeName;if(d==="SPAN")c.style.color=a;else if(d!=="IMG")c.filled=a!=="none",this.setAttr("fillcolor",this.renderer.color(a,c,b,this))},"fill-opacitySetter":function(a,b,c){da(this.renderer.prepVML(["<",b.split("-")[0],' opacity="',a,'"/>']),null,null,c)},opacitySetter:W,rotationSetter:function(a,b,c){c=c.style;this[b]=c[b]=a;c.left=-x(qa(a*la)+1)+"px";c.top=x(ka(a*la))+"px"},strokeSetter:function(a,b,c){this.setAttr("strokecolor",this.renderer.color(a,c,b,this))},"stroke-widthSetter":function(a, -b,c){c.stroked=!!a;this[b]=a;F(a)&&(a+="px");this.setAttr("strokeweight",a)},titleSetter:function(a,b){this.setAttr(b,a)},visibilitySetter:function(a,b,c){a==="inherit"&&(a="visible");this.shadows&&n(this.shadows,function(c){c.style[b]=a});c.nodeName==="DIV"&&(a=a==="hidden"?"-999em":0,fb||(c.style[b]=a?"visible":"hidden"),b="top");c.style[b]=a},xSetter:function(a,b,c){this[b]=a;b==="x"?b="left":b==="y"&&(b="top");this.updateClipping?(this[b]=a,this.updateClipping()):c.style[b]=a},zIndexSetter:function(a, -b,c){c.style[b]=a}},U["stroke-opacitySetter"]=U["fill-opacitySetter"],s.VMLElement=U=ba(Q,U),U.prototype.ySetter=U.prototype.widthSetter=U.prototype.heightSetter=U.prototype.xSetter,U={Element:U,isIE8:ya.indexOf("MSIE 8.0")>-1,init:function(a,b,c,d){var e;this.alignedObjects=[];d=this.createElement(Ja).css(r(this.getStyle(d),{position:"relative"}));e=d.element;a.appendChild(d.element);this.isVML=!0;this.box=e;this.boxWrapper=d;this.gradients={};this.cache={};this.cacheKeys=[];this.imgCount=0;this.setSize(b, -c,!1);if(!z.namespaces.hcv){z.namespaces.add("hcv","urn:schemas-microsoft-com:vml");try{z.createStyleSheet().cssText="hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke{ behavior:url(#default#VML); display: inline-block; } "}catch(f){z.styleSheets[0].cssText+="hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke{ behavior:url(#default#VML); display: inline-block; } "}}},isHidden:function(){return!this.box.offsetWidth},clipRect:function(a,b,c,d){var e=this.createElement(),f=aa(a);return r(e,{members:[], -count:0,left:(f?a.x:a)+1,top:(f?a.y:b)+1,width:(f?a.width:c)-1,height:(f?a.height:d)-1,getCSS:function(a){var b=a.element,c=b.nodeName,a=a.inverted,d=this.top-(c==="shape"?b.offsetTop:0),e=this.left,b=e+this.width,f=d+this.height,d={clip:"rect("+x(a?e:d)+"px,"+x(a?f:b)+"px,"+x(a?b:f)+"px,"+x(a?d:e)+"px)"};!a&&fb&&c==="DIV"&&r(d,{width:b+"px",height:f+"px"});return d},updateClipping:function(){n(e.members,function(a){a.element&&a.css(e.getCSS(a))})}})},color:function(a,b,c,d){var e=this,f,g=/^rgba/, -h,i,k="none";a&&a.linearGradient?i="gradient":a&&a.radialGradient&&(i="pattern");if(i){var j,l,m=a.linearGradient||a.radialGradient,p,q,A,o,v,B="",a=a.stops,P,t=[],r=function(){h=[''];da(e.prepVML(h),null,null,b)};p=a[0];P=a[a.length-1];p[0]>0&&a.unshift([0,p[1]]);P[0]<1&&a.push([1,P[1]]);n(a,function(a,b){g.test(a[1])?(f=T(a[1]),j=f.get("rgb"),l=f.get("a")):(j=a[1],l=1);t.push(a[0]*100+ -"% "+j);b?(A=l,o=j):(q=l,v=j)});if(c==="fill")if(i==="gradient")c=m.x1||m[0]||0,a=m.y1||m[1]||0,p=m.x2||m[2]||0,m=m.y2||m[3]||0,B='angle="'+(90-H.atan((m-a)/(p-c))*180/pb)+'"',r();else{var k=m.r,ca=k*2,u=k*2,s=m.cx,w=m.cy,x=b.radialReference,z,k=function(){x&&(z=d.getBBox(),s+=(x[0]-z.x)/z.width-0.5,w+=(x[1]-z.y)/z.height-0.5,ca*=x[2]/z.width,u*=x[2]/z.height);B='src="'+L.global.VMLRadialGradientURL+'" size="'+ca+","+u+'" origin="0.5,0.5" position="'+s+","+w+'" color2="'+v+'" ';r()};d.added?k():d.onAdd= -k;k=o}else k=j}else if(g.test(a)&&b.tagName!=="IMG")f=T(a),d[c+"-opacitySetter"](f.get("a"),c,b),k=f.get("rgb");else{k=b.getElementsByTagName(c);if(k.length)k[0].opacity=1,k[0].type="solid";k=a}return k},prepVML:function(a){var b=this.isIE8,a=a.join("");b?(a=a.replace("/>",' xmlns="urn:schemas-microsoft-com:vml" />'),a=a.indexOf('style="')===-1?a.replace("/>",' style="display:inline-block;behavior:url(#default#VML);" />'):a.replace('style="','style="display:inline-block;behavior:url(#default#VML);')): -a=a.replace("<"," -1&&f.attr({x:b,y:c,width:d,height:e});return f},createElement:function(a){return a==="rect"?this.symbol(a):oa.prototype.createElement.call(this,a)},invertChild:function(a,b){var c=this,d=b.style,e=a.tagName==="IMG"&&a.style;M(a,{flip:"x",left:E(d.width)-(e?E(e.top):1),top:E(d.height)-(e?E(e.left):1),rotation:-90});n(a.childNodes,function(b){c.invertChild(b,a)})},symbols:{arc:function(a,b,c,d,e){var f=e.start,g=e.end,h=e.r||c||d,c=e.innerR,d=ka(f),i=qa(f),k=ka(g),j=qa(g);if(g-f===0)return["x"];f=["wa", -a-h,b-h,a+h,b+h,a+h*d,b+h*i,a+h*k,b+h*j];e.open&&!c&&f.push("e","M",a,b);f.push("at",a-c,b-c,a+c,b+c,a+c*k,b+c*j,a+c*d,b+c*i,"x","e");f.isArc=!0;return f},circle:function(a,b,c,d,e){e&&(c=d=2*e.r);e&&e.isCircle&&(a-=c/2,b-=d/2);return["wa",a,b,a+c,b+d,a+c,b+d/2,a+c,b+d/2,"e"]},rect:function(a,b,c,d,e){return oa.prototype.symbols[!t(e)||!e.r?"square":"callout"].call(0,a,b,c,d,e)}}},s.VMLRenderer=Za=function(){this.init.apply(this,arguments)},Za.prototype=C(oa.prototype,U),Ta=Za;oa.prototype.measureSpanWidth= -function(a,b){var c=z.createElement("span"),d;d=z.createTextNode(a);c.appendChild(d);M(c,b);this.box.appendChild(c);d=c.offsetWidth;Qa(c);return d};var Ib;if(ma)s.CanVGRenderer=U=function(){Fa="http://www.w3.org/1999/xhtml"},U.prototype.symbols={},Ib=function(){function a(){var a=b.length,d;for(d=0;d0&&c+i*k>e&&(p=x((d-c)/ka(h*la)));else if(d=c+(1-i)*k,c-i*ke&&(l=e-a.x+l*i,m=-1),l=I(j,l),ll||b.autoRotation&&g.styles.width)p=l;if(p){q.width=p;if(!b.options.labels.style.textOverflow)q.textOverflow="ellipsis";g.css(q)}},getPosition:function(a,b,c,d){var e=this.axis,f=e.chart,g=d&&f.oldChartHeight||f.chartHeight;return{x:a?e.translate(b+c,null,null,d)+e.transB:e.left+e.offset+ -(e.opposite?(d&&f.oldChartWidth||f.chartWidth)-e.right-e.left:0),y:a?g-e.bottom+e.offset-(e.opposite?e.height:0):g-e.translate(b+c,null,null,d)-e.transB}},getLabelPosition:function(a,b,c,d,e,f,g,h){var i=this.axis,k=i.transA,j=i.reversed,l=i.staggerLines,m=i.tickRotCorr||{x:0,y:0},p=e.y;t(p)||(p=i.side===0?c.rotation?-8:-c.getBBox().height:i.side===2?m.y+8:ka(c.rotation*la)*(m.y-c.getBBox(!1,0).height/2));a=a+e.x+m.x-(f&&d?f*k*(j?-1:1):0);b=b+p-(f&&!d?f*k*(j?1:-1):0);l&&(c=g/(h||1)%l,i.opposite&& -(c=l-c-1),b+=c*(i.labelOffset/l));return{x:a,y:x(b)}},getMarkPath:function(a,b,c,d,e,f){return f.crispLine(["M",a,b,"L",a+(e?0:-c),b+(e?c:0)],d)},render:function(a,b,c){var d=this.axis,e=d.options,f=d.chart.renderer,g=d.horiz,h=this.type,i=this.label,k=this.pos,j=e.labels,l=this.gridLine,m=h?h+"Grid":"grid",p=h?h+"Tick":"tick",q=e[m+"LineWidth"],A=e[m+"LineColor"],n=e[m+"LineDashStyle"],m=d.tickSize(p),p=e[p+"Color"],v=this.mark,B=j.step,P=!0,t=d.tickmarkOffset,r=this.getPosition(g,k,t,b),ca=r.x, -r=r.y,u=g&&ca===d.pos+d.len||!g&&r===d.pos?-1:1,c=o(c,1);this.isActive=!0;if(q){k=d.getPlotLinePath(k+t,q*u,b,!0);if(l===w){l={stroke:A,"stroke-width":q};if(n)l.dashstyle=n;if(!h)l.zIndex=1;if(b)l.opacity=0;this.gridLine=l=q?f.path(k).attr(l).add(d.gridGroup):null}if(!b&&l&&k)l[this.isNew?"attr":"animate"]({d:k,opacity:c})}if(m)d.opposite&&(m[0]=-m[0]),h=this.getMarkPath(ca,r,m[0],m[1]*u,g,f),v?v.animate({d:h,opacity:c}):this.mark=f.path(h).attr({stroke:p,"stroke-width":m[1],opacity:c}).add(d.axisGroup); -if(i&&F(ca))i.xy=r=this.getLabelPosition(ca,r,i,g,j,t,a,B),this.isFirst&&!this.isLast&&!o(e.showFirstLabel,1)||this.isLast&&!this.isFirst&&!o(e.showLastLabel,1)?P=!1:g&&!d.isRadial&&!j.step&&!j.rotation&&!b&&c!==0&&this.handleOverflow(r),B&&a%B&&(P=!1),P&&F(r.y)?(r.opacity=c,i[this.isNew?"attr":"animate"](r),this.isNew=!1):i.attr("y",-9999)},destroy:function(){db(this,this.axis)}};var X=s.Axis=function(){this.init.apply(this,arguments)};X.prototype={defaultOptions:{dateTimeLabelFormats:{millisecond:"%H:%M:%S.%L", -second:"%H:%M:%S",minute:"%H:%M",hour:"%H:%M",day:"%e. %b",week:"%e. %b",month:"%b '%y",year:"%Y"},endOnTick:!1,gridLineColor:"#D8D8D8",labels:{enabled:!0,style:{color:"#606060",cursor:"default",fontSize:"11px"},x:0},lineColor:"#C0D0E0",lineWidth:1,minPadding:0.01,maxPadding:0.01,minorGridLineColor:"#E0E0E0",minorGridLineWidth:1,minorTickColor:"#A0A0A0",minorTickLength:2,minorTickPosition:"outside",startOfWeek:1,startOnTick:!1,tickColor:"#C0D0E0",tickLength:10,tickmarkPlacement:"between",tickPixelInterval:100, -tickPosition:"outside",title:{align:"middle",style:{color:"#707070"}},type:"linear"},defaultYAxisOptions:{endOnTick:!0,gridLineWidth:1,tickPixelInterval:72,showLastLabel:!0,labels:{x:-8},lineWidth:0,maxPadding:0.05,minPadding:0.05,startOnTick:!0,title:{rotation:270,text:"Values"},stackLabels:{enabled:!1,formatter:function(){return s.numberFormat(this.total,-1)},style:C(Z.line.dataLabels.style,{color:"#000000"})}},defaultLeftAxisOptions:{labels:{x:-15},title:{rotation:270}},defaultRightAxisOptions:{labels:{x:15}, -title:{rotation:90}},defaultBottomAxisOptions:{labels:{autoRotation:[-45],x:0},title:{rotation:0}},defaultTopAxisOptions:{labels:{autoRotation:[-45],x:0},title:{rotation:0}},init:function(a,b){var c=b.isX;this.chart=a;this.horiz=a.inverted?!c:c;this.coll=(this.isXAxis=c)?"xAxis":"yAxis";this.opposite=b.opposite;this.side=b.side||(this.horiz?this.opposite?0:2:this.opposite?1:3);this.setOptions(b);var d=this.options,e=d.type;this.labelFormatter=d.labels.formatter||this.defaultLabelFormatter;this.userOptions= -b;this.minPixelPadding=0;this.reversed=d.reversed;this.visible=d.visible!==!1;this.zoomEnabled=d.zoomEnabled!==!1;this.categories=d.categories||e==="category";this.names=this.names||[];this.isLog=e==="logarithmic";this.isDatetimeAxis=e==="datetime";this.isLinked=t(d.linkedTo);this.ticks={};this.labelEdge=[];this.minorTicks={};this.plotLinesAndBands=[];this.alternateBands={};this.len=0;this.minRange=this.userMinRange=d.minRange||d.maxZoom;this.range=d.range;this.offset=d.offset||0;this.stacks={};this.oldStacks= -{};this.stacksTouched=0;this.min=this.max=null;this.crosshair=o(d.crosshair,ra(a.options.tooltip.crosshairs)[c?0:1],!1);var f,d=this.options.events;za(this,a.axes)===-1&&(c&&!this.isColorAxis?a.axes.splice(a.xAxis.length,0,this):a.axes.push(this),a[this.coll].push(this));this.series=this.series||[];if(a.inverted&&c&&this.reversed===w)this.reversed=!0;this.removePlotLine=this.removePlotBand=this.removePlotBandOrLine;for(f in d)N(this,f,d[f]);if(this.isLog)this.val2lin=this.log2lin,this.lin2val=this.lin2log}, -setOptions:function(a){this.options=C(this.defaultOptions,this.isXAxis?{}:this.defaultYAxisOptions,[this.defaultTopAxisOptions,this.defaultRightAxisOptions,this.defaultBottomAxisOptions,this.defaultLeftAxisOptions][this.side],C(L[this.coll],a))},defaultLabelFormatter:function(){var a=this.axis,b=this.value,c=a.categories,d=this.dateTimeLabelFormat,e=L.lang.numericSymbols,f=e&&e.length,g,h=a.options.labels.format,a=a.isLog?b:a.tickInterval;if(h)g=Ea(h,this);else if(c)g=b;else if(d)g=Oa(d,b);else if(f&& -a>=1E3)for(;f--&&g===w;)c=Math.pow(1E3,f+1),a>=c&&b*10%c===0&&e[f]!==null&&(g=s.numberFormat(b/c,-1)+e[f]);g===w&&(g=S(b)>=1E4?s.numberFormat(b,-1):s.numberFormat(b,-1,w,""));return g},getSeriesExtremes:function(){var a=this,b=a.chart;a.hasVisibleSeries=!1;a.dataMin=a.dataMax=a.threshold=null;a.softThreshold=!a.isXAxis;a.buildStacks&&a.buildStacks();n(a.series,function(c){if(c.visible||!b.options.chart.ignoreHiddenSeries){var d=c.options,e=d.threshold,f;a.hasVisibleSeries=!0;a.isLog&&e<=0&&(e=null); -if(a.isXAxis){if(d=c.xData,d.length)c=Pa(d),!F(c)&&!(c instanceof sa)&&(d=Ha(d,function(a){return F(a)}),c=Pa(d)),a.dataMin=I(o(a.dataMin,d[0]),c),a.dataMax=u(o(a.dataMax,d[0]),Ia(d))}else{c.getExtremes();f=c.dataMax;c=c.dataMin;if(t(c)&&t(f))a.dataMin=I(o(a.dataMin,c),c),a.dataMax=u(o(a.dataMax,f),f);if(t(e))a.threshold=e;if(!d.softThreshold||a.isLog)a.softThreshold=!1}}})},translate:function(a,b,c,d,e,f){var g=this.linkedParent||this,h=1,i=0,k=d?g.oldTransA:g.transA,d=d?g.oldMin:g.min,j=g.minPixelPadding, -e=(g.isOrdinal||g.isBroken||g.isLog&&e)&&g.lin2val;if(!k)k=g.transA;if(c)h*=-1,i=g.len;g.reversed&&(h*=-1,i-=h*(g.sector||g.len));b?(a=a*h+i,a-=j,a=a/k+d,e&&(a=g.lin2val(a))):(e&&(a=g.val2lin(a)),f==="between"&&(f=0.5),a=h*(a-d)*k+i+h*j+(F(f)?k*f*g.pointRange:0));return a},toPixels:function(a,b){return this.translate(a,!1,!this.horiz,null,!0)+(b?0:this.pos)},toValue:function(a,b){return this.translate(a-(b?0:this.pos),!0,!this.horiz,null,!0)},getPlotLinePath:function(a,b,c,d,e){var f=this.chart,g= -this.left,h=this.top,i,k,j=c&&f.oldChartHeight||f.chartHeight,l=c&&f.oldChartWidth||f.chartWidth,m;i=this.transB;var p=function(a,b,c){if(ac)d?a=I(u(b,a),c):m=!0;return a},e=o(e,this.translate(a,null,null,c)),a=c=x(e+i);i=k=x(j-e-i);F(e)?this.horiz?(i=h,k=j-this.bottom,a=c=p(a,g,g+this.width)):(a=g,c=l-this.right,i=k=p(i,h,h+this.height)):m=!0;return m&&!d?null:f.renderer.crispLine(["M",a,i,"L",c,k],b||1)},getLinearTickPositions:function(a,b,c){var d,e=pa(fa(b/a)*a),f=pa(ta(c/a)*a),g=[];if(b=== -c&&F(b))return[b];for(b=e;b<=f;){g.push(b);b=pa(b+a);if(b===d)break;d=b}return g},getMinorTickPositions:function(){var a=this.options,b=this.tickPositions,c=this.minorTickInterval,d=[],e,f=this.pointRangePadding||0;e=this.min-f;var f=this.max+f,g=f-e;if(g&&g/c=this.minRange,f,g,h,i,k,j;if(this.isXAxis&&this.minRange===w&&!this.isLog)t(a.min)||t(a.max)?this.minRange=null:(n(this.series,function(a){i=a.xData;for(g=k=a.xIncrement?1:i.length-1;g>0;g--)if(h=i[g]-i[g-1],f===w||h=q?(ha=q,j=0):b.dataMax<=q&&(v=q,k=0)),b.min=o(B,ha,b.dataMin),b.max=o(P,v,b.dataMax));if(e)!a&&I(b.min,o(b.dataMin,b.min))<=0&&V(10,1),b.min=pa(f(b.min),15),b.max=pa(f(b.max),15);if(b.range&&t(b.max))b.userMin=b.min=B=u(b.min,b.minFromRange()),b.userMax=P=b.max,b.range=null;G(b,"foundExtremes");b.beforePadding&&b.beforePadding();b.adjustForMinRange();if(!p&&!b.axisPointRange&&!b.usePercentage&&!i&&t(b.min)&&t(b.max)&&(f=b.max-b.min))!t(B)&&j&&(b.min-=f*j),!t(P)&&k&&(b.max+= -f*k);if(F(d.floor))b.min=u(b.min,d.floor);if(F(d.ceiling))b.max=I(b.max,d.ceiling);if(A&&t(b.dataMin))if(q=q||0,!t(B)&&b.min=q)b.min=q;else if(!t(P)&&b.max>q&&b.dataMax<=q)b.max=q;b.tickInterval=b.min===b.max||b.min===void 0||b.max===void 0?1:i&&!l&&m===b.linkedParent.options.tickPixelInterval?l=b.linkedParent.tickInterval:o(l,this.tickAmount?(b.max-b.min)/u(this.tickAmount-1,1):void 0,p?1:(b.max-b.min)*m/u(b.len,m));h&&!a&&n(b.series,function(a){a.processData(b.min!==b.oldMin||b.max!== -b.oldMax)});b.setAxisTranslation(!0);b.beforeSetTickPositions&&b.beforeSetTickPositions();if(b.postProcessTickInterval)b.tickInterval=b.postProcessTickInterval(b.tickInterval);if(b.pointRange&&!l)b.tickInterval=u(b.pointRange,b.tickInterval);a=o(d.minTickInterval,b.isDatetimeAxis&&b.closestPointRange);if(!l&&b.tickInterval0.5&&b.tickInterval<5&&b.max> -1E3&&b.max<9999)),!!this.tickAmount);if(!this.tickAmount&&this.len)b.tickInterval=b.unsquish();this.setTickPositions()},setTickPositions:function(){var a=this.options,b,c=a.tickPositions,d=a.tickPositioner,e=a.startOnTick,f=a.endOnTick,g;this.tickmarkOffset=this.categories&&a.tickmarkPlacement==="between"&&this.tickInterval===1?0.5:0;this.minorTickInterval=a.minorTickInterval==="auto"&&this.tickInterval?this.tickInterval/5:a.minorTickInterval;this.tickPositions=b=c&&c.slice();if(!b&&(b=this.isDatetimeAxis? -this.getTimeTicks(this.normalizeTimeTickInterval(this.tickInterval,a.units),this.min,this.max,a.startOfWeek,this.ordinalPositions,this.closestPointRange,!0):this.isLog?this.getLogTickPositions(this.tickInterval,this.min,this.max):this.getLinearTickPositions(this.tickInterval,this.min,this.max),b.length>this.len&&(b=[b[0],b.pop()]),this.tickPositions=b,d&&(d=d.apply(this,[this.min,this.max]))))this.tickPositions=b=d;if(!this.isLinked)this.trimTicks(b,e,f),this.min===this.max&&t(this.min)&&!this.tickAmount&& -(g=!0,this.min-=0.5,this.max+=0.5),this.single=g,!c&&!d&&this.adjustTickAmount()},trimTicks:function(a,b,c){var d=a[0],e=a[a.length-1],f=this.minPointOffset||0;if(b)this.min=d;else for(;this.min-f>a[0];)a.shift();if(c)this.max=e;else for(;this.max+fc&&(this.tickInterval*=2,this.setTickPositions());if(t(d)){for(a=c=b.length;a--;)(d===3&&a%2===1||d<=2&&a>0&&a=e&&(b=e));this.displayBtn=a!==w||b!==w;this.setExtremes(a,b,!1,w,{trigger:"zoom"});return!0},setAxisSize:function(){var a=this.chart,b=this.options,c=b.offsetLeft||0,d=this.horiz,e=o(b.width,a.plotWidth-c+(b.offsetRight||0)),f=o(b.height,a.plotHeight),g=o(b.top,a.plotTop),b=o(b.left,a.plotLeft+c),c= -/%$/;c.test(f)&&(f=Math.round(parseFloat(f)/100*a.plotHeight));c.test(g)&&(g=Math.round(parseFloat(g)/100*a.plotHeight+a.plotTop));this.left=b;this.top=g;this.width=e;this.height=f;this.bottom=a.chartHeight-f-g;this.right=a.chartWidth-e-b;this.len=u(d?e:f,0);this.pos=d?b:g},getExtremes:function(){var a=this.isLog,b=this.lin2log;return{min:a?pa(b(this.min)):this.min,max:a?pa(b(this.max)):this.max,dataMin:this.dataMin,dataMax:this.dataMax,userMin:this.userMin,userMax:this.userMax}},getThreshold:function(a){var b= -this.isLog,c=this.lin2log,d=b?c(this.min):this.min,b=b?c(this.max):this.max;a===null?a=b<0?b:d:d>a?a=d:b15&&a<165?"right":a>195&&a<345?"left":"center"},tickSize:function(a){var b=this.options,c=b[a+"Length"],d=o(b[a+"Width"],a==="tick"&&this.isXAxis?1:0);if(d&&c)return b[a+"Position"]==="inside"&&(c=-c),[c,d]},labelMetrics:function(){return this.chart.renderer.fontMetrics(this.options.labels.style.fontSize, -this.ticks[0]&&this.ticks[0].label)},unsquish:function(){var a=this.options.labels,b=this.horiz,c=this.tickInterval,d=c,e=this.len/(((this.categories?1:0)+this.max-this.min)/c),f,g=a.rotation,h=this.labelMetrics(),i,k=Number.MAX_VALUE,j,l=function(a){a/=e||1;a=a>1?ta(a):1;return a*c};b?(j=!a.staggerLines&&!a.step&&(t(g)?[g]:e=-90&&a<=90)i=l(S(h.h/qa(la*a))),b=i+S(a/360),bm)m=a.labelLength}),m>h&&m>k.h?i.rotation=this.labelRotation:this.labelRotation=0;else if(g&&(l={width:h+"px"},!j)){l.textOverflow="clip";for(p=c.length;!f&&p--;)if(q=c[p],h=d[q].label)if(h.styles.textOverflow==="ellipsis"?h.css({textOverflow:"clip"}):d[q].labelLength>g&&h.css({width:g+"px"}),h.getBBox().height>this.len/c.length-(k.h-k.f))h.specCss={textOverflow:"ellipsis"}}if(i.rotation&& -(l={width:(m>a.chartHeight*0.5?a.chartHeight*0.33:a.chartHeight)+"px"},!j))l.textOverflow="ellipsis";if(this.labelAlign=e.align||this.autoLabelAlign(this.labelRotation))i.align=this.labelAlign;n(c,function(a){var b=(a=d[a])&&a.label;if(b)b.attr(i),l&&b.css(C(l,b.specCss)),delete b.specCss,a.rotation=i.rotation});this.tickRotCorr=b.rotCorr(k.b,this.labelRotation||0,this.side!==0)},hasData:function(){return this.hasVisibleSeries||t(this.min)&&t(this.max)&&!!this.tickPositions},getOffset:function(){var a= -this,b=a.chart,c=b.renderer,d=a.options,e=a.tickPositions,f=a.ticks,g=a.horiz,h=a.side,i=b.inverted?[1,0,3,2][h]:h,k,j,l=0,m,p=0,q=d.title,A=d.labels,ha=0,v=a.opposite,B=b.axisOffset,b=b.clipOffset,P=[-1,1,1,-1][h],r,s=a.axisParent,ca=this.tickSize("tick");k=a.hasData();a.showAxis=j=k||o(d.showEmpty,!0);a.staggerLines=a.horiz&&A.staggerLines;if(!a.axisGroup)a.gridGroup=c.g("grid").attr({zIndex:d.gridZIndex||1}).add(s),a.axisGroup=c.g("axis").attr({zIndex:d.zIndex||2}).add(s),a.labelGroup=c.g("axis-labels").attr({zIndex:A.zIndex|| -7}).addClass("highcharts-"+a.coll.toLowerCase()+"-labels").add(s);if(k||a.isLinked){if(n(e,function(b){f[b]?f[b].addLabel():f[b]=new Sa(a,b)}),a.renderUnsquish(),A.reserveSpace!==!1&&(h===0||h===2||{1:"left",3:"right"}[h]===a.labelAlign||a.labelAlign==="center")&&n(e,function(a){ha=u(f[a].getLabelSize(),ha)}),a.staggerLines)ha*=a.staggerLines,a.labelOffset=ha*(a.opposite?-1:1)}else for(r in f)f[r].destroy(),delete f[r];if(q&&q.text&&q.enabled!==!1){if(!a.axisTitle)(r=q.textAlign)||(r=(g?{low:"left", -middle:"center",high:"right"}:{low:v?"right":"left",middle:"center",high:v?"left":"right"})[q.align]),a.axisTitle=c.text(q.text,0,0,q.useHTML).attr({zIndex:7,rotation:q.rotation||0,align:r}).addClass("highcharts-"+this.coll.toLowerCase()+"-title").css(q.style).add(a.axisGroup),a.axisTitle.isNew=!0;if(j)l=a.axisTitle.getBBox()[g?"height":"width"],m=q.offset,p=t(m)?0:o(q.margin,g?5:10);a.axisTitle[j?"show":"hide"](!0)}a.offset=P*o(d.offset,B[h]);a.tickRotCorr=a.tickRotCorr||{x:0,y:0};c=h===0?-a.labelMetrics().h: -h===2?a.tickRotCorr.y:0;p=Math.abs(ha)+p;ha&&(p-=c,p+=P*(g?o(A.y,a.tickRotCorr.y+P*8):A.x));a.axisTitleMargin=o(m,p);B[h]=u(B[h],a.axisTitleMargin+l+P*a.offset,p,k&&e.length&&ca?ca[0]:0);d=d.offset?0:fa(d.lineWidth/2)*2;b[i]=u(b[i],d)},getLinePath:function(a){var b=this.chart,c=this.opposite,d=this.offset,e=this.horiz,f=this.left+(c?this.width:0)+d,d=b.chartHeight-this.bottom-(c?this.height:0)+d;c&&(a*=-1);return b.renderer.crispLine(["M",e?this.left:f,e?d:this.top,"L",e?b.chartWidth-this.right:f, -e?d:b.chartHeight-this.bottom],a)},getTitlePosition:function(){var a=this.horiz,b=this.left,c=this.top,d=this.len,e=this.options.title,f=a?b:c,g=this.opposite,h=this.offset,i=e.x||0,k=e.y||0,j=E(e.style.fontSize||12),d={low:f+(a?0:d),middle:f+d/2,high:f+(a?d:0)}[e.align],b=(a?c+this.height:b)+(a?1:-1)*(g?-1:1)*this.axisTitleMargin+(this.side===2?j:0);return{x:a?d+i:b+(g?this.width:0)+h+i,y:a?b+k-(g?this.height:0)+h:d+k}},render:function(){var a=this,b=a.chart,c=b.renderer,d=a.options,e=a.isLog,f= -a.lin2log,g=a.isLinked,h=a.tickPositions,i=a.axisTitle,k=a.ticks,j=a.minorTicks,l=a.alternateBands,m=d.stackLabels,p=d.alternateGridColor,q=a.tickmarkOffset,A=d.lineWidth,o,v=b.hasRendered&&F(a.oldMin),B=a.showAxis,r=Ra(c.globalAnimation),t,u;a.labelEdge.length=0;a.overlap=!1;n([k,j,l],function(a){for(var b in a)a[b].isActive=!1});if(a.hasData()||g){a.minorTickInterval&&!a.categories&&n(a.getMinorTickPositions(),function(b){j[b]||(j[b]=new Sa(a,b,"minor"));v&&j[b].isNew&&j[b].render(null,!0);j[b].render(null, -!1,1)});if(h.length&&(n(h,function(b,c){if(!g||b>=a.min&&b<=a.max)k[b]||(k[b]=new Sa(a,b)),v&&k[b].isNew&&k[b].render(c,!0,0.1),k[b].render(c)}),q&&(a.min===0||a.single)))k[-1]||(k[-1]=new Sa(a,-1,null,!0)),k[-1].render(-1);p&&n(h,function(c,d){u=h[d+1]!==w?h[d+1]+q:a.max-q;if(d%2===0&&c=0.5)a=x(a),i=this.getLinearTickPositions(a,b,c);else if(a>=0.08)for(var f=fa(b),k,j,l,m,p,e=a>0.3?[1,2,4]:a>0.15?[1,2,4,6,8]:[1,2,3,4,5,6,7,8,9];fb&&(!d||m<=c)&&m!==w&&i.push(m),m>c&&(p=!0),m=l}else if(b=g(b),c=g(c),a=e[d?"minorTickInterval":"tickInterval"],a=o(a==="auto"?null:a,this._minorAutoInterval,(c-b)*(e.tickPixelInterval/(d?5:1))/((d?f/this.tickPositions.length:f)||1)), -a=ub(a,null,H.pow(10,fa(H.log(a)/H.LN10))),i=Aa(this.getLinearTickPositions(a,b,c),h),!d)this._minorAutoInterval=a/5;if(!d)this.tickInterval=a;return i};X.prototype.log2lin=function(a){return H.log(a)/H.LN10};X.prototype.lin2log=function(a){return H.pow(10,a)};var Jb=s.Tooltip=function(){this.init.apply(this,arguments)};Jb.prototype={init:function(a,b){var c=b.borderWidth,d=b.style,e=E(d.padding);this.chart=a;this.options=b;this.crosshairs=[];this.now={x:0,y:0};this.isHidden=!0;this.label=a.renderer.label("", -0,0,b.shape||"callout",null,null,b.useHTML,null,"tooltip").attr({padding:e,fill:b.backgroundColor,"stroke-width":c,r:b.borderRadius,zIndex:8}).css(d).css({padding:0}).add().attr({y:-9999});ma||this.label.shadow(b.shadow);this.shared=b.shared},destroy:function(){if(this.label)this.label=this.label.destroy();clearTimeout(this.hideTimer);clearTimeout(this.tooltipTimeout)},move:function(a,b,c,d){var e=this,f=e.now,g=e.options.animation!==!1&&!e.isHidden&&(S(a-f.x)>1||S(b-f.y)>1),h=e.followPointer||e.len> -1;r(f,{x:g?(2*f.x+a)/3:a,y:g?(f.y+b)/2:b,anchorX:h?w:g?(2*f.anchorX+c)/3:c,anchorY:h?w:g?(f.anchorY+d)/2:d});e.label.attr(f);if(g)clearTimeout(this.tooltipTimeout),this.tooltipTimeout=setTimeout(function(){e&&e.move(a,b,c,d)},32)},hide:function(a){var b=this;clearTimeout(this.hideTimer);a=o(a,this.options.hideDelay,500);if(!this.isHidden)this.hideTimer=Na(function(){b.label[a?"fadeOut":"hide"]();b.isHidden=!0},a)},getAnchor:function(a,b){var c,d=this.chart,e=d.inverted,f=d.plotTop,g=d.plotLeft,h= -0,i=0,k,j,a=ra(a);c=a[0].tooltipPos;this.followPointer&&b&&(b.chartX===w&&(b=d.pointer.normalize(b)),c=[b.chartX-d.plotLeft,b.chartY-f]);c||(n(a,function(a){k=a.series.yAxis;j=a.series.xAxis;h+=a.plotX+(!e&&j?j.left-g:0);i+=(a.plotLow?(a.plotLow+a.plotHigh)/2:a.plotY)+(!e&&k?k.top-f:0)}),h/=a.length,i/=a.length,c=[e?d.plotWidth-i:h,this.shared&&!e&&a.length>1&&b?b.chartY-f:e?d.plotHeight-h:i]);return Aa(c,x)},getPosition:function(a,b,c){var d=this.chart,e=this.distance,f={},g=c.h||0,h,i=["y",d.chartHeight, -b,c.plotY+d.plotTop,d.plotTop,d.plotTop+d.plotHeight],k=["x",d.chartWidth,a,c.plotX+d.plotLeft,d.plotLeft,d.plotLeft+d.plotWidth],j=!this.followPointer&&o(c.ttBelow,!d.inverted===!!c.negative),l=function(a,b,c,d,h,i){var k=cb?d:d+g);else return!1},m=function(a,b,c,d){var g;db-e?g=!1:f[a]=db-c/2?b-c-2:d-c/2;return g},p=function(a){var b=i;i=k;k=b;h=a},q=function(){l.apply(0, -i)!==!1?m.apply(0,k)===!1&&!h&&(p(!0),q()):h?f.x=f.y=0:(p(!0),q())};(d.inverted||this.len>1)&&p();q();return f},defaultFormatter:function(a){var b=this.points||ra(this),c;c=[a.tooltipFooterHeaderFormatter(b[0])];c=c.concat(a.bodyFormatter(b));c.push(a.tooltipFooterHeaderFormatter(b[0],!0));return c.join("")},refresh:function(a,b){var c=this.chart,d=this.label,e=this.options,f,g,h,i={},k,j=[];k=e.formatter||this.defaultFormatter;var i=c.hoverPoints,l,m=this.shared;clearTimeout(this.hideTimer);this.followPointer= -ra(a)[0].series.tooltipOptions.followPointer;h=this.getAnchor(a,b);f=h[0];g=h[1];m&&(!a.series||!a.series.noSharedTooltip)?(c.hoverPoints=a,i&&n(i,function(a){a.setState()}),n(a,function(a){a.setState("hover");j.push(a.getLabelConfig())}),i={x:a[0].category,y:a[0].y},i.points=j,this.len=j.length,a=a[0]):i=a.getLabelConfig();k=k.call(i,this);i=a.series;this.distance=o(i.tooltipOptions.distance,16);k===!1?this.hide():(this.isHidden&&(La(d),d.attr("opacity",1).show()),d.attr({text:k}),l=e.borderColor|| -a.color||i.color||"#606060",d.attr({stroke:l}),this.updatePosition({plotX:f,plotY:g,negative:a.negative,ttBelow:a.ttBelow,h:h[2]||0}),this.isHidden=!1);G(c,"tooltipRefresh",{text:k,x:f+c.plotLeft,y:g+c.plotTop,borderColor:l})},updatePosition:function(a){var b=this.chart,c=this.label,c=(this.options.positioner||this.getPosition).call(this,c.width,c.height,a);this.move(x(c.x),x(c.y||0),a.plotX+b.plotLeft,a.plotY+b.plotTop)},getXDateFormat:function(a,b,c){var d,b=b.dateTimeLabelFormats,e=c&&c.closestPointRange, -f,g={millisecond:15,second:12,minute:9,hour:6,day:3},h,i="millisecond";if(e){h=Oa("%m-%d %H:%M:%S.%L",a.x);for(f in hb){if(e===hb.week&&+Oa("%w",a.x)===c.options.startOfWeek&&h.substr(6)==="00:00:00.000"){f="week";break}if(hb[f]>e){f=i;break}if(g[f]&&h.substr(g[f])!=="01-01 00:00:00.000".substr(g[f]))break;f!=="week"&&(i=f)}f&&(d=b[f])}else d=b.day;return d||b.year},tooltipFooterHeaderFormatter:function(a,b){var c=b?"footer":"header",d=a.series,e=d.tooltipOptions,f=e.xDateFormat,g=d.xAxis,h=g&&g.options.type=== -"datetime"&&F(a.key),c=e[c+"Format"];h&&!f&&(f=this.getXDateFormat(a,e,g));h&&f&&(c=c.replace("{point.key}","{point.key:"+f+"}"));return Ea(c,{point:a,series:d})},bodyFormatter:function(a){return Aa(a,function(a){var c=a.series.tooltipOptions;return(c.pointFormatter||a.point.tooltipFormatter).call(a.point,c.pointFormat)})}};var ia;Wa=z&&z.documentElement.ontouchstart!==w;var Ba=s.Pointer=function(a,b){this.init(a,b)};Ba.prototype={init:function(a,b){var c=b.chart,d=c.events,e=ma?"":c.zoomType,c=a.inverted, -f;this.options=b;this.chart=a;this.zoomX=f=/x/.test(e);this.zoomY=e=/y/.test(e);this.zoomHor=f&&!c||e&&c;this.zoomVert=e&&!c||f&&c;this.hasZoom=f||e;this.runChartClick=d&&!!d.click;this.pinchDown=[];this.lastValidTouch={};if(s.Tooltip&&b.tooltip.enabled)a.tooltip=new Jb(a,b.tooltip),this.followTouchMove=o(b.tooltip.followTouchMove,!0);this.setDOMEvents()},normalize:function(a,b){var c,d,a=a||D.event;if(!a.target)a.target=a.srcElement;d=a.touches?a.touches.length?a.touches.item(0):a.changedTouches[0]: -a;if(!b)this.chartPosition=b=rb(this.chart.container);d.pageX===w?(c=u(a.x,a.clientX-b.left),d=a.y):(c=d.pageX-b.left,d=d.pageY-b.top);return r(a,{chartX:x(c),chartY:x(d)})},getCoordinates:function(a){var b={xAxis:[],yAxis:[]};n(this.chart.axes,function(c){b[c.isXAxis?"xAxis":"yAxis"].push({axis:c,value:c.toValue(a[c.horiz?"chartX":"chartY"])})});return b},runPointActions:function(a){var b=this.chart,c=b.series,d=b.tooltip,e=d?d.shared:!1,f=b.hoverPoint,g=b.hoverSeries,h,i=[Number.MAX_VALUE,Number.MAX_VALUE], -k,j,l=[],m=[],p;if(!e&&!g)for(h=0;h=m[c].series.group.zIndex;if(a[b]h+k&&(d=h+k),ei+j&&(e=i+j),this.hasDragged=Math.sqrt(Math.pow(p-d,2)+Math.pow(q-e,2)),this.hasDragged>10){l=b.isInsidePlot(p-h,q-i);if(b.hasCartesianSeries&&(this.zoomX||this.zoomY)&&l&&!n&&!m)this.selectionMarker=m=b.renderer.rect(h,i,f?1:k,g?1:j,0).attr({fill:c.selectionMarkerFill||"rgba(69,114,167,0.25)",zIndex:7}).add();m&&f&&(d-=p,m.attr({width:S(d),x:(d>0?0:d)+p}));m&&g&&(d=e-q,m.attr({height:S(d), -y:(d>0?0:d)+q}));l&&!m&&c.panning&&b.pan(a,c.panning)}},drop:function(a){var b=this,c=this.chart,d=this.hasPinched;if(this.selectionMarker){var e={originalEvent:a,xAxis:[],yAxis:[]},f=this.selectionMarker,g=f.attr?f.attr("x"):f.x,h=f.attr?f.attr("y"):f.y,i=f.attr?f.attr("width"):f.width,k=f.attr?f.attr("height"):f.height,j;if(this.hasDragged||d)n(c.axes,function(c){if(c.zoomEnabled&&t(c.min)&&(d||b[{xAxis:"zoomX",yAxis:"zoomY"}[c.coll]])){var f=c.horiz,p=a.type==="touchend"?c.minPixelPadding:0,q= -c.toValue((f?g:h)+p),f=c.toValue((f?g+i:h+k)-p);e[c.coll].push({axis:c,min:I(q,f),max:u(q,f)});j=!0}}),j&&G(c,"selection",e,function(a){c.zoom(r(a,d?{animation:!1}:null))});this.selectionMarker=this.selectionMarker.destroy();d&&this.scaleGroups()}if(c)M(c.container,{cursor:c._cursor}),c.cancelClick=this.hasDragged>10,c.mouseIsDown=this.hasDragged=this.hasPinched=!1,this.pinchDown=[]},onContainerMouseDown:function(a){a=this.normalize(a);a.preventDefault&&a.preventDefault();this.dragStart(a)},onDocumentMouseUp:function(a){O[ia]&& -O[ia].pointer.drop(a)},onDocumentMouseMove:function(a){var b=this.chart,c=this.chartPosition,a=this.normalize(a,c);c&&!this.inClass(a.target,"highcharts-tracker")&&!b.isInsidePlot(a.chartX-b.plotLeft,a.chartY-b.plotTop)&&this.reset()},onContainerMouseLeave:function(a){var b=O[ia];if(b&&(a.relatedTarget||a.toElement))b.pointer.reset(),b.pointer.chartPosition=null},onContainerMouseMove:function(a){var b=this.chart;if(!t(ia)||!O[ia]||!O[ia].mouseIsDown)ia=b.index;a=this.normalize(a);a.returnValue=!1; -b.mouseIsDown==="mousedown"&&this.drag(a);(this.inClass(a.target,"highcharts-tracker")||b.isInsidePlot(a.chartX-b.plotLeft,a.chartY-b.plotTop))&&!b.openMenu&&this.runPointActions(a)},inClass:function(a,b){for(var c;a;){if(c=K(a,"class")){if(c.indexOf(b)!==-1)return!0;if(c.indexOf("highcharts-container")!==-1)return!1}a=a.parentNode}},onTrackerMouseOut:function(a){var b=this.chart.hoverSeries,a=a.relatedTarget||a.toElement;if(b&&a&&!b.options.stickyTracking&&!this.inClass(a,"highcharts-tooltip")&& -!this.inClass(a,"highcharts-series-"+b.index))b.onMouseOut()},onContainerClick:function(a){var b=this.chart,c=b.hoverPoint,d=b.plotLeft,e=b.plotTop,a=this.normalize(a);b.cancelClick||(c&&this.inClass(a.target,"highcharts-tracker")?(G(c.series,"click",r(a,{point:c})),b.hoverPoint&&c.firePointEvent("click",a)):(r(a,this.getCoordinates(a)),b.isInsidePlot(a.chartX-d,a.chartY-e)&&G(b,"click",a)))},setDOMEvents:function(){var a=this,b=a.chart.container;b.onmousedown=function(b){a.onContainerMouseDown(b)}; -b.onmousemove=function(b){a.onContainerMouseMove(b)};b.onclick=function(b){a.onContainerClick(b)};N(b,"mouseleave",a.onContainerMouseLeave);Xa===1&&N(z,"mouseup",a.onDocumentMouseUp);if(Wa)b.ontouchstart=function(b){a.onContainerTouchStart(b)},b.ontouchmove=function(b){a.onContainerTouchMove(b)},Xa===1&&N(z,"touchend",a.onDocumentTouchEnd)},destroy:function(){var a;Y(this.chart.container,"mouseleave",this.onContainerMouseLeave);Xa||(Y(z,"mouseup",this.onDocumentMouseUp),Y(z,"touchend",this.onDocumentTouchEnd)); -clearInterval(this.tooltipTimeout);for(a in this)this[a]=null}};r(s.Pointer.prototype,{pinchTranslate:function(a,b,c,d,e,f){(this.zoomHor||this.pinchHor)&&this.pinchTranslateDirection(!0,a,b,c,d,e,f);(this.zoomVert||this.pinchVert)&&this.pinchTranslateDirection(!1,a,b,c,d,e,f)},pinchTranslateDirection:function(a,b,c,d,e,f,g,h){var i=this.chart,k=a?"x":"y",j=a?"X":"Y",l="chart"+j,m=a?"width":"height",p=i["plot"+(a?"Left":"Top")],q,n,o=h||1,v=i.inverted,B=i.bounds[a?"h":"v"],r=b.length===1,t=b[0][l], -u=c[0][l],s=!r&&b[1][l],w=!r&&c[1][l],x,c=function(){!r&&S(t-s)>20&&(o=h||S(u-w)/S(t-s));n=(p-u)/o+t;q=i["plot"+(a?"Width":"Height")]/o};c();b=n;bB.max&&(b=B.max-q,x=!0);x?(u-=0.8*(u-g[k][0]),r||(w-=0.8*(w-g[k][1])),c()):g[k]=[u,w];v||(f[k]=n-p,f[m]=q);f=v?1/o:o;e[m]=q;e[k]=b;d[v?a?"scaleY":"scaleX":"scale"+j]=o;d["translate"+j]=f*p+(u-f*t)},pinch:function(a){var b=this,c=b.chart,d=b.pinchDown,e=a.touches,f=e.length,g=b.lastValidTouch,h=b.hasZoom,i=b.selectionMarker,k={}, -j=f===1&&(b.inClass(a.target,"highcharts-tracker")&&c.runTrackerClick||b.runChartClick),l={};if(f>1)b.initiated=!0;h&&b.initiated&&!j&&a.preventDefault();Aa(e,function(a){return b.normalize(a)});if(a.type==="touchstart")n(e,function(a,b){d[b]={chartX:a.chartX,chartY:a.chartY}}),g.x=[d[0].chartX,d[1]&&d[1].chartX],g.y=[d[0].chartY,d[1]&&d[1].chartY],n(c.axes,function(a){if(a.zoomEnabled){var b=c.bounds[a.horiz?"h":"v"],d=a.minPixelPadding,e=a.toPixels(o(a.options.min,a.dataMin)),f=a.toPixels(o(a.options.max, -a.dataMax)),g=I(e,f),e=u(e,f);b.min=I(a.pos,g-d);b.max=u(a.pos+a.len,e+d)}}),b.res=!0;else if(d.length){if(!i)b.selectionMarker=i=r({destroy:W,touch:!0},c.plotBox);b.pinchTranslate(d,e,k,i,l,g);b.hasPinched=h;b.scaleGroups(k,l);if(!h&&b.followTouchMove&&f===1)this.runPointActions(b.normalize(a));else if(b.res)b.res=!1,this.reset(!1,0)}},touch:function(a,b){var c=this.chart,d;ia=c.index;if(a.touches.length===1)if(a=this.normalize(a),c.isInsidePlot(a.chartX-c.plotLeft,a.chartY-c.plotTop)&&!c.openMenu){b&& -this.runPointActions(a);if(a.type==="touchmove")c=this.pinchDown,d=c[0]?Math.sqrt(Math.pow(c[0].chartX-a.chartX,2)+Math.pow(c[0].chartY-a.chartY,2))>=4:!1;o(d,!0)&&this.pinch(a)}else b&&this.reset();else a.touches.length===2&&this.pinch(a)},onContainerTouchStart:function(a){this.touch(a,!0)},onContainerTouchMove:function(a){this.touch(a)},onDocumentTouchEnd:function(a){O[ia]&&O[ia].pointer.drop(a)}});if(D.PointerEvent||D.MSPointerEvent){var ua={},sb=!!D.PointerEvent,Pb=function(){var a,b=[];b.item= -function(a){return this[a]};for(a in ua)ua.hasOwnProperty(a)&&b.push({pageX:ua[a].pageX,pageY:ua[a].pageY,target:ua[a].target});return b},tb=function(a,b,c,d){if((a.pointerType==="touch"||a.pointerType===a.MSPOINTER_TYPE_TOUCH)&&O[ia])d(a),d=O[ia].pointer,d[b]({type:c,target:a.currentTarget,preventDefault:W,touches:Pb()})};r(Ba.prototype,{onContainerPointerDown:function(a){tb(a,"onContainerTouchStart","touchstart",function(a){ua[a.pointerId]={pageX:a.pageX,pageY:a.pageY,target:a.currentTarget}})}, -onContainerPointerMove:function(a){tb(a,"onContainerTouchMove","touchmove",function(a){ua[a.pointerId]={pageX:a.pageX,pageY:a.pageY};if(!ua[a.pointerId].target)ua[a.pointerId].target=a.currentTarget})},onDocumentPointerUp:function(a){tb(a,"onDocumentTouchEnd","touchend",function(a){delete ua[a.pointerId]})},batchMSEvents:function(a){a(this.chart.container,sb?"pointerdown":"MSPointerDown",this.onContainerPointerDown);a(this.chart.container,sb?"pointermove":"MSPointerMove",this.onContainerPointerMove); -a(z,sb?"pointerup":"MSPointerUp",this.onDocumentPointerUp)}});ga(Ba.prototype,"init",function(a,b,c){a.call(this,b,c);this.hasZoom&&M(b.container,{"-ms-touch-action":"none","touch-action":"none"})});ga(Ba.prototype,"setDOMEvents",function(a){a.apply(this);(this.hasZoom||this.followTouchMove)&&this.batchMSEvents(N)});ga(Ba.prototype,"destroy",function(a){this.batchMSEvents(Y);a.call(this)})}var $a=s.Legend=function(a,b){this.init(a,b)};$a.prototype={init:function(a,b){var c=this,d=b.itemStyle,e=b.itemMarginTop|| -0;this.options=b;if(b.enabled)c.itemStyle=d,c.itemHiddenStyle=C(d,b.itemHiddenStyle),c.itemMarginTop=e,c.padding=d=o(b.padding,8),c.initialItemX=d,c.initialItemY=d-5,c.maxItemWidth=0,c.chart=a,c.itemHeight=0,c.symbolWidth=o(b.symbolWidth,16),c.pages=[],c.render(),N(c.chart,"endResize",function(){c.positionCheckboxes()})},colorizeItem:function(a,b){var c=this.options,d=a.legendItem,e=a.legendLine,f=a.legendSymbol,g=this.itemHiddenStyle.color,c=b?c.itemStyle.color:g,h=b?a.legendColor||a.color||"#CCC": -g,g=a.options&&a.options.marker,i={fill:h},k;d&&d.css({fill:c,color:c});e&&e.attr({stroke:h});if(f){if(g&&f.isMarker)for(k in i.stroke=h,g=a.convertAttribs(g),g)d=g[k],d!==w&&(i[k]=d);f.attr(i)}},positionItem:function(a){var b=this.options,c=b.symbolPadding,b=!b.rtl,d=a._legendItemPos,e=d[0],d=d[1],f=a.checkbox;(a=a.legendGroup)&&a.element&&a.translate(b?e:this.legendWidth-e-2*c-4,d);if(f)f.x=e,f.y=d},destroyItem:function(a){var b=a.checkbox;n(["legendItem","legendLine","legendSymbol","legendGroup"], -function(b){a[b]&&(a[b]=a[b].destroy())});b&&Qa(a.checkbox)},destroy:function(){var a=this.group,b=this.box;if(b)this.box=b.destroy();if(a)this.group=a.destroy()},positionCheckboxes:function(a){var b=this.group.alignAttr,c,d=this.clipHeight||this.legendHeight,e=this.titleHeight;if(b)c=b.translateY,n(this.allItems,function(f){var g=f.checkbox,h;g&&(h=c+e+g.y+(a||0)+3,M(g,{left:b.translateX+f.checkboxOffset+g.x-20+"px",top:h+"px",display:h>c-6&&h(m||b.chartWidth-2*k-n-d.x))this.itemX=n,this.itemY+=q+this.lastLineHeight+p,this.lastLineHeight=0;this.maxItemWidth=u(this.maxItemWidth,f);this.lastItemY=q+this.itemY+p;this.lastLineHeight=u(g,this.lastLineHeight);a._legendItemPos=[this.itemX,this.itemY];e?this.itemX+=f:(this.itemY+=q+g+p,this.lastLineHeight=g);this.offsetWidth=m||u((e?this.itemX-n-j:f)+k,this.offsetWidth)},getAllItems:function(){var a=[];n(this.chart.series,function(b){var c=b.options;if(o(c.showInLegend,!t(c.linkedTo)?w:!1, -!0))a=a.concat(b.legendItems||(c.legendType==="point"?b.data:b))});return a},adjustMargins:function(a,b){var c=this.chart,d=this.options,e=d.align.charAt(0)+d.verticalAlign.charAt(0)+d.layout.charAt(0);this.display&&!d.floating&&n([/(lth|ct|rth)/,/(rtv|rm|rbv)/,/(rbh|cb|lbh)/,/(lbv|lm|ltv)/],function(f,g){f.test(e)&&!t(a[g])&&(c[ib[g]]=u(c[ib[g]],c.legend[(g+1)%2?"legendHeight":"legendWidth"]+[1,-1,-1,1][g]*d[g%2?"x":"y"]+o(d.margin,12)+b[g]))})},render:function(){var a=this,b=a.chart,c=b.renderer, -d=a.group,e,f,g,h,i=a.box,k=a.options,j=a.padding,l=k.borderWidth,m=k.backgroundColor;a.itemX=a.initialItemX;a.itemY=a.initialItemY;a.offsetWidth=0;a.lastItemY=0;if(!d)a.group=d=c.g("legend").attr({zIndex:7}).add(),a.contentGroup=c.g().attr({zIndex:1}).add(d),a.scrollGroup=c.g().add(a.contentGroup);a.renderTitle();e=a.getAllItems();cb(e,function(a,b){return(a.options&&a.options.legendIndex||0)-(b.options&&b.options.legendIndex||0)});k.reversed&&e.reverse();a.allItems=e;a.display=f=!!e.length;a.lastLineHeight= -0;n(e,function(b){a.renderItem(b)});g=(k.width||a.offsetWidth)+j;h=a.lastItemY+a.lastLineHeight+a.titleHeight;h=a.handleOverflow(h);h+=j;if(l||m){if(i){if(g>0&&h>0)i[i.isNew?"attr":"animate"](i.crisp({width:g,height:h})),i.isNew=!1}else a.box=i=c.rect(0,0,g,h,k.borderRadius,l||0).attr({stroke:k.borderColor,"stroke-width":l||0,fill:m||"none"}).add(d).shadow(k.shadow),i.isNew=!0;i[f?"show":"hide"]()}a.legendWidth=g;a.legendHeight=h;n(e,function(b){a.positionItem(b)});f&&d.align(r({width:g,height:h}, -k),!0,"spacingBox");b.isResizing||this.positionCheckboxes()},handleOverflow:function(a){var b=this,c=this.chart,d=c.renderer,e=this.options,f=e.y,f=c.spacingBox.height+(e.verticalAlign==="top"?-f:f)-this.padding,g=e.maxHeight,h,i=this.clipRect,k=e.navigation,j=o(k.animation,!0),l=k.arrowSize||12,m=this.nav,p=this.pages,q=this.padding,A,r=this.allItems,v=function(a){i.attr({height:a});if(b.contentGroup.div)b.contentGroup.div.style.clip="rect("+q+"px,9999px,"+(q+a)+"px,0)"};e.layout==="horizontal"&& -(f/=2);g&&(f=I(f,g));p.length=0;if(a>f&&k.enabled!==!1){this.clipHeight=h=u(f-20-this.titleHeight-q,0);this.currentPage=o(this.currentPage,1);this.fullHeight=a;n(r,function(a,b){var c=a._legendItemPos[1],d=x(a.legendItem.getBBox().height),e=p.length;if(!e||c-p[e-1]>h&&(A||c)!==p[e-1])p.push(A||c),e++;b===r.length-1&&c+d-p[e-1]>h&&p.push(c);c!==A&&(A=c)});if(!i)i=b.clipRect=d.clipRect(0,q,9999,0),b.contentGroup.clip(i);v(h);if(!m)this.nav=m=d.g().attr({zIndex:1}).add(this.group),this.up=d.symbol("triangle", -0,0,l,l).on("click",function(){b.scroll(-1,j)}).add(m),this.pager=d.text("",15,10).css(k.style).add(m),this.down=d.symbol("triangle-down",0,0,l,l).on("click",function(){b.scroll(1,j)}).add(m);b.scroll(0);a=f}else if(m)v(c.chartHeight),m.hide(),this.scrollGroup.attr({translateY:1}),this.clipHeight=0;return a},scroll:function(a,b){var c=this.pages,d=c.length,e=this.currentPage+a,f=this.clipHeight,g=this.options.navigation,h=g.activeColor,g=g.inactiveColor,i=this.pager,k=this.padding;e>d&&(e=d);if(e> -0)b!==w&&Va(b,this.chart),this.nav.attr({translateX:k,translateY:f+this.padding+7+this.titleHeight,visibility:"visible"}),this.up.attr({fill:e===1?g:h}).css({cursor:e===1?"default":"pointer"}),i.attr({text:e+"/"+d}),this.down.attr({x:18+this.pager.getBBox().width,fill:e===d?g:h}).css({cursor:e===d?"default":"pointer"}),c=-c[e-1]+this.initialItemY,this.scrollGroup.animate({translateY:c}),this.currentPage=e,this.positionCheckboxes(c)}};var ab=s.LegendSymbolMixin={drawRectangle:function(a,b){var c=a.options.symbolHeight|| -a.fontMetrics.f;b.legendSymbol=this.chart.renderer.rect(0,a.baseline-c+1,a.symbolWidth,c,a.options.symbolRadius||0).attr({zIndex:3}).add(b.legendGroup)},drawLineMarker:function(a){var b=this.options,c=b.marker,d=a.symbolWidth,e=this.chart.renderer,f=this.legendGroup,a=a.baseline-x(a.fontMetrics.b*0.3),g;if(b.lineWidth){g={"stroke-width":b.lineWidth};if(b.dashStyle)g.dashstyle=b.dashStyle;this.legendLine=e.path(["M",0,a,"L",d,a]).attr(g).add(f)}if(c&&c.enabled!==!1)b=c.radius,this.legendSymbol=c=e.symbol(this.symbol, -d/2-b,a-b,2*b,2*b,c).add(f),c.isMarker=!0}};(/Trident\/7\.0/.test(ya)||Ka)&&ga($a.prototype,"positionItem",function(a,b){var c=this,d=function(){b._legendItemPos&&a.call(c,b)};d();setTimeout(d)});var ja=s.Chart=function(){this.getArgs.apply(this,arguments)};s.chart=function(a,b,c){return new ja(a,b,c)};ja.prototype={callbacks:[],getArgs:function(){var a=[].slice.call(arguments);if(va(a[0])||a[0].nodeName)this.renderTo=a.shift();this.init(a[0],a[1])},init:function(a,b){var c,d=a.series;a.series=null; -c=C(L,a);c.series=a.series=d;this.userOptions=a;d=c.chart;this.margin=this.splashArray("margin",d);this.spacing=this.splashArray("spacing",d);var e=d.events;this.bounds={h:{},v:{}};this.callback=b;this.isResizing=0;this.options=c;this.axes=[];this.series=[];this.hasCartesianSeries=d.showAxes;var f=this,g;f.index=O.length;O.push(f);Xa++;d.reflow!==!1&&N(f,"load",function(){f.initReflow()});if(e)for(g in e)N(f,g,e[g]);f.xAxis=[];f.yAxis=[];f.animation=ma?!1:o(d.animation,!0);f.pointCount=f.colorCounter= -f.symbolCounter=0;f.firstRender()},initSeries:function(a){var b=this.options.chart;(b=y[a.type||b.type||b.defaultSeriesType])||V(17,!0);b=new b;b.init(this,a);return b},isInsidePlot:function(a,b,c){var d=c?b:a,a=c?a:b;return d>=0&&d<=this.plotWidth&&a>=0&&a<=this.plotHeight},redraw:function(a){var b=this.axes,c=this.series,d=this.pointer,e=this.legend,f=this.isDirtyLegend,g,h,i=this.hasCartesianSeries,k=this.isDirtyBox,j=c.length,l=j,m=this.renderer,p=m.isHidden(),q=[];Va(a,this);p&&this.cloneRenderTo(); -for(this.layOutTitles();l--;)if(a=c[l],a.options.stacking&&(g=!0,a.isDirty)){h=!0;break}if(h)for(l=j;l--;)if(a=c[l],a.options.stacking)a.isDirty=!0;n(c,function(a){a.isDirty&&a.options.legendType==="point"&&(a.updateTotals&&a.updateTotals(),f=!0);a.isDirtyData&&G(a,"updatedData")});if(f&&e.options.enabled)e.render(),this.isDirtyLegend=!1;g&&this.getStacks();if(i&&!this.isResizing)this.maxTicks=null,n(b,function(a){a.setScale()});this.getMargins();i&&(n(b,function(a){a.isDirty&&(k=!0)}),n(b,function(a){var b= -a.min+","+a.max;if(a.extKey!==b)a.extKey=b,q.push(function(){G(a,"afterSetExtremes",r(a.eventArgs,a.getExtremes()));delete a.eventArgs});(k||g)&&a.redraw()}));k&&this.drawChartBox();n(c,function(a){a.isDirty&&a.visible&&(!a.isCartesian||a.xAxis)&&a.redraw()});d&&d.reset(!0);m.draw();G(this,"redraw");p&&this.cloneRenderTo(!0);n(q,function(a){a.call()})},get:function(a){var b=this.axes,c=this.series,d,e;for(d=0;d19?this.containerHeight:400))},cloneRenderTo:function(a){var b=this.renderToClone,c=this.container;a?b&&(this.renderTo.appendChild(c),Qa(b),delete this.renderToClone):(c&& -c.parentNode===this.renderTo&&this.renderTo.removeChild(c),this.renderToClone=b=this.renderTo.cloneNode(0),M(b,{position:"absolute",top:"-9999px",display:"block"}),b.style.setProperty&&b.style.setProperty("display","block","important"),z.body.appendChild(b),c&&b.appendChild(c))},getContainer:function(){var a,b=this.options,c=b.chart,d,e;a=this.renderTo;var f="highcharts-"+qb++;if(!a)this.renderTo=a=c.renderTo;if(va(a))this.renderTo=a=z.getElementById(a);a||V(13,!0);d=E(K(a,"data-highcharts-chart")); -F(d)&&O[d]&&O[d].hasRendered&&O[d].destroy();K(a,"data-highcharts-chart",this.index);a.innerHTML="";!c.skipClone&&!a.offsetWidth&&this.cloneRenderTo();this.getChartSize();d=this.chartWidth;e=this.chartHeight;this.container=a=da(Ja,{className:"highcharts-container"+(c.className?" "+c.className:""),id:f},r({position:"relative",overflow:"hidden",width:d+"px",height:e+"px",textAlign:"left",lineHeight:"normal",zIndex:0,"-webkit-tap-highlight-color":"rgba(0,0,0,0)"},c.style),this.renderToClone||a);this._cursor= -a.style.cursor;this.renderer=new (s[c.renderer]||Ta)(a,d,e,c.style,c.forExport,b.exporting&&b.exporting.allowHTML);ma&&this.renderer.create(this,a,d,e);this.renderer.chartIndex=this.index},getMargins:function(a){var b=this.spacing,c=this.margin,d=this.titleOffset;this.resetMargins();if(d&&!t(c[0]))this.plotTop=u(this.plotTop,d+this.options.title.margin+b[0]);this.legend.adjustMargins(c,b);this.extraBottomMargin&&(this.marginBottom+=this.extraBottomMargin);this.extraTopMargin&&(this.plotTop+=this.extraTopMargin); -a||this.getAxisMargins()},getAxisMargins:function(){var a=this,b=a.axisOffset=[0,0,0,0],c=a.margin;a.hasCartesianSeries&&n(a.axes,function(a){a.visible&&a.getOffset()});n(ib,function(d,e){t(c[e])||(a[d]+=b[e])});a.setChartSize()},reflow:function(a){var b=this,c=b.options.chart,d=b.renderTo,e=c.width||na(d,"width"),f=c.height||na(d,"height"),c=a?a.target:D;if(!b.hasUserSize&&!b.isPrinting&&e&&f&&(c===D||c===z)){if(e!==b.containerWidth||f!==b.containerHeight)clearTimeout(b.reflowTimeout),b.reflowTimeout= -Na(function(){if(b.container)b.setSize(e,f,!1),b.hasUserSize=null},a?100:0);b.containerWidth=e;b.containerHeight=f}},initReflow:function(){var a=this,b=function(b){a.reflow(b)};N(D,"resize",b);N(a,"destroy",function(){Y(D,"resize",b)})},setSize:function(a,b,c){var d=this,e,f,g=d.renderer;d.isResizing+=1;Va(c,d);d.oldChartHeight=d.chartHeight;d.oldChartWidth=d.chartWidth;if(t(a))d.chartWidth=e=u(0,x(a)),d.hasUserSize=!!e;if(t(b))d.chartHeight=f=u(0,x(b));a=g.globalAnimation;(a?Ua:M)(d.container,{width:e+ -"px",height:f+"px"},a);d.setChartSize(!0);g.setSize(e,f,c);d.maxTicks=null;n(d.axes,function(a){a.isDirty=!0;a.setScale()});n(d.series,function(a){a.isDirty=!0});d.isDirtyLegend=!0;d.isDirtyBox=!0;d.layOutTitles();d.getMargins();d.redraw(c);d.oldChartHeight=null;G(d,"resize");Na(function(){d&&G(d,"endResize",null,function(){d.isResizing-=1})},Ra(a).duration)},setChartSize:function(a){var b=this.inverted,c=this.renderer,d=this.chartWidth,e=this.chartHeight,f=this.options.chart,g=this.spacing,h=this.clipOffset, -i,k,j,l;this.plotLeft=i=x(this.plotLeft);this.plotTop=k=x(this.plotTop);this.plotWidth=j=u(0,x(d-i-this.marginRight));this.plotHeight=l=u(0,x(e-k-this.marginBottom));this.plotSizeX=b?l:j;this.plotSizeY=b?j:l;this.plotBorderWidth=f.plotBorderWidth||0;this.spacingBox=c.spacingBox={x:g[3],y:g[0],width:d-g[3]-g[1],height:e-g[0]-g[2]};this.plotBox=c.plotBox={x:i,y:k,width:j,height:l};d=2*fa(this.plotBorderWidth/2);b=ta(u(d,h[3])/2);c=ta(u(d,h[0])/2);this.clipBox={x:b,y:c,width:fa(this.plotSizeX-u(d,h[1])/ -2-b),height:u(0,fa(this.plotSizeY-u(d,h[2])/2-c))};a||n(this.axes,function(a){a.setAxisSize();a.setAxisTranslation()})},resetMargins:function(){var a=this;n(ib,function(b,c){a[b]=o(a.margin[c],a.spacing[c])});a.axisOffset=[0,0,0,0];a.clipOffset=[0,0,0,0]},drawChartBox:function(){var a=this.options.chart,b=this.renderer,c=this.chartWidth,d=this.chartHeight,e=this.chartBackground,f=this.plotBackground,g=this.plotBorder,h=this.plotBGImage,i=a.borderWidth||0,k=a.backgroundColor,j=a.plotBackgroundColor, -l=a.plotBackgroundImage,m=a.plotBorderWidth||0,p,q=this.plotLeft,n=this.plotTop,o=this.plotWidth,v=this.plotHeight,r=this.plotBox,t=this.clipRect,u=this.clipBox;p=i+(a.shadow?8:0);if(i||k)if(e)e.animate(e.crisp({width:c-p,height:d-p}));else{e={fill:k||"none"};if(i)e.stroke=a.borderColor,e["stroke-width"]=i;this.chartBackground=b.rect(p/2,p/2,c-p,d-p,a.borderRadius,i).attr(e).addClass("highcharts-background").add().shadow(a.shadow)}if(j)f?f.animate(r):this.plotBackground=b.rect(q,n,o,v,0).attr({fill:j}).add().shadow(a.plotShadow); -if(l)h?h.animate(r):this.plotBGImage=b.image(l,q,n,o,v).add();t?t.animate({width:u.width,height:u.height}):this.clipRect=b.clipRect(u);if(m)g?(g.strokeWidth=-m,g.animate(g.crisp({x:q,y:n,width:o,height:v}))):this.plotBorder=b.rect(q,n,o,v,0,-m).attr({stroke:a.plotBorderColor,"stroke-width":m,fill:"none",zIndex:1}).add();this.isDirtyBox=!1},propFromSeries:function(){var a=this,b=a.options.chart,c,d=a.options.series,e,f;n(["inverted","angular","polar"],function(g){c=y[b.type||b.defaultSeriesType];f= -a[g]||b[g]||c&&c.prototype[g];for(e=d&&d.length;!f&&e--;)(c=y[d[e].type])&&c.prototype[g]&&(f=!0);a[g]=f})},linkSeries:function(){var a=this,b=a.series;n(b,function(a){a.linkedSeries.length=0});n(b,function(b){var d=b.options.linkedTo;if(va(d)&&(d=d===":previous"?a.series[b.index-1]:a.get(d)))d.linkedSeries.push(b),b.linkedParent=d,b.visible=o(b.options.visible,d.options.visible,b.visible)})},renderSeries:function(){n(this.series,function(a){a.translate();a.render()})},renderLabels:function(){var a= -this,b=a.options.labels;b.items&&n(b.items,function(c){var d=r(b.style,c.style),e=E(d.left)+a.plotLeft,f=E(d.top)+a.plotTop+12;delete d.left;delete d.top;a.renderer.text(c.html,e,f).attr({zIndex:2}).css(d).add()})},render:function(){var a=this.axes,b=this.renderer,c=this.options,d,e,f,g;this.setTitle();this.legend=new $a(this,c.legend);this.getStacks&&this.getStacks();this.getMargins(!0);this.setChartSize();d=this.plotWidth;e=this.plotHeight-=21;n(a,function(a){a.setScale()});this.getAxisMargins(); -f=d/this.plotWidth>1.1;g=e/this.plotHeight>1.05;if(f||g)this.maxTicks=null,n(a,function(a){(a.horiz&&f||!a.horiz&&g)&&a.setTickInterval(!0)}),this.getMargins();this.drawChartBox();this.hasCartesianSeries&&n(a,function(a){a.visible&&a.render()});if(!this.seriesGroup)this.seriesGroup=b.g("series-group").attr({zIndex:3}).add();this.renderSeries();this.renderLabels();this.showCredits(c.credits);this.hasRendered=!0},showCredits:function(a){if(a.enabled&&!this.credits)this.credits=this.renderer.text(a.text, -0,0).on("click",function(){if(a.href)D.location.href=a.href}).attr({align:a.position.align,zIndex:8}).css(a.style).add().align(a.position)},destroy:function(){var a=this,b=a.axes,c=a.series,d=a.container,e,f=d&&d.parentNode;G(a,"destroy");O[a.index]=w;Xa--;a.renderTo.removeAttribute("data-highcharts-chart");Y(a);for(e=b.length;e--;)b[e]=b[e].destroy();for(e=c.length;e--;)c[e]=c[e].destroy();n("title,subtitle,chartBackground,plotBackground,plotBGImage,plotBorder,seriesGroup,clipRect,credits,pointer,scroller,rangeSelector,legend,resetZoomButton,tooltip,renderer".split(","), -function(b){var c=a[b];c&&c.destroy&&(a[b]=c.destroy())});if(d)d.innerHTML="",Y(d),f&&Qa(d);for(e in a)delete a[e]},isReadyToRender:function(){var a=this;return!ea&&D==D.top&&z.readyState!=="complete"||ma&&!D.canvg?(ma?Ib.push(function(){a.firstRender()},a.options.global.canvasToolsURL):z.attachEvent("onreadystatechange",function(){z.detachEvent("onreadystatechange",a.firstRender);z.readyState==="complete"&&a.firstRender()}),!1):!0},firstRender:function(){var a=this,b=a.options;if(a.isReadyToRender()){a.getContainer(); -G(a,"init");a.resetMargins();a.setChartSize();a.propFromSeries();a.getAxes();n(b.series||[],function(b){a.initSeries(b)});a.linkSeries();G(a,"beforeRender");if(s.Pointer)a.pointer=new Ba(a,b);a.render();a.renderer.draw();if(!a.renderer.imgCount&&a.onload)a.onload();a.cloneRenderTo(!0)}},onload:function(){var a=this;n([this.callback].concat(this.callbacks),function(b){b&&a.index!==void 0&&b.apply(a,[a])});G(a,"load");this.onload=null},splashArray:function(a,b){var c=b[a],c=aa(c)?c:[c,c,c,c];return[o(b[a+ -"Top"],c[0]),o(b[a+"Right"],c[1]),o(b[a+"Bottom"],c[2]),o(b[a+"Left"],c[3])]}};var $=function(){};$.prototype={init:function(a,b,c){this.series=a;this.color=a.color;this.applyOptions(b,c);this.pointAttr={};if(a.options.colorByPoint&&(b=a.options.colors||a.chart.options.colors,this.color=this.color||b[a.colorCounter++],a.colorCounter===b.length))a.colorCounter=0;a.chart.pointCount++;return this},applyOptions:function(a,b){var c=this.series,d=c.options.pointValKey||c.pointValKey,a=$.prototype.optionsToObject.call(this, -a);r(this,a);this.options=this.options?r(this.options,a):a;if(d)this.y=this[d];this.isNull=this.x===null||this.y===null;if(this.x===void 0&&c)this.x=b===void 0?c.autoIncrement():b;return this},optionsToObject:function(a){var b={},c=this.series,d=c.options.keys,e=d||c.pointArrayMap||["y"],f=e.length,g=0,h=0;if(F(a)||a===null)b[e[0]]=a;else if(Ca(a)){if(!d&&a.length>f){c=typeof a[0];if(c==="string")b.name=a[0];else if(c==="number")b.x=a[0];g++}for(;hp){for(c=0;j===null&&ci||this.forceCrop))if(b[d-1]q)b=[],c=[];else if(b[0]q)e=this.cropData(this.xData,this.yData,p,q),b=e.xData,c=e.yData,e=e.start,f=!0;for(i=b.length||1;--i;)d=m?k(b[i])-k(b[i- -1]):b[i]-b[i-1],d>0&&(g===w||d=c){f=u(0,i-h);break}for(c=i;cd){g=c+h;break}return{xData:a.slice(f,g),yData:b.slice(f,g),start:f,end:g}},generatePoints:function(){var a=this.options.data,b=this.data,c,d=this.processedXData,e=this.processedYData, -f=this.pointClass,g=d.length,h=this.cropStart||0,i,k=this.hasGroupedData,j,l=[],m;if(!b&&!k)b=[],b.length=a.length,b=this.data=b;for(m=0;m0),k=this.getExtremesFromAll||this.options.getExtremesFromAll||this.cropped||(c[l+1]||k)>=g&&(c[l-1]||k)<=h,i&&k)if(i=j.length)for(;i--;)j[i]!==null&&(e[f++]=j[i]);else e[f++]=j;this.dataMin=Pa(e);this.dataMax=Ia(e)},translate:function(){this.processedXData||this.processData();this.generatePoints();for(var a= -this.options,b=a.stacking,c=this.xAxis,d=c.categories,e=this.yAxis,f=this.points,g=f.length,h=!!this.modifyValue,i=a.pointPlacement,k=i==="between"||F(i),j=a.threshold,l=a.startFromThreshold?j:0,m,p,q,n,r=Number.MAX_VALUE,a=0;a=0&&p<=e.len&&m>=0&&m<=c.len;v.clientX=k?c.translate(B,0,0,0,1):m;v.negative=v.y<(j||0);v.category=d&& -d[v.x]!==w?d[v.x]:v.x;v.isNull||(q!==void 0&&(r=I(r,S(m-q))),q=m)}this.closestPointRangePx=r},getValidPoints:function(a,b){var c=this.chart;return Ha(a||this.points||[],function(a){return b&&!c.isInsidePlot(a.plotX,a.plotY,c.inverted)?!1:!a.isNull})},setClip:function(a){var b=this.chart,c=this.options,d=b.renderer,e=b.inverted,f=this.clipBox,g=f||b.clipBox,h=this.sharedClipKey||["_sharedClip",a&&a.duration,a&&a.easing,g.height,c.xAxis,c.yAxis].join(","),i=b[h],k=b[h+"m"];if(!i){if(a)g.width=0,b[h+ -"m"]=k=d.clipRect(-99,e?-b.plotLeft:-b.plotTop,99,e?b.chartWidth:b.chartHeight);b[h]=i=d.clipRect(g)}a&&(i.count+=1);if(c.clip!==!1)this.group.clip(a||f?i:b.clipRect),this.markerGroup.clip(k),this.sharedClipKey=h;a||(i.count-=1,i.count<=0&&h&&b[h]&&(f||(b[h]=b[h].destroy()),b[h+"m"]&&(b[h+"m"]=b[h+"m"].destroy())))},animate:function(a){var b=this.chart,c=this.options.animation,d;if(c&&!aa(c))c=Z[this.type].animation;a?this.setClip(c):(d=this.sharedClipKey,(a=b[d])&&a.animate({width:b.plotSizeX},c), -b[d+"m"]&&b[d+"m"].animate({width:b.plotSizeX+99},c),this.animate=null)},afterAnimate:function(){this.setClip();G(this,"afterAnimate")},drawPoints:function(){var a,b=this.points,c=this.chart,d,e,f,g,h,i,k,j,l=this.options.marker,m=this.pointAttr[""],p,q,n,t=this.markerGroup,v=o(l.enabled,this.xAxis.isRadial,this.closestPointRangePx>2*l.radius);if(l.enabled!==!1||this._hasPointMarkers)for(f=b.length;f--;)if(g=b[f],d=fa(g.plotX),e=g.plotY,j=g.graphic,p=g.marker||{},q=!!g.marker,a=v&&p.enabled===w|| -p.enabled,n=g.isInside,a&&F(e)&&g.y!==null)if(a=g.pointAttr[g.selected?"select":""]||m,h=a.r,i=o(p.symbol,this.symbol),k=i.indexOf("url")===0,j)j[n?"show":"hide"](!0).attr(a).animate(r({x:d-h,y:e-h},j.symbolName?{width:2*h,height:2*h}:{}));else{if(n&&(h>0||k))g.graphic=c.renderer.symbol(i,d-h,e-h,2*h,2*h,q?p:l).attr(a).add(t)}else if(j)g.graphic=j.destroy()},convertAttribs:function(a,b,c,d){var e=this.pointAttrToOptions,f,g,h={},a=a||{},b=b||{},c=c||{},d=d||{};for(f in e)g=e[f],h[f]=o(a[g],b[f],c[f], -d[f]);return h},getAttribs:function(){var a=this,b=a.options,c=Z[a.type].marker?b.marker:b,d=c.states,e=d.hover,f,g=a.color,h=a.options.negativeColor,i={stroke:g,fill:g},k=a.points||[],j,l=[],m,p=a.pointAttrToOptions;f=a.hasPointSpecificOptions;var q=c.lineColor,A=c.fillColor;j=b.turboThreshold;var s=a.zones,v=a.zoneAxis||"y",B,u;b.marker?(e.radius=e.radius||c.radius+e.radiusPlus,e.lineWidth=e.lineWidth||c.lineWidth+e.lineWidthPlus):(e.color=e.color||T(e.color||g).brighten(e.brightness).get(),e.negativeColor= -e.negativeColor||T(e.negativeColor||h).brighten(e.brightness).get());l[""]=a.convertAttribs(c,i);n(["hover","select"],function(b){l[b]=a.convertAttribs(d[b],l[""])});a.pointAttr=l;g=k.length;if(!j||g=i.value;)i=s[++f];j.color=j.fillColor=i=o(i.color,a.color)}f=b.colorByPoint||j.color;if(j.options)for(u in p)t(c[p[u]])&&(f=!0);if(f){c=c||{};m=[];d=c.states||{};f= -d.hover=d.hover||{};if(!b.marker||j.negative&&!f.fillColor&&!e.fillColor)f[a.pointAttrToOptions.fill]=f.color||!j.options.color&&e[j.negative&&h?"negativeColor":"color"]||T(j.color).brighten(f.brightness||e.brightness).get();B={color:j.color};if(!A)B.fillColor=j.color;if(!q)B.lineColor=j.color;c.hasOwnProperty("color")&&!c.color&&delete c.color;if(i&&!e.fillColor)f.fillColor=i;m[""]=a.convertAttribs(r(B,c),l[""]);m.hover=a.convertAttribs(d.hover,l.hover,m[""]);m.select=a.convertAttribs(d.select,l.select, -m[""])}else m=l;j.pointAttr=m}},destroy:function(){var a=this,b=a.chart,c=/AppleWebKit\/533/.test(ya),d,e=a.data||[],f,g,h;G(a,"destroy");Y(a);n(a.axisTypes||[],function(b){if(h=a[b])wa(h.series,a),h.isDirty=h.forceRedraw=!0});a.legendItem&&a.chart.legend.destroyItem(a);for(d=e.length;d--;)(f=e[d])&&f.destroy&&f.destroy();a.points=null;clearTimeout(a.animationTimeout);for(g in a)a[g]instanceof Q&&!a[g].survive&&(d=c&&g==="group"?"hide":"destroy",a[g][d]());if(b.hoverSeries===a)b.hoverSeries=null; -wa(b.series,a);for(g in a)delete a[g]},getGraphPath:function(a,b,c){var d=this,e=d.options,f=e.step,g,h=[],i,a=a||d.points;(g=a.reversed)&&a.reverse();(f={right:1,center:2}[f]||f&&3)&&g&&(f=4-f);e.connectNulls&&!b&&!c&&(a=this.getValidPoints(a));n(a,function(g,j){var l=g.plotX,m=g.plotY,p=a[j-1];if((g.leftCliff||p&&p.rightCliff)&&!c)i=!0;g.isNull&&!t(b)&&j>0?i=!e.connectNulls:g.isNull&&!b?i=!0:(j===0||i?p=["M",g.plotX,g.plotY]:d.getPointSpline?p=d.getPointSpline(a,g,j):f?(p=f===1?["L",p.plotX,m]: -f===2?["L",(p.plotX+l)/2,p.plotY,"L",(p.plotX+l)/2,m]:["L",l,p.plotY],p.push("L",l,m)):p=["L",l,m],h.push.apply(h,p),i=!1)});return d.graphPath=h},drawGraph:function(){var a=this,b=this.options,c=[["graph",b.lineColor||this.color,b.dashStyle]],d=b.lineWidth,e=b.linecap!=="square",f=(this.gappedPath||this.getGraphPath).call(this),g=this.fillGraph&&this.color||"none";n(this.zones,function(d,e){c.push(["zoneGraph"+e,d.color||a.color,d.dashStyle||b.dashStyle])});n(c,function(c,i){var k=c[0],j=a[k];if(j)j.animate({d:f}); -else if((d||g)&&f.length)j={stroke:c[1],"stroke-width":d,fill:g,zIndex:1},c[2]?j.dashstyle=c[2]:e&&(j["stroke-linecap"]=j["stroke-linejoin"]="round"),a[k]=a.chart.renderer.path(f).attr(j).add(a.group).shadow(i<2&&b.shadow)})},applyZones:function(){var a=this,b=this.chart,c=b.renderer,d=this.zones,e,f,g=this.clips||[],h,i=this.graph,k=this.area,j=u(b.chartWidth,b.chartHeight),l=this[(this.zoneAxis||"y")+"Axis"],m,p=l.reversed,q=b.inverted,A=l.horiz,r,v,B,t=!1;if(d.length&&(i||k)&&l.min!==w)i&&i.hide(), -k&&k.hide(),m=l.getExtremes(),n(d,function(d,n){e=p?A?b.plotWidth:0:A?0:l.toPixels(m.min);e=I(u(o(f,e),0),j);f=I(u(x(l.toPixels(o(d.value,m.max),!0)),0),j);t&&(e=f=l.toPixels(m.max));r=Math.abs(e-f);v=I(e,f);B=u(e,f);if(l.isXAxis){if(h={x:q?B:v,y:0,width:r,height:j},!A)h.x=b.plotHeight-h.x}else if(h={x:0,y:q?B:v,width:j,height:r},A)h.y=b.plotWidth-h.y;b.inverted&&c.isVML&&(h=l.isXAxis?{x:0,y:p?v:B,height:h.width,width:b.chartWidth}:{x:h.y-b.plotLeft-b.spacingBox.x,y:0,width:h.height,height:b.chartHeight}); -g[n]?g[n].animate(h):(g[n]=c.clipRect(h),i&&a["zoneGraph"+n].clip(g[n]),k&&a["zoneArea"+n].clip(g[n]));t=d.value>m.max}),this.clips=g},invertGroups:function(){function a(){var a={width:b.yAxis.len,height:b.xAxis.len};n(["group","markerGroup"],function(c){b[c]&&b[c].attr(a).invert()})}var b=this,c=b.chart;if(b.xAxis)N(c,"resize",a),N(b,"destroy",function(){Y(c,"resize",a)}),a(),b.invertGroups=a},plotGroup:function(a,b,c,d,e){var f=this[a],g=!f;g&&(this[a]=f=this.chart.renderer.g(b).attr({zIndex:d|| -0.1}).add(e),f.addClass("highcharts-series-"+this.index));f.attr({visibility:c})[g?"attr":"animate"](this.getPlotBox());return f},getPlotBox:function(){var a=this.chart,b=this.xAxis,c=this.yAxis;if(a.inverted)b=c,c=this.xAxis;return{translateX:b?b.left:a.plotLeft,translateY:c?c.top:a.plotTop,scaleX:1,scaleY:1}},render:function(){var a=this,b=a.chart,c,d=a.options,e=!!a.animate&&b.renderer.isSVG&&Ra(d.animation).duration,f=a.visible?"inherit":"hidden",g=d.zIndex,h=a.hasRendered,i=b.seriesGroup;c=a.plotGroup("group", -"series",f,g,i);a.markerGroup=a.plotGroup("markerGroup","markers",f,g,i);e&&a.animate(!0);a.getAttribs();c.inverted=a.isCartesian?b.inverted:!1;a.drawGraph&&(a.drawGraph(),a.applyZones());n(a.points,function(a){a.redraw&&a.redraw()});a.drawDataLabels&&a.drawDataLabels();a.visible&&a.drawPoints();a.drawTracker&&a.options.enableMouseTracking!==!1&&a.drawTracker();b.inverted&&a.invertGroups();d.clip!==!1&&!a.sharedClipKey&&!h&&c.clip(b.clipRect);e&&a.animate();if(!h)a.animationTimeout=Na(function(){a.afterAnimate()}, -e);a.isDirty=a.isDirtyData=!1;a.hasRendered=!0},redraw:function(){var a=this.chart,b=this.isDirty||this.isDirtyData,c=this.group,d=this.xAxis,e=this.yAxis;c&&(a.inverted&&c.attr({width:a.plotWidth,height:a.plotHeight}),c.animate({translateX:o(d&&d.left,a.plotLeft),translateY:o(e&&e.top,a.plotTop)}));this.translate();this.render();b&&delete this.kdTree},kdDimensions:1,kdAxisArray:["clientX","plotY"],searchPoint:function(a,b){var c=this.xAxis,d=this.yAxis,e=this.chart.inverted;return this.searchKDTree({clientX:e? -c.len-a.chartY+c.pos:a.chartX-c.pos,plotY:e?d.len-a.chartX+d.pos:a.chartY-d.pos},b)},buildKDTree:function(){function a(c,e,f){var g,h;if(h=c&&c.length)return g=b.kdAxisArray[e%f],c.sort(function(a,b){return a[g]-b[g]}),h=Math.floor(h/2),{point:c[h],left:a(c.slice(0,h),e+1,f),right:a(c.slice(h+1),e+1,f)}}var b=this,c=b.kdDimensions;delete b.kdTree;Na(function(){b.kdTree=a(b.getValidPoints(null,!b.directTouch),c,c)},b.options.kdNow?0:1)},searchKDTree:function(a,b){function c(a,b,k,j){var l=b.point, -m=d.kdAxisArray[k%j],p,n,o=l;n=t(a[e])&&t(l[e])?Math.pow(a[e]-l[e],2):null;p=t(a[f])&&t(l[f])?Math.pow(a[f]-l[f],2):null;p=(n||0)+(p||0);l.dist=t(p)?Math.sqrt(p):Number.MAX_VALUE;l.distX=t(n)?Math.sqrt(n):Number.MAX_VALUE;m=a[m]-l[m];p=m<0?"left":"right";n=m<0?"right":"left";b[p]&&(p=c(a,b[p],k+1,j),o=p[g]m;)d--;e.updateParallelArrays(i,"splice",d,0,0);e.updateParallelArrays(i,d);if(j&&i.name)j[m]=i.name;h.splice(d,0,a);p&&(e.data.splice(d,0,null),e.processData()); -f.legendType==="point"&&e.generatePoints();c&&(g[0]&&g[0].remove?g[0].remove(!1):(g.shift(),e.updateParallelArrays(i,"shift"),h.shift()));e.isDirty=!0;e.isDirtyData=!0;b&&(e.getAttribs(),k.redraw())},removePoint:function(a,b,c){var d=this,e=d.data,f=e[a],g=d.points,h=d.chart,i=function(){g&&g.length===e.length&&g.splice(a,1);e.splice(a,1);d.options.data.splice(a,1);d.updateParallelArrays(f||{series:d},"splice",a,1);f&&f.destroy();d.isDirty=!0;d.isDirtyData=!0;b&&h.redraw()};Va(c,h);b=o(b,!0);f?f.firePointEvent("remove", -null,i):i()},remove:function(a,b){var c=this,d=c.chart;G(c,"remove",null,function(){c.destroy();d.isDirtyLegend=d.isDirtyBox=!0;d.linkSeries();o(a,!0)&&d.redraw(b)})},update:function(a,b){var c=this,d=this.chart,e=this.userOptions,f=this.type,g=y[f].prototype,h=["group","markerGroup","dataLabelsGroup"],i;if(a.type&&a.type!==f||a.zIndex!==void 0)h.length=0;n(h,function(a){h[a]=c[a];delete c[a]});a=C(e,{animation:!1,index:this.index,pointStart:this.xData[0]},{data:this.options.data},a);this.remove(!1); -for(i in g)this[i]=w;r(this,y[a.type||f].prototype);n(h,function(a){c[a]=h[a]});this.init(d,a);d.linkSeries();o(b,!0)&&d.redraw(!1)}});r(X.prototype,{update:function(a,b){var c=this.chart,a=c.options[this.coll][this.options.index]=C(this.userOptions,a);this.destroy(!0);this._addedPlotLB=this.chart._labelPanes=w;this.init(c,r(a,{events:w}));c.isDirtyBox=!0;o(b,!0)&&c.redraw()},remove:function(a){for(var b=this.chart,c=this.coll,d=this.series,e=d.length;e--;)d[e]&&d[e].remove(!1);wa(b.axes,this);wa(b[c], -this);b.options[c].splice(this.options.index,1);n(b[c],function(a,b){a.options.index=b});this.destroy();b.isDirtyBox=!0;o(a,!0)&&b.redraw()},setTitle:function(a,b){this.update({title:a},b)},setCategories:function(a,b){this.update({categories:a},b)}});U=ba(R);y.line=U;Z.column=C(Ya,{borderColor:"#FFFFFF",borderRadius:0,groupPadding:0.2,marker:null,pointPadding:0.1,minPointLength:0,cropThreshold:50,pointRange:null,states:{hover:{brightness:0.1,shadow:!1,halo:!1},select:{color:"#C0C0C0",borderColor:"#000000", -shadow:!1}},dataLabels:{align:null,verticalAlign:null,y:null},softThreshold:!1,startFromThreshold:!0,stickyTracking:!1,tooltip:{distance:6},threshold:0});U=ba(R,{type:"column",pointAttrToOptions:{stroke:"borderColor",fill:"color",r:"borderRadius"},cropShoulder:0,directTouch:!0,trackerGroups:["group","dataLabelsGroup"],negStacks:!0,init:function(){R.prototype.init.apply(this,arguments);var a=this,b=a.chart;b.hasRendered&&n(b.series,function(b){if(b.type===a.type)b.isDirty=!0})},getColumnMetrics:function(){var a= -this,b=a.options,c=a.xAxis,d=a.yAxis,e=c.reversed,f,g={},h=0;b.grouping===!1?h=1:n(a.chart.series,function(b){var c=b.options,e=b.yAxis,i;if(b.type===a.type&&b.visible&&d.len===e.len&&d.pos===e.pos)c.stacking?(f=b.stackKey,g[f]===w&&(g[f]=h++),i=g[f]):c.grouping!==!1&&(i=h++),b.columnIndex=i});var i=I(S(c.transA)*(c.ordinalSlope||b.pointRange||c.closestPointRange||c.tickInterval||1),c.len),k=i*b.groupPadding,j=(i-2*k)/h,b=I(b.maxPointWidth||c.len,o(b.pointWidth,j*(1-2*b.pointPadding)));a.columnMetrics= -{width:b,offset:(j-b)/2+(k+((a.columnIndex||0)+(e?1:0))*j-i/2)*(e?-1:1)};return a.columnMetrics},crispCol:function(a,b,c,d){var e=this.chart,f=this.borderWidth,g=-(f%2?0.5:0),f=f%2?0.5:1;e.inverted&&e.renderer.isVML&&(f+=1);c=Math.round(a+c)+g;a=Math.round(a)+g;c-=a;d=Math.round(b+d)+f;g=S(b)<=0.5&&d>0.5;b=Math.round(b)+f;d-=b;g&&d&&(b-=1,d+=1);return{x:a,y:b,width:c,height:d}},translate:function(){var a=this,b=a.chart,c=a.options,d=a.borderWidth=o(c.borderWidth,a.closestPointRange*a.xAxis.transA< -2?0:1),e=a.yAxis,f=a.translatedThreshold=e.getThreshold(c.threshold),g=o(c.minPointLength,5),h=a.getColumnMetrics(),i=h.width,k=a.barW=u(i,1+2*d),j=a.pointXOffset=h.offset;b.inverted&&(f-=0.5);c.pointPadding&&(k=ta(k));R.prototype.translate.apply(a);n(a.points,function(c){var d=I(o(c.yBottom,f),9E4),h=999+S(d),h=I(u(-h,c.plotY),e.len+h),n=c.plotX+j,r=k,t=I(h,d),v,B=u(h,d)-t;S(B)g?d-g:f-(v?g:0));c.barX=n;c.pointWidth=i;c.tooltipPos= -b.inverted?[e.len+e.pos-b.plotLeft-h,a.xAxis.len-n-r/2,B]:[n+r/2,h+e.pos-b.plotTop,B];c.shapeType="rect";c.shapeArgs=a.crispCol(n,t,r,B)})},getSymbol:W,drawLegendSymbol:ab.drawRectangle,drawGraph:W,drawPoints:function(){var a=this,b=this.chart,c=a.options,d=b.renderer,e=c.animationLimit||250,f,g;n(a.points,function(h){var i=h.graphic,k;if(F(h.plotY)&&h.y!==null)f=h.shapeArgs,k=t(a.borderWidth)?{"stroke-width":a.borderWidth}:{},g=h.pointAttr[h.selected?"select":""]||a.pointAttr[""],i?(La(i),i.attr(k).attr(g)[b.pointCount< -e?"animate":"attr"](C(f))):h.graphic=d[h.shapeType](f).attr(k).attr(g).add(h.group||a.group).shadow(c.shadow,null,c.stacking&&!c.borderRadius);else if(i)h.graphic=i.destroy()})},animate:function(a){var b=this,c=this.yAxis,d=b.options,e=this.chart.inverted,f={};if(ea)a?(f.scaleY=0.001,a=I(c.pos+c.len,u(c.pos,c.toPixels(d.threshold))),e?f.translateX=a-c.len:f.translateY=a,b.group.attr(f)):(f[e?"translateX":"translateY"]=c.pos,b.group.animate(f,r(Ra(b.options.animation),{step:function(a,c){b.group.attr({scaleY:u(0.001, -c.pos)})}})),b.animate=null)},remove:function(){var a=this,b=a.chart;b.hasRendered&&n(b.series,function(b){if(b.type===a.type)b.isDirty=!0});R.prototype.remove.apply(a,arguments)}});y.column=U;Z.scatter=C(Ya,{lineWidth:0,marker:{enabled:!0},tooltip:{headerFormat:'\u25cf {series.name}
',pointFormat:"x: {point.x}
y: {point.y}
"}});Ya=ba(R,{type:"scatter",sorted:!1,requireSorting:!1,noSharedTooltip:!0, -trackerGroups:["group","markerGroup","dataLabelsGroup"],takeOrdinalPosition:!1,kdDimensions:2,drawGraph:function(){this.options.lineWidth&&R.prototype.drawGraph.call(this)}});y.scatter=Ya;R.prototype.drawDataLabels=function(){var a=this,b=a.options,c=b.cursor,d=b.dataLabels,e=a.points,f,g,h=a.hasRendered||0,i,k,j=o(d.defer,!0),l=a.chart.renderer;if(d.enabled||a._hasPointLabels)a.dlProcessOptions&&a.dlProcessOptions(d),k=a.plotGroup("dataLabelsGroup","data-labels",j&&!h?"hidden":"visible",d.zIndex|| -6),j&&(k.attr({opacity:+h}),h||N(a,"afterAnimate",function(){a.visible&&k.show();k[b.animation?"animate":"attr"]({opacity:1},{duration:200})})),g=d,n(e,function(e){var h,j=e.dataLabel,n,s,v=e.connector,B=!0,u,x={};f=e.dlOptions||e.options&&e.options.dataLabels;h=o(f&&f.enabled,g.enabled)&&e.y!==null;if(j&&!h)e.dataLabel=j.destroy();else if(h){d=C(g,f);u=d.style;h=d.rotation;n=e.getLabelConfig();i=d.format?Ea(d.format,n):d.formatter.call(n,d);u.color=o(d.color,u.color,a.color,"black");if(j)if(t(i))j.attr({text:i}), -B=!1;else{if(e.dataLabel=j=j.destroy(),v)e.connector=v.destroy()}else if(t(i)){j={fill:d.backgroundColor,stroke:d.borderColor,"stroke-width":d.borderWidth,r:d.borderRadius||0,rotation:h,padding:d.padding,zIndex:1};if(u.color==="contrast")x.color=d.inside||d.distance<0||b.stacking?l.getContrast(e.color||a.color):"#000000";if(c)x.cursor=c;for(s in j)j[s]===w&&delete j[s];j=e.dataLabel=l[h?"text":"label"](i,0,-9999,d.shape,null,null,d.useHTML).attr(j).css(r(u,x)).add(k).shadow(d.shadow)}j&&a.alignDataLabel(e, -j,d,null,B)}})};R.prototype.alignDataLabel=function(a,b,c,d,e){var f=this.chart,g=f.inverted,h=o(a.plotX,-9999),i=o(a.plotY,-9999),k=b.getBBox(),j=f.renderer.fontMetrics(c.style.fontSize).b,l=c.rotation,m=c.align,n=this.visible&&(a.series.forceDL||f.isInsidePlot(h,x(i),g)||d&&f.isInsidePlot(h,g?d.x+1:d.y+d.height-1,g)),q=o(c.overflow,"justify")==="justify";if(n)d=r({x:g?f.plotWidth-i:h,y:x(g?f.plotHeight-h:i),width:0,height:0},d),r(c,{width:k.width,height:k.height}),l?(q=!1,g=f.renderer.rotCorr(j, -l),g={x:d.x+c.x+d.width/2+g.x,y:d.y+c.y+{top:0,middle:0.5,bottom:1}[c.verticalAlign]*d.height},b[e?"attr":"animate"](g).attr({align:m}),h=(l+720)%360,h=h>180&&h<360,m==="left"?g.y-=h?k.height:0:m==="center"?(g.x-=k.width/2,g.y-=k.height/2):m==="right"&&(g.x-=k.width,g.y-=h?0:k.height)):(b.align(c,null,d),g=b.alignAttr),q?this.justifyDataLabel(b,c,g,k,d,e):o(c.crop,!0)&&(n=f.isInsidePlot(g.x,g.y)&&f.isInsidePlot(g.x+k.width,g.y+k.height)),c.shape&&!l&&b.attr({anchorX:a.plotX,anchorY:a.plotY});if(!n)La(b), -b.attr({y:-9999}),b.placed=!1};R.prototype.justifyDataLabel=function(a,b,c,d,e,f){var g=this.chart,h=b.align,i=b.verticalAlign,k,j,l=a.box?0:a.padding||0;k=c.x+l;if(k<0)h==="right"?b.align="left":b.x=-k,j=!0;k=c.x+d.width-l;if(k>g.plotWidth)h==="left"?b.align="right":b.x=g.plotWidth-k,j=!0;k=c.y+l;if(k<0)i==="bottom"?b.verticalAlign="top":b.y=-k,j=!0;k=c.y+d.height-l;if(k>g.plotHeight)i==="top"?b.verticalAlign="bottom":b.y=g.plotHeight-k,j=!0;if(j)a.placed=!f,a.align(b,null,e)};if(y.pie)y.pie.prototype.drawDataLabels= -function(){var a=this,b=a.data,c,d=a.chart,e=a.options.dataLabels,f=o(e.connectorPadding,10),g=o(e.connectorWidth,1),h=d.plotWidth,i=d.plotHeight,k,j,l=o(e.softConnector,!0),m=e.distance,p=a.center,q=p[2]/2,r=p[1],t=m>0,v,s,w,z=[[],[]],C,y,D,E,J,F=[0,0,0,0],M=function(a,b){return b.y-a.y};if(a.visible&&(e.enabled||a._hasPointLabels)){R.prototype.drawDataLabels.apply(a);n(b,function(a){if(a.dataLabel&&a.visible)z[a.half].push(a),a.dataLabel._pos=null});for(E=2;E--;){var G=[],N=[],H=z[E],L=H.length, -K;if(L){a.sortByAngle(H,E-0.5);for(J=b=0;!b&&H[J];)b=H[J]&&H[J].dataLabel&&(H[J].dataLabel.getBBox().height||21),J++;if(m>0){s=I(r+q+m,d.plotHeight);for(J=u(0,r-q-m);J<=s;J+=b)G.push(J);s=G.length;if(L>s){c=[].concat(H);c.sort(M);for(J=L;J--;)c[J].rank=J;for(J=L;J--;)H[J].rank>=s&&H.splice(J,1);L=H.length}for(J=0;J0){if(s=N.pop(),K=s.i,y=s.y,c>y&&G[K+1]!==null||ch-f&&(F[1]=u(x(C+s-h+f),F[1])),y- -b/2<0?F[0]=u(x(-y+b/2),F[0]):y+b/2>i&&(F[2]=u(x(y+b/2-i),F[2]))}}}if(Ia(F)===0||this.verifyDataLabelOverflow(F))this.placeDataLabels(),t&&g&&n(this.points,function(b){k=b.connector;w=b.labelPos;if((v=b.dataLabel)&&v._pos&&b.visible)D=v._attr.visibility,C=v.connX,y=v.connY,j=l?["M",C+(w[6]==="left"?5:-5),y,"C",C,y,2*w[2]-w[4],2*w[3]-w[5],w[2],w[3],"L",w[4],w[5]]:["M",C+(w[6]==="left"?5:-5),y,"L",w[2],w[3],"L",w[4],w[5]],k?(k.animate({d:j}),k.attr("visibility",D)):b.connector=k=a.chart.renderer.path(j).attr({"stroke-width":g, -stroke:e.connectorColor||b.color||"#606060",visibility:D}).add(a.dataLabelsGroup);else if(k)b.connector=k.destroy()})}},y.pie.prototype.placeDataLabels=function(){n(this.points,function(a){var b=a.dataLabel;if(b&&a.visible)(a=b._pos)?(b.attr(b._attr),b[b.moved?"animate":"attr"](a),b.moved=!0):b&&b.attr({y:-9999})})},y.pie.prototype.alignDataLabel=W,y.pie.prototype.verifyDataLabelOverflow=function(a){var b=this.center,c=this.options,d=c.center,e=c.minSize||80,f=e,g;d[0]!==null?f=u(b[2]-u(a[1],a[3]), -e):(f=u(b[2]-a[1]-a[3],e),b[0]+=(a[3]-a[1])/2);d[1]!==null?f=u(I(f,b[2]-u(a[0],a[2])),e):(f=u(I(f,b[2]-a[0]-a[2]),e),b[1]+=(a[0]-a[2])/2);fo(this.translatedThreshold, -g.yAxis.len)),k=o(c.inside,!!this.options.stacking);if(h){d=C(h);if(d.y<0)d.height+=d.y,d.y=0;h=d.y+d.height-g.yAxis.len;h>0&&(d.height-=h);f&&(d={x:g.yAxis.len-d.y-d.height,y:g.xAxis.len-d.x-d.width,width:d.height,height:d.width});if(!k)f?(d.x+=i?0:d.width,d.width=0):(d.y+=i?d.height:0,d.height=0)}c.align=o(c.align,!f||k?"center":i?"right":"left");c.verticalAlign=o(c.verticalAlign,f||k?"middle":i?"top":"bottom");R.prototype.alignDataLabel.call(this,a,b,c,d,e)};(function(a){var b=a.Chart,c=a.each, -d=a.pick,e=a.addEvent;b.prototype.callbacks.push(function(a){function b(){var e=[];c(a.series,function(a){var b=a.options.dataLabels,f=a.dataLabelCollections||["dataLabel"];(b.enabled||a._hasPointLabels)&&!b.allowOverlap&&a.visible&&c(f,function(b){c(a.points,function(a){if(a[b])a[b].labelrank=d(a.labelrank,a.shapeArgs&&a.shapeArgs.height),e.push(a[b])})})});a.hideOverlappingLabels(e)}b();e(a,"redraw",b)});b.prototype.hideOverlappingLabels=function(a){var b=a.length,d,e,k,j,l,m,n,o,r;for(e=0;el.x+n.translateX+(k.width-r)||m.x+o.translateX+(j.width-r)l.y+n.translateY+(k.height-r)||m.y+o.translateY+(j.height-r)a.minPixelPadding||a.min===a.dataMin&&a.max===a.dataMax)c=0;a.minPixelPadding-=c}});ga(X.prototype,"render",function(a){a.call(this);this.fixTo=null});var bb=s.ColorAxis=function(){this.isColorAxis=!0;this.init.apply(this,arguments)};r(bb.prototype,X.prototype);r(bb.prototype,{defaultColorAxisOptions:{lineWidth:0,minPadding:0,maxPadding:0,gridLineWidth:1,tickPixelInterval:72,startOnTick:!0,endOnTick:!0, -offset:0,marker:{animation:{duration:50},color:"gray",width:0.01},labels:{overflow:"justify"},minColor:"#EFEFFF",maxColor:"#003875",tickLength:5},init:function(a,b){var c=a.options.legend.layout!=="vertical",d;d=C(this.defaultColorAxisOptions,{side:c?2:1,reversed:!c},b,{opposite:!c,showEmpty:!1,title:null,isColor:!0});X.prototype.init.call(this,a,d);b.dataClasses&&this.initDataClasses(b);this.initStops(b);this.horiz=c;this.zoomEnabled=!1},tweenColors:function(a,b,c){var d;!b.rgba.length||!a.rgba.length? -a=b.input||"none":(a=a.rgba,b=b.rgba,d=b[3]!==1||a[3]!==1,a=(d?"rgba(":"rgb(")+Math.round(b[0]+(a[0]-b[0])*(1-c))+","+Math.round(b[1]+(a[1]-b[1])*(1-c))+","+Math.round(b[2]+(a[2]-b[2])*(1-c))+(d?","+(b[3]+(a[3]-b[3])*(1-c)):"")+")");return a},initDataClasses:function(a){var b=this,c=this.chart,d,e=0,f=this.options,g=a.dataClasses.length;this.dataClasses=d=[];this.legendItems=[];n(a.dataClasses,function(a,i){var k,a=C(a);d.push(a);if(!a.color)f.dataClassColor==="category"?(k=c.options.colors,a.color= -k[e++],e===k.length&&(e=0)):a.color=b.tweenColors(T(f.minColor),T(f.maxColor),g<2?0.5:i/(g-1))})},initStops:function(a){this.stops=a.stops||[[0,this.options.minColor],[1,this.options.maxColor]];n(this.stops,function(a){a.color=T(a[1])})},setOptions:function(a){X.prototype.setOptions.call(this,a);this.options.crosshair=this.options.marker;this.coll="colorAxis"},setAxisSize:function(){var a=this.legendSymbol,b=this.chart,c,d,e;if(a)this.left=c=a.attr("x"),this.top=d=a.attr("y"),this.width=e=a.attr("width"), -this.height=a=a.attr("height"),this.right=b.chartWidth-c-e,this.bottom=b.chartHeight-d-a,this.len=this.horiz?e:a,this.pos=this.horiz?c:d},toColor:function(a,b){var c,d=this.stops,e,f=this.dataClasses,g,h;if(f)for(h=f.length;h--;){if(g=f[h],e=g.from,d=g.to,(e===w||a>=e)&&(d===w||a<=d)){c=g.color;if(b)b.dataClass=h;break}}else{this.isLog&&(a=this.val2lin(a));c=1-(this.max-a)/(this.max-this.min||1);for(h=d.length;h--;)if(c>d[h][0])break;e=d[h]||d[h+1];d=d[h+1]||e;c=1-(d[0]-c)/(d[0]-e[0]||1);c=this.tweenColors(e.color, -d.color,c)}return c},getOffset:function(){var a=this.legendGroup,b=this.chart.axisOffset[this.side];if(a){this.axisParent=a;X.prototype.getOffset.call(this);if(!this.added)this.added=!0,this.labelLeft=0,this.labelRight=this.width;this.chart.axisOffset[this.side]=b}},setLegendColor:function(){var a,b=this.options,c=this.reversed;a=c?1:0;c=c?0:1;a=this.horiz?[a,0,c,0]:[0,c,0,a];this.legendColor={linearGradient:{x1:a[0],y1:a[1],x2:a[2],y2:a[3]},stops:b.stops||[[0,b.minColor],[1,b.maxColor]]}},drawLegendSymbol:function(a, -b){var c=a.padding,d=a.options,e=this.horiz,f=o(d.symbolWidth,e?200:12),g=o(d.symbolHeight,e?12:200),h=o(d.labelPadding,e?16:30),d=o(d.itemDistance,10);this.setLegendColor();b.legendSymbol=this.chart.renderer.rect(0,a.baseline-11,f,g).attr({zIndex:1}).add(b.legendGroup);this.legendItemWidth=f+c+(e?d:h);this.legendItemHeight=g+c+(e?h:0)},setState:W,visible:!0,setVisible:W,getSeriesExtremes:function(){var a;if(this.series.length)a=this.series[0],this.dataMin=a.valueMin,this.dataMax=a.valueMax},drawCrosshair:function(a, -b){var c=b&&b.plotX,d=b&&b.plotY,e,f=this.pos,g=this.len;if(b)e=this.toPixels(b[b.series.colorKey]),ef+g&&(e=f+g+2),b.plotX=e,b.plotY=this.len-e,X.prototype.drawCrosshair.call(this,a,b),b.plotX=c,b.plotY=d,this.cross&&this.cross.attr({fill:this.crosshair.color}).add(this.legendGroup)},getPlotLinePath:function(a,b,c,d,e){return F(e)?this.horiz?["M",e-4,this.top-6,"L",e+4,this.top-6,e,this.top,"Z"]:["M",this.left,e,"L",this.left-6,e+6,this.left-6,e-6,"Z"]:X.prototype.getPlotLinePath.call(this, -a,b,c,d)},update:function(a,b){var c=this.chart,d=c.legend;n(this.series,function(a){a.isDirtyData=!0});if(a.dataClasses&&d.allItems)n(d.allItems,function(a){a.isDataClass&&a.legendGroup.destroy()}),c.isDirtyLegend=!0;c.options[this.coll]=C(this.userOptions,a);X.prototype.update.call(this,a,b);this.legendItem&&(this.setLegendColor(),d.colorizeItem(this,!0))},getDataClassLegendSymbols:function(){var a=this,b=this.chart,c=this.legendItems,d=b.options.legend,e=d.valueDecimals,f=d.valueSuffix||"",g;c.length|| -n(this.dataClasses,function(d,i){var k=!0,j=d.from,l=d.to;g="";j===w?g="< ":l===w&&(g="> ");j!==w&&(g+=s.numberFormat(j,e)+f);j!==w&&l!==w&&(g+=" - ");l!==w&&(g+=s.numberFormat(l,e)+f);c.push(r({chart:b,name:g,options:{},drawLegendSymbol:ab.drawRectangle,visible:!0,setState:W,isDataClass:!0,setVisible:function(){k=this.visible=!k;n(a.series,function(a){n(a.points,function(a){a.dataClass===i&&a.setVisible(k)})});b.legend.colorizeItem(this,k)}},d))});return c},name:""});n(["fill","stroke"],function(a){s.Fx.prototype[a+ -"Setter"]=function(){this.elem.attr(a,bb.prototype.tweenColors(T(this.start),T(this.end),this.pos))}});ga(ja.prototype,"getAxes",function(a){var b=this.options.colorAxis;a.call(this);this.colorAxis=[];b&&new bb(this,b)});ga($a.prototype,"getAllItems",function(a){var b=[],c=this.chart.colorAxis[0];c&&(c.options.dataClasses?b=b.concat(c.getDataClassLegendSymbols()):b.push(c),n(c.series,function(a){a.options.showInLegend=!1}));return b.concat(a.call(this))});var Ma={setVisible:function(a){var b=this, -c=a?"show":"hide";n(["graphic","dataLabel"],function(a){if(b[a])b[a][c]()})}},Kb={pointAttrToOptions:{stroke:"borderColor","stroke-width":"borderWidth",fill:"color",dashstyle:"dashStyle"},pointArrayMap:["value"],axisTypes:["xAxis","yAxis","colorAxis"],optionalAxis:"colorAxis",trackerGroups:["group","markerGroup","dataLabelsGroup"],getSymbol:W,parallelArrays:["x","y","value"],colorKey:"value",translateColors:function(){var a=this,b=this.options.nullColor,c=this.colorAxis,d=this.colorKey;n(this.data, -function(e){var f=e[d];if(f=e.options.color||(f===null?b:c&&f!==void 0?c.toColor(f,e):e.color||a.color))e.color=f})}},jb=z.documentElement.style.vectorEffect!==void 0;Z.map=C(Z.scatter,{allAreas:!0,animation:!1,nullColor:"#F8F8F8",borderColor:"silver",borderWidth:1,marker:null,stickyTracking:!1,dataLabels:{formatter:function(){return this.point.value},inside:!0,verticalAlign:"middle",crop:!1,overflow:!1,padding:0},turboThreshold:0,tooltip:{followPointer:!0,pointFormat:"{point.name}: {point.value}
"}, -states:{normal:{animation:!0},hover:{brightness:0.2,halo:null}}});var Lb=ba($,r({applyOptions:function(a,b){var c=$.prototype.applyOptions.call(this,a,b),d=this.series,e=d.joinBy;if(d.mapData)if(e=c[e[1]]!==void 0&&d.mapMap[c[e[1]]]){if(d.xyFromShape)c.x=e._midX,c.y=e._midY;r(c,e)}else c.value=c.value||null;return c},onMouseOver:function(a){clearTimeout(this.colorInterval);if(this.value!==null)$.prototype.onMouseOver.call(this,a);else this.series.onMouseOut(a)},onMouseOut:function(){var a=this,b= -+new sa,c=T(a.color),d=T(a.pointAttr.hover.fill),e=Ra(a.series.options.states.normal.animation).duration,f;if(e&&c.rgba.length===4&&d.rgba.length===4&&a.state!=="select")f=a.pointAttr[""].fill,delete a.pointAttr[""].fill,clearTimeout(a.colorInterval),a.colorInterval=setInterval(function(){var f=(new sa-b)/e,h=a.graphic;f>1&&(f=1);h&&h.attr("fill",bb.prototype.tweenColors.call(0,d,c,f));f>=1&&clearTimeout(a.colorInterval)},13);$.prototype.onMouseOut.call(a);if(f)a.pointAttr[""].fill=f},zoomTo:function(){var a= -this.series;a.xAxis.setExtremes(this._minX,this._maxX,!1);a.yAxis.setExtremes(this._minY,this._maxY,!1);a.chart.redraw()}},Ma));y.map=ba(y.scatter,C(Kb,{type:"map",pointClass:Lb,supportsDrilldown:!0,getExtremesFromAll:!0,useMapGeometry:!0,forceDL:!0,searchPoint:W,directTouch:!0,preserveAspectRatio:!0,getBox:function(a){var b=Number.MAX_VALUE,c=-b,d=b,e=-b,f=b,g=b,h=this.xAxis,i=this.yAxis,k;n(a||[],function(a){if(a.path){if(typeof a.path==="string")a.path=s.splitPath(a.path);var h=a.path||[],i=h.length, -n=!1,q=-b,r=b,t=-b,v=b,u=a.properties;if(!a._foundBox){for(;i--;)F(h[i])&&(n?(q=Math.max(q,h[i]),r=Math.min(r,h[i])):(t=Math.max(t,h[i]),v=Math.min(v,h[i])),n=!n);a._midX=r+(q-r)*(a.middleX||u&&u["hc-middle-x"]||0.5);a._midY=v+(t-v)*(a.middleY||u&&u["hc-middle-y"]||0.5);a._maxX=q;a._minX=r;a._maxY=t;a._minY=v;a.labelrank=o(a.labelrank,(q-r)*(t-v));a._foundBox=!0}c=Math.max(c,a._maxX);d=Math.min(d,a._minX);e=Math.max(e,a._maxY);f=Math.min(f,a._minY);g=Math.min(a._maxX-a._minX,a._maxY-a._minY,g);k= -!0}});if(k){this.minY=Math.min(f,o(this.minY,b));this.maxY=Math.max(e,o(this.maxY,-b));this.minX=Math.min(d,o(this.minX,b));this.maxX=Math.max(c,o(this.maxX,-b));if(h&&h.options.minRange===void 0)h.minRange=Math.min(5*g,(this.maxX-this.minX)/5,h.minRange||b);if(i&&i.options.minRange===void 0)i.minRange=Math.min(5*g,(this.maxY-this.minY)/5,i.minRange||b)}},getExtremes:function(){R.prototype.getExtremes.call(this,this.valueData);this.chart.hasRendered&&this.isDirtyData&&this.getBox(this.options.data); -this.valueMin=this.dataMin;this.valueMax=this.dataMax;this.dataMin=this.minY;this.dataMax=this.maxY},translatePath:function(a){var b=!1,c=this.xAxis,d=this.yAxis,e=c.min,f=c.transA,c=c.minPixelPadding,g=d.min,h=d.transA,d=d.minPixelPadding,i,k=[];if(a)for(i=a.length;i--;)F(a[i])?(k[i]=b?(a[i]-e)*f+c:(a[i]-g)*h+d,b=!b):k[i]=a[i];return k},setData:function(a,b,c,d){var e=this.options,f=e.mapData,g=e.joinBy,h=g===null,i=[],k={},j,l,m;h&&(g="_i");g=this.joinBy=s.splat(g);g[1]||(g[1]=g[0]);a&&n(a,function(b, -c){F(b)&&(a[c]={value:b});if(h)a[c]._i=c});this.getBox(a);if(f){if(f.type==="FeatureCollection"){if(f["hc-transform"])for(j in this.chart.mapTransforms=l=f["hc-transform"],l)if(l.hasOwnProperty(j)&&j.rotation)j.cosAngle=Math.cos(j.rotation),j.sinAngle=Math.sin(j.rotation);f=s.geojson(f,this.type,this)}this.mapData=f;for(m=0;m0.99&&g<1.01&&d>0.99&&d<1.01&&(d=g=1,b=Math.round(b),c=Math.round(c)),this.transformGroup.animate({translateX:b,translateY:c,scaleX:g,scaleY:d}));jb||a.group.element.setAttribute("stroke-width", -a.options.borderWidth/(g||1));this.drawMapDataLabels()},drawMapDataLabels:function(){R.prototype.drawDataLabels.call(this);this.dataLabelsGroup&&this.dataLabelsGroup.clip(this.chart.clipRect)},render:function(){var a=this,b=R.prototype.render;a.chart.renderer.isVML&&a.data.length>3E3?setTimeout(function(){b.call(a)}):b.call(a)},animate:function(a){var b=this.options.animation,c=this.group,d=this.xAxis,e=this.yAxis,f=d.pos,g=e.pos;if(this.chart.renderer.isSVG)b===!0&&(b={duration:1E3}),a?c.attr({translateX:f+ -d.len/2,translateY:g+e.len/2,scaleX:0.001,scaleY:0.001}):(c.animate({translateX:f,translateY:g,scaleX:1,scaleY:1},b),this.animate=null)},animateDrilldown:function(a){var b=this.chart.plotBox,c=this.chart.drilldownLevels[this.chart.drilldownLevels.length-1],d=c.bBox,e=this.chart.options.drilldown.animation;if(!a)a=Math.min(d.width/b.width,d.height/b.height),c.shapeArgs={scaleX:a,scaleY:a,translateX:d.x,translateY:d.y},n(this.points,function(a){a.graphic&&a.graphic.attr(c.shapeArgs).animate({scaleX:1, -scaleY:1,translateX:0,translateY:0},e)}),this.animate=null},drawLegendSymbol:ab.drawRectangle,animateDrillupFrom:function(a){y.column.prototype.animateDrillupFrom.call(this,a)},animateDrillupTo:function(a){y.column.prototype.animateDrillupTo.call(this,a)}}));(function(a){var b=a.Chart,c=a.each,d=a.pick,e=a.addEvent;b.prototype.callbacks.push(function(a){function b(){var e=[];c(a.series,function(a){var b=a.options.dataLabels,f=a.dataLabelCollections||["dataLabel"];(b.enabled||a._hasPointLabels)&&!b.allowOverlap&& -a.visible&&c(f,function(b){c(a.points,function(a){if(a[b])a[b].labelrank=d(a.labelrank,a.shapeArgs&&a.shapeArgs.height),e.push(a[b])})})});a.hideOverlappingLabels(e)}b();e(a,"redraw",b)});b.prototype.hideOverlappingLabels=function(a){var b=a.length,d,e,k,j,l,m,n,o,r;for(e=0;el.x+n.translateX+(k.width-r)||m.x+o.translateX+(j.width-r)l.y+n.translateY+(k.height-r)||m.y+o.translateY+(j.height-r)b[d]+b[c]&&(a[c]>b[c]?(a[c]=b[c],a[d]=b[d]):a[d]=b[d]+b[c]-a[c]);a[c]>b[c]&&(a[c]=b[c]);a[d]d.scaleY,this.pinchTranslateDirection(!a,b,c,d,e,f,g,a?d.scaleX:d.scaleY))});Z.mapline= -C(Z.map,{lineWidth:1,fillColor:"none"});y.mapline=ba(y.map,{type:"mapline",pointAttrToOptions:{stroke:"color","stroke-width":"lineWidth",fill:"fillColor",dashstyle:"dashStyle"},drawLegendSymbol:y.line.prototype.drawLegendSymbol});Z.mappoint=C(Z.scatter,{dataLabels:{enabled:!0,formatter:function(){return this.point.name},crop:!1,defer:!1,overflow:!1,style:{color:"#000000"}}});y.mappoint=ba(y.scatter,{type:"mappoint",forceDL:!0,pointClass:ba($,{applyOptions:function(a,b){var c=$.prototype.applyOptions.call(this, -a,b);a.lat!==void 0&&a.lon!==void 0&&(c=r(c,this.series.chart.fromLatLonToPoint(c)));return c}})});Z.bubble=C(Z.scatter,{dataLabels:{formatter:function(){return this.point.z},inside:!0,verticalAlign:"middle"},marker:{lineColor:null,lineWidth:1},minSize:8,maxSize:"20%",softThreshold:!1,states:{hover:{halo:{size:5}}},tooltip:{pointFormat:"({point.x}, {point.y}), Size: {point.z}"},turboThreshold:0,zThreshold:0,zoneAxis:"z"});var Qb=ba($,{haloPath:function(){return $.prototype.haloPath.call(this,this.shapeArgs.r+ -this.series.options.states.hover.halo.size)},ttBelow:!1});y.bubble=ba(y.scatter,{type:"bubble",pointClass:Qb,pointArrayMap:["y","z"],parallelArrays:["x","y","z"],trackerGroups:["group","dataLabelsGroup"],bubblePadding:!0,zoneAxis:"z",pointAttrToOptions:{stroke:"lineColor","stroke-width":"lineWidth",fill:"fillColor"},applyOpacity:function(a){var b=this.options.marker,c=o(b.fillOpacity,0.5),a=a||b.fillColor||this.color;c!==1&&(a=T(a).setOpacity(c).get("rgba"));return a},convertAttribs:function(){var a= -R.prototype.convertAttribs.apply(this,arguments);a.fill=this.applyOpacity(a.fill);return a},getRadii:function(a,b,c,d){var e,f,g,h=this.zData,i=[],k=this.options,j=k.sizeBy!=="width",l=k.zThreshold,m=b-a;for(f=0,e=h.length;f0?(g-a)/m:0.5,j&&g>=0&&(g=Math.sqrt(g)),g=H.ceil(c+g*(d-c))/2),i.push(g);this.radii=i},animate:function(a){var b=this.options.animation;if(!a)n(this.points, -function(a){var d=a.graphic,a=a.shapeArgs;d&&a&&(d.attr("r",1),d.animate({r:a.r},b))}),this.animate=null},translate:function(){var a,b=this.data,c,d,e=this.radii;y.scatter.prototype.translate.call(this);for(a=b.length;a--;)c=b[a],d=e?e[a]:0,F(d)&&d>=this.minPxSize/2?(c.shapeType="circle",c.shapeArgs={x:c.plotX,y:c.plotY,r:d},c.dlBox={x:c.plotX-d,y:c.plotY-d,width:2*d,height:2*d}):c.shapeArgs=c.plotY=c.dlBox=w},drawLegendSymbol:function(a,b){var c=this.chart.renderer,d=c.fontMetrics(a.itemStyle.fontSize).f/ -2;b.legendSymbol=c.circle(d,a.baseline-d,d).attr({zIndex:3}).add(b.legendGroup);b.legendSymbol.isMarker=!0},drawPoints:y.column.prototype.drawPoints,alignDataLabel:y.column.prototype.alignDataLabel,buildKDTree:W,applyZones:W});X.prototype.beforePadding=function(){var a=this,b=this.len,c=this.chart,d=0,e=b,f=this.isXAxis,g=f?"xData":"yData",h=this.min,i={},k=H.min(c.plotWidth,c.plotHeight),j=Number.MAX_VALUE,l=-Number.MAX_VALUE,m=this.max-h,p=b/m,q=[];n(this.series,function(b){var d=b.options;if(b.bubblePadding&& -(b.visible||!c.options.chart.ignoreHiddenSeries))if(a.allowZoomOutside=!0,q.push(b),f)n(["minSize","maxSize"],function(a){var b=d[a],c=/%$/.test(b),b=E(b);i[a]=c?k*b/100:b}),b.minPxSize=i.minSize,b.maxPxSize=i.maxSize,b=b.zData,b.length&&(j=o(d.zMin,H.min(j,H.max(Pa(b),d.displayNegative===!1?d.zThreshold:-Number.MAX_VALUE))),l=o(d.zMax,H.max(l,Ia(b))))});n(q,function(b){var c=b[g],i=c.length,k;f&&b.getRadii(j,l,b.minPxSize,b.maxPxSize);if(m>0)for(;i--;)F(c[i])&&a.dataMin<=c[i]&&c[i]<=a.dataMax&&(k= -b.radii[i],d=Math.min((c[i]-h)*p-k,d),e=Math.max((c[i]-h)*p+k,e))});q.length&&m>0&&!this.isLog&&(e-=b,p*=(b+d-e)/b,n([["min","userMin",d],["max","userMax",e]],function(b){o(a.options[b[0]],a[b[1]])===w&&(a[b[0]]+=b[2]/p)}))};if(y.bubble)Z.mapbubble=C(Z.bubble,{animationLimit:500,tooltip:{pointFormat:"{point.name}: {point.z}"}}),y.mapbubble=ba(y.bubble,{pointClass:ba($,{applyOptions:function(a,b){var c;a&&a.lat!==void 0&&a.lon!==void 0?(c=$.prototype.applyOptions.call(this,a,b),c=r(c,this.series.chart.fromLatLonToPoint(c))): -c=Lb.prototype.applyOptions.call(this,a,b);return c},ttBelow:!1}),xyFromShape:!0,type:"mapbubble",pointArrayMap:["z"],getMapData:y.map.prototype.getMapData,getBox:y.map.prototype.getBox,setData:y.map.prototype.setData});ja.prototype.transformFromLatLon=function(a,b){if(D.proj4===void 0)return V(21),{x:0,y:null};var c=D.proj4(b.crs,[a.lon,a.lat]),d=b.cosAngle||b.rotation&&Math.cos(b.rotation),e=b.sinAngle||b.rotation&&Math.sin(b.rotation),c=b.rotation?[c[0]*d+c[1]*e,-c[0]*e+c[1]*d]:c;return{x:((c[0]- -(b.xoffset||0))*(b.scale||1)+(b.xpan||0))*(b.jsonres||1)+(b.jsonmarginX||0),y:(((b.yoffset||0)-c[1])*(b.scale||1)+(b.ypan||0))*(b.jsonres||1)-(b.jsonmarginY||0)}};ja.prototype.transformToLatLon=function(a,b){if(D.proj4===void 0)V(21);else{var c={x:((a.x-(b.jsonmarginX||0))/(b.jsonres||1)-(b.xpan||0))/(b.scale||1)+(b.xoffset||0),y:((-a.y-(b.jsonmarginY||0))/(b.jsonres||1)+(b.ypan||0))/(b.scale||1)+(b.yoffset||0)},d=b.cosAngle||b.rotation&&Math.cos(b.rotation),e=b.sinAngle||b.rotation&&Math.sin(b.rotation), -c=D.proj4(b.crs,"WGS84",b.rotation?{x:c.x*d+c.y*-e,y:c.x*e+c.y*d}:c);return{lat:c.y,lon:c.x}}};ja.prototype.fromPointToLatLon=function(a){var b=this.mapTransforms,c;if(b){for(c in b)if(b.hasOwnProperty(c)&&b[c].hitZone&&Db({x:a.x,y:-a.y},b[c].hitZone.coordinates[0]))return this.transformToLatLon(a,b[c]);return this.transformToLatLon(a,b["default"])}else V(22)};ja.prototype.fromLatLonToPoint=function(a){var b=this.mapTransforms,c,d;if(!b)return V(22),{x:0,y:null};for(c in b)if(b.hasOwnProperty(c)&& -b[c].hitZone&&(d=this.transformFromLatLon(a,b[c]),Db({x:d.x,y:-d.y},b[c].hitZone.coordinates[0])))return d;return this.transformFromLatLon(a,b["default"])};s.geojson=function(a,b,c){var d=[],e=[],f=function(a){var b,c=a.length;e.push("M");for(b=0;b{geojson.copyrightShort}
'),mapTextFull:o(h.mapTextFull,"{geojson.copyright}")},xAxis:f,yAxis:C(f,{reversed:!0})},e,{chart:{inverted:!1,alignTicks:!1}}); -e.series=g;return d?new ja(a,e,c):new ja(e,b)};L.plotOptions.heatmap=C(L.plotOptions.scatter,{animation:!1,borderWidth:0,nullColor:"#F8F8F8",dataLabels:{formatter:function(){return this.point.value},inside:!0,verticalAlign:"middle",crop:!1,overflow:!1,padding:0},marker:null,pointRange:null,tooltip:{pointFormat:"{point.x}, {point.y}: {point.value}
"},states:{normal:{animation:!0},hover:{halo:!1,brightness:0.2}}});y.heatmap=ba(y.scatter,C(Kb,{type:"heatmap",pointArrayMap:["y","value"],hasPointSpecificOptions:!0, -pointClass:ba($,Ma),supportsDrilldown:!0,getExtremesFromAll:!0,directTouch:!0,init:function(){var a;y.scatter.prototype.init.apply(this,arguments);a=this.options;a.pointRange=o(a.pointRange,a.colsize||1);this.yAxis.axisPointRange=a.rowsize||1},translate:function(){var a=this.options,b=this.xAxis,c=this.yAxis,d=function(a,b,c){return Math.min(Math.max(b,a),c)};this.generatePoints();n(this.points,function(e){var f=(a.colsize||1)/2,g=(a.rowsize||1)/2,h=d(Math.round(b.len-b.translate(e.x-f,0,1,0,1)), --b.len,2*b.len),f=d(Math.round(b.len-b.translate(e.x+f,0,1,0,1)),-b.len,2*b.len),i=d(Math.round(c.translate(e.y-g,0,1,0,1)),-c.len,2*c.len),g=d(Math.round(c.translate(e.y+g,0,1,0,1)),-c.len,2*c.len);e.plotX=e.clientX=(h+f)/2;e.plotY=(i+g)/2;e.shapeType="rect";e.shapeArgs={x:Math.min(h,f),y:Math.min(i,g),width:Math.abs(f-h),height:Math.abs(g-i)}});this.translateColors();this.chart.hasRendered&&n(this.points,function(a){a.shapeArgs.fill=a.options.color||a.color})},drawPoints:y.column.prototype.drawPoints, -animate:W,getBox:W,drawLegendSymbol:ab.drawRectangle,alignDataLabel:y.column.prototype.alignDataLabel,getExtremes:function(){R.prototype.getExtremes.call(this,this.valueData);this.valueMin=this.dataMin;this.valueMax=this.dataMax;R.prototype.getExtremes.call(this)}}));Ma=s.TrackerMixin={drawTrackerPoint:function(){var a=this,b=a.chart,c=b.pointer,d=a.options.cursor,e=d&&{cursor:d},f=function(a){for(var c=a.target,d;c&&!d;)d=c.point,c=c.parentNode;if(d!==w&&d!==b.hoverPoint)d.onMouseOver(a)};n(a.points, -function(a){if(a.graphic)a.graphic.element.point=a;if(a.dataLabel)a.dataLabel.element.point=a});if(!a._hasTracking)n(a.trackerGroups,function(b){if(a[b]&&(a[b].addClass("highcharts-tracker").on("mouseover",f).on("mouseout",function(a){c.onTrackerMouseOut(a)}).css(e),Wa))a[b].on("touchstart",f)}),a._hasTracking=!0},drawTrackerGraph:function(){var a=this,b=a.options,c=b.trackByArea,d=[].concat(c?a.areaPath:a.graphPath),e=d.length,f=a.chart,g=f.pointer,h=f.renderer,i=f.options.tooltip.snap,k=a.tracker, -j=b.cursor,l=j&&{cursor:j},m=function(){if(f.hoverSeries!==a)a.onMouseOver()},o="rgba(192,192,192,"+(ea?1.0E-4:0.002)+")";if(e&&!c)for(j=e+1;j--;)d[j]==="M"&&d.splice(j+1,0,d[j+1]-i,d[j+2],"L"),(j&&d[j]==="M"||j===e)&&d.splice(j,0,"L",d[j-2]+i,d[j-1]);k?k.attr({d:d}):(a.tracker=h.path(d).attr({"stroke-linejoin":"round",visibility:a.visible?"visible":"hidden",stroke:o,fill:c?o:"none","stroke-width":b.lineWidth+(c?0:2*i),zIndex:2}).add(a.group),n([a.tracker,a.markerGroup],function(a){a.addClass("highcharts-tracker").on("mouseover", -m).on("mouseout",function(a){g.onTrackerMouseOut(a)}).css(l);if(Wa)a.on("touchstart",m)}))}};if(y.column)U.prototype.drawTracker=Ma.drawTrackerPoint;if(y.pie)y.pie.prototype.drawTracker=Ma.drawTrackerPoint;if(y.scatter)Ya.prototype.drawTracker=Ma.drawTrackerPoint;r($a.prototype,{setItemEvents:function(a,b,c,d,e){var f=this;(c?b:a.legendGroup).on("mouseover",function(){a.setState("hover");b.css(f.options.itemHoverStyle)}).on("mouseout",function(){b.css(a.visible?d:e);a.setState()}).on("click",function(b){var c= -function(){a.setVisible&&a.setVisible()},b={browserEvent:b};a.firePointEvent?a.firePointEvent("legendItemClick",b,c):G(a,"legendItemClick",b,c)})},createCheckboxForItem:function(a){a.checkbox=da("input",{type:"checkbox",checked:a.selected,defaultChecked:a.selected},this.options.itemCheckboxStyle,this.chart.container);N(a.checkbox,"click",function(b){G(a.series||a,"checkboxClick",{checked:b.target.checked,item:a},function(){a.select()})})}});L.legend.itemStyle.cursor="pointer";r(ja.prototype,{showResetZoom:function(){var a= -this,b=L.lang,c=a.options.chart.resetZoomButton,d=c.theme,e=d.states,f=c.relativeTo==="chart"?null:"plotBox";this.resetZoomButton=a.renderer.button(b.resetZoom,null,null,function(){a.zoomOut()},d,e&&e.hover).attr({align:c.position.align,title:b.resetZoomTitle}).add().align(c.position,!1,f)},zoomOut:function(){var a=this;G(a,"selection",{resetSelection:!0},function(){a.zoom()})},zoom:function(a){var b,c=this.pointer,d=!1,e;!a||a.resetSelection?n(this.axes,function(a){b=a.zoom()}):n(a.xAxis.concat(a.yAxis), -function(a){var e=a.axis,h=e.isXAxis;if(c[h?"zoomX":"zoomY"]||c[h?"pinchX":"pinchY"])b=e.zoom(a.min,a.max),e.displayBtn&&(d=!0)});e=this.resetZoomButton;if(d&&!e)this.showResetZoom();else if(!d&&aa(e))this.resetZoomButton=e.destroy();b&&this.redraw(o(this.options.chart.animation,a&&a.animation,this.pointCount<100))},pan:function(a,b){var c=this,d=c.hoverPoints,e;d&&n(d,function(a){a.setState()});n(b==="xy"?[1,0]:[1],function(b){var b=c[b?"xAxis":"yAxis"][0],d=b.horiz,h=a[d?"chartX":"chartY"],d=d? -"mouseDownX":"mouseDownY",i=c[d],k=(b.pointRange||0)/2,j=b.getExtremes(),l=b.toValue(i-h,!0)+k,k=b.toValue(i+b.len-h,!0)-k,i=i>h;if(b.series.length&&(i||l>I(j.dataMin,j.min))&&(!i||k