diff options
35 files changed, 2263 insertions, 1951 deletions
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ee3fd4aa..9a044571 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,8 +27,8 @@ slf4j = "1.7.36" # See https://github.com/JetBrains/kotlin-wrappers kotlin-wrappers = "1.0.0-pre.488" -kotlin-blueprintjs-core = "4.15.0-7" -kotlin-blueprintjs-icons = "4.13.0-7" +kotlin-blueprintjs-core = "4.15.0-8" +kotlin-blueprintjs-icons = "4.13.0-8" [libraries] @@ -44,8 +44,8 @@ micrometer-registry-prometheus = { module = "io.micrometer:micrometer-registry-p slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } kotlin-wrappers-bom = { module = "org.jetbrains.kotlin-wrappers:kotlin-wrappers-bom", version.ref = "kotlin-wrappers" } +kotlin-wrappers-emotion = { module = "org.jetbrains.kotlin-wrappers:kotlin-emotion" } # typed CSS styles kotlin-wrappers-react-base = { module = "org.jetbrains.kotlin-wrappers:kotlin-react" } -kotlin-wrappers-react-legacy = { module = "org.jetbrains.kotlin-wrappers:kotlin-react-legacy" } kotlin-wrappers-react-dom = { module = "org.jetbrains.kotlin-wrappers:kotlin-react-dom" } kotlin-wrappers-react-redux = { module = "org.jetbrains.kotlin-wrappers:kotlin-react-redux" } kotlin-wrappers-react-router-dom = { module = "org.jetbrains.kotlin-wrappers:kotlin-react-router-dom" } diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index ba54d3b4..6699179f 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -2,6 +2,51 @@ # yarn lockfile v1 +"@babel/code-frame@^7.0.0": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + dependencies: + "@babel/highlight" "^7.18.6" + +"@babel/helper-module-imports@^7.16.7": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" + integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-plugin-utils@^7.18.6": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" + integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== + +"@babel/helper-string-parser@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" + integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== + +"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" + integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== + +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/plugin-syntax-jsx@^7.17.12": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0" + integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/runtime@^7.1.2", "@babel/runtime@^7.15.4", "@babel/runtime@^7.9.2": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa" @@ -9,7 +54,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": version "7.20.13" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.13.tgz#7055ab8a7cff2b8f6058bf6ae45ff84ad2aded4b" integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA== @@ -23,6 +68,15 @@ dependencies: regenerator-runtime "^0.13.10" +"@babel/types@^7.18.6": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.7.tgz#54ec75e252318423fc07fb644dc6a58a64c09b7f" + integrity sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg== + dependencies: + "@babel/helper-string-parser" "^7.19.4" + "@babel/helper-validator-identifier" "^7.19.1" + to-fast-properties "^2.0.0" + "@blueprintjs/colors@^4.1.13": version "4.1.13" resolved "https://registry.yarnpkg.com/@blueprintjs/colors/-/colors-4.1.13.tgz#885c8b2df419709bdf06a32e54209c0132bee55b" @@ -64,6 +118,125 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz#d5e0706cf8c6acd8c6032f8d54070af261bbbb2f" integrity sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA== +"@emotion/babel-plugin@^11.10.5": + version "11.10.5" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz#65fa6e1790ddc9e23cc22658a4c5dea423c55c3c" + integrity sha512-xE7/hyLHJac7D2Ve9dKroBBZqBT7WuPQmWcq7HSGb84sUuP4mlOWoB8dvVfD9yk5DHkU1m6RW7xSoDtnQHNQeA== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/plugin-syntax-jsx" "^7.17.12" + "@babel/runtime" "^7.18.3" + "@emotion/hash" "^0.9.0" + "@emotion/memoize" "^0.8.0" + "@emotion/serialize" "^1.1.1" + babel-plugin-macros "^3.1.0" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.1.3" + +"@emotion/cache@^11.10.5": + version "11.10.5" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.10.5.tgz#c142da9351f94e47527ed458f7bbbbe40bb13c12" + integrity sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA== + dependencies: + "@emotion/memoize" "^0.8.0" + "@emotion/sheet" "^1.2.1" + "@emotion/utils" "^1.2.0" + "@emotion/weak-memoize" "^0.3.0" + stylis "4.1.3" + +"@emotion/css@^11.10.5": + version "11.10.5" + resolved "https://registry.yarnpkg.com/@emotion/css/-/css-11.10.5.tgz#ca01bb83ce60517bc3a5c01d27ccf552fed84d9d" + integrity sha512-maJy0wG82hWsiwfJpc3WrYsyVwUbdu+sdIseKUB+/OLjB8zgc3tqkT6eO0Yt0AhIkJwGGnmMY/xmQwEAgQ4JHA== + dependencies: + "@emotion/babel-plugin" "^11.10.5" + "@emotion/cache" "^11.10.5" + "@emotion/serialize" "^1.1.1" + "@emotion/sheet" "^1.2.1" + "@emotion/utils" "^1.2.0" + +"@emotion/hash@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.0.tgz#c5153d50401ee3c027a57a177bc269b16d889cb7" + integrity sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ== + +"@emotion/is-prop-valid@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz#7f2d35c97891669f7e276eb71c83376a5dc44c83" + integrity sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg== + dependencies: + "@emotion/memoize" "^0.8.0" + +"@emotion/memoize@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.0.tgz#f580f9beb67176fa57aae70b08ed510e1b18980f" + integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA== + +"@emotion/react@^11.10.5": + version "11.10.5" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.10.5.tgz#95fff612a5de1efa9c0d535384d3cfa115fe175d" + integrity sha512-TZs6235tCJ/7iF6/rvTaOH4oxQg2gMAcdHemjwLKIjKz4rRuYe1HJ2TQJKnAcRAfOUDdU8XoDadCe1rl72iv8A== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.10.5" + "@emotion/cache" "^11.10.5" + "@emotion/serialize" "^1.1.1" + "@emotion/use-insertion-effect-with-fallbacks" "^1.0.0" + "@emotion/utils" "^1.2.0" + "@emotion/weak-memoize" "^0.3.0" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.1.tgz#0595701b1902feded8a96d293b26be3f5c1a5cf0" + integrity sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA== + dependencies: + "@emotion/hash" "^0.9.0" + "@emotion/memoize" "^0.8.0" + "@emotion/unitless" "^0.8.0" + "@emotion/utils" "^1.2.0" + csstype "^3.0.2" + +"@emotion/sheet@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.1.tgz#0767e0305230e894897cadb6c8df2c51e61a6c2c" + integrity sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA== + +"@emotion/styled@^11.10.5": + version "11.10.5" + resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.10.5.tgz#1fe7bf941b0909802cb826457e362444e7e96a79" + integrity sha512-8EP6dD7dMkdku2foLoruPCNkRevzdcBaY6q0l0OsbyJK+x8D9HWjX27ARiSIKNF634hY9Zdoedh8bJCiva8yZw== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.10.5" + "@emotion/is-prop-valid" "^1.2.0" + "@emotion/serialize" "^1.1.1" + "@emotion/use-insertion-effect-with-fallbacks" "^1.0.0" + "@emotion/utils" "^1.2.0" + +"@emotion/unitless@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.0.tgz#a4a36e9cbdc6903737cd20d38033241e1b8833db" + integrity sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw== + +"@emotion/use-insertion-effect-with-fallbacks@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz#ffadaec35dbb7885bd54de3fa267ab2f860294df" + integrity sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A== + +"@emotion/utils@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.0.tgz#9716eaccbc6b5ded2ea5a90d65562609aab0f561" + integrity sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw== + +"@emotion/weak-memoize@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz#ea89004119dc42db2e1dba0f97d553f7372f6fcb" + integrity sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg== + "@hypnosphi/create-react-context@^0.3.1": version "0.3.1" resolved "https://registry.yarnpkg.com/@hypnosphi/create-react-context/-/create-react-context-0.3.1.tgz#f8bfebdc7665f5d426cba3753e0e9c7d3154d7c6" @@ -217,6 +390,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.8.tgz#50d680c8a8a78fe30abe6906453b21ad8ab0ad7b" integrity sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg== +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + "@types/prop-types@*": version "15.7.4" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" @@ -528,6 +706,13 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" @@ -558,6 +743,15 @@ array-flatten@^2.1.2: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== +babel-plugin-macros@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" + integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg== + dependencies: + "@babel/runtime" "^7.12.5" + cosmiconfig "^7.0.0" + resolve "^1.19.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -688,6 +882,11 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + camel-case@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" @@ -715,6 +914,15 @@ capital-case@^1.0.4: tslib "^2.0.3" upper-case-first "^2.0.2" +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -799,6 +1007,13 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -806,6 +1021,11 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" @@ -892,6 +1112,11 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +convert-source-map@^1.5.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -920,6 +1145,17 @@ cors@~2.8.5: object-assign "^4" vary "^1" +cosmiconfig@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -929,13 +1165,6 @@ cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -css-in-js-utils@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz#640ae6a33646d401fc720c54fc61c42cd76ae2bb" - integrity sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A== - dependencies: - hyphenate-style-name "^1.0.3" - csstype@^3.0.2: version "3.0.10" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5" @@ -1153,6 +1382,13 @@ envinfo@^7.7.3: resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + es-module-lexer@^0.9.0: version "0.9.3" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" @@ -1168,11 +1404,16 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= -escape-string-regexp@4.0.0: +escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + eslint-scope@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" @@ -1280,11 +1521,6 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-loops@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/fast-loops/-/fast-loops-1.1.3.tgz#ce96adb86d07e7bf9b4822ab9c6fac9964981f75" - integrity sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g== - fastest-levenshtein@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" @@ -1330,6 +1566,11 @@ finalhandler@1.2.0: statuses "2.0.1" unpipe "~1.0.0" +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + find-up@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" @@ -1473,6 +1714,11 @@ handle-thing@^2.0.0: resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" @@ -1517,7 +1763,7 @@ history@^5.3.0: dependencies: "@babel/runtime" "^7.7.6" -hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -1606,11 +1852,6 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -hyphenate-style-name@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" - integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== - iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -1625,6 +1866,14 @@ iconv-lite@^0.6.3: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + import-local@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" @@ -1651,14 +1900,6 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== -inline-style-prefixer@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-7.0.0.tgz#991d550735d42069f528ac1bcdacd378d1305442" - integrity sha512-I7GEdScunP1dQ6IM2mQWh6v0mOYdYmH3Bp31UecKdrcUgcURTcctSe1IECdUznSHKSmsHtjrT3CwCPI1pyxfUQ== - dependencies: - css-in-js-utils "^3.1.0" - fast-loops "^1.1.3" - interpret@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" @@ -1682,6 +1923,11 @@ is-arguments@^1.0.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -1696,6 +1942,13 @@ is-core-module@^2.8.0: dependencies: has "^1.0.3" +is-core-module@^2.9.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" + integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== + dependencies: + has "^1.0.3" + is-date-object@^1.0.1: version "1.0.5" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" @@ -1801,7 +2054,7 @@ jest-worker@^27.4.1: merge-stream "^2.0.0" supports-color "^8.0.0" -"js-tokens@^3.0.0 || ^4.0.0": +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -1813,7 +2066,7 @@ js-yaml@4.1.0: dependencies: argparse "^2.0.1" -json-parse-even-better-errors@^2.3.1: +json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== @@ -1902,6 +2155,11 @@ kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + loader-runner@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384" @@ -2289,6 +2547,23 @@ param-case@^3.0.4: dot-case "^3.0.4" tslib "^2.0.3" +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -2335,6 +2610,11 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -2596,11 +2876,25 @@ resolve-cwd@^3.0.0: dependencies: resolve-from "^5.0.0" +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + resolve-from@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve@^1.19.0: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^1.9.0: version "1.21.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.21.0.tgz#b51adc97f3472e6a5cf4444d34bc9d6b9037591f" @@ -2845,6 +3139,11 @@ source-map-support@~0.5.20: buffer-from "^1.0.0" source-map "^0.6.0" +source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + source-map@^0.6.0, source-map@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -2937,6 +3236,11 @@ strip-json-comments@3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +stylis@4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7" + integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA== + supports-color@8.1.1, supports-color@^8.0.0: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" @@ -2944,6 +3248,13 @@ supports-color@8.1.1, supports-color@^8.0.0: dependencies: has-flag "^4.0.0" +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -2993,6 +3304,11 @@ tmp@^0.2.1: dependencies: rimraf "^3.0.0" +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -3302,6 +3618,11 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + yargs-parser@20.2.4: version "20.2.4" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" diff --git a/sw-ui/build.gradle.kts b/sw-ui/build.gradle.kts index d1c4e6a8..6fb7d8b8 100644 --- a/sw-ui/build.gradle.kts +++ b/sw-ui/build.gradle.kts @@ -20,9 +20,9 @@ kotlin { implementation(libs.kotlin.wrappers.react.dom) implementation(libs.kotlin.wrappers.react.redux) implementation(libs.kotlin.wrappers.react.router.dom) - implementation(libs.kotlin.wrappers.styled.next) implementation(libs.kotlin.wrappers.blueprintjs.core) implementation(libs.kotlin.wrappers.blueprintjs.icons) + implementation(libs.kotlin.wrappers.emotion) } } test { diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt index 44faede8..0bd3400e 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt @@ -5,35 +5,33 @@ import kotlinx.coroutines.* import org.luxons.sevenwonders.ui.components.* import org.luxons.sevenwonders.ui.redux.* import org.luxons.sevenwonders.ui.redux.sagas.* -import react.dom.* +import react.* +import react.dom.client.* import react.redux.* import redux.* -import web.dom.* import web.dom.document +import web.html.* fun main() { - window.onload = { - val rootElement = document.getElementById("root") - if (rootElement != null) { - initializeAndRender(rootElement) - } else { - console.error("Element with ID 'root' was not found, cannot bootstrap react app") - } + window.onload = { init() } +} + +private fun init() { + val rootElement = document.getElementById("root") + if (rootElement == null) { + console.error("Element with ID 'root' was not found, cannot bootstrap react app") + return } + renderRoot(rootElement) } -private fun initializeAndRender(rootElement: Element) { +private fun renderRoot(rootElement: HTMLElement) { val store = initRedux() - - // With the new API this might look something like: - // createRoot(rootElement).render(FC<Props> { .. }.create()) - // See: https://github.com/karakum-team/kotlin-mui-showcase/blob/main/src/main/kotlin/team/karakum/App.kt - @Suppress("DEPRECATION") - render(rootElement) { - provider(store) { - application() - } + val connectedApp = Provider.create { + this.store = store + Application() } + createRoot(rootElement).render(connectedApp) } @OptIn(DelicateCoroutinesApi::class) diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/Application.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/Application.kt index 51e7e78f..9c210538 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/Application.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/Application.kt @@ -1,40 +1,43 @@ package org.luxons.sevenwonders.ui.components -import org.luxons.sevenwonders.ui.components.errors.errorDialog -import org.luxons.sevenwonders.ui.components.game.gameScene -import org.luxons.sevenwonders.ui.components.gameBrowser.gameBrowser -import org.luxons.sevenwonders.ui.components.home.home -import org.luxons.sevenwonders.ui.components.lobby.lobby -import org.luxons.sevenwonders.ui.router.SwRoute -import react.Props -import react.RBuilder -import react.RElementBuilder -import react.createElement -import react.router.Navigate -import react.router.Route -import react.router.Routes -import react.router.RoutesProps +import org.luxons.sevenwonders.ui.components.errors.* +import org.luxons.sevenwonders.ui.components.game.* +import org.luxons.sevenwonders.ui.components.gameBrowser.* +import org.luxons.sevenwonders.ui.components.home.* +import org.luxons.sevenwonders.ui.components.lobby.* +import org.luxons.sevenwonders.ui.router.* +import react.* +import react.router.* import react.router.dom.* -fun RBuilder.application() = HashRouter { - errorDialog() - Routes { - route(SwRoute.GAME_BROWSER.path) { gameBrowser() } - route(SwRoute.GAME.path) { gameScene() } - route(SwRoute.LOBBY.path) { lobby() } - route(SwRoute.HOME.path) { home() } - route("*") { - Navigate { - attrs.to = "/" - attrs.replace = true +val Application = VFC("Application") { + HashRouter { + ErrorDialog() + Routes { + Route { + path = SwRoute.GAME_BROWSER.path + element = GameBrowser.create() + } + Route { + path = SwRoute.GAME.path + element = GameScene.create() + } + Route { + path = SwRoute.LOBBY.path + element = Lobby.create() + } + Route { + path = SwRoute.HOME.path + element = Home.create() + } + Route { + path = "*" + element = Navigate.create { + to = "/" + replace = true + } } } } } -private fun RElementBuilder<RoutesProps>.route(path: String, render: RBuilder.() -> Unit) { - Route { - attrs.path = path - attrs.element = createElement<Props>(render) - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/GlobalStyles.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/GlobalStyles.kt index 9ba5951a..bd12537e 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/GlobalStyles.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/GlobalStyles.kt @@ -1,19 +1,20 @@ package org.luxons.sevenwonders.ui.components -import kotlinx.css.* -import kotlinx.css.properties.* -import styled.StyleSheet +import csstype.* +import emotion.css.* +import org.luxons.sevenwonders.ui.utils.* -object GlobalStyles : StyleSheet("GlobalStyles", isStatic = true) { + +object GlobalStyles { val preGameWidth = 60.rem - val zeusBackground by css { - background = "url('images/backgrounds/zeus-temple.jpg') center no-repeat" - backgroundSize = "cover" + val zeusBackground = ClassName { + background = "url('images/backgrounds/zeus-temple.jpg') center no-repeat".unsafeCast<Background>() + backgroundSize = BackgroundSize.cover } - val fullscreen by css { + val fullscreen = ClassName { position = Position.fixed top = 0.px left = 0.px @@ -22,30 +23,26 @@ object GlobalStyles : StyleSheet("GlobalStyles", isStatic = true) { overflow = Overflow.hidden } - val papyrusBackground by css { - background = "url('images/backgrounds/papyrus.jpg')" - backgroundSize = "cover" + val papyrusBackground = ClassName { + background = "url('images/backgrounds/papyrus.jpg')".unsafeCast<Background>() + backgroundSize = BackgroundSize.cover } - val fixedCenter by css { - position = Position.fixed - +centerLeftTopTransform + val centerLeftTopTransform = ClassName { + left = 50.pct + top = 50.pct + transform = translate((-50).pct, (-50).pct) } - val centerInPositionedParent by css { - position = Position.absolute - +centerLeftTopTransform + val fixedCenter = ClassName(centerLeftTopTransform) { + position = Position.fixed } - val centerLeftTopTransform by css { - left = 50.pct - top = 50.pct - transform { - translate((-50).pct, (-50).pct) - } + val centerInPositionedParent = ClassName(centerLeftTopTransform) { + position = Position.absolute } - val noPadding by css { - padding(all = 0.px) + val noPadding = ClassName { + padding = Padding(all = 0.px) } } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/errors/ErrorDialog.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/errors/ErrorDialog.kt index 5ff56055..5399f60d 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/errors/ErrorDialog.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/errors/ErrorDialog.kt @@ -6,44 +6,46 @@ import kotlinx.browser.* import org.luxons.sevenwonders.ui.redux.* import org.luxons.sevenwonders.ui.router.* import react.* -import react.dom.p -import styled.* +import react.dom.html.ReactHTML.p +import react.redux.* +import redux.* -external interface ErrorDialogStateProps : PropsWithChildren { - var errorMessage: String? +val ErrorDialog = VFC { + val dispatch = useDispatch<RAction, WrapperAction>() + + ErrorDialogPresenter { + errorMessage = useSwSelector { it.fatalError } + goHome = { dispatch(Navigate(SwRoute.HOME)) } + } } -external interface ErrorDialogDispatchProps : PropsWithChildren { +private external interface ErrorDialogProps : Props { + var errorMessage: String? var goHome: () -> Unit } -external interface ErrorDialogProps : ErrorDialogDispatchProps, ErrorDialogStateProps +private val ErrorDialogPresenter = FC<ErrorDialogProps>("ErrorDialogPresenter") { props -> + val errorMessage = props.errorMessage + BpDialog { + isOpen = errorMessage != null + titleText = "Oops!" + icon = BpIcon.create { + icon = IconNames.ERROR + intent = Intent.DANGER + } + onClose = { goHomeAndRefresh() } -class ErrorDialogPresenter(props: ErrorDialogProps) : RComponent<ErrorDialogProps, State>(props) { - override fun RBuilder.render() { - val errorMessage = props.errorMessage - bpDialog( - isOpen = errorMessage != null, - title = "Oops!", - icon = IconNames.ERROR, - iconIntent = Intent.DANGER, - onClose = { goHomeAndRefresh() } - ) { - styledDiv { - css { - classes.add(Classes.DIALOG_BODY) - } - p { - +(errorMessage ?: "fatal error") - } + BpDialogBody { + p { + +(errorMessage ?: "fatal error") } - styledDiv { - css { - classes.add(Classes.DIALOG_FOOTER) - } - bpButton(icon = IconNames.LOG_OUT, onClick = { goHomeAndRefresh() }) { - +"HOME" - } + } + BpDialogFooter { + BpButton { + icon = IconNames.LOG_OUT + onClick = { goHomeAndRefresh() } + + +"HOME" } } } @@ -53,15 +55,3 @@ private fun goHomeAndRefresh() { // we don't use a redux action here because we actually want to redirect and refresh the page window.location.href = SwRoute.HOME.path } - -fun RBuilder.errorDialog() = errorDialog {} - -private val errorDialog = connectStateAndDispatch<ErrorDialogStateProps, ErrorDialogDispatchProps, ErrorDialogProps>( - clazz = ErrorDialogPresenter::class, - mapStateToProps = { state, _ -> - errorMessage = state.fatalError - }, - mapDispatchToProps = { dispatch, _ -> - goHome = { dispatch(Navigate(SwRoute.HOME)) } - }, -) diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Board.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Board.kt index c3dc6460..9ca56efa 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Board.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Board.kt @@ -1,37 +1,34 @@ package org.luxons.sevenwonders.ui.components.game -import kotlinx.css.* -import kotlinx.css.properties.* -import kotlinx.html.DIV -import kotlinx.html.HTMLTag -import kotlinx.html.IMG -import kotlinx.html.title -import org.luxons.sevenwonders.model.boards.Board -import org.luxons.sevenwonders.model.boards.Military -import org.luxons.sevenwonders.model.cards.TableCard -import org.luxons.sevenwonders.model.wonders.ApiWonder -import org.luxons.sevenwonders.model.wonders.ApiWonderStage -import react.RBuilder -import react.dom.* -import styled.StyledDOMBuilder -import styled.css -import styled.styledDiv -import styled.styledImg +import csstype.* +import emotion.react.* +import org.luxons.sevenwonders.model.boards.* +import org.luxons.sevenwonders.model.cards.* +import org.luxons.sevenwonders.model.wonders.* +import react.* +import react.dom.html.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.img +import web.html.* // card offsets in % of their size when displayed in columns private const val xOffset = 20 private const val yOffset = 21 -fun RBuilder.boardComponent(board: Board, block: StyledDOMBuilder<DIV>.() -> Unit = {}) { - styledDiv { - block() - tableCards(cardColumns = board.playedCards) - wonderComponent(wonder = board.wonder, military = board.military) +external interface BoardComponentProps : PropsWithClassName { + var board: Board +} + +val BoardComponent = FC<BoardComponentProps>("Board") { props -> + div { + className = props.className + tableCards(cardColumns = props.board.playedCards) + wonderComponent(wonder = props.board.wonder, military = props.board.military) } } -private fun RBuilder.tableCards(cardColumns: List<List<TableCard>>) { - styledDiv { +private fun ChildrenBuilder.tableCards(cardColumns: List<List<TableCard>>) { + div { css { display = Display.flex justifyContent = JustifyContent.spaceAround @@ -39,101 +36,109 @@ private fun RBuilder.tableCards(cardColumns: List<List<TableCard>>) { width = 100.pct } cardColumns.forEach { cards -> - tableCardColumn(cards = cards) { - attrs { - key = cards.first().color.toString() - } + TableCardColumn { + this.key = cards.first().color.toString() + this.cards = cards } } } } -private fun RBuilder.tableCardColumn(cards: List<TableCard>, block: StyledDOMBuilder<DIV>.() -> Unit = {}) { - styledDiv { +private external interface TableCardColumnProps : PropsWithClassName { + var cards: List<TableCard> +} + +private val TableCardColumn = FC<TableCardColumnProps>("TableCardColumn") { props -> + div { css { height = 100.pct width = 13.pct marginRight = 4.pct position = Position.relative } - block() - cards.forEachIndexed { index, card -> - tableCard(card = card, indexInColumn = index) { - attrs { key = card.name } + props.cards.forEachIndexed { index, card -> + TableCard { + this.card = card + this.indexInColumn = index + this.key = card.name } } } } -private fun RBuilder.tableCard(card: TableCard, indexInColumn: Int, block: StyledDOMBuilder<IMG>.() -> Unit = {}) { - val highlightColor = if (card.playedDuringLastMove) Color.gold else null - cardImage(card = card, highlightColor = highlightColor) { +private external interface TableCardProps : PropsWithClassName { + var card: TableCard + var indexInColumn: Int +} + +private val TableCard = FC<TableCardProps>("TableCard") { props -> + val highlightColor = if (props.card.playedDuringLastMove) NamedColor.gold else null + CardImage { + this.card = props.card + this.highlightColor = highlightColor + css { position = Position.absolute - zIndex = indexInColumn + 2 // go above the board and the built wonder cards - transform { - translate( - tx = (indexInColumn * xOffset).pct, - ty = (indexInColumn * yOffset).pct, - ) - } + zIndex = integer(props.indexInColumn + 2) // go above the board and the built wonder cards + transform = translate( + tx = (props.indexInColumn * xOffset).pct, + ty = (props.indexInColumn * yOffset).pct, + ) maxWidth = 100.pct maxHeight = 70.pct hover { - zIndex = 1000 + zIndex = integer(1000) maxWidth = 110.pct maxHeight = 75.pct hoverHighlightStyle() } } - block() } } -private fun RBuilder.wonderComponent(wonder: ApiWonder, military: Military) { - styledDiv { +private fun ChildrenBuilder.wonderComponent(wonder: ApiWonder, military: Military) { + div { css { position = Position.relative width = 100.pct height = 40.pct } - styledDiv { + div { css { position = Position.absolute left = 50.pct top = 0.px - transform { translateX((-50).pct) } + transform = translatex((-50).pct) height = 100.pct maxWidth = 95.pct // same as wonder // bring to the foreground on hover - hover { zIndex = 1000 } + hover { zIndex = integer(1000) } } - styledImg(src = "/images/wonders/${wonder.image}") { + img { + src = "/images/wonders/${wonder.image}" + title = wonder.name + alt = "Wonder ${wonder.name}" + css { - classes.add("wonder-image") - declarations["border-radius"] = "0.5%/1.5%" - boxShadow(color = Color.black, offsetX = 0.2.rem, offsetY = 0.2.rem, blurRadius = 0.5.rem) + borderRadius = "0.5%/1.5%".unsafeCast<BorderRadius>() + boxShadow = BoxShadow(color = NamedColor.black, offsetX = 0.2.rem, offsetY = 0.2.rem, blurRadius = 0.5.rem) maxHeight = 100.pct maxWidth = 100.pct - zIndex = 1 // go above the built wonder cards, but below the table cards + zIndex = integer(1) // go above the built wonder cards, but below the table cards hover { hoverHighlightStyle() } } - attrs { - this.title = wonder.name - this.alt = "Wonder ${wonder.name}" - } } - styledDiv { + div { css { position = Position.absolute top = 20.pct right = (-80).px display = Display.flex flexDirection = FlexDirection.column - alignItems = Align.start + alignItems = AlignItems.start } victoryPoints(military.victoryPoints) { css { @@ -147,7 +152,8 @@ private fun RBuilder.wonderComponent(wonder: ApiWonder, military: Military) { } } wonder.stages.forEachIndexed { index, stage -> - wonderStageElement(stage) { + WonderStageElement { + this.stage = stage css { wonderCardStyle(index, wonder.stages.size) } @@ -157,15 +163,15 @@ private fun RBuilder.wonderComponent(wonder: ApiWonder, military: Military) { } } -private fun RBuilder.victoryPoints(points: Int, block: StyledDOMBuilder<DIV>.() -> Unit = {}) { +private fun ChildrenBuilder.victoryPoints(points: Int, block: HTMLAttributes<HTMLDivElement>.() -> Unit = {}) { boardToken("military/victory1", points, block) } -private fun RBuilder.defeatTokenCount(nbDefeatTokens: Int, block: StyledDOMBuilder<DIV>.() -> Unit = {}) { +private fun ChildrenBuilder.defeatTokenCount(nbDefeatTokens: Int, block: HTMLAttributes<HTMLDivElement>.() -> Unit = {}) { boardToken("military/defeat1", nbDefeatTokens, block) } -private fun RBuilder.boardToken(tokenName: String, count: Int, block: StyledDOMBuilder<DIV>.() -> Unit) { +private fun ChildrenBuilder.boardToken(tokenName: String, count: Int, block: HTMLAttributes<HTMLDivElement>.() -> Unit) { tokenWithCount( tokenName = tokenName, count = count, @@ -173,34 +179,40 @@ private fun RBuilder.boardToken(tokenName: String, count: Int, block: StyledDOMB brightText = true, ) { css { - filter = "drop-shadow(0.2rem 0.2rem 0.5rem black)" + filter = dropShadow(0.2.rem, 0.2.rem, 0.5.rem, NamedColor.black) height = 15.pct } block() } } -private fun RBuilder.wonderStageElement(stage: ApiWonderStage, block: StyledDOMBuilder<HTMLTag>.() -> Unit) { - val back = stage.cardBack +private external interface WonderStageElementProps : PropsWithClassName { + var stage: ApiWonderStage +} + +private val WonderStageElement = FC<WonderStageElementProps>("WonderStageElement") { props -> + val back = props.stage.cardBack if (back != null) { - val highlightColor = if (stage.builtDuringLastMove) Color.gold else null - cardBackImage(cardBack = back, highlightColor = highlightColor) { - block() + val highlightColor = if (props.stage.builtDuringLastMove) NamedColor.gold else null + CardBackImage { + this.cardBack = back + this.highlightColor = highlightColor + this.className = props.className } } else { - cardPlaceholderImage { - block() + CardPlaceholderImage { + this.className = props.className } } } -private fun CssBuilder.wonderCardStyle(stageIndex: Int, nbStages: Int) { +private fun PropertiesBuilder.wonderCardStyle(stageIndex: Int, nbStages: Int) { position = Position.absolute top = 60.pct // makes the cards stick out of the bottom of the wonder left = stagePositionPercent(stageIndex, nbStages).pct maxWidth = 24.pct // ratio of card width to wonder width maxHeight = 90.pct // ratio of card height to wonder height - zIndex = -1 // below wonder (somehow 0 is not sufficient) + zIndex = integer(-1) // below wonder (somehow 0 is not sufficient) } private fun stagePositionPercent(stageIndex: Int, nbStages: Int): Double = when (nbStages) { @@ -209,6 +221,6 @@ private fun stagePositionPercent(stageIndex: Int, nbStages: Int): Double = when else -> 7.9 + stageIndex * 30.0 } -private fun CssBuilder.hoverHighlightStyle() { - highlightStyle(Color.paleGoldenrod) +private fun PropertiesBuilder.hoverHighlightStyle() { + highlightStyle(NamedColor.palegoldenrod) } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/BoardSummary.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/BoardSummary.kt index ec7ea464..76487db5 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/BoardSummary.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/BoardSummary.kt @@ -1,109 +1,134 @@ package org.luxons.sevenwonders.ui.components.game -import blueprintjs.core.PopoverPosition -import blueprintjs.core.bpDivider -import blueprintjs.core.bpPopover -import kotlinx.css.* -import kotlinx.html.* -import org.luxons.sevenwonders.model.api.PlayerDTO -import org.luxons.sevenwonders.model.boards.Board -import org.luxons.sevenwonders.ui.components.gameBrowser.playerInfo +import blueprintjs.core.* +import csstype.* +import emotion.css.* +import emotion.react.* +import org.luxons.sevenwonders.model.api.* +import org.luxons.sevenwonders.model.boards.* +import org.luxons.sevenwonders.ui.components.gameBrowser.* +import org.luxons.sevenwonders.ui.utils.* import react.* -import styled.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.hr enum class BoardSummarySide( val tokenCountPosition: TokenCountPosition, - val alignment: Align, + val alignment: AlignItems, val popoverPosition: PopoverPosition, ) { - LEFT(TokenCountPosition.RIGHT, Align.flexStart, PopoverPosition.RIGHT), - TOP(TokenCountPosition.OVER, Align.flexStart, PopoverPosition.BOTTOM), - RIGHT(TokenCountPosition.LEFT, Align.flexEnd, PopoverPosition.LEFT), - BOTTOM(TokenCountPosition.OVER, Align.flexStart, PopoverPosition.TOP), + LEFT(TokenCountPosition.RIGHT, AlignItems.flexStart, PopoverPosition.RIGHT), + TOP(TokenCountPosition.OVER, AlignItems.flexStart, PopoverPosition.BOTTOM), + RIGHT(TokenCountPosition.LEFT, AlignItems.flexEnd, PopoverPosition.LEFT), + BOTTOM(TokenCountPosition.OVER, AlignItems.flexStart, PopoverPosition.TOP), } -fun RBuilder.boardSummaryWithPopover( - player: PlayerDTO, - board: Board, - boardSummarySide: BoardSummarySide, - block: StyledDOMBuilder<DIV>.() -> Unit = {}, -) { - val popoverClass = GameStyles.getClassName { it::fullBoardPreviewPopover } - bpPopover( - content = createFullBoardPreview(board), - position = boardSummarySide.popoverPosition, - popoverClassName = popoverClass, - ) { - boardSummary( - player = player, - board = board, - side = boardSummarySide, - block = block, - ) - } +external interface BoardSummaryWithPopoverProps : PropsWithClassName { + var player: PlayerDTO + var board: Board + var side: BoardSummarySide } -private fun createFullBoardPreview(board: Board): ReactElement<*> = buildElement { - boardComponent(board = board) { - css { - +GameStyles.fullBoardPreview +val BoardSummaryWithPopover = FC<BoardSummaryWithPopoverProps>("BoardSummaryWithPopover") { props -> + BpPopover { + content = BoardComponent.create { + className = GameStyles.fullBoardPreview + board = props.board + } + position = props.side.popoverPosition + interactionKind = PopoverInteractionKind.HOVER + popoverClassName = ClassName { + val bgColor = GameStyles.sandBgColor.withAlpha(0.7) + backgroundColor = bgColor + borderRadius = 0.5.rem + padding = Padding(all = 0.5.rem) + + children(".bp4-popover-content") { + background = None.none // overrides default white background + } + descendants(".bp4-popover-arrow-fill") { + set(Variable("fill"), bgColor.toString()) // overrides default white arrow + } + descendants(".bp4-popover-arrow::before") { + // The popover arrow is implemented with a simple square rotated 45 degrees (like a rhombus). + // Since we use a semi-transparent background, we can see the box shadow of the rest of the arrow through + // the popover, and thus we see the square. This boxShadow(transparent) is to avoid that. + boxShadow = BoxShadow(0.px, 0.px, 0.px, 0.px, NamedColor.transparent) + } + }.toString() + + BoardSummary { + this.className = props.className + this.player = props.player + this.board = props.board + this.side = props.side } } } -fun RBuilder.boardSummary( - player: PlayerDTO, - board: Board, - side: BoardSummarySide, - showPreparationStatus: Boolean = true, - block: StyledDOMBuilder<DIV>.() -> Unit = {}, -) { - styledDiv { - css { +external interface BoardSummaryProps : PropsWithClassName { + var player: PlayerDTO + var board: Board + var side: BoardSummarySide + var showPreparationStatus: Boolean? +} + +val BoardSummary = FC<BoardSummaryProps>("BoardSummary") { props -> + div { + css(props.className) { display = Display.flex flexDirection = FlexDirection.column - alignItems = side.alignment - padding(all = 0.5.rem) - backgroundColor = Color.paleGoldenrod.withAlpha(0.5) - zIndex = 50 // above table cards + alignItems = props.side.alignment + padding = Padding(all = 0.5.rem) + backgroundColor = NamedColor.palegoldenrod.withAlpha(0.5) + zIndex = integer(50) // above table cards hover { - backgroundColor = Color.paleGoldenrod + backgroundColor = NamedColor.palegoldenrod } } - topBar(player, side, showPreparationStatus) - styledHr { + val showPreparationStatus = props.showPreparationStatus ?: true + topBar(props.player, props.side, showPreparationStatus) + hr { css { - margin(vertical = 0.5.rem) + margin = Margin(vertical = 0.5.rem, horizontal = 0.rem) width = 100.pct } } - bottomBar(side, board, player, showPreparationStatus) - block() + bottomBar(props.side, props.board, props.player, showPreparationStatus) } } -private fun RBuilder.topBar(player: PlayerDTO, side: BoardSummarySide, showPreparationStatus: Boolean) { +private fun ChildrenBuilder.topBar(player: PlayerDTO, side: BoardSummarySide, showPreparationStatus: Boolean) { val playerIconSize = 25 if (showPreparationStatus && side == BoardSummarySide.TOP) { - styledDiv { + div { css { display = Display.flex flexDirection = FlexDirection.row justifyContent = JustifyContent.spaceBetween width = 100.pct } - playerInfo(player, iconSize = playerIconSize) - playerPreparedCard(player) + PlayerInfo { + this.player = player + this.iconSize = playerIconSize + } + PlayerPreparedCard { + this.playerDisplayName = player.displayName + this.username = player.username + } } } else { - playerInfo(player, iconSize = playerIconSize) + PlayerInfo { + this.player = player + this.iconSize = playerIconSize + } } } -private fun RBuilder.bottomBar(side: BoardSummarySide, board: Board, player: PlayerDTO, showPreparationStatus: Boolean) { - styledDiv { +private fun ChildrenBuilder.bottomBar(side: BoardSummarySide, board: Board, player: PlayerDTO, showPreparationStatus: Boolean) { + div { css { display = Display.flex flexDirection = if (side == BoardSummarySide.TOP || side == BoardSummarySide.BOTTOM) FlexDirection.row else FlexDirection.column @@ -114,26 +139,29 @@ private fun RBuilder.bottomBar(side: BoardSummarySide, board: Board, player: Pla } val tokenSize = 2.rem generalCounts(board, tokenSize, side.tokenCountPosition) - bpDivider() + BpDivider() scienceTokens(board, tokenSize, side.tokenCountPosition) if (showPreparationStatus && side != BoardSummarySide.TOP) { - bpDivider() - styledDiv { + BpDivider() + div { css { width = 100.pct - alignItems = Align.center + alignItems = AlignItems.center display = Display.flex flexDirection = FlexDirection.column } - playerPreparedCard(player) + PlayerPreparedCard { + this.playerDisplayName = player.displayName + this.username = player.username + } } } } } -private fun StyledDOMBuilder<DIV>.generalCounts( +private fun ChildrenBuilder.generalCounts( board: Board, - tokenSize: LinearDimension, + tokenSize: Length, countPosition: TokenCountPosition, ) { goldIndicator(amount = board.gold, imgSize = tokenSize, amountPosition = countPosition) @@ -153,9 +181,9 @@ private fun StyledDOMBuilder<DIV>.generalCounts( ) } -private fun RBuilder.scienceTokens( +private fun ChildrenBuilder.scienceTokens( board: Board, - tokenSize: LinearDimension, + tokenSize: Length, sciencePosition: TokenCountPosition, ) { tokenWithCount( diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/CardImage.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/CardImage.kt index 409c4dac..093384a2 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/CardImage.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/CardImage.kt @@ -1,77 +1,72 @@ package org.luxons.sevenwonders.ui.components.game -import kotlinx.css.* -import kotlinx.css.properties.* -import kotlinx.html.IMG -import kotlinx.html.title -import org.luxons.sevenwonders.model.cards.Card -import org.luxons.sevenwonders.model.cards.CardBack -import react.RBuilder -import react.dom.attrs -import styled.StyledDOMBuilder -import styled.css -import styled.styledImg +import csstype.* +import csstype.Color +import emotion.react.* +import org.luxons.sevenwonders.model.cards.* +import react.* +import react.dom.html.ReactHTML.img -fun RBuilder.cardImage( - card: Card, - faceDown: Boolean = false, - highlightColor: Color? = null, - block: StyledDOMBuilder<IMG>.() -> Unit = {}, -) { - if (faceDown) { - cardBackImage(card.back, highlightColor, block) - return - } - styledImg(src = "/images/cards/${card.image}") { - css { - cardImageStyle(highlightColor) +external interface CardImageProps : PropsWithClassName { + var card: Card + var faceDown: Boolean? + var highlightColor: Color? +} + +val CardImage = FC<CardImageProps>("CardImage") { props -> + if (props.faceDown == true) { + CardBackImage { + cardBack = props.card.back + highlightColor = props.highlightColor } - attrs { - title = card.name - alt = "Card ${card.name}" + } else { + img { + src = "/images/cards/${props.card.image}" + title = props.card.name + alt = "Card ${props.card.name}" + + css(props.className) { + cardImageStyle(props.highlightColor) + } } - block() } } -fun RBuilder.cardBackImage( - cardBack: CardBack, - highlightColor: Color? = null, - block: StyledDOMBuilder<IMG>.() -> Unit = {}, -) { - styledImg(src = "/images/cards/back/${cardBack.image}") { - css { - cardImageStyle(highlightColor) - } - attrs { - alt = "Card back (${cardBack.image})" +external interface CardBackImageProps : PropsWithClassName { + var cardBack: CardBack + var highlightColor: Color? +} + +val CardBackImage = FC<CardBackImageProps>("CardBackImage") { props -> + img { + src = "/images/cards/back/${props.cardBack.image}" + alt = "Card back (${props.cardBack.image})" + css(props.className) { + cardImageStyle(props.highlightColor) } - block() } } -fun RBuilder.cardPlaceholderImage(block: StyledDOMBuilder<IMG>.() -> Unit = {}) { - styledImg(src = "/images/cards/back/placeholder.png") { - css { - opacity = 0.20 +val CardPlaceholderImage = FC<PropsWithClassName>("CardPlaceholderImage") { props -> + img { + src = "/images/cards/back/placeholder.png" + alt = "Card placeholder" + css(props.className) { + opacity = number(0.20) borderRadius = 5.pct } - attrs { - alt = "Card placeholder" - } - block() } } -private fun CssBuilder.cardImageStyle(highlightColor: Color?) { +private fun PropertiesBuilder.cardImageStyle(highlightColor: Color?) { borderRadius = 5.pct - boxShadow(offsetX = 2.px, offsetY = 2.px, blurRadius = 5.px, color = Color.black) + boxShadow = BoxShadow(offsetX = 2.px, offsetY = 2.px, blurRadius = 5.px, color = NamedColor.black) highlightStyle(highlightColor) } -internal fun CssBuilder.highlightStyle(highlightColor: Color?) { +internal fun PropertiesBuilder.highlightStyle(highlightColor: Color?) { if (highlightColor != null) { - boxShadow( + boxShadow = BoxShadow( offsetX = 0.px, offsetY = 0.px, blurRadius = 1.rem, diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt index cec41d6a..843caaf7 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt @@ -1,309 +1,309 @@ package org.luxons.sevenwonders.ui.components.game import blueprintjs.core.* -import blueprintjs.icons.IconNames -import kotlinx.css.* -import kotlinx.css.Position -import kotlinx.css.properties.transform -import kotlinx.css.properties.translate -import org.luxons.sevenwonders.client.GameState -import org.luxons.sevenwonders.client.getBoard -import org.luxons.sevenwonders.client.getNonNeighbourBoards -import org.luxons.sevenwonders.client.getOwnBoard -import org.luxons.sevenwonders.model.MoveType -import org.luxons.sevenwonders.model.PlayerMove -import org.luxons.sevenwonders.model.TurnAction -import org.luxons.sevenwonders.model.api.PlayerDTO -import org.luxons.sevenwonders.model.boards.Board -import org.luxons.sevenwonders.model.boards.RelativeBoardPosition -import org.luxons.sevenwonders.model.cards.HandCard -import org.luxons.sevenwonders.model.resources.ResourceTransactionOptions -import org.luxons.sevenwonders.ui.components.GlobalStyles +import blueprintjs.icons.* +import csstype.* +import csstype.Position +import emotion.react.* +import org.luxons.sevenwonders.client.* +import org.luxons.sevenwonders.model.* +import org.luxons.sevenwonders.model.api.* +import org.luxons.sevenwonders.model.boards.* +import org.luxons.sevenwonders.model.cards.* +import org.luxons.sevenwonders.model.resources.* +import org.luxons.sevenwonders.ui.components.* import org.luxons.sevenwonders.ui.redux.* import org.luxons.sevenwonders.ui.utils.* import react.* -import styled.css -import styled.styledDiv +import react.dom.html.ReactHTML.div -external interface GameSceneStateProps : PropsWithChildren { +external interface GameSceneProps : Props { var currentPlayer: PlayerDTO? var players: List<PlayerDTO> - var game: GameState? + var game: GameState var preparedMove: PlayerMove? var preparedCard: HandCard? -} - -external interface GameSceneDispatchProps : PropsWithChildren { var sayReady: () -> Unit var prepareMove: (move: PlayerMove) -> Unit var unprepareMove: () -> Unit var leaveGame: () -> Unit } -interface GameSceneProps : GameSceneStateProps, GameSceneDispatchProps - -data class GameSceneState( - var transactionSelector: TransactionSelectorState? -) : State - data class TransactionSelectorState( val moveType: MoveType, val card: HandCard, val transactionsOptions: ResourceTransactionOptions, ) -private class GameScene(props: GameSceneProps) : RComponent<GameSceneProps, GameSceneState>(props) { +val GameScene = VFC("GameScene") { - override fun GameSceneState.init() { - transactionSelector = null - } + val player = useSwSelector { it.currentPlayer } + val gameState = useSwSelector { it.gameState } + val dispatch = useSwDispatch() - override fun RBuilder.render() { - styledDiv { - css { - +GlobalStyles.papyrusBackground - +GlobalStyles.fullscreen + div { + css(GlobalStyles.papyrusBackground, GlobalStyles.fullscreen) {} + + if (gameState == null) { + BpNonIdealState { + icon = IconNames.ERROR + titleText = "Error: no game data" } - val game = props.game - if (game == null) { - bpNonIdealState(icon = IconNames.ERROR, title = "Error: no game data") - } else { - boardScene(game) + } else { + GameScenePresenter { + currentPlayer = player + players = gameState.players + game = gameState + preparedMove = gameState.currentPreparedMove + preparedCard = gameState.currentPreparedCard + + prepareMove = { move -> dispatch(RequestPrepareMove(move)) } + unprepareMove = { dispatch(RequestUnprepareMove()) } + sayReady = { dispatch(RequestSayReady()) } + leaveGame = { dispatch(RequestLeaveGame()) } } } } +} - private fun RBuilder.boardScene(game: GameState) { - val board = game.getOwnBoard() - styledDiv { +private val GameScenePresenter = FC<GameSceneProps>("GameScenePresenter") { props -> + var transactionSelectorState by useState<TransactionSelectorState>() + + val game = props.game + val board = game.getOwnBoard() + div { + val maybeRed = GameStyles.pulsatingRed.takeIf { game.everyoneIsWaitingForMe() } + css(maybeRed) { + height = 100.pct + } + val action = game.action + if (action is TurnAction.WatchScore) { + scoreTableOverlay(action.scoreBoard, props.players, props.leaveGame) + } + actionInfo(game.action.message) + BoardComponent { + this.board = board css { + padding = Padding(vertical = 7.rem, horizontal = 7.rem) // to fit the action info message & board summaries + width = 100.pct height = 100.pct - if (everyoneIsWaitingForMe()) { - +GameStyles.pulsatingRed - } } - val action = game.action - if (action is TurnAction.WatchScore) { - scoreTableOverlay(action.scoreBoard, props.players, props.leaveGame) + } + transactionsSelectorDialog( + state = transactionSelectorState, + neighbours = playerNeighbours(props.currentPlayer, props.players), + prepareMove = { move -> + props.prepareMove(move) + transactionSelectorState = null + }, + cancelTransactionSelection = { transactionSelectorState = null }, + ) + boardSummaries(game) + handRotationIndicator(game.handRotationDirection) + handCards( + game = game, + prepareMove = props.prepareMove, + startTransactionsSelection = { + transactionSelectorState = it } - actionInfo(game.action.message) - boardComponent(board = board) { - css { - padding(all = 7.rem) // to fit the action info message & board summaries - width = 100.pct - height = 100.pct + ) + val card = props.preparedCard + val move = props.preparedMove + if (card != null && move != null) { + BpOverlay { + isOpen = true + onClose = { props.unprepareMove() } + + preparedMove(card, move, props.unprepareMove) { + css(GlobalStyles.fixedCenter) {} } } - transactionsSelectorDialog( - state = state.transactionSelector, - neighbours = playerNeighbours(), - prepareMove = ::prepareMoveAndCloseTransactions, - cancelTransactionSelection = ::resetTransactionSelector, - ) - boardSummaries(game) - handRotationIndicator(game.handRotationDirection) - handCards(game, props.prepareMove, ::startTransactionSelection) - val card = props.preparedCard - val move = props.preparedMove - if (card != null && move != null) { - preparedMove(card, move) - } - if (game.action is TurnAction.SayReady) { - sayReadyButton() + } + if (game.action is TurnAction.SayReady) { + SayReadyButton { + currentPlayer = props.currentPlayer + players = props.players + sayReady = props.sayReady } } } +} - private fun prepareMoveAndCloseTransactions(move: PlayerMove) { - props.prepareMove(move) - setState { transactionSelector = null } +private fun GameState.everyoneIsWaitingForMe(): Boolean { + val onlyMeInTheGame = players.count { it.isHuman } == 1 + if (onlyMeInTheGame || currentPreparedMove != null) { + return false } + return preparedCardsByUsername.values.count { it != null } == players.size - 1 +} - private fun startTransactionSelection(selectorState: TransactionSelectorState) { - setState { transactionSelector = selectorState } - } +private fun playerNeighbours(currentPlayer: PlayerDTO?, players: List<PlayerDTO>): Pair<PlayerDTO, PlayerDTO> { + val me = currentPlayer?.username ?: error("we shouldn't be trying to display this if there is no player") + val size = players.size + val myIndex = players.indexOfFirst { it.username == me } + return players[(myIndex - 1 + size) % size] to players[(myIndex + 1) % size] +} - private fun resetTransactionSelector() { - setState { transactionSelector = null } - } +private fun ChildrenBuilder.actionInfo(message: String) { + div { + css(ClassName(Classes.DARK)) { + position = Position.fixed + top = 0.pct + left = 0.pct + margin = Margin(vertical = 0.4.rem, horizontal = 0.4.rem) + maxWidth = 25.pct // leave space for 4 board summaries when there are 7 players + } + BpCard { + elevation = Elevation.TWO + css { + padding = Padding(all = 0.px) + } - private fun everyoneIsWaitingForMe(): Boolean { - val onlyMeInTheGame = props.players.count { it.isHuman } == 1 - if (onlyMeInTheGame || props.preparedMove != null) { - return false + BpCallout { + intent = Intent.PRIMARY + icon = IconNames.INFO_SIGN + +message + } } - val gameState = props.game ?: return false - return gameState.preparedCardsByUsername.values.count { it != null } == props.players.size - 1 } +} - private fun playerNeighbours(): Pair<PlayerDTO, PlayerDTO> { - val me = props.currentPlayer?.username ?: error("we shouldn't be trying to display this if there is no player") - val players = props.players - val size = players.size - val myIndex = players.indexOfFirst { it.username == me } - return players[(myIndex - 1 + size) % size] to players[(myIndex + 1) % size] +private fun ChildrenBuilder.boardSummaries(game: GameState) { + val leftBoard = game.getBoard(RelativeBoardPosition.LEFT) + val rightBoard = game.getBoard(RelativeBoardPosition.RIGHT) + val topBoards = game.getNonNeighbourBoards().reversed() + selfBoardSummary(game.getOwnBoard(), game.players) + leftPlayerBoardSummary(leftBoard, game.players) + rightPlayerBoardSummary(rightBoard, game.players) + if (topBoards.isNotEmpty()) { + topPlayerBoardsSummaries(topBoards, game.players) } +} + +private fun ChildrenBuilder.leftPlayerBoardSummary(board: Board, players: List<PlayerDTO>) { + div { + css { + position = Position.absolute + left = 0.px + bottom = 40.pct + } + BoardSummaryWithPopover { + this.player = players[board.playerIndex] + this.board = board + this.side = BoardSummarySide.LEFT - private fun RBuilder.actionInfo(message: String) { - styledDiv { css { - classes.add(Classes.DARK) - position = Position.fixed - top = 0.pct - left = 0.pct - margin(all = 0.4.rem) - maxWidth = 25.pct // leave space for 4 board summaries when there are 7 players - } - bpCard(elevation = Elevation.TWO) { - attrs { - this.className = GlobalStyles.getTypedClassName { it::noPadding } - } - bpCallout(intent = Intent.PRIMARY, icon = IconNames.INFO_SIGN) { +message } + borderTopRightRadius = 0.4.rem + borderBottomRightRadius = 0.4.rem } } } +} - private fun RBuilder.boardSummaries(game: GameState) { - val leftBoard = game.getBoard(RelativeBoardPosition.LEFT) - val rightBoard = game.getBoard(RelativeBoardPosition.RIGHT) - val topBoards = game.getNonNeighbourBoards().reversed() - selfBoardSummary(game.getOwnBoard()) - leftPlayerBoardSummary(leftBoard) - rightPlayerBoardSummary(rightBoard) - if (topBoards.isNotEmpty()) { - topPlayerBoardsSummaries(topBoards) +private fun ChildrenBuilder.rightPlayerBoardSummary(board: Board, players: List<PlayerDTO>) { + div { + css { + position = Position.absolute + right = 0.px + bottom = 40.pct } - } + BoardSummaryWithPopover { + this.player = players[board.playerIndex] + this.board = board + this.side = BoardSummarySide.RIGHT - private fun RBuilder.leftPlayerBoardSummary(board: Board) { - styledDiv { css { - position = Position.absolute - left = 0.px - bottom = 40.pct - } - boardSummaryWithPopover(props.players[board.playerIndex], board, BoardSummarySide.LEFT) { - css { - borderTopRightRadius = 0.4.rem - borderBottomRightRadius = 0.4.rem - } + borderTopLeftRadius = 0.4.rem + borderBottomLeftRadius = 0.4.rem } } } +} + +private fun ChildrenBuilder.topPlayerBoardsSummaries(boards: List<Board>, players: List<PlayerDTO>) { + div { + css { + position = Position.absolute + top = 0.px + left = 50.pct + transform = translate((-50).pct) + display = Display.flex + flexDirection = FlexDirection.row + } + boards.forEach { board -> + BoardSummaryWithPopover { + this.player = players[board.playerIndex] + this.board = board + this.side = BoardSummarySide.TOP - private fun RBuilder.rightPlayerBoardSummary(board: Board) { - styledDiv { - css { - position = Position.absolute - right = 0.px - bottom = 40.pct - } - boardSummaryWithPopover(props.players[board.playerIndex], board, BoardSummarySide.RIGHT) { css { - borderTopLeftRadius = 0.4.rem borderBottomLeftRadius = 0.4.rem + borderBottomRightRadius = 0.4.rem + margin = Margin(vertical = 0.rem, horizontal = 2.rem) } } } } +} - private fun RBuilder.topPlayerBoardsSummaries(boards: List<Board>) { - styledDiv { - css { - position = Position.absolute - top = 0.px - left = 50.pct - transform { translate((-50).pct) } - display = Display.flex - flexDirection = FlexDirection.row - } - boards.forEach { board -> - boardSummaryWithPopover(props.players[board.playerIndex], board, BoardSummarySide.TOP) { - css { - borderBottomLeftRadius = 0.4.rem - borderBottomRightRadius = 0.4.rem - margin(horizontal = 2.rem) - } - } - } +private fun ChildrenBuilder.selfBoardSummary(board: Board, players: List<PlayerDTO>) { + div { + css { + position = Position.absolute + bottom = 0.px + left = 0.px } - } + BoardSummary { + this.player = players[board.playerIndex] + this.board = board + this.side = BoardSummarySide.BOTTOM + this.showPreparationStatus = false - private fun RBuilder.selfBoardSummary(board: Board) { - styledDiv { css { - position = Position.absolute - bottom = 0.px - left = 0.px - } - boardSummary( - player = props.players[board.playerIndex], - board = board, BoardSummarySide.BOTTOM, - showPreparationStatus = false, - ) { - css { - borderTopLeftRadius = 0.4.rem - borderTopRightRadius = 0.4.rem - margin(horizontal = 2.rem) - } + borderTopLeftRadius = 0.4.rem + borderTopRightRadius = 0.4.rem + margin = Margin(vertical = 0.rem, horizontal = 2.rem) } } } +} - private fun RBuilder.preparedMove(card: HandCard, move: PlayerMove) { - bpOverlay(isOpen = true, onClose = props.unprepareMove) { - preparedMove(card, move, props.unprepareMove) { - css { +GlobalStyles.fixedCenter } - } +private external interface SayReadyButtonProps : Props { + var currentPlayer: PlayerDTO? + var players: List<PlayerDTO> + var sayReady: () -> Unit +} + +private val SayReadyButton = FC<SayReadyButtonProps>("SayReadyButton") { props -> + val isReady = props.currentPlayer?.isReady == true + val intent = if (isReady) Intent.SUCCESS else Intent.PRIMARY + div { + css { + position = Position.absolute + bottom = 6.rem + left = 50.pct + transform = translate(tx = (-50).pct) + zIndex = integer(2) // go above the wonder (1) and wonder-upgrade cards (0) } - } + BpButtonGroup { + BpButton { + this.large = true + this.disabled = isReady + this.intent = intent + this.icon = if (isReady) IconNames.TICK_CIRCLE else IconNames.PLAY + this.onClick = { props.sayReady() } - private fun RBuilder.sayReadyButton() { - val isReady = props.currentPlayer?.isReady == true - val intent = if (isReady) Intent.SUCCESS else Intent.PRIMARY - styledDiv { - css { - position = Position.absolute - bottom = 6.rem - left = 50.pct - transform { translate(tx = (-50).pct) } - zIndex = 2 // go above the wonder (1) and wonder-upgrade cards (0) + +"READY" } - bpButtonGroup { - bpButton( - large = true, - disabled = isReady, - intent = intent, - icon = if (isReady) IconNames.TICK_CIRCLE else IconNames.PLAY, - onClick = { props.sayReady() }, - ) { - +"READY" - } - // not really a button, but nice for style - bpButton(large = true, icon = IconNames.PEOPLE, disabled = isReady, intent = intent) { - +"${props.players.count { it.isReady }}/${props.players.size}" - } + // not really a button, but nice for style + BpButton { + this.large = true + this.icon = IconNames.PEOPLE + this.disabled = isReady + this.intent = intent + + +"${props.players.count { it.isReady }}/${props.players.size}" } } } } - -fun RBuilder.gameScene() = gameScene {} - -private val gameScene: ComponentClass<GameSceneProps> = - connectStateAndDispatch<GameSceneStateProps, GameSceneDispatchProps, GameSceneProps>( - clazz = GameScene::class, - mapDispatchToProps = { dispatch, _ -> - prepareMove = { move -> dispatch(RequestPrepareMove(move)) } - unprepareMove = { dispatch(RequestUnprepareMove()) } - sayReady = { dispatch(RequestSayReady()) } - leaveGame = { dispatch(RequestLeaveGame()) } - }, - mapStateToProps = { state, _ -> - currentPlayer = state.currentPlayer - players = state.gameState?.players ?: emptyList() - game = state.gameState - preparedMove = state.gameState?.currentPreparedMove - preparedCard = state.gameState?.currentPreparedCard - }, - ) diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameStyles.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameStyles.kt index 86d2d101..bccee3f1 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameStyles.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameStyles.kt @@ -1,100 +1,86 @@ package org.luxons.sevenwonders.ui.components.game -import kotlinx.css.* -import kotlinx.css.properties.* -import styled.StyleSheet -import styled.animation +import csstype.* +import emotion.css.* +import org.luxons.sevenwonders.ui.utils.* -object GameStyles : StyleSheet("GameStyles", isStatic = true) { +object GameStyles { - val totalScore by css { + val totalScore = ClassName { fontWeight = FontWeight.bold } - val civilScore by scoreTagColorCss(Color("#2a73c9")) - val scienceScore by scoreTagColorCss(Color("#0f9960")) - val militaryScore by scoreTagColorCss(Color("#d03232")) - val tradeScore by scoreTagColorCss(Color("#e2c11b")) - val guildScore by scoreTagColorCss(Color("#663399")) - val wonderScore by scoreTagColorCss(Color.darkCyan) - val goldScore by scoreTagColorCss(Color.goldenrod) + val civilScore = scoreTagColorCss(Color("#2a73c9")) + val scienceScore = scoreTagColorCss(Color("#0f9960")) + val militaryScore = scoreTagColorCss(Color("#d03232")) + val tradeScore = scoreTagColorCss(Color("#e2c11b")) + val guildScore = scoreTagColorCss(Color("#663399")) + val wonderScore = scoreTagColorCss(NamedColor.darkcyan) + val goldScore = scoreTagColorCss(NamedColor.goldenrod) - private val sandBgColor = Color.paleGoldenrod + val sandBgColor = NamedColor.palegoldenrod - val fullBoardPreviewPopover by css { - val bgColor = sandBgColor.withAlpha(0.7) - backgroundColor = bgColor - borderRadius = 0.5.rem - padding(all = 0.5.rem) - children(".bp4-popover-content") { - background = "none" // overrides default white background - } - descendants(".bp4-popover-arrow-fill") { - put("fill", bgColor.toString()) // overrides default white arrow - } - descendants(".bp4-popover-arrow::before") { - // The popover arrow is implemented with a simple square rotated 45 degrees (like a rhombus). - // Since we use a semi-transparent background, we can see the box shadow of the rest of the arrow through - // the popover, and thus we see the square. This boxShadow(transparent) is to avoid that. - boxShadow(Color.transparent) - } - } - - val fullBoardPreview by css { + val fullBoardPreview = ClassName { width = 40.vw height = 50.vh } - val dimmedCard by css { - filter = "brightness(60%) grayscale(50%)" + val dimmedCard = ClassName { + filter = brightness(60.pct) + grayscale(50.pct) } - val transactionsSelector by css { + val transactionsSelector = ClassName { backgroundColor = sandBgColor width = 40.rem // default is 500px, we want to fit players on the side children(".bp4-dialog-header") { - background = "none" // overrides default white background + background = None.none // overrides default white background } } - val bestPrice by css { + val bestPrice = ClassName { fontWeight = FontWeight.bold color = rgb(50, 120, 50) - transform { - rotate((-20).deg) - } + transform = rotate((-20).deg) } - val discardMoveText by css { + val discardMoveText = ClassName { display = Display.flex - alignItems = Align.center + alignItems = AlignItems.center height = 3.rem fontSize = 2.rem - color = Color.goldenrod + color = NamedColor.goldenrod fontWeight = FontWeight.bold - borderTop(0.2.rem, BorderStyle.solid, Color.goldenrod) - borderBottom(0.2.rem, BorderStyle.solid, Color.goldenrod) + borderTop = Border(0.2.rem, LineStyle.solid, NamedColor.goldenrod) + borderBottom = Border(0.2.rem, LineStyle.solid, NamedColor.goldenrod) } - val scoreBoard by css { + val scoreBoard = ClassName { backgroundColor = sandBgColor } - private fun scoreTagColorCss(color: Color) = css { + private fun scoreTagColorCss(color: Color) = ClassName { backgroundColor = color } - val pulsatingRed by css { - animation( + val pulsatingRed = ClassName { + animation = Animation( + name = keyframes { + to { + boxShadow = BoxShadow( + inset = BoxShadowInset.inset, + offsetX = 0.px, + offsetY = 0.px, + blurRadius = 20.px, + spreadRadius = 8.px, + color = NamedColor.red, + ) + } + }, duration = 2.s, - iterationCount = IterationCount.infinite, - direction = AnimationDirection.alternate, - ) { - to { - boxShadowInset(color = Color.red, blurRadius = 20.px, spreadRadius = 8.px) - } - } + ) + animationIterationCount = AnimationIterationCount.infinite + animationDirection = AnimationDirection.alternate } } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt index da17c987..a136465d 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt @@ -1,26 +1,38 @@ package org.luxons.sevenwonders.ui.components.game import blueprintjs.core.* -import blueprintjs.icons.IconNames -import kotlinx.css.* -import kotlinx.css.Position -import kotlinx.css.properties.* -import kotlinx.html.DIV -import org.luxons.sevenwonders.client.GameState -import org.luxons.sevenwonders.client.getOwnBoard +import blueprintjs.icons.* +import csstype.* +import csstype.Position +import emotion.react.* +import org.luxons.sevenwonders.client.* import org.luxons.sevenwonders.model.* -import org.luxons.sevenwonders.model.boards.Board -import org.luxons.sevenwonders.model.cards.CardPlayability -import org.luxons.sevenwonders.model.cards.HandCard -import org.luxons.sevenwonders.model.resources.ResourceTransactionOptions -import org.luxons.sevenwonders.model.wonders.WonderBuildability +import org.luxons.sevenwonders.model.boards.* +import org.luxons.sevenwonders.model.cards.* +import org.luxons.sevenwonders.model.resources.* +import org.luxons.sevenwonders.model.wonders.* +import org.luxons.sevenwonders.ui.utils.* import react.* -import react.dom.attrs -import styled.StyledDOMBuilder -import styled.css -import styled.styledDiv -import web.html.* -import kotlin.math.absoluteValue +import react.dom.html.ReactHTML.div +import kotlin.math.* + +fun ChildrenBuilder.handCards( + game: GameState, + prepareMove: (PlayerMove) -> Unit, + startTransactionsSelection: (TransactionSelectorState) -> Unit, +) { + HandCards { + this.action = game.action + this.ownBoard = game.getOwnBoard() + this.preparedMove = game.currentPreparedMove + this.prepareMove = { moveType: MoveType, card: HandCard, transactionOptions: ResourceTransactionOptions -> + when (transactionOptions.size) { + 1 -> prepareMove(PlayerMove(moveType, card.name, transactionOptions.single())) + else -> startTransactionsSelection(TransactionSelectorState(moveType, card, transactionOptions)) + } + } + } +} private enum class HandAction( val buttonTitle: String, @@ -33,145 +45,154 @@ private enum class HandAction( COPY_GUILD("Copy this guild card", MoveType.COPY_GUILD, "duplicate") } -external interface HandProps : PropsWithChildren { +external interface HandCardsProps : Props { var action: TurnAction var ownBoard: Board var preparedMove: PlayerMove? - var prepareMove: (PlayerMove) -> Unit - var startTransactionsSelection: (TransactionSelectorState) -> Unit + var prepareMove: (MoveType, HandCard, ResourceTransactionOptions) -> Unit } -class HandComponent(props: HandProps) : RComponent<HandProps, State>(props) { - - override fun RBuilder.render() { - val hand = props.action.cardsToPlay() ?: return - styledDiv { - css { - handStyle() - } - hand.filter { it.name != props.preparedMove?.cardName }.forEachIndexed { index, c -> - handCard(card = c) { - attrs { - key = index.toString() - } - } +private val HandCards = FC<HandCardsProps>("HandCards") { props -> + val hand = props.action.cardsToPlay() ?: return@FC + div { + css { + handStyle() + } + hand.filter { it.name != props.preparedMove?.cardName }.forEachIndexed { index, c -> + HandCard { + card = c + action = props.action + ownBoard = props.ownBoard + prepareMove = props.prepareMove + key = index.toString() } } } +} - private fun TurnAction.cardsToPlay(): List<HandCard>? = when (this) { - is TurnAction.PlayFromHand -> hand - is TurnAction.PlayFromDiscarded -> discardedCards - is TurnAction.PickNeighbourGuild -> neighbourGuildCards - is TurnAction.SayReady, - is TurnAction.Wait, - is TurnAction.WatchScore -> null - } +private fun TurnAction.cardsToPlay(): List<HandCard>? = when (this) { + is TurnAction.PlayFromHand -> hand + is TurnAction.PlayFromDiscarded -> discardedCards + is TurnAction.PickNeighbourGuild -> neighbourGuildCards + is TurnAction.SayReady, + is TurnAction.Wait, + is TurnAction.WatchScore -> null +} - private fun RBuilder.handCard( - card: HandCard, - block: StyledDOMBuilder<DIV>.() -> Unit, - ) { - styledDiv { +private external interface HandCardProps : Props { + var card: HandCard + var action: TurnAction + var ownBoard: Board + var prepareMove: (MoveType, HandCard, ResourceTransactionOptions) -> Unit +} + +private val HandCard = FC<HandCardProps>("HandCard") { props -> + div { + css(ClassName("hand-card")) { + alignItems = AlignItems.flexEnd + display = Display.grid + margin = Margin(all = 0.2.rem) + } + CardImage { css { - handCardStyle() - } - block() - cardImage(card) { - css { - val isPlayable = card.playability.isPlayable || props.ownBoard.canPlayAnyCardForFree - handCardImgStyle(isPlayable) - } + val isPlayable = props.card.playability.isPlayable || props.ownBoard.canPlayAnyCardForFree + handCardImgStyle(isPlayable) } - actionButtons(card) + this.card = props.card } + actionButtons(props.card, props.action, props.ownBoard, props.prepareMove) } +} - private fun RBuilder.actionButtons(card: HandCard) { - styledDiv { - css { - justifyContent = JustifyContent.center - alignItems = Align.flexEnd - display = Display.none - gridRow = GridRow("1") - gridColumn = GridColumn("1") +private fun ChildrenBuilder.actionButtons( + card: HandCard, + action: TurnAction, + ownBoard: Board, + prepareMove: (MoveType, HandCard, ResourceTransactionOptions) -> Unit, +) { + div { + css { + justifyContent = JustifyContent.center + alignItems = AlignItems.flexEnd + display = None.none + gridRow = integer(1) + gridColumn = integer(1) - ancestorHover(".hand-card") { - display = Display.flex - } + ancestorHover(".hand-card") { + display = Display.flex } - bpButtonGroup { - val action = props.action - when (action) { - is TurnAction.PlayFromHand -> { - playCardButton(card, HandAction.PLAY) - if (props.ownBoard.canPlayAnyCardForFree) { - playCardButton(card.copy(playability = CardPlayability.SPECIAL_FREE), HandAction.PLAY_FREE) - } + } + BpButtonGroup { + when (action) { + is TurnAction.PlayFromHand -> { + playCardButton(card, HandAction.PLAY, prepareMove) + if (ownBoard.canPlayAnyCardForFree) { + playCardButton(card.copy(playability = CardPlayability.SPECIAL_FREE), HandAction.PLAY_FREE, prepareMove) } - is TurnAction.PlayFromDiscarded -> playCardButton(card, HandAction.PLAY_FREE_DISCARDED) - is TurnAction.PickNeighbourGuild -> playCardButton(card, HandAction.COPY_GUILD) - is TurnAction.SayReady, - is TurnAction.Wait, - is TurnAction.WatchScore -> error("unsupported action in hand card: $action") - } - - if (action.allowsBuildingWonder()) { - upgradeWonderButton(card) - } - if (action.allowsDiscarding()) { - discardButton(card) } + is TurnAction.PlayFromDiscarded -> playCardButton(card, HandAction.PLAY_FREE_DISCARDED, prepareMove) + is TurnAction.PickNeighbourGuild -> playCardButton(card, HandAction.COPY_GUILD, prepareMove) + is TurnAction.SayReady, + is TurnAction.Wait, + is TurnAction.WatchScore -> error("unsupported action in hand card: $action") } - } - } - private fun RElementBuilder<ButtonGroupProps>.playCardButton(card: HandCard, handAction: HandAction) { - bpButton( - title = "${handAction.buttonTitle} (${cardPlayabilityInfo(card.playability)})", - large = true, - intent = Intent.SUCCESS, - disabled = !card.playability.isPlayable, - onClick = { prepareMove(handAction.moveType, card, card.playability.transactionOptions) }, - ) { - bpIcon(handAction.icon) - if (card.playability.isPlayable && !card.playability.isFree) { - priceInfo(card.playability.minPrice) + if (action.allowsBuildingWonder()) { + upgradeWonderButton(card, ownBoard.wonder.buildability, prepareMove) + } + if (action.allowsDiscarding()) { + discardButton(card, prepareMove) } } } +} - private fun RElementBuilder<ButtonGroupProps>.upgradeWonderButton(card: HandCard) { - val wonderBuildability = props.ownBoard.wonder.buildability - bpButton( - title = "UPGRADE WONDER (${wonderBuildabilityInfo(wonderBuildability)})", - large = true, - intent = Intent.PRIMARY, - disabled = !wonderBuildability.isBuildable, - onClick = { prepareMove(MoveType.UPGRADE_WONDER, card, wonderBuildability.transactionsOptions) }, - ) { - bpIcon("key-shift") - if (wonderBuildability.isBuildable && !wonderBuildability.isFree) { - priceInfo(wonderBuildability.minPrice) - } +private fun ChildrenBuilder.playCardButton( + card: HandCard, + handAction: HandAction, + prepareMove: (MoveType, HandCard, ResourceTransactionOptions) -> Unit, +) { + BpButton { + title = "${handAction.buttonTitle} (${cardPlayabilityInfo(card.playability)})" + large = true + intent = Intent.SUCCESS + disabled = !card.playability.isPlayable + onClick = { prepareMove(handAction.moveType, card, card.playability.transactionOptions) } + + BpIcon { icon = handAction.icon } + + if (card.playability.isPlayable && !card.playability.isFree) { + priceInfo(card.playability.minPrice) } } +} - private fun prepareMove(moveType: MoveType, card: HandCard, transactionOptions: ResourceTransactionOptions) { - when (transactionOptions.size) { - 1 -> props.prepareMove(PlayerMove(moveType, card.name, transactionOptions.single())) - else -> props.startTransactionsSelection(TransactionSelectorState(moveType, card, transactionOptions)) +private fun ChildrenBuilder.upgradeWonderButton( + card: HandCard, + wonderBuildability: WonderBuildability, + prepareMove: (MoveType, HandCard, ResourceTransactionOptions) -> Unit, +) { + BpButton { + title = "UPGRADE WONDER (${wonderBuildabilityInfo(wonderBuildability)})" + large = true + intent = Intent.PRIMARY + disabled = !wonderBuildability.isBuildable + onClick = { prepareMove(MoveType.UPGRADE_WONDER, card, wonderBuildability.transactionsOptions) } + + BpIcon { icon = IconNames.KEY_SHIFT } + if (wonderBuildability.isBuildable && !wonderBuildability.isFree) { + priceInfo(wonderBuildability.minPrice) } } +} - private fun RElementBuilder<ButtonGroupProps>.discardButton(card: HandCard) { - bpButton( - title = "DISCARD (+3 coins)", // TODO remove hardcoded value - large = true, - intent = Intent.DANGER, - icon = IconNames.CROSS, - onClick = { props.prepareMove(PlayerMove(MoveType.DISCARD, card.name)) }, - ) +private fun ChildrenBuilder.discardButton(card: HandCard, prepareMove: (MoveType, HandCard, ResourceTransactionOptions) -> Unit) { + BpButton { + title = "DISCARD (+3 coins)" // TODO remove hardcoded value + large = true + intent = Intent.DANGER + icon = IconNames.CROSS + onClick = { prepareMove(MoveType.DISCARD, card, emptyList()) } } } @@ -196,15 +217,14 @@ private fun pricePrefix(amount: Int) = when { else -> "" } -private fun RElementBuilder<ButtonProps<HTMLButtonElement>>.priceInfo(amount: Int) { - val size = 1.rem +private fun ChildrenBuilder.priceInfo(amount: Int) { goldIndicator( amount = amount, amountPosition = TokenCountPosition.OVER, - imgSize = size, + imgSize = 1.rem, customCountStyle = { - fontFamily = "sans-serif" - fontSize = size * 0.8 + fontFamily = FontFamily.sansSerif + fontSize = 0.8.rem }, ) { css { @@ -215,68 +235,41 @@ private fun RElementBuilder<ButtonProps<HTMLButtonElement>>.priceInfo(amount: In } } -private fun CssBuilder.handStyle() { - alignItems = Align.center +private fun PropertiesBuilder.handStyle() { + alignItems = AlignItems.center bottom = 0.px display = Display.flex height = 345.px left = 50.pct maxHeight = 25.vw position = Position.absolute - transform { - translate(tx = (-50).pct, ty = 65.pct) - } - transition(duration = 0.5.s) - zIndex = 30 + transform = translate(tx = (-50).pct, ty = 65.pct) + transition = Transition(TransitionProperty.all, duration = 0.5.s, timingFunction = TransitionTimingFunction.ease) + zIndex = integer(30) hover { bottom = 1.rem - zIndex = 60 - transform { - translate(tx = (-50).pct, ty = 0.pct) - } + zIndex = integer(60) + transform = translate(tx = (-50).pct, ty = 0.pct) } } -private fun CssBuilder.handCardStyle() { - classes.add("hand-card") - alignItems = Align.flexEnd - display = Display.grid - margin(all = 0.2.rem) -} - -private fun CssBuilder.handCardImgStyle(isPlayable: Boolean) { - gridRow = GridRow("1") - gridColumn = GridColumn("1") +private fun PropertiesBuilder.handCardImgStyle(isPlayable: Boolean) { + gridRow = integer(1) + gridColumn = integer(1) maxWidth = 13.vw maxHeight = 60.vh - transition(duration = 0.1.s) + transition = Transition(TransitionProperty.all, duration = 0.1.s, timingFunction = TransitionTimingFunction.ease) width = 11.rem ancestorHover(".hand-card") { - boxShadow(offsetX = 0.px, offsetY = 10.px, blurRadius = 40.px, color = Color.black) + boxShadow = BoxShadow(offsetX = 0.px, offsetY = 10.px, blurRadius = 40.px, color = NamedColor.black) width = 14.rem maxWidth = 15.vw maxHeight = 90.vh } if (!isPlayable) { - filter = "grayscale(50%) contrast(50%)" - } -} - -fun RBuilder.handCards( - game: GameState, - prepareMove: (PlayerMove) -> Unit, - startTransactionsSelection: (TransactionSelectorState) -> Unit, -) { - child(HandComponent::class) { - attrs { - this.action = game.action - this.ownBoard = game.getOwnBoard() - this.preparedMove = game.currentPreparedMove - this.prepareMove = prepareMove - this.startTransactionsSelection = startTransactionsSelection - } + filter = grayscale(50.pct) + contrast(50.pct) } } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/HandRotationIndicator.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/HandRotationIndicator.kt index 4761ac13..9e6874d3 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/HandRotationIndicator.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/HandRotationIndicator.kt @@ -1,44 +1,51 @@ package org.luxons.sevenwonders.ui.components.game -import blueprintjs.core.bpIcon -import kotlinx.css.* -import kotlinx.html.title -import org.luxons.sevenwonders.model.cards.HandRotationDirection -import react.RBuilder -import react.dom.attrs -import styled.css -import styled.styledDiv -import styled.styledImg +import blueprintjs.core.* +import blueprintjs.icons.* +import csstype.* +import csstype.Position +import emotion.react.* +import org.luxons.sevenwonders.model.cards.* +import react.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.img -fun RBuilder.handRotationIndicator(direction: HandRotationDirection) { - styledDiv { +fun ChildrenBuilder.handRotationIndicator(direction: HandRotationDirection) { + div { css { position = Position.absolute display = Display.flex - alignItems = Align.center + alignItems = AlignItems.center bottom = 25.vh } - attrs { - title = "Your hand will be passed to the player on your $direction after playing this card." - } + + title = "Your hand will be passed to the player on your $direction after playing this card." + val sideDistance = 2.rem when (direction) { HandRotationDirection.LEFT -> { css { left = sideDistance } - bpIcon("arrow-left", size = 25) + BpIcon { + icon = IconNames.ARROW_LEFT + size = 25 + } handCardsImg() } HandRotationDirection.RIGHT -> { css { right = sideDistance } handCardsImg() - bpIcon("arrow-right", size = 25) + BpIcon { + icon = IconNames.ARROW_RIGHT + size = 25 + } } } } } -private fun RBuilder.handCardsImg() { - styledImg(src = "images/hand-cards5.png") { +private fun ChildrenBuilder.handCardsImg() { + img { + src = "images/hand-cards5.png" css { width = 4.rem } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PlayerPreparedCard.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PlayerPreparedCard.kt deleted file mode 100644 index b42d3b81..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PlayerPreparedCard.kt +++ /dev/null @@ -1,82 +0,0 @@ -package org.luxons.sevenwonders.ui.components.game - -import kotlinx.css.* -import kotlinx.css.properties.* -import kotlinx.html.title -import org.luxons.sevenwonders.model.api.PlayerDTO -import org.luxons.sevenwonders.model.cards.CardBack -import org.luxons.sevenwonders.ui.redux.connectStateWithOwnProps -import react.* -import react.dom.attrs -import styled.animation -import styled.css -import styled.styledDiv -import styled.styledImg - -external interface PlayerPreparedCardProps : PropsWithChildren { - var playerDisplayName: String - var cardBack: CardBack? -} - -external interface PlayerPreparedCardContainerProps : PropsWithChildren { - var playerDisplayName: String - var username: String -} - -fun RBuilder.playerPreparedCard(player: PlayerDTO) = playerPreparedCard { - attrs { - this.playerDisplayName = player.displayName - this.username = player.username - } -} - -private class PlayerPreparedCard(props: PlayerPreparedCardProps) : RComponent<PlayerPreparedCardProps, State>(props) { - - override fun RBuilder.render() { - val cardBack = props.cardBack - val sideSize = 30.px - styledDiv { - css { - width = sideSize - height = sideSize - } - attrs { - title = if (cardBack == null) { - "${props.playerDisplayName} is still thinking…" - } else { - "${props.playerDisplayName} is ready to play this turn" - } - } - if (cardBack != null) { - cardBackImage(cardBack) { - css { - maxHeight = sideSize - } - } - } else { - styledImg(src = "images/gear-50.png") { - css { - maxHeight = sideSize - animation( - duration = 1.5.s, - iterationCount = IterationCount.infinite, - timing = cubicBezier(0.2, 0.9, 0.7, 1.3) - ) { - to { - transform { rotate(360.deg) } - } - } - } - } - } - } - } -} - -private val playerPreparedCard = connectStateWithOwnProps( - clazz = PlayerPreparedCard::class, - mapStateToProps = { state, ownProps: PlayerPreparedCardContainerProps -> - playerDisplayName = ownProps.playerDisplayName - cardBack = state.gameState?.preparedCardsByUsername?.get(ownProps.username) - }, -) diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PlayerPreparedCardPresenter.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PlayerPreparedCardPresenter.kt new file mode 100644 index 00000000..26678309 --- /dev/null +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PlayerPreparedCardPresenter.kt @@ -0,0 +1,79 @@ +package org.luxons.sevenwonders.ui.components.game + +import csstype.* +import emotion.css.* +import emotion.react.* +import org.luxons.sevenwonders.model.cards.* +import org.luxons.sevenwonders.ui.redux.* +import org.luxons.sevenwonders.ui.utils.* +import react.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.img + +external interface PlayerPreparedCardProps : Props { + var playerDisplayName: String + var username: String +} + +val PlayerPreparedCard = FC<PlayerPreparedCardProps>("PlayerPreparedCard") { props -> + val cardBack = useSwSelector { it.gameState?.preparedCardsByUsername?.get(props.username) } + + PlayerPreparedCardPresenter { + this.playerDisplayName = props.playerDisplayName + this.cardBack = cardBack + } +} + +external interface PlayerPreparedCardPresenterProps : Props { + var playerDisplayName: String + var cardBack: CardBack? +} + +private val PlayerPreparedCardPresenter = FC<PlayerPreparedCardPresenterProps>("PlayerPreparedCardPresenter") { props -> + val cardBack = props.cardBack + val sideSize = 30.px + div { + css { + width = sideSize + height = sideSize + } + title = if (cardBack == null) { + "${props.playerDisplayName} is still thinking…" + } else { + "${props.playerDisplayName} is ready to play this turn" + } + + if (cardBack != null) { + CardBackImage { + this.cardBack = cardBack + css { + maxHeight = sideSize + } + } + } else { + RotatingGear { + css { + maxHeight = sideSize + } + } + } + } +} + +private val RotatingGear = FC<PropsWithClassName> { props -> + img { + src = "images/gear-50.png" + css(props.className) { + animation = Animation( + name = keyframes { + to { + transform = rotate(360.deg) + } + }, + duration = 1.5.s, + timingFunction = cubicBezier(0.2, 0.9, 0.7, 1.3), + ) + animationIterationCount = AnimationIterationCount.infinite + } + } +} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PreparedMove.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PreparedMove.kt index a56bbc00..b03505d6 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PreparedMove.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PreparedMove.kt @@ -1,32 +1,31 @@ package org.luxons.sevenwonders.ui.components.game -import blueprintjs.core.Intent -import blueprintjs.core.bpButton -import blueprintjs.icons.IconNames -import kotlinx.css.* -import kotlinx.css.properties.* -import kotlinx.html.DIV -import org.luxons.sevenwonders.model.MoveType -import org.luxons.sevenwonders.model.PlayerMove -import org.luxons.sevenwonders.model.cards.HandCard -import org.luxons.sevenwonders.ui.components.GlobalStyles -import react.RBuilder -import styled.StyledDOMBuilder -import styled.css -import styled.styledDiv -import styled.styledImg +import blueprintjs.core.* +import blueprintjs.icons.* +import csstype.* +import csstype.Position +import emotion.react.* +import org.luxons.sevenwonders.model.* +import org.luxons.sevenwonders.model.cards.* +import org.luxons.sevenwonders.ui.components.* +import react.* +import react.dom.html.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.img +import web.html.* -fun RBuilder.preparedMove( +fun ChildrenBuilder.preparedMove( card: HandCard, move: PlayerMove, unprepareMove: () -> Unit, - block: StyledDOMBuilder<DIV>.() -> Unit, + block: HTMLAttributes<HTMLDivElement>.() -> Unit, ) { - styledDiv { + div { block() - cardImage(card) { + CardImage { + this.card = card if (move.type == MoveType.DISCARD || move.type == MoveType.UPGRADE_WONDER) { - css { +GameStyles.dimmedCard } + this.className = GameStyles.dimmedCard } } if (move.type == MoveType.DISCARD) { @@ -35,43 +34,39 @@ fun RBuilder.preparedMove( if (move.type == MoveType.UPGRADE_WONDER) { upgradeWonderSymbol() } - styledDiv { + div { css { position = Position.absolute top = 0.px right = 0.px } - bpButton( - icon = IconNames.CROSS, - title = "Cancel prepared move", - small = true, - intent = Intent.DANGER, - onClick = { unprepareMove() }, - ) + BpButton { + icon = IconNames.CROSS + title = "Cancel prepared move" + small = true + intent = Intent.DANGER + onClick = { unprepareMove() } + } } } } -private fun StyledDOMBuilder<DIV>.discardText() { - styledDiv { - css { - +GlobalStyles.centerInPositionedParent - +GameStyles.discardMoveText - } +private fun ChildrenBuilder.discardText() { + div { + css(GlobalStyles.centerInPositionedParent, GameStyles.discardMoveText) {} +"DISCARD" } } -private fun StyledDOMBuilder<DIV>.upgradeWonderSymbol() { - styledImg(src = "/images/wonder-upgrade-bright.png") { +private fun ChildrenBuilder.upgradeWonderSymbol() { + img { + src = "/images/wonder-upgrade-bright.png" css { width = 8.rem position = Position.absolute left = 10.pct top = 50.pct - transform { - translate((-50).pct, (-50).pct) - } + transform = translate((-50).pct, (-50).pct) } } } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/ScoreTable.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/ScoreTable.kt index 01bf139b..c9e530ce 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/ScoreTable.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/ScoreTable.kt @@ -1,52 +1,55 @@ package org.luxons.sevenwonders.ui.components.game import blueprintjs.core.* +import blueprintjs.icons.* import csstype.* -import kotlinx.css.* -import kotlinx.css.Display -import kotlinx.css.FlexDirection -import kotlinx.css.TextAlign -import kotlinx.css.VerticalAlign -import kotlinx.css.px -import kotlinx.css.rem -import kotlinx.html.TD -import kotlinx.html.TH -import org.luxons.sevenwonders.model.api.PlayerDTO -import org.luxons.sevenwonders.model.score.ScoreBoard -import org.luxons.sevenwonders.model.score.ScoreCategory -import org.luxons.sevenwonders.ui.components.GlobalStyles +import emotion.react.* +import org.luxons.sevenwonders.model.api.* +import org.luxons.sevenwonders.model.score.* +import org.luxons.sevenwonders.ui.components.* import org.luxons.sevenwonders.ui.utils.* -import react.RBuilder -import react.dom.* -import styled.* - -fun RBuilder.scoreTableOverlay(scoreBoard: ScoreBoard, players: List<PlayerDTO>, leaveGame: () -> Unit) { - bpOverlay(isOpen = true) { - bpCard { - attrs { - val fixedCenterClass = GlobalStyles.getClassName { it::fixedCenter } - val scoreBoardClass = GameStyles.getClassName { it::scoreBoard } - className = ClassName("$fixedCenterClass $scoreBoardClass") - } - styledDiv { - css { +import react.* +import react.dom.html.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.h1 +import react.dom.html.ReactHTML.sup +import react.dom.html.ReactHTML.tbody +import react.dom.html.ReactHTML.td +import react.dom.html.ReactHTML.th +import react.dom.html.ReactHTML.thead +import react.dom.html.ReactHTML.tr + +fun ChildrenBuilder.scoreTableOverlay(scoreBoard: ScoreBoard, players: List<PlayerDTO>, leaveGame: () -> Unit) { + BpOverlay { + isOpen = true + + BpCard { + css(GlobalStyles.fixedCenter, GameStyles.scoreBoard) {} + + div { + // FIXME this doesn't look right, the scoreBoard class is applied at both levels + css(GameStyles.scoreBoard) { // loads the styles so that they can be picked up by bpCard display = Display.flex flexDirection = FlexDirection.column - alignItems = Align.center - +GameStyles.scoreBoard // loads the styles so that they can be picked up by bpCard + alignItems = AlignItems.center } - styledH1 { + h1 { css { marginTop = 0.px } +"Score Board" } scoreTable(scoreBoard, players) - styledDiv { + div { css { marginTop = 1.rem } - bpButton(intent = Intent.WARNING, rightIcon = "log-out", large = true, onClick = { leaveGame() }) { + BpButton { + intent = Intent.WARNING + rightIcon = "log-out" + large = true + onClick = { leaveGame() } + +"LEAVE" } } @@ -55,18 +58,32 @@ fun RBuilder.scoreTableOverlay(scoreBoard: ScoreBoard, players: List<PlayerDTO>, } } -private fun RBuilder.scoreTable(scoreBoard: ScoreBoard, players: List<PlayerDTO>) { - bpHtmlTable(bordered = false, interactive = true) { +private fun ChildrenBuilder.scoreTable(scoreBoard: ScoreBoard, players: List<PlayerDTO>) { + BpHTMLTable { + bordered = false + interactive = true + thead { tr { - centeredTh { +"Rank" } - centeredTh { - attrs { colSpan = "2" } + th { + fullCenterInlineStyle() + +"Rank" + } + th { + fullCenterInlineStyle() + colSpan = 2 + +"Player" } - centeredTh { +"Score" } + th { + fullCenterInlineStyle() + +"Score" + } ScoreCategory.values().forEach { - centeredTh { +it.title } + th { + fullCenterInlineStyle() + +it.title + } } } } @@ -74,28 +91,44 @@ private fun RBuilder.scoreTable(scoreBoard: ScoreBoard, players: List<PlayerDTO> scoreBoard.scores.forEachIndexed { index, score -> val player = players[score.playerIndex] tr { - centeredTd { ordinal(scoreBoard.ranks[index]) } - centeredTd { bpIcon(player.icon?.name ?: "user", size = 25) } - styledTd { + td { + fullCenterInlineStyle() + ordinal(scoreBoard.ranks[index]) + } + td { + fullCenterInlineStyle() + BpIcon { + icon = player.icon?.name ?: IconNames.USER + size = 25 + } + } + td { inlineStyles { verticalAlign = VerticalAlign.middle } +player.displayName } - centeredTd { - bpTag(large = true, round = true, minimal = true) { - attrs { - this.className = GameStyles.getTypedClassName { it::totalScore } - } + td { + fullCenterInlineStyle() + BpTag { + large = true + round = true + minimal = true + className = GameStyles.totalScore + +"${score.totalPoints}" } } ScoreCategory.values().forEach { cat -> - centeredTd { - bpTag(large = true, round = true, icon = cat.icon, fill = true) { - attrs { - this.className = classNameForCategory(cat) - } + td { + fullCenterInlineStyle() + BpTag { + large = true + round = true + fill = true + icon = cat.icon + className = classNameForCategory(cat) + +"${score.pointsByCategory[cat]}" } } @@ -106,7 +139,7 @@ private fun RBuilder.scoreTable(scoreBoard: ScoreBoard, players: List<PlayerDTO> } } -private fun RBuilder.ordinal(value: Int) { +private fun ChildrenBuilder.ordinal(value: Int) { +"$value" sup { +value.ordinalIndicator() } } @@ -118,49 +151,33 @@ private fun Int.ordinalIndicator() = when { else -> "th" } -private fun RBuilder.centeredTh(block: RDOMBuilder<TH>.() -> Unit) { - th { - // inline styles necessary to overcome blueprintJS overrides - inlineStyles { - textAlign = TextAlign.center - verticalAlign = VerticalAlign.middle - } - block() +private fun HTMLAttributes<*>.fullCenterInlineStyle() { + // inline styles necessary to overcome blueprintJS overrides + inlineStyles { + textAlign = TextAlign.center + verticalAlign = VerticalAlign.middle } } -private fun RBuilder.centeredTd(block: RDOMBuilder<TD>.() -> Unit) { - td { - // inline styles necessary to overcome blueprintJS overrides - inlineStyles { - textAlign = TextAlign.center - verticalAlign = VerticalAlign.middle - } - block() - } -} - -private fun classNameForCategory(cat: ScoreCategory): ClassName = GameStyles.getTypedClassName { - when (cat) { - ScoreCategory.CIVIL -> it::civilScore - ScoreCategory.SCIENCE -> it::scienceScore - ScoreCategory.MILITARY -> it::militaryScore - ScoreCategory.TRADE -> it::tradeScore - ScoreCategory.GUILD -> it::guildScore - ScoreCategory.WONDER -> it::wonderScore - ScoreCategory.GOLD -> it::goldScore - } +private fun classNameForCategory(cat: ScoreCategory): ClassName = when (cat) { + ScoreCategory.CIVIL -> GameStyles.civilScore + ScoreCategory.SCIENCE -> GameStyles.scienceScore + ScoreCategory.MILITARY -> GameStyles.militaryScore + ScoreCategory.TRADE -> GameStyles.tradeScore + ScoreCategory.GUILD -> GameStyles.guildScore + ScoreCategory.WONDER -> GameStyles.wonderScore + ScoreCategory.GOLD -> GameStyles.goldScore } private val ScoreCategory.icon: String get() = when (this) { - ScoreCategory.CIVIL -> "office" - ScoreCategory.SCIENCE -> "lab-test" - ScoreCategory.MILITARY -> "cut" - ScoreCategory.TRADE -> "swap-horizontal" - ScoreCategory.GUILD -> "clean" // stars - ScoreCategory.WONDER -> "symbol-triangle-up" - ScoreCategory.GOLD -> "dollar" + ScoreCategory.CIVIL -> IconNames.OFFICE + ScoreCategory.SCIENCE -> IconNames.LAB_TEST + ScoreCategory.MILITARY -> IconNames.CUT + ScoreCategory.TRADE -> IconNames.SWAP_HORIZONTAL + ScoreCategory.GUILD -> IconNames.CLEAN // stars + ScoreCategory.WONDER -> IconNames.SYMBOL_TRIANGLE_UP + ScoreCategory.GOLD -> IconNames.DOLLAR } // Potentially useful emojis: diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Tokens.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Tokens.kt index da5fc5ed..c7942f59 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Tokens.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Tokens.kt @@ -1,14 +1,15 @@ package org.luxons.sevenwonders.ui.components.game -import kotlinx.css.* -import kotlinx.html.DIV -import kotlinx.html.IMG -import kotlinx.html.title -import org.luxons.sevenwonders.model.resources.ResourceType -import org.luxons.sevenwonders.ui.components.GlobalStyles -import react.RBuilder -import react.dom.attrs -import styled.* +import csstype.* +import emotion.react.* +import org.luxons.sevenwonders.model.resources.* +import org.luxons.sevenwonders.ui.components.* +import react.* +import react.dom.html.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.img +import react.dom.html.ReactHTML.span +import web.html.* private fun getResourceTokenName(resourceType: ResourceType) = "resources/${resourceType.toString().lowercase()}" @@ -20,12 +21,12 @@ enum class TokenCountPosition { OVER, } -fun RBuilder.goldIndicator( +fun ChildrenBuilder.goldIndicator( amount: Int, amountPosition: TokenCountPosition = TokenCountPosition.OVER, - imgSize: LinearDimension = 3.rem, - customCountStyle: CssBuilder.() -> Unit = {}, - block: StyledDOMBuilder<DIV>.() -> Unit = {}, + imgSize: Length = 3.rem, + customCountStyle: PropertiesBuilder.() -> Unit = {}, + block: HTMLAttributes<HTMLDivElement>.() -> Unit = {}, ) { tokenWithCount( tokenName = "coin", @@ -38,54 +39,39 @@ fun RBuilder.goldIndicator( ) } -fun RBuilder.resourceWithCount( +fun ChildrenBuilder.resourceImage( resourceType: ResourceType, - count: Int, title: String = resourceType.toString(), - imgSize: LinearDimension? = null, - countPosition: TokenCountPosition = TokenCountPosition.RIGHT, - brightText: Boolean = false, - customCountStyle: CssBuilder.() -> Unit = {}, - block: StyledDOMBuilder<DIV>.() -> Unit = {}, + size: Length?, ) { - tokenWithCount( - tokenName = getResourceTokenName(resourceType), - count = count, - title = title, - imgSize = imgSize, - countPosition = countPosition, - brightText = brightText, - customCountStyle = customCountStyle, - block = block - ) -} - -fun RBuilder.resourceImage( - resourceType: ResourceType, - title: String = resourceType.toString(), - size: LinearDimension?, - block: StyledDOMBuilder<IMG>.() -> Unit = {}, -) { - tokenImage(getResourceTokenName(resourceType), title, size, block) + TokenImage { + this.tokenName = getResourceTokenName(resourceType) + this.title = title + this.size = size + } } -fun RBuilder.tokenWithCount( +fun ChildrenBuilder.tokenWithCount( tokenName: String, count: Int, title: String = tokenName, - imgSize: LinearDimension? = null, + imgSize: Length? = null, countPosition: TokenCountPosition = TokenCountPosition.RIGHT, brightText: Boolean = false, - customCountStyle: CssBuilder.() -> Unit = {}, - block: StyledDOMBuilder<DIV>.() -> Unit = {}, + customCountStyle: PropertiesBuilder.() -> Unit = {}, + block: HTMLAttributes<HTMLDivElement>.() -> Unit = {}, ) { - styledDiv { + div { block() - val tokenCountSize = if (imgSize != null) imgSize * 0.6 else 1.5.rem + val tokenCountSize = if (imgSize != null) 0.6 * imgSize else 1.5.rem when (countPosition) { TokenCountPosition.RIGHT -> { - tokenImage(tokenName, title = title, size = imgSize) - styledSpan { + TokenImage { + this.tokenName = tokenName + this.title = title + this.size = imgSize + } + span { css { tokenCountStyle(tokenCountSize, brightText, customCountStyle) marginLeft = 0.2.rem @@ -93,27 +79,36 @@ fun RBuilder.tokenWithCount( +"× $count" } } + TokenCountPosition.LEFT -> { - styledSpan { + span { css { tokenCountStyle(tokenCountSize, brightText, customCountStyle) marginRight = 0.2.rem } +"$count ×" } - tokenImage(tokenName, title = title, size = imgSize) + TokenImage { + this.tokenName = tokenName + this.title = title + this.size = imgSize + } } + TokenCountPosition.OVER -> { - styledDiv { + div { css { position = Position.relative // if container becomes large, this one stays small so that children stay on top of each other - width = LinearDimension.fitContent + width = Length.fitContent } - tokenImage(tokenName, title = title, size = imgSize) - styledSpan { - css { - +GlobalStyles.centerInPositionedParent + TokenImage { + this.tokenName = tokenName + this.title = title + this.size = imgSize + } + span { + css(GlobalStyles.centerInPositionedParent) { tokenCountStyle(tokenCountSize, brightText, customCountStyle) } +"$count" @@ -124,36 +119,36 @@ fun RBuilder.tokenWithCount( } } -fun RBuilder.tokenImage( - tokenName: String, - title: String = tokenName, - size: LinearDimension?, - block: StyledDOMBuilder<IMG>.() -> Unit = {}, -) { - styledImg(src = getTokenImagePath(tokenName)) { +external interface TokenImageProps : Props { + var tokenName: String + var title: String? + var size: Length? +} + +val TokenImage = FC<TokenImageProps> { props -> + img { + src = getTokenImagePath(props.tokenName) + title = props.title ?: props.tokenName + alt = props.tokenName + css { - height = size ?: 100.pct - if (size != null) { - width = size + height = props.size ?: 100.pct + if (props.size != null) { + width = props.size } verticalAlign = VerticalAlign.middle } - attrs { - this.title = title - this.alt = tokenName - } - block() } } -private fun CssBuilder.tokenCountStyle( - size: LinearDimension, +private fun PropertiesBuilder.tokenCountStyle( + size: Length, brightText: Boolean, - customStyle: CssBuilder.() -> Unit = {}, + customStyle: PropertiesBuilder.() -> Unit = {}, ) { - fontFamily = "Acme" + fontFamily = string("Acme") fontSize = size verticalAlign = VerticalAlign.middle - color = if (brightText) Color.white else Color.black + color = if (brightText) NamedColor.white else NamedColor.black customStyle() } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/TransactionsSelector.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/TransactionsSelector.kt index 66bc44d3..966a40eb 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/TransactionsSelector.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/TransactionsSelector.kt @@ -1,64 +1,65 @@ package org.luxons.sevenwonders.ui.components.game import blueprintjs.core.* -import kotlinx.css.* -import kotlinx.html.DIV -import kotlinx.html.TBODY -import kotlinx.html.TD -import kotlinx.html.classes -import kotlinx.html.js.onClickFunction -import org.luxons.sevenwonders.model.PlayerMove -import org.luxons.sevenwonders.model.api.PlayerDTO +import blueprintjs.icons.* +import csstype.* +import emotion.react.* +import org.luxons.sevenwonders.model.* +import org.luxons.sevenwonders.model.api.* import org.luxons.sevenwonders.model.resources.* import org.luxons.sevenwonders.model.resources.Provider -import org.luxons.sevenwonders.ui.components.gameBrowser.playerInfo +import org.luxons.sevenwonders.ui.components.gameBrowser.* import org.luxons.sevenwonders.ui.utils.* import react.* -import react.dom.* -import styled.* +import react.dom.html.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.p +import react.dom.html.ReactHTML.tbody +import react.dom.html.ReactHTML.td +import react.dom.html.ReactHTML.tr +import web.html.* -fun RBuilder.transactionsSelectorDialog( +fun ChildrenBuilder.transactionsSelectorDialog( state: TransactionSelectorState?, neighbours: Pair<PlayerDTO, PlayerDTO>, prepareMove: (PlayerMove) -> Unit, cancelTransactionSelection: () -> Unit, ) { - bpDialog( - isOpen = state != null, - title = "Trading time!", - canEscapeKeyClose = true, - canOutsideClickClose = true, - isCloseButtonShown = true, - onClose = cancelTransactionSelection, - ) { - attrs { - className = GameStyles.getTypedClassName { it::transactionsSelector } - } - div { - attrs { - classes += Classes.DIALOG_BODY - } + BpDialog { + isOpen = state != null + titleText = "Trading time!" + canEscapeKeyClose = true + canOutsideClickClose = true + isCloseButtonShown = true + onClose = { cancelTransactionSelection() } + + className = GameStyles.transactionsSelector + + BpDialogBody { p { +"You don't have enough resources to perform this move, but you can buy them from neighbours. " +"Please pick an option:" } if (state != null) { // should always be true when the dialog is rendered - styledDiv { + div { css { - margin(all = LinearDimension.auto) + margin = Margin(all = Auto.auto) display = Display.flex - alignItems = Align.center + alignItems = AlignItems.center } neighbour(neighbours.first) - styledDiv { + div { css { - flex(Flex.GROW) - margin(horizontal = 0.5.rem) + flexGrow = number(1.0) + margin = Margin(vertical = 0.rem, horizontal = 0.5.rem) display = Display.flex flexDirection = FlexDirection.column - alignItems = Align.center + alignItems = AlignItems.center + } + OptionsTable { + this.state = state + this.prepareMove = prepareMove } - optionsTable(state, prepareMove) } neighbour(neighbours.second) } @@ -67,28 +68,21 @@ fun RBuilder.transactionsSelectorDialog( } } -private fun StyledDOMBuilder<DIV>.neighbour(player: PlayerDTO) { - styledDiv { +private fun ChildrenBuilder.neighbour(player: PlayerDTO) { + div { css { width = 12.rem // center the icon display = Display.flex flexDirection = FlexDirection.column - alignItems = Align.center + alignItems = AlignItems.center } - playerInfo(player, iconSize = 40, orientation = FlexDirection.column, ellipsize = false) - } -} - -private fun RBuilder.optionsTable( - state: TransactionSelectorState, - prepareMove: (PlayerMove) -> Unit, -) { - child(optionsTable) { - attrs { - this.state = state - this.prepareMove = prepareMove + PlayerInfo { + this.player = player + this.iconSize = 40 + this.orientation = FlexDirection.column + this.ellipsize = false } } } @@ -98,7 +92,7 @@ private external interface OptionsTableProps : PropsWithChildren { var prepareMove: (PlayerMove) -> Unit } -private val optionsTable = fc<OptionsTableProps> { props -> +private val OptionsTable = FC<OptionsTableProps> { props -> val state = props.state val prepareMove = props.prepareMove @@ -107,7 +101,8 @@ private val optionsTable = fc<OptionsTableProps> { props -> val bestPrice = state.transactionsOptions.bestPrice val (cheapestOptions, otherOptions) = state.transactionsOptions.partition { it.totalPrice == bestPrice } - bpHtmlTable(interactive = true) { + BpHTMLTable { + interactive = true tbody { cheapestOptions.forEach { transactions -> transactionsOptionRow( @@ -130,84 +125,86 @@ private val optionsTable = fc<OptionsTableProps> { props -> if (otherOptions.isNotEmpty()) { val icon = if (expanded) "chevron-up" else "chevron-down" val text = if (expanded) "Hide expensive options" else "Show more expensive options" - bpButton( - minimal = true, - small = true, - icon = icon, - rightIcon = icon, - onClick = { expanded = !expanded }, - ) { + BpButton { + this.minimal = true + this.small = true + this.icon = icon + this.rightIcon = icon + this.onClick = { expanded = !expanded } + +text } } } -private fun RDOMBuilder<TBODY>.transactionsOptionRow( +private fun ChildrenBuilder.transactionsOptionRow( transactions: PricedResourceTransactions, showBestPriceIndicator: Boolean, onClick: () -> Unit, ) { - styledTr { + tr { css { cursor = Cursor.pointer - alignItems = Align.center - } - attrs { - onClickFunction = { onClick() } + alignItems = AlignItems.center } + this.onClick = { onClick() } // there should be at most one of each val leftTr = transactions.firstOrNull { it.provider == Provider.LEFT_PLAYER } val rightTr = transactions.firstOrNull { it.provider == Provider.RIGHT_PLAYER } - styledTd { + td { transactionCellCss() - styledDiv { - css { opacity = if (leftTr == null) 0.5 else 1 } + div { + css { opacity = number(if (leftTr == null) 0.5 else 1.0) } transactionCellInnerCss() - bpIcon(name = "caret-left", size = IconSize.LARGE) + BpIcon { + icon = IconNames.CARET_LEFT + size = IconSize.LARGE + } goldIndicator(leftTr?.totalPrice ?: 0, imgSize = 2.5.rem) } } - styledTd { + td { transactionCellCss() if (leftTr != null) { resourceList(leftTr.resources) } } - styledTd { + td { transactionCellCss() css { width = 1.5.rem } if (showBestPriceIndicator) { bestPriceIndicator() } } - styledTd { + td { transactionCellCss() if (rightTr != null) { resourceList(rightTr.resources) } } - styledTd { + td { transactionCellCss() - styledDiv { - css { opacity = if (rightTr == null) 0.5 else 1 } + div { + css { opacity = number(if (rightTr == null) 0.5 else 1.0) } transactionCellInnerCss() goldIndicator(rightTr?.totalPrice ?: 0, imgSize = 2.5.rem) - bpIcon(name = "caret-right", size = IconSize.LARGE) + BpIcon { + icon = IconNames.CARET_RIGHT + size = IconSize.LARGE + } } } } } -private fun StyledDOMBuilder<TD>.bestPriceIndicator() { - styledDiv { - css { - +GameStyles.bestPrice - } +private fun ChildrenBuilder.bestPriceIndicator() { + div { + css(GameStyles.bestPrice){} +"Best\nprice!" } } -private fun StyledDOMBuilder<TD>.transactionCellCss() { +private fun HTMLAttributes<HTMLTableCellElement>.transactionCellCss() { // we need inline styles to win over BlueprintJS's styles (which are more specific than .class) inlineStyles { verticalAlign = VerticalAlign.middle @@ -215,15 +212,15 @@ private fun StyledDOMBuilder<TD>.transactionCellCss() { } } -private fun StyledDOMBuilder<DIV>.transactionCellInnerCss() { +private fun HTMLAttributes<HTMLDivElement>.transactionCellInnerCss() { css { display = Display.flex flexDirection = FlexDirection.row - alignItems = Align.center + alignItems = AlignItems.center } } -private fun RBuilder.resourceList(countedResources: List<CountedResource>) { +private fun ChildrenBuilder.resourceList(countedResources: List<CountedResource>) { val resources = countedResources.toRepeatedTypesList() // The biggest card is the Palace and requires 7 resources (1 of each). @@ -231,35 +228,35 @@ private fun RBuilder.resourceList(countedResources: List<CountedResource>) { // Therefore, 3 by row seems decent. When there are 4 items, it's visually better to have a 2x2 matrix, though. val rows = resources.chunked(if (resources.size == 4) 2 else 3) - val imgSize = 1.5.rem - styledDiv { + val imgSize = 1.5 + div { css { display = Display.flex flexDirection = FlexDirection.column - alignItems = Align.center + alignItems = AlignItems.center justifyContent = JustifyContent.center - flex(Flex.GROW) + flexGrow = number(1.0) // this ensures stable dimensions, no matter how many resources (up to 2x3 matrix) - width = imgSize * 3 - height = imgSize * 2 + width = (imgSize * 3).rem + height = (imgSize * 2).rem } rows.forEach { row -> - styledDiv { + div { resourceRowCss() row.forEach { - resourceImage(it, size = imgSize) + resourceImage(it, size = imgSize.rem) } } } } } -private fun StyledDOMBuilder<DIV>.resourceRowCss() { +private fun HTMLAttributes<HTMLDivElement>.resourceRowCss() { css { display = Display.flex flexDirection = FlexDirection.row - alignItems = Align.center - margin(horizontal = LinearDimension.auto) + alignItems = AlignItems.center + margin = Margin(vertical = 0.px, horizontal = Auto.auto) } } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/CreateGameForm.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/CreateGameForm.kt index 0d148744..ae79125b 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/CreateGameForm.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/CreateGameForm.kt @@ -2,67 +2,48 @@ package org.luxons.sevenwonders.ui.components.gameBrowser import blueprintjs.core.* import blueprintjs.icons.* -import kotlinx.css.* -import kotlinx.html.js.* +import csstype.* +import emotion.react.* import org.luxons.sevenwonders.ui.redux.* import react.* -import react.dom.* -import styled.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.form -private external interface CreateGameFormProps : PropsWithChildren { - var createGame: (String) -> Unit -} - -private external interface CreateGameFormState : State { - var gameName: String -} - -private class CreateGameForm(props: CreateGameFormProps) : RComponent<CreateGameFormProps, CreateGameFormState>(props) { +val CreateGameForm = VFC { + var gameName by useState("") - override fun CreateGameFormState.init(props: CreateGameFormProps) { - gameName = "" - } + val dispatch = useSwDispatch() + val createGame = { dispatch(RequestCreateGame(gameName)) } - override fun RBuilder.render() { - styledDiv { - css { - display = Display.flex - flexDirection = FlexDirection.row - justifyContent = JustifyContent.spaceBetween + div { + css { + display = Display.flex + flexDirection = FlexDirection.row + justifyContent = JustifyContent.spaceBetween + } + form { + onSubmit = { e -> + e.preventDefault() + createGame() } - form { - attrs { - onSubmitFunction = { e -> - e.preventDefault() + + BpInputGroup { + large = true + placeholder = "Game name" + onChange = { e -> + val input = e.currentTarget + gameName = input.value + } + rightElement = BpButton.create { + minimal = true + intent = Intent.PRIMARY + icon = IconNames.ADD + onClick = { e -> + e.preventDefault() // prevents refreshing the page when pressing Enter createGame() } } - - bpInputGroup( - large = true, - placeholder = "Game name", - onChange = { e -> - val input = e.currentTarget - state.gameName = input.value - }, - rightElement = createGameButton(), - ) } } } - - private fun createGameButton() = buildElement { - bpButton(minimal = true, intent = Intent.PRIMARY, icon = IconNames.ADD, onClick = { e -> - e.preventDefault() // prevents refreshing the page when pressing Enter - createGame() - }) - } - - private fun createGame() { - props.createGame(state.gameName) - } -} - -val createGameForm: ComponentClass<PropsWithChildren> = connectDispatch(CreateGameForm::class) { dispatch, _ -> - createGame = { name -> dispatch(RequestCreateGame(name)) } } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameBrowser.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameBrowser.kt index 579e6cb2..16b0965d 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameBrowser.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameBrowser.kt @@ -1,51 +1,69 @@ package org.luxons.sevenwonders.ui.components.gameBrowser import blueprintjs.core.* -import kotlinx.css.* -import kotlinx.html.* -import org.luxons.sevenwonders.ui.components.GlobalStyles +import csstype.* +import emotion.react.* +import org.luxons.sevenwonders.ui.components.* +import org.luxons.sevenwonders.ui.redux.* import org.luxons.sevenwonders.ui.utils.* import react.* -import react.dom.* -import styled.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.h1 +import react.dom.html.ReactHTML.h2 -fun RBuilder.gameBrowser() = styledDiv { - css { - +GlobalStyles.fullscreen - +GlobalStyles.zeusBackground - padding(all = 1.rem) - } - styledDiv { - attrs { - classes += Classes.DARK - } - css { - margin(horizontal = LinearDimension.auto) - maxWidth = GlobalStyles.preGameWidth +val GameBrowser = VFC { + div { + css(GlobalStyles.fullscreen, GlobalStyles.zeusBackground) { + padding = Padding(all = 1.rem) } - styledDiv { - css { - display = Display.flex - justifyContent = JustifyContent.spaceBetween + div { + css(ClassName(Classes.DARK)) { + margin = Margin(vertical = 0.px, horizontal = Auto.auto) + maxWidth = GlobalStyles.preGameWidth } - h1 { +"Games" } - currentPlayerInfo() - } + div { + css { + display = Display.flex + justifyContent = JustifyContent.spaceBetween + } + h1 { +"Games" } + CurrentPlayerInfo() + } + + BpCard { + css { + marginBottom = 1.rem + } - bpCard(className = GameBrowserStyles.getTypedClassName { it::createGameCard }) { - styledH2 { - css { +GameBrowserStyles.cardTitle } - +"Create a Game" + h2 { + css { + marginTop = 0.px + } + +"Create a Game" + } + CreateGameForm() } - createGameForm {} - } - bpCard { - styledH2 { - css { +GameBrowserStyles.cardTitle } - +"Join a Game" + BpCard { + h2 { + css { + marginTop = 0.px + } + +"Join a Game" + } + GameList() } - gameList() } } } + +val CurrentPlayerInfo = VFC { + val connectedPlayer = useSwSelector { it.connectedPlayer } + PlayerInfo { + player = connectedPlayer + iconSize = 30 + showUsername = true + orientation = FlexDirection.row + ellipsize = false + } +} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameBrowserStyles.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameBrowserStyles.kt deleted file mode 100644 index 611991c2..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameBrowserStyles.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.luxons.sevenwonders.ui.components.gameBrowser - -import kotlinx.css.* -import styled.StyleSheet - -object GameBrowserStyles : StyleSheet("GameBrowserStyles", isStatic = true) { - - val cardTitle by css { - marginTop = 0.px - } - - val createGameCard by css { - marginBottom = 1.rem - } - - val gameTable by css { - width = 100.pct - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameList.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameList.kt index a1f47045..2437d9e0 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameList.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameList.kt @@ -1,41 +1,50 @@ package org.luxons.sevenwonders.ui.components.gameBrowser import blueprintjs.core.* -import blueprintjs.icons.IconNames +import blueprintjs.icons.* import csstype.* -import kotlinx.css.* -import kotlinx.css.Display -import kotlinx.css.FlexDirection -import kotlinx.css.JustifyContent -import kotlinx.css.TextAlign -import kotlinx.css.VerticalAlign -import kotlinx.css.rem -import kotlinx.html.classes -import kotlinx.html.title -import org.luxons.sevenwonders.model.api.ConnectedPlayer -import org.luxons.sevenwonders.model.api.LobbyDTO +import emotion.react.* +import org.luxons.sevenwonders.model.api.* import org.luxons.sevenwonders.model.api.State -import org.luxons.sevenwonders.ui.redux.RequestJoinGame -import org.luxons.sevenwonders.ui.redux.connectStateAndDispatch +import org.luxons.sevenwonders.ui.redux.* +import org.luxons.sevenwonders.ui.utils.* import react.* -import react.dom.* -import styled.* +import react.dom.html.ReactHTML.col +import react.dom.html.ReactHTML.colgroup +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.span +import react.dom.html.ReactHTML.tbody +import react.dom.html.ReactHTML.td +import react.dom.html.ReactHTML.th +import react.dom.html.ReactHTML.thead +import react.dom.html.ReactHTML.tr import react.State as RState -external interface GameListStateProps : PropsWithChildren { +external interface GameListStateProps : Props { var connectedPlayer: ConnectedPlayer var games: List<LobbyDTO> } -external interface GameListDispatchProps : PropsWithChildren { +external interface GameListDispatchProps : Props { var joinGame: (Long) -> Unit } external interface GameListProps : GameListStateProps, GameListDispatchProps -class GameListPresenter(props: GameListProps) : RComponent<GameListProps, RState>(props) { +val GameList = connectStateAndDispatch<GameListStateProps, GameListDispatchProps, GameListProps>( + clazz = GameListPresenter::class, + mapStateToProps = { state, _ -> + connectedPlayer = state.connectedPlayer ?: error("there should be a connected player") + games = state.games + }, + mapDispatchToProps = { dispatch, _ -> + joinGame = { gameId -> dispatch(RequestJoinGame(gameId = gameId)) } + }, +) + +private class GameListPresenter(props: GameListProps) : Component<GameListProps, RState>(props) { - override fun RBuilder.render() { + override fun render() = Fragment.create { if (props.games.isEmpty()) { noGamesInfo() } else { @@ -43,16 +52,13 @@ class GameListPresenter(props: GameListProps) : RComponent<GameListProps, RState } } - private fun RBuilder.noGamesInfo() { - bpNonIdealState( - icon = IconNames.GEOSEARCH, - title = "No games to join", - ) { - styledDiv { - attrs { - classes += Classes.RUNNING_TEXT - } - css { + private fun ChildrenBuilder.noGamesInfo() { + BpNonIdealState { + icon = IconNames.GEOSEARCH + titleText = "No games to join" + + div { + css(ClassName(Classes.RUNNING_TEXT)) { maxWidth = 35.rem } +"Nobody seems to be playing at the moment. " @@ -61,11 +67,12 @@ class GameListPresenter(props: GameListProps) : RComponent<GameListProps, RState } } - private fun RBuilder.gamesTable() { - bpHtmlTable { - attrs { - className = ClassName(GameBrowserStyles.getClassName { it::gameTable }) + private fun ChildrenBuilder.gamesTable() { + BpHTMLTable { + css { + width = 100.pct } + columnWidthsSpec() thead { gameListHeaderRow() @@ -78,26 +85,26 @@ class GameListPresenter(props: GameListProps) : RComponent<GameListProps, RState } } - private fun RElementBuilder<HTMLTableProps>.columnWidthsSpec() { + private fun ChildrenBuilder.columnWidthsSpec() { colgroup { - styledCol { + col { css { width = 40.rem } } - styledCol { + col { css { width = 5.rem textAlign = TextAlign.center } } - styledCol { + col { css { width = 5.rem textAlign = TextAlign.center // use inline style on th instead to overcome blueprint style } } - styledCol { + col { css { width = 3.rem textAlign = TextAlign.center @@ -106,7 +113,7 @@ class GameListPresenter(props: GameListProps) : RComponent<GameListProps, RState } } - private fun RBuilder.gameListHeaderRow() = tr { + private fun ChildrenBuilder.gameListHeaderRow() = tr { th { +"Name" } @@ -124,10 +131,8 @@ class GameListPresenter(props: GameListProps) : RComponent<GameListProps, RState } } - private fun RBuilder.gameListItemRow(lobby: LobbyDTO) = styledTr { - attrs { - key = lobby.id.toString() - } + private fun ChildrenBuilder.gameListItemRow(lobby: LobbyDTO) = tr { + key = lobby.id.toString() // inline styles necessary to overcome BlueprintJS's verticalAlign=top td { inlineStyles { gameTableCellStyle() } @@ -150,37 +155,41 @@ class GameListPresenter(props: GameListProps) : RComponent<GameListProps, RState } } - private fun StyledElement.gameTableHeaderCellStyle() { + private fun PropertiesBuilder.gameTableHeaderCellStyle() { textAlign = TextAlign.center } - private fun StyledElement.gameTableCellStyle() { + private fun PropertiesBuilder.gameTableCellStyle() { verticalAlign = VerticalAlign.middle } - private fun RBuilder.gameStatus(state: State) { + private fun ChildrenBuilder.gameStatus(state: State) { val intent = when (state) { State.LOBBY -> Intent.SUCCESS State.PLAYING -> Intent.WARNING State.FINISHED -> Intent.DANGER } - bpTag(minimal = true, intent = intent) { + BpTag { + this.minimal = true + this.intent = intent + +state.toString() } } - private fun RBuilder.playerCount(nPlayers: Int) { - styledDiv { + private fun ChildrenBuilder.playerCount(nPlayers: Int) { + div { css { display = Display.flex flexDirection = FlexDirection.row justifyContent = JustifyContent.center } - attrs { - title = "Number of players" + title = "Number of players" + BpIcon { + icon = IconNames.PEOPLE + title = null } - bpIcon(name = "people", title = null) - styledSpan { + span { css { marginLeft = 0.3.rem } @@ -189,28 +198,15 @@ class GameListPresenter(props: GameListProps) : RComponent<GameListProps, RState } } - private fun RBuilder.joinButton(lobby: LobbyDTO) { + private fun ChildrenBuilder.joinButton(lobby: LobbyDTO) { val joinability = lobby.joinability(props.connectedPlayer.displayName) - bpButton( - minimal = true, - large = true, - title = joinability.tooltip, - icon = "arrow-right", - disabled = !joinability.canDo, - onClick = { props.joinGame(lobby.id) }, - ) + BpButton { + minimal = true + large = true + title = joinability.tooltip + icon = "arrow-right" + disabled = !joinability.canDo + onClick = { props.joinGame(lobby.id) } + } } } - -fun RBuilder.gameList() = gameList {} - -private val gameList = connectStateAndDispatch<GameListStateProps, GameListDispatchProps, GameListProps>( - clazz = GameListPresenter::class, - mapStateToProps = { state, _ -> - connectedPlayer = state.connectedPlayer ?: error("there should be a connected player") - games = state.games - }, - mapDispatchToProps = { dispatch, _ -> - joinGame = { gameId -> dispatch(RequestJoinGame(gameId = gameId)) } - }, -) diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/PlayerInfo.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/PlayerInfo.kt index aa1033a0..cae25ba5 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/PlayerInfo.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/PlayerInfo.kt @@ -1,51 +1,52 @@ package org.luxons.sevenwonders.ui.components.gameBrowser -import blueprintjs.core.bpIcon -import kotlinx.css.* -import kotlinx.html.title -import org.luxons.sevenwonders.model.api.BasicPlayerInfo -import org.luxons.sevenwonders.model.api.PlayerDTO -import org.luxons.sevenwonders.ui.redux.connectState +import blueprintjs.core.* +import csstype.* +import emotion.react.* +import org.luxons.sevenwonders.model.api.* import react.* -import react.dom.attrs -import styled.css -import styled.styledDiv -import styled.styledSpan +import react.State +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.span external interface PlayerInfoProps : PropsWithChildren { var player: BasicPlayerInfo? - var showUsername: Boolean - var iconSize: Int - var orientation: FlexDirection - var ellipsize: Boolean + var showUsername: Boolean? + var iconSize: Int? + var orientation: FlexDirection? + var ellipsize: Boolean? } -class PlayerInfoPresenter(props: PlayerInfoProps) : RComponent<PlayerInfoProps, State>(props) { +val PlayerInfo = PlayerInfoPresenter::class.react - override fun RBuilder.render() { - styledDiv { - css { - display = Display.flex - alignItems = Align.center - flexDirection = props.orientation +private class PlayerInfoPresenter(props: PlayerInfoProps) : Component<PlayerInfoProps, State>(props) { + + override fun render() = div.create { + val orientation = props.orientation ?: FlexDirection.row + css { + display = Display.flex + alignItems = AlignItems.center + flexDirection = orientation + } + props.player?.let { + BpIcon { + icon = it.icon?.name ?: "user" + size = props.iconSize ?: 30 } - props.player?.let { - bpIcon(name = it.icon?.name ?: "user", size = props.iconSize) - if (props.showUsername) { - playerNameWithUsername(it.displayName, it.username) { - iconSeparationMargin() - } - } else { - playerName(it.displayName) { - iconSeparationMargin() - } + if (props.showUsername == true) { + playerNameWithUsername(it.displayName, it.username) { + iconSeparationMargin(orientation) + } + } else { + playerName(it.displayName) { + iconSeparationMargin(orientation) } } } } - private fun RBuilder.playerName(displayName: String, style: CssBuilder.() -> Unit = {}) { - styledSpan { + private fun ChildrenBuilder.playerName(displayName: String, style: PropertiesBuilder.() -> Unit = {}) { + span { css { fontSize = 1.rem if (props.orientation == FlexDirection.column) { @@ -55,10 +56,9 @@ class PlayerInfoPresenter(props: PlayerInfoProps) : RComponent<PlayerInfoProps, } // TODO replace by BlueprintJS's Text elements (built-in ellipsize based on width) val maxDisplayNameLength = 15 - if (props.ellipsize && displayName.length > maxDisplayNameLength) { - attrs { - title = displayName - } + val ellipsize = props.ellipsize ?: true + if (ellipsize && displayName.length > maxDisplayNameLength) { + title = displayName +displayName.ellipsize(maxDisplayNameLength) } else { +displayName @@ -68,33 +68,33 @@ class PlayerInfoPresenter(props: PlayerInfoProps) : RComponent<PlayerInfoProps, private fun String.ellipsize(maxLength: Int) = take(maxLength - 1) + "…" - private fun CssBuilder.iconSeparationMargin() { + private fun PropertiesBuilder.iconSeparationMargin(orientation: FlexDirection) { val margin = 0.4.rem - when (props.orientation) { + when (orientation) { FlexDirection.row -> marginLeft = margin FlexDirection.column -> marginTop = margin FlexDirection.rowReverse -> marginRight = margin FlexDirection.columnReverse -> marginBottom = margin - else -> error("Unsupported orientation '${props.orientation}' for player info component") + else -> error("Unsupported orientation '$orientation' for player info component") } } - private fun RBuilder.playerNameWithUsername( + private fun ChildrenBuilder.playerNameWithUsername( displayName: String, username: String, - style: CssBuilder.() -> Unit = {} + style: PropertiesBuilder.() -> Unit = {} ) { - styledDiv { + div { css { display = Display.flex flexDirection = FlexDirection.column style() } playerName(displayName) - styledSpan { + span { css { marginTop = 0.1.rem - color = Color.lightGray + color = NamedColor.lightgray fontSize = 0.8.rem } +"($username)" @@ -102,32 +102,3 @@ class PlayerInfoPresenter(props: PlayerInfoProps) : RComponent<PlayerInfoProps, } } } - -fun RBuilder.playerInfo( - player: PlayerDTO, - showUsername: Boolean = false, - iconSize: Int = 30, - orientation: FlexDirection = FlexDirection.row, - ellipsize: Boolean = true, -) = child(PlayerInfoPresenter::class) { - attrs { - this.player = player - this.showUsername = showUsername - this.iconSize = iconSize - this.orientation = orientation - this.ellipsize = ellipsize - } -} - -fun RBuilder.currentPlayerInfo() = playerInfo {} - -private val playerInfo = connectState( - clazz = PlayerInfoPresenter::class, - mapStateToProps = { state, _ -> - player = state.connectedPlayer - iconSize = 30 - showUsername = true - orientation = FlexDirection.row - ellipsize = false - }, -) diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt index 27a2057c..8de9f53e 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt @@ -2,93 +2,89 @@ package org.luxons.sevenwonders.ui.components.home import blueprintjs.core.* import blueprintjs.icons.* -import kotlinx.css.* -import kotlinx.html.js.* +import csstype.* +import emotion.react.* import org.luxons.sevenwonders.ui.redux.* +import org.luxons.sevenwonders.ui.utils.* import react.* -import styled.* +import react.dom.events.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.form +import web.html.* -private external interface ChooseNameFormProps : PropsWithChildren { - var chooseUsername: (String) -> Unit +val ChooseNameForm = VFC { + val dispatch = useSwDispatch() + ChooseNameFormPresenter { + chooseUsername = { name -> dispatch(RequestChooseName(name)) } + } } -private external interface ChooseNameFormState : State { - var username: String +private external interface ChooseNameFormPresenterProps : PropsWithChildren { + var chooseUsername: (String) -> Unit } -private class ChooseNameForm(props: ChooseNameFormProps) : RComponent<ChooseNameFormProps, ChooseNameFormState>(props) { - - override fun ChooseNameFormState.init(props: ChooseNameFormProps) { - username = "" - } +private val ChooseNameFormPresenter = FC<ChooseNameFormPresenterProps> { props -> + var usernameState by useState("") - override fun RBuilder.render() { - styledForm { - css { - display = Display.flex - flexDirection = FlexDirection.row + form { + css { + display = Display.flex + flexDirection = FlexDirection.row + } + onSubmit = { e -> + e.preventDefault() + props.chooseUsername(usernameState) + } + RandomNameButton { + onClick = { usernameState = randomGreekName() } + } + spacer() + BpInputGroup { + large = true + placeholder = "Username" + rightElement = SubmitButton.create { + onClick = { e -> + e.preventDefault() + props.chooseUsername(usernameState) + } } - attrs.onSubmitFunction = { e -> - e.preventDefault() - chooseUsername() + value = usernameState + onChange = { e -> + val input = e.currentTarget + usernameState = input.value } - randomNameButton() - spacer() - bpInputGroup( - large = true, - placeholder = "Username", - rightElement = submitButton(), - value = state.username, - onChange = { e -> - val input = e.currentTarget - setState { - username = input.value - } - }, - ) } } +} - private fun submitButton(): ReactElement<*> = buildElement { - bpButton( - minimal = true, - icon = IconNames.ARROW_RIGHT, - intent = Intent.PRIMARY, - onClick = { e -> - e.preventDefault() - chooseUsername() - }, - ) - } - - private fun RBuilder.randomNameButton() { - bpButton( - title = "Generate random name", - large = true, - icon = IconNames.RANDOM, - intent = Intent.PRIMARY, - onClick = { fillRandomUsername() }, - ) - } +private external interface SpecificButtonProps : Props { + var onClick: MouseEventHandler<HTMLElement>? +} - private fun fillRandomUsername() { - setState { username = randomGreekName() } +private val SubmitButton = FC<SpecificButtonProps> { props -> + BpButton { + minimal = true + icon = IconNames.ARROW_RIGHT + intent = Intent.PRIMARY + onClick = props.onClick } +} - private fun chooseUsername() { - props.chooseUsername(state.username) +private val RandomNameButton = FC<SpecificButtonProps> { props -> + BpButton { + title = "Generate random name" + large = true + icon = IconNames.RANDOM + intent = Intent.PRIMARY + onClick = props.onClick } +} - // TODO this is so bad I'm dying inside - private fun RBuilder.spacer() { - styledDiv { - css { - margin(2.px) - } +// TODO this is so bad I'm dying inside +private fun ChildrenBuilder.spacer() { + div { + css { + margin = Margin(all = 2.px) } } } - -val chooseNameForm: ComponentClass<PropsWithChildren> = connectDispatch(ChooseNameForm::class) { dispatch, _ -> - chooseUsername = { name -> dispatch(RequestChooseName(name)) } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/Home.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/Home.kt index 4b209979..b821b23d 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/Home.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/Home.kt @@ -1,21 +1,22 @@ package org.luxons.sevenwonders.ui.components.home -import org.luxons.sevenwonders.ui.components.GlobalStyles -import react.RBuilder -import react.dom.* -import styled.css -import styled.styledDiv +import emotion.react.* +import org.luxons.sevenwonders.ui.components.* +import react.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.img private const val LOGO = "images/logo-7-wonders.png" -fun RBuilder.home() = styledDiv { - css { - +GlobalStyles.fullscreen - +GlobalStyles.zeusBackground - +HomeStyles.centerChildren - } +val Home = VFC("Home") { + div { + css(GlobalStyles.fullscreen, GlobalStyles.zeusBackground, HomeStyles.centerChildren) {} - img(src = LOGO, alt = "Seven Wonders") {} + img { + src = LOGO + alt = "Seven Wonders" + } - chooseNameForm {} + ChooseNameForm() + } } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/HomeStyles.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/HomeStyles.kt index 10037b36..fa0a83ad 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/HomeStyles.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/HomeStyles.kt @@ -1,14 +1,14 @@ package org.luxons.sevenwonders.ui.components.home -import kotlinx.css.* -import styled.StyleSheet +import csstype.* +import emotion.css.* -object HomeStyles : StyleSheet("HomeStyles", isStatic = true) { +object HomeStyles { - val centerChildren by css { + val centerChildren = ClassName { display = Display.flex flexDirection = FlexDirection.column - alignItems = Align.center + alignItems = AlignItems.center justifyContent = JustifyContent.center } } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt index 7435ffb0..83f6aa7b 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt @@ -3,31 +3,51 @@ package org.luxons.sevenwonders.ui.components.lobby import blueprintjs.core.* import blueprintjs.icons.* import csstype.* -import kotlinx.css.* -import kotlinx.css.Display -import kotlinx.css.JustifyContent -import kotlinx.css.Position -import kotlinx.css.pct -import kotlinx.css.properties.* -import kotlinx.css.px -import kotlinx.css.rem +import csstype.Position +import emotion.react.* import org.luxons.sevenwonders.model.api.* import org.luxons.sevenwonders.model.wonders.* -import org.luxons.sevenwonders.ui.components.GlobalStyles +import org.luxons.sevenwonders.ui.components.* import org.luxons.sevenwonders.ui.redux.* +import org.luxons.sevenwonders.ui.utils.* import react.* -import react.State -import react.dom.* -import styled.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.h1 +import react.dom.html.ReactHTML.h2 +import react.dom.html.ReactHTML.h3 +import react.dom.html.ReactHTML.h4 private val BOT_NAMES = listOf("Wall-E", "B-Max", "Sonny", "T-800", "HAL", "GLaDOS", "R2-D2", "Bender", "AWESOM-O") -external interface LobbyStateProps : PropsWithChildren { - var currentGame: LobbyDTO? - var currentPlayer: PlayerDTO? +val Lobby = VFC(displayName = "Lobby") { + val lobby = useSwSelector { it.currentLobby } + val player = useSwSelector { it.currentPlayer } + + val dispatch = useSwDispatch() + + if (lobby == null || player == null) { + BpNonIdealState { + icon = IconNames.ERROR + titleText = "Error: no current game" + } + } else { + LobbyPresenter { + currentGame = lobby + currentPlayer = player + + startGame = { dispatch(RequestStartGame()) } + addBot = { name -> dispatch(RequestAddBot(name)) } + leaveLobby = { dispatch(RequestLeaveLobby()) } + disbandLobby = { dispatch(RequestDisbandLobby()) } + reorderPlayers = { orderedPlayers -> dispatch(RequestReorderPlayers(orderedPlayers)) } + reassignWonders = { wonders -> dispatch(RequestReassignWonders(wonders)) } + } + } } -external interface LobbyDispatchProps : PropsWithChildren { +private external interface LobbyPresenterProps : Props { + var currentGame: LobbyDTO + var currentPlayer: PlayerDTO var startGame: () -> Unit var addBot: (displayName: String) -> Unit var leaveLobby: () -> Unit @@ -36,235 +56,217 @@ external interface LobbyDispatchProps : PropsWithChildren { var reassignWonders: (wonders: List<AssignedWonder>) -> Unit } -interface LobbyProps : LobbyDispatchProps, LobbyStateProps - -class LobbyPresenter(props: LobbyProps) : RComponent<LobbyProps, State>(props) { - - override fun RBuilder.render() { - val currentGame = props.currentGame - val currentPlayer = props.currentPlayer - if (currentGame == null || currentPlayer == null) { - bpNonIdealState(icon = IconNames.ERROR, title = "Error: no current game") - return +private val LobbyPresenter = FC<LobbyPresenterProps> { props -> + div { + css(GlobalStyles.fullscreen, GlobalStyles.zeusBackground) { + padding = Padding(all = 1.rem) } - styledDiv { - css { - +GlobalStyles.fullscreen - +GlobalStyles.zeusBackground - padding(all = 1.rem) + div { + css(ClassName(Classes.DARK), LobbyStyles.contentContainer) { + margin = Margin(vertical = 0.rem, horizontal = Auto.auto) + maxWidth = GlobalStyles.preGameWidth } - styledDiv { + h1 { +"${props.currentGame.name} — Lobby" } + + radialPlayerList(props.currentGame.players, props.currentPlayer) { css { - classes.add(Classes.DARK) - +LobbyStyles.contentContainer - } - h1 { +"${currentGame.name} — Lobby" } - - radialPlayerList(currentGame.players, currentPlayer) { - css { - // to make players more readable on the background - background = "radial-gradient(closest-side, black 20%, transparent)" - // make it bigger so the background covers more ground - width = 40.rem - height = 40.rem - } + // to make players more readable on the background + background = "radial-gradient(closest-side, black 20%, transparent)".unsafeCast<Gradient>() + // make it bigger so the background covers more ground + width = 40.rem + height = 40.rem } - actionButtons(currentPlayer, currentGame) + } + actionButtons(props.currentPlayer, props.currentGame, props.startGame, props.leaveLobby, props.disbandLobby, props.addBot) - if (currentPlayer.isGameOwner) { - setupPanel(currentGame) - } + if (props.currentPlayer.isGameOwner) { + setupPanel(props.currentGame, props.reorderPlayers, props.reassignWonders) } } } +} + +private fun ChildrenBuilder.actionButtons( + currentPlayer: PlayerDTO, + currentGame: LobbyDTO, + startGame: () -> Unit, + leaveLobby: () -> Unit, + disbandLobby: () -> Unit, + addBot: (String) -> Unit, +) { + div { + css { + position = Position.absolute + bottom = 2.rem + left = 50.pct + transform = translate((-50).pct) - private fun RBuilder.actionButtons(currentPlayer: PlayerDTO, currentGame: LobbyDTO) { - styledDiv { - css { - position = Position.absolute - bottom = 2.rem - left = 50.pct - transform { translate((-50).pct) } - - width = 70.pct - display = Display.flex - justifyContent = JustifyContent.spaceAround + width = 70.pct + display = Display.flex + justifyContent = JustifyContent.spaceAround + } + if (currentPlayer.isGameOwner) { + BpButtonGroup { + leaveButton(leaveLobby) + disbandButton(disbandLobby) } - if (currentPlayer.isGameOwner) { - bpButtonGroup { - leaveButton() - disbandButton() - } - bpButtonGroup { - addBotButton(currentGame) - startButton(currentGame, currentPlayer) - } - } else { - leaveButton() + BpButtonGroup { + addBotButton(currentGame, addBot) + startButton(currentGame.startability(currentPlayer.username), startGame) } + } else { + leaveButton(leaveLobby) } } +} - private fun RBuilder.startButton(currentGame: LobbyDTO, currentPlayer: PlayerDTO) { - val startability = currentGame.startability(currentPlayer.username) - bpButton( - large = true, - intent = Intent.PRIMARY, - icon = IconNames.PLAY, - title = startability.tooltip, - disabled = !startability.canDo, - onClick = { props.startGame() }, - ) { - +"START" - } +private fun ChildrenBuilder.startButton(startability: Actionability, startGame: () -> Unit) { + BpButton { + large = true + intent = Intent.PRIMARY + icon = IconNames.PLAY + title = startability.tooltip + disabled = !startability.canDo + onClick = { startGame() } + + +"START" } +} - private fun RBuilder.setupPanel(currentGame: LobbyDTO) { - styledDiv { - css { - +LobbyStyles.setupPanel - } - bpCard(Elevation.TWO, className = ClassName(Classes.DARK)) { - styledH2 { - css { - margin(top = 0.px) - } - +"Game setup" - } - bpDivider() - h3 { - +"Players" - } - reorderPlayersButton(currentGame) - h3 { - +"Wonders" +private fun ChildrenBuilder.setupPanel( + currentGame: LobbyDTO, + reorderPlayers: (usernames: List<String>) -> Unit, + reassignWonders: (wonders: List<AssignedWonder>) -> Unit, +) { + div { + className = LobbyStyles.setupPanel + + BpCard { + elevation = Elevation.TWO + className = ClassName(Classes.DARK) + + h2 { + css { + marginTop = 0.px } - randomizeWondersButton(currentGame) - wonderSideSelectionGroup(currentGame) + +"Game setup" + } + BpDivider() + h3 { + +"Players" + } + reorderPlayersButton(currentGame, reorderPlayers) + h3 { + +"Wonders" + } + WonderSettingsGroup { + this.currentGame = currentGame + this.reassignWonders = reassignWonders } } } +} - private fun RBuilder.addBotButton(currentGame: LobbyDTO) { - bpButton( - large = true, - icon = IconNames.PLUS, - rightIcon = IconNames.DESKTOP, - intent = Intent.PRIMARY, - title = if (currentGame.maxPlayersReached) "Max players reached" else "Add a bot to this game", - disabled = currentGame.maxPlayersReached, - onClick = { addBot(currentGame) }, - ) +private fun ChildrenBuilder.addBotButton(currentGame: LobbyDTO, addBot: (String) -> Unit) { + BpButton { + large = true + icon = IconNames.PLUS + rightIcon = IconNames.DESKTOP + intent = Intent.PRIMARY + title = if (currentGame.maxPlayersReached) "Max players reached" else "Add a bot to this game" + disabled = currentGame.maxPlayersReached + onClick = { addBot(randomBotNameUnusedIn(currentGame)) } } +} - private fun addBot(currentGame: LobbyDTO) { - val availableBotNames = BOT_NAMES.filter { name -> - currentGame.players.all { it.displayName != name } - } - props.addBot(availableBotNames.random()) +private fun randomBotNameUnusedIn(currentGame: LobbyDTO): String { + val availableBotNames = BOT_NAMES.filter { name -> + currentGame.players.none { it.displayName == name } } + return availableBotNames.random() +} - private fun RBuilder.reorderPlayersButton(currentGame: LobbyDTO) { - bpButton( - icon = IconNames.RANDOM, - rightIcon = IconNames.PEOPLE, - title = "Re-order players randomly", - onClick = { reorderPlayers(currentGame) }, - ) { - +"Reorder players" - } - } +private fun ChildrenBuilder.reorderPlayersButton(currentGame: LobbyDTO, reorderPlayers: (usernames: List<String>) -> Unit) { + BpButton { + icon = IconNames.RANDOM + rightIcon = IconNames.PEOPLE + title = "Re-order players randomly" + onClick = { reorderPlayers(currentGame.players.map { it.username }.shuffled()) } - private fun reorderPlayers(currentGame: LobbyDTO) { - props.reorderPlayers(currentGame.players.map { it.username }.shuffled()) + +"Reorder players" } +} - private fun RBuilder.randomizeWondersButton(currentGame: LobbyDTO) { - bpButton( - icon = IconNames.RANDOM, - title = "Re-assign wonders to players randomly", - onClick = { randomizeWonders(currentGame) }, - ) { - +"Randomize wonders" - } +private external interface WonderSettingsGroupProps : Props { + var currentGame: LobbyDTO + var reassignWonders: (List<AssignedWonder>) -> Unit +} + +private val WonderSettingsGroup = FC<WonderSettingsGroupProps> { props -> + val reassignWonders = props.reassignWonders + + BpButton { + icon = IconNames.RANDOM + title = "Re-assign wonders to players randomly" + onClick = { reassignWonders(randomWonderAssignments(props.currentGame)) } + + +"Randomize wonders" + } + h4 { + +"Select wonder sides:" } + BpButtonGroup { + BpButton { + icon = IconNames.RANDOM + title = "Re-roll wonder sides randomly" + onClick = { reassignWonders(assignedWondersWithRandomSides(props.currentGame)) } + } + BpButton { + title = "Choose side A for everyone" + onClick = { reassignWonders(assignedWondersWithForcedSide(props.currentGame, WonderSide.A)) } - private fun RBuilder.wonderSideSelectionGroup(currentGame: LobbyDTO) { - h4 { - +"Select wonder sides:" + +"A" } - bpButtonGroup { - bpButton( - icon = IconNames.RANDOM, - title = "Re-roll wonder sides randomly", - onClick = { randomizeWonderSides(currentGame) }, - ) - bpButton( - title = "Choose side A for everyone", - onClick = { setWonderSides(currentGame, WonderSide.A) }, - ) { - +"A" - } - bpButton( - title = "Choose side B for everyone", - onClick = { setWonderSides(currentGame, WonderSide.B) }, - ) { - +"B" - } + BpButton { + title = "Choose side B for everyone" + onClick = { reassignWonders(assignedWondersWithForcedSide(props.currentGame, WonderSide.B)) } + + +"B" } } +} - private fun randomizeWonders(currentGame: LobbyDTO) { - props.reassignWonders(currentGame.allWonders.deal(currentGame.players.size)) - } +private fun randomWonderAssignments(currentGame: LobbyDTO): List<AssignedWonder> = + currentGame.allWonders.deal(currentGame.players.size) - private fun randomizeWonderSides(currentGame: LobbyDTO) { - props.reassignWonders(currentGame.players.map { currentGame.findWonder(it.wonder.name).withRandomSide() }) - } +private fun assignedWondersWithForcedSide( + currentGame: LobbyDTO, + side: WonderSide +) = currentGame.players.map { currentGame.findWonder(it.wonder.name).withSide(side) } - private fun setWonderSides(currentGame: LobbyDTO, side: WonderSide) { - props.reassignWonders(currentGame.players.map { currentGame.findWonder(it.wonder.name).withSide(side) }) - } +private fun assignedWondersWithRandomSides(currentGame: LobbyDTO) = + currentGame.players.map { currentGame.findWonder(it.wonder.name) }.map { it.withRandomSide() } - private fun RBuilder.leaveButton() { - bpButton( - large = true, - intent = Intent.WARNING, - icon = "arrow-left", - title = "Leave the lobby and go back to the game browser", - onClick = { props.leaveLobby() }, - ) { - +"LEAVE" - } - } +private fun ChildrenBuilder.leaveButton(leaveLobby: () -> Unit) { + BpButton { + large = true + intent = Intent.WARNING + icon = "arrow-left" + title = "Leave the lobby and go back to the game browser" + onClick = { leaveLobby() } - private fun RBuilder.disbandButton() { - bpButton( - large = true, - intent = Intent.DANGER, - icon = IconNames.DELETE, - title = "Disband the group and go back to the game browser", - onClick = { props.disbandLobby() }, - ) { - +"DISBAND" - } + +"LEAVE" } } -fun RBuilder.lobby() = lobby {} - -private val lobby = connectStateAndDispatch<LobbyStateProps, LobbyDispatchProps, LobbyProps>( - clazz = LobbyPresenter::class, - mapStateToProps = { state, _ -> - currentGame = state.currentLobby - currentPlayer = state.currentPlayer - }, - mapDispatchToProps = { dispatch, _ -> - startGame = { dispatch(RequestStartGame()) } - addBot = { name -> dispatch(RequestAddBot(name)) } - leaveLobby = { dispatch(RequestLeaveLobby()) } - disbandLobby = { dispatch(RequestDisbandLobby()) } - reorderPlayers = { orderedPlayers -> dispatch(RequestReorderPlayers(orderedPlayers)) } - reassignWonders = { wonders -> dispatch(RequestReassignWonders(wonders)) } - }, -) +private fun ChildrenBuilder.disbandButton(disbandLobby: () -> Unit) { + BpButton { + large = true + intent = Intent.DANGER + icon = IconNames.DELETE + title = "Disband the group and go back to the game browser" + onClick = { disbandLobby() } + + +"DISBAND" + } +} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/LobbyStyles.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/LobbyStyles.kt index bbfc491f..fe20ac86 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/LobbyStyles.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/LobbyStyles.kt @@ -1,17 +1,17 @@ package org.luxons.sevenwonders.ui.components.lobby -import kotlinx.css.* -import org.luxons.sevenwonders.ui.components.GlobalStyles -import styled.StyleSheet +import csstype.* +import emotion.css.* +import org.luxons.sevenwonders.ui.components.* -object LobbyStyles : StyleSheet("LobbyStyles", isStatic = true) { +object LobbyStyles { - val contentContainer by css { - margin(horizontal = LinearDimension.auto) + val contentContainer = ClassName { + margin = Margin(vertical = 0.px, horizontal = Auto.auto) maxWidth = GlobalStyles.preGameWidth } - val setupPanel by css { + val setupPanel = ClassName { position = Position.fixed top = 2.rem right = 1.rem diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialList.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialList.kt index 8e99c23d..e9853588 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialList.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialList.kt @@ -1,19 +1,16 @@ package org.luxons.sevenwonders.ui.components.lobby -import kotlinx.css.* -import kotlinx.css.properties.* -import kotlinx.html.DIV -import org.luxons.sevenwonders.ui.components.GlobalStyles -import react.RBuilder -import react.ReactElement -import react.dom.* -import styled.StyledDOMBuilder -import styled.css -import styled.styledDiv -import styled.styledLi -import styled.styledUl +import csstype.* +import emotion.react.* +import org.luxons.sevenwonders.ui.components.* +import react.* +import react.dom.html.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.li +import react.dom.html.ReactHTML.ul +import web.html.* -fun <T> RBuilder.radialList( +fun <T> ChildrenBuilder.radialList( items: List<T>, centerElement: ReactElement<*>, renderItem: (T) -> ReactElement<*>, @@ -21,15 +18,14 @@ fun <T> RBuilder.radialList( itemWidth: Int, itemHeight: Int, options: RadialConfig = RadialConfig(), - block: StyledDOMBuilder<DIV>.() -> Unit = {}, + block: HTMLAttributes<HTMLDivElement>.() -> Unit = {}, ) { val containerWidth = options.diameter + itemWidth val containerHeight = options.diameter + itemHeight - styledDiv { - css { + div { + css(GlobalStyles.fixedCenter) { zeroMargins() - +GlobalStyles.fixedCenter width = containerWidth.px height = containerHeight.px } @@ -39,18 +35,22 @@ fun <T> RBuilder.radialList( } } -private fun <T> RBuilder.radialListItems( +private fun <T> ChildrenBuilder.radialListItems( items: List<T>, renderItem: (T) -> ReactElement<*>, getKey: (T) -> String, radialConfig: RadialConfig, ) { val offsets = offsetsFromCenter(items.size, radialConfig) - styledUl { + ul { css { zeroMargins() - transition(property = "all", duration = 500.ms, timing = Timing.easeInOut) - zIndex = 1 + transition = Transition( + property = TransitionProperty.all, + duration = 500.ms, + timingFunction = TransitionTimingFunction.easeInOut, + ) + zIndex = integer(1) width = radialConfig.diameter.px height = radialConfig.diameter.px absoluteCenter() @@ -67,52 +67,50 @@ private fun <T> RBuilder.radialListItems( } } -private fun RBuilder.radialListItem(item: ReactElement<*>, key: String, offset: CartesianCoords) { - styledLi { +private fun ChildrenBuilder.radialListItem(item: ReactElement<*>, key: String, offset: CartesianCoords) { + li { css { display = Display.block position = Position.absolute top = 50.pct left = 50.pct zeroMargins() - listStyleType = ListStyleType.unset - transition("all", 500.ms, Timing.easeInOut) - zIndex = 1 - transform { - translate(offset.x.px, offset.y.px) - translate((-50).pct, (-50).pct) - } - } - attrs { - this.key = key + listStyleType = Globals.unset + transition = Transition( + property = TransitionProperty.all, + duration = 500.ms, + timingFunction = TransitionTimingFunction.easeInOut, + ) + zIndex = integer(1) + transform = translate(offset.x.px - 50.pct, offset.y.px - 50.pct) } + this.key = key + child(item) } } -private fun RBuilder.radialListCenter(centerElement: ReactElement<*>?) { +private fun ChildrenBuilder.radialListCenter(centerElement: ReactElement<*>?) { if (centerElement == null) { return } - styledDiv { + div { css { - zIndex = 0 + zIndex = integer(0) absoluteCenter() } child(centerElement) } } -private fun CssBuilder.absoluteCenter() { +private fun PropertiesBuilder.absoluteCenter() { position = Position.absolute left = 50.pct top = 50.pct - transform { - translate((-50).pct, (-50).pct) - } + transform = translate((-50).pct, (-50).pct) } -private fun CssBuilder.zeroMargins() { - margin(all = 0.px) - padding(all = 0.px) +private fun PropertiesBuilder.zeroMargins() { + margin = Margin(vertical = 0.px, horizontal = 0.px) + padding = Padding(vertical = 0.px, horizontal = 0.px) } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialPlayerList.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialPlayerList.kt index 2084b7c0..290dd83f 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialPlayerList.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialPlayerList.kt @@ -1,39 +1,36 @@ package org.luxons.sevenwonders.ui.components.lobby -import blueprintjs.core.bpIcon -import blueprintjs.core.bpTag +import blueprintjs.core.* +import blueprintjs.icons.* import csstype.* -import kotlinx.css.* -import kotlinx.css.Color -import kotlinx.css.Display -import kotlinx.css.FlexDirection -import kotlinx.css.px -import kotlinx.css.rem -import kotlinx.html.DIV -import org.luxons.sevenwonders.model.api.PlayerDTO +import emotion.react.* +import org.luxons.sevenwonders.model.api.* import org.luxons.sevenwonders.model.api.actions.Icon -import org.luxons.sevenwonders.model.wonders.WonderSide -import react.RBuilder -import react.ReactElement -import react.buildElement -import styled.* +import org.luxons.sevenwonders.model.wonders.* +import org.luxons.sevenwonders.ui.utils.* +import react.* +import react.dom.html.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.span +import web.html.* -fun RBuilder.radialPlayerList( +fun ChildrenBuilder.radialPlayerList( players: List<PlayerDTO>, currentPlayer: PlayerDTO, - block: StyledDOMBuilder<DIV>.() -> Unit = {}, + block: HTMLAttributes<HTMLDivElement>.() -> Unit = {}, ) { val playerItems = players // .map { PlayerItem.Player(it) } .growWithPlaceholders(targetSize = 3) .withUserFirst(currentPlayer) - val tableImg = buildElement { lobbyWoodenTable(diameter = 200.px, borderSize = 15.px) } - radialList( items = playerItems, - centerElement = tableImg, - renderItem = { buildElement { playerElement(it) } }, + centerElement = LobbyWoodenTable.create { + diameter = 200.px + borderSize = 15.px + }, + renderItem = { PlayerElement.create { playerItem = it } }, getKey = { it.key }, itemWidth = 120, itemHeight = 100, @@ -60,74 +57,79 @@ private fun List<PlayerItem>.withUserFirst(me: PlayerDTO): List<PlayerItem> { private sealed class PlayerItem { abstract val key: String abstract val playerText: String - abstract val opacity: Double + abstract val opacity: Opacity abstract val icon: ReactElement<*> data class Player(val player: PlayerDTO) : PlayerItem() { override val key = player.username override val playerText = player.displayName - override val opacity = 1.0 - override val icon = buildElement { - userIcon( - icon = player.icon ?: when { - player.isGameOwner -> Icon("badge") - else -> Icon("user") - }, - title = if (player.isGameOwner) "Game owner" else null, - ) - } + override val opacity = number(1.0) + override val icon = createUserIcon( + icon = player.icon ?: when { + player.isGameOwner -> Icon(IconNames.BADGE) + else -> Icon(IconNames.USER) + }, + title = if (player.isGameOwner) "Game owner" else null, + ) } data class Placeholder(val index: Int) : PlayerItem() { override val key = "player-placeholder-$index" override val playerText = "?" - override val opacity = 0.4 - override val icon = buildElement { - userIcon( - icon = Icon("user"), - title = "Waiting for player...", - ) - } + override val opacity = number(0.4) + override val icon = createUserIcon( + icon = Icon(IconNames.USER), + title = "Waiting for player...", + ) } } -private fun RBuilder.userIcon(icon: Icon, title: String?) = bpIcon( - name = icon.name, - size = 50, - title = title, -) +private fun createUserIcon(icon: Icon, title: String?) = BpIcon.create { + this.icon = icon.name + this.size = 50 + this.title = title +} -private fun RBuilder.playerElement(playerItem: PlayerItem) { - styledDiv { +private external interface PlayerElementProps : Props { + var playerItem: PlayerItem +} + +private val PlayerElement = FC<PlayerElementProps>(displayName = "PlayerElement") { props -> + val playerItem = props.playerItem + div { css { display = Display.flex flexDirection = FlexDirection.column - alignItems = Align.center + alignItems = AlignItems.center opacity = playerItem.opacity } child(playerItem.icon) - styledSpan { + span { css { fontSize = if (playerItem is PlayerItem.Placeholder) 1.5.rem else 0.9.rem } +playerItem.playerText } if (playerItem is PlayerItem.Player) { - styledDiv { + div { val wonder = playerItem.player.wonder + css { marginTop = 0.3.rem - // this is to overcome ".bp4-dark .bp4-tag" on the nested bpTag children(".wonder-tag") { color = Color("#f5f8fa") // blueprintjs dark theme color (removed by .bp4-tag) backgroundColor = when (wonder.side) { - WonderSide.A -> Color.seaGreen - WonderSide.B -> Color.darkRed + WonderSide.A -> NamedColor.seagreen + WonderSide.B -> NamedColor.darkred } } } - bpTag(round = true, className = ClassName("wonder-tag")) { + + BpTag { + round = true + className = ClassName("wonder-tag") + +"${wonder.name} ${wonder.side}" } } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Table.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Table.kt index b9c799e2..2fa3b246 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Table.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Table.kt @@ -1,81 +1,96 @@ package org.luxons.sevenwonders.ui.components.lobby -import kotlinx.css.* -import kotlinx.css.properties.* -import kotlinx.html.DIV -import react.RBuilder -import styled.StyledDOMBuilder -import styled.animation -import styled.css -import styled.styledDiv +import csstype.* +import emotion.css.* +import emotion.react.* +import emotion.styled.* +import org.luxons.sevenwonders.ui.utils.* +import react.* +import react.dom.html.ReactHTML.div -private const val FIRE_REFLECTION_COLOR = "#b85e00" +private val FIRE_REFLECTION_COLOR = Color("#b85e00") + +private external interface CircleProps : PropsWithChildren, PropsWithClassName { + var diameter: Length +} + +private val Circle = FC<CircleProps>("Circle") { props -> + div { + css(props.className) { + width = props.diameter + height = props.diameter + borderRadius = 50.pct + } + child(props.children) + } +} + +private val OverlayCircle = Circle.styled { _, _ -> + position = Position.absolute + top = 0.px + left = 0.px +} + +external interface LobbyWoodenTableProps : Props { + var diameter: Length + var borderSize: Length +} + +val LobbyWoodenTable = FC<LobbyWoodenTableProps>("LobbyWoodenTable") { props -> + Circle { + diameter = props.diameter -fun RBuilder.lobbyWoodenTable(diameter: LinearDimension, borderSize: LinearDimension = 20.px) { - circle(diameter) { css { backgroundColor = Color("#3d1e0e") } - circle(diameter = diameter - borderSize) { + + Circle { + diameter = props.diameter - props.borderSize css { position = Position.absolute - top = borderSize / 2 - left = borderSize / 2 - background = "linear-gradient(45deg, #88541e, #995645, #52251a)" + top = props.borderSize / 2 + left = props.borderSize / 2 + background = linearGradient(45.deg, Color("#88541e"), Color("#995645"), Color("#52251a")) } } // flame reflection coming from bottom-right - overlayCircle(diameter) { + OverlayCircle { + diameter = props.diameter + css { - background = "linear-gradient(-45deg, $FIRE_REFLECTION_COLOR 10%, transparent 50%)" + background = + linearGradient((-45).deg, stop(FIRE_REFLECTION_COLOR, 10.pct), stop(NamedColor.transparent, 50.pct)) opacityAnimation(duration = 1.3.s) } } // flame reflection coming from bottom-left - overlayCircle(diameter) { + OverlayCircle { + diameter = props.diameter + css { - background = "linear-gradient(45deg, $FIRE_REFLECTION_COLOR 20%, transparent 40%)" + background = + linearGradient(45.deg, stop(FIRE_REFLECTION_COLOR, 20.pct), stop(NamedColor.transparent, 40.pct)) opacityAnimation(duration = 0.8.s) } } } } -private fun RBuilder.overlayCircle(diameter: LinearDimension, block: StyledDOMBuilder<DIV>.() -> Unit) { - circle(diameter) { - css { - position = Position.absolute - top = 0.px - left = 0.px - } - block() - } -} - -private fun RBuilder.circle(diameter: LinearDimension, block: StyledDOMBuilder<DIV>.() -> Unit) { - styledDiv { - css { - width = diameter - height = diameter - borderRadius = 50.pct - } - block() - } -} - -private fun CssBuilder.opacityAnimation(duration: Time) { - animation( - duration = duration, - direction = AnimationDirection.alternate, - iterationCount = IterationCount.infinite, - timing = cubicBezier(0.4, 0.4, 0.4, 2.0) - ) { +private fun PropertiesBuilder.opacityAnimation(duration: Time) { + val keyframes = keyframes { from { - opacity = 0.0 + opacity = number(0.0) } to { - opacity = 0.35 + opacity = number(0.35) } } + animation = Animation( + name = keyframes, + duration = duration, + timingFunction = cubicBezier(0.4, 0.4, 0.4, 2.0), + ) + animationDirection = AnimationDirection.alternate + animationIterationCount = AnimationIterationCount.infinite } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Utils.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Utils.kt index d5b3fffd..eb182dc7 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Utils.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Utils.kt @@ -1,43 +1,31 @@ package org.luxons.sevenwonders.ui.redux import react.* -import react.redux.rConnect -import redux.RAction -import redux.WrapperAction -import kotlin.reflect.KClass +import react.redux.* +import redux.* +import kotlin.reflect.* -fun <DP : PropsWithChildren> connectDispatch( - clazz: KClass<out RComponent<DP, out State>>, - mapDispatchToProps: DP.((RAction) -> WrapperAction, PropsWithChildren) -> Unit, -): ComponentClass<PropsWithChildren> { - val connect = rConnect(mapDispatchToProps = mapDispatchToProps) - return connect.invoke(clazz.js.unsafeCast<ComponentClass<DP>>()) -} - -fun <SP : PropsWithChildren> connectState( - clazz: KClass<out RComponent<SP, out State>>, - mapStateToProps: SP.(SwState, PropsWithChildren) -> Unit, -): ComponentClass<PropsWithChildren> { - val connect = rConnect(mapStateToProps = mapStateToProps) - return connect.invoke(clazz.js.unsafeCast<ComponentClass<SP>>()) -} +fun <R> useSwSelector(selector: (SwState) -> R) = useSelector(selector) +fun useSwDispatch() = useDispatch<RAction, WrapperAction>() -fun <SP : PropsWithChildren, OP : PropsWithChildren> connectStateWithOwnProps( - clazz: KClass<out RComponent<SP, out State>>, - mapStateToProps: SP.(SwState, OP) -> Unit, -): ComponentClass<OP> { - val connect = rConnect(mapStateToProps = mapStateToProps) - return connect.invoke(clazz.js.unsafeCast<ComponentClass<SP>>()) -} +fun <SP : Props, DP : Props, P : Props> connectStateAndDispatch( + clazz: KClass<out Component<P, out State>>, + mapStateToProps: SP.(SwState, Props) -> Unit, + mapDispatchToProps: DP.((RAction) -> WrapperAction, Props) -> Unit, +): ComponentClass<Props> = connectStateAndDispatch( + component = clazz.react, + mapStateToProps = mapStateToProps, + mapDispatchToProps = mapDispatchToProps, +) -fun <SP : PropsWithChildren, DP : PropsWithChildren, P : PropsWithChildren> connectStateAndDispatch( - clazz: KClass<out RComponent<P, out State>>, - mapStateToProps: SP.(SwState, PropsWithChildren) -> Unit, - mapDispatchToProps: DP.((RAction) -> WrapperAction, PropsWithChildren) -> Unit, -): ComponentClass<PropsWithChildren> { - val connect = rConnect<SwState, RAction, WrapperAction, PropsWithChildren, SP, DP, P>( +fun <SP : Props, DP : Props, P : Props> connectStateAndDispatch( + component: ComponentClass<P>, + mapStateToProps: SP.(SwState, Props) -> Unit, + mapDispatchToProps: DP.((RAction) -> WrapperAction, Props) -> Unit, +): ComponentClass<Props> { + val connect = rConnect<SwState, RAction, WrapperAction, Props, SP, DP, P>( mapStateToProps = mapStateToProps, mapDispatchToProps = mapDispatchToProps, ) - return connect.invoke(clazz.js.unsafeCast<ComponentClass<P>>()) + return connect.invoke(component) } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/utils/StyleUtils.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/utils/StyleUtils.kt index d438c259..5dd274de 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/utils/StyleUtils.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/utils/StyleUtils.kt @@ -1,10 +1,42 @@ package org.luxons.sevenwonders.ui.utils -import csstype.ClassName -import kotlinx.css.* -import styled.* -import kotlin.reflect.* +import csstype.* +import js.core.* +import react.dom.html.* -fun <T : StyleSheet> T.getTypedClassName(getClass: (T) -> KProperty0<RuleSet>): ClassName { - return ClassName(getClassName(getClass)) +/** + * The cubic-bezier() function defines a Cubic Bezier curve. + * + * A Cubic Bezier curve is defined by four points P0, P1, P2, and P3. P0 and P3 are the start and the end of the curve + * and, in CSS these points are fixed as the coordinates are ratios. P0 is (0, 0) and represents the initial time and + * the initial state, P3 is (1, 1) and represents the final time and the final state. + * + * The x coordinates provided here must be between 0 and 1 (the bezier curve points should be between the start time + * and end time, giving other values would make the curve go back in the past or further into the future). + * + * The y coordinates may be any value: the intermediate states can be below or above the start (0) or end (1) values. + */ +fun cubicBezier(x1: Double, y1: Double, x2: Double, y2: Double) = + "cubic-bezier($x1, $y1, $x2, $y2)".unsafeCast<AnimationTimingFunction>() + +fun Margin(all: AutoLength) = Margin(vertical = all, horizontal = all) + +fun Padding(all: Length) = Padding(vertical = all, horizontal = all) + +// this should work because NamedColor is ultimately a hex string in JS, not the actual name +fun NamedColor.withAlpha(alpha: Double) = "$this${(alpha * 255).toInt().toString(16)}".unsafeCast<BackgroundColor>() + +operator fun FilterFunction.plus(other: FilterFunction) = "$this $other".unsafeCast<FilterFunction>() + +fun PropertiesBuilder.ancestorHover(selector: String, block: PropertiesBuilder.() -> Unit) = + "$selector:hover &".invoke(block) + +fun PropertiesBuilder.children(selector: String, block: PropertiesBuilder.() -> Unit) = + "& > $selector".invoke(block) + +fun PropertiesBuilder.descendants(selector: String, block: PropertiesBuilder.() -> Unit) = + "& $selector".invoke(block) + +fun HTMLAttributes<*>.inlineStyles(block: PropertiesBuilder.() -> Unit) { + style = jso(block) } |