diff --git a/.circleci/config.yml b/.circleci/config.yml index f386894d3..74624b633 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -225,6 +225,7 @@ workflows: ignore: - master - develop + - beta - /^hotfix\/.*/ push-translations: triggers: @@ -254,6 +255,7 @@ workflows: ignore: - master - develop + - beta - /^hotfix\/.*/ build-test-deploy: jobs: @@ -265,6 +267,7 @@ workflows: only: - master - develop + - beta - /^hotfix\/.*/ - lint: context: @@ -296,6 +299,7 @@ workflows: only: - master - develop + - beta - /^hotfix\/.*/ - store_dist: context: @@ -307,6 +311,7 @@ workflows: only: - master - develop + - beta - /^hotfix\/.*/ - deploy-npm: context: @@ -322,6 +327,7 @@ workflows: only: - master - develop + - beta - /^hotfix\/.*/ - deploy-gh-pages: context: diff --git a/README.md b/README.md index 4477a140c..77aeb5f09 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,12 @@ If you want to watch the browser as it runs the test, rather than running headle USE_HEADLESS=no $(npm bin)/jest --runInBand test/integration/backpack.test.js ``` +_Note: If you are seeing failed tests related to `chromedriver` being incompatible with your version of Chrome, you may need to update `chromedriver` with:_ + +```bash +npm install chromedriver@{version} +``` + ## Troubleshooting ### Ignoring optional dependencies diff --git a/package-lock.json b/package-lock.json index 038fa6586..836223c1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,11 +16,11 @@ "cat-blocks": "npm:scratch-blocks@0.1.0-prerelease.20220318143026", "classnames": "2.2.6", "computed-style-to-inline-style": "3.0.0", + "cookie": "0.5.0", "copy-webpack-plugin": "6.4.1", "core-js": "2.5.7", "css-loader": "^1.0.0", "es6-object-assign": "1.1.0", - "file-loader": "2.0.0", "get-float-time-domain-data": "0.1.0", "get-user-media-promise": "1.1.4", "immutable": "3.8.2", @@ -58,9 +58,9 @@ "redux": "3.7.2", "redux-throttle": "0.1.1", "scratch-audio": "0.1.0-prerelease.20221123180128", - "scratch-blocks": "0.1.0-prerelease.20230507065123", + "scratch-blocks": "0.2.0-prerelease.20230509131059", "scratch-l10n": "3.15.20230507032520", - "scratch-paint": "1.1.54", + "scratch-paint": "1.2.0-beta.1", "scratch-render": "0.1.0-prerelease.20230318150639", "scratch-render-fonts": "1.0.0-prerelease.20221102164332", "scratch-storage": "2.2.1", @@ -95,6 +95,7 @@ "eslint-plugin-import": "2.23.4", "eslint-plugin-jest": "22.17.0", "eslint-plugin-react": "7.24.0", + "file-loader": "6.2.0", "gh-pages": "3.2.3", "html-webpack-plugin": "3.2.0", "husky": "8.0.1", @@ -109,6 +110,7 @@ "selenium-webdriver": "3.6.0", "semantic-release": "19.0.5", "uglifyjs-webpack-plugin": "1.3.0", + "url-loader": "4.1.1", "web-audio-test-api": "0.5.2", "webpack": "4.46.0", "webpack-cli": "3.3.12", @@ -4872,12 +4874,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@commitlint/config-validator/node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, "node_modules/@commitlint/config-validator/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -7389,10 +7385,9 @@ } }, "node_modules/@types/json-schema": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz", - "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==", - "dev": true + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==" }, "node_modules/@types/json5": { "version": "0.0.29", @@ -7813,14 +7808,18 @@ } }, "node_modules/ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dependencies": { - "fast-deep-equal": "^2.0.1", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/ajv-errors": { @@ -7832,9 +7831,9 @@ } }, "node_modules/ajv-keywords": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", - "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "peerDependencies": { "ajv": "^6.9.1" } @@ -8548,37 +8547,6 @@ "webpack": ">=2" } }, - "node_modules/babel-loader/node_modules/@types/json-schema": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", - "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==", - "dev": true - }, - "node_modules/babel-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/babel-loader/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/babel-loader/node_modules/emojis-list": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", @@ -8588,12 +8556,6 @@ "node": ">= 4" } }, - "node_modules/babel-loader/node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, "node_modules/babel-loader/node_modules/find-cache-dir": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", @@ -10257,15 +10219,6 @@ "node": ">= 0.6" } }, - "node_modules/compressible/node_modules/mime-db": { - "version": "1.45.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", - "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/compression": { "version": "1.7.4", "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", @@ -10503,10 +10456,9 @@ } }, "node_modules/cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", - "dev": true, + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", "engines": { "node": ">= 0.6" } @@ -10578,34 +10530,6 @@ "webpack": "^4.37.0 || ^5.0.0" } }, - "node_modules/copy-webpack-plugin/node_modules/@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==" - }, - "node_modules/copy-webpack-plugin/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/copy-webpack-plugin/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/copy-webpack-plugin/node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -10669,11 +10593,6 @@ "node": ">= 4" } }, - "node_modules/copy-webpack-plugin/node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, "node_modules/copy-webpack-plugin/node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -13641,6 +13560,15 @@ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", "dev": true }, + "node_modules/express/node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/express/node_modules/qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", @@ -13895,9 +13823,9 @@ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" }, "node_modules/fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.2.6", @@ -14096,18 +14024,64 @@ } }, "node_modules/file-loader": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-2.0.0.tgz", - "integrity": "sha512-YCsBfd1ZGCyonOKLxPiKPdu+8ld9HAaMEvJewzz+b2eTF7uL5Zm/HdBF6FjCrpCMRq25Mi0U1gl4pwn2TlH7hQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dev": true, "dependencies": { - "loader-utils": "^1.0.2", - "schema-utils": "^1.0.0" + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" }, "engines": { - "node": ">= 6.9.0 < 7.0.0 || >= 8.9.0" + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^2.0.0 || ^3.0.0 || ^4.0.0" + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-loader/node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/file-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/file-type": { @@ -19955,19 +19929,19 @@ } }, "node_modules/mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dependencies": { - "mime-db": "1.40.0" + "mime-db": "1.52.0" }, "engines": { "node": ">= 0.6" @@ -25902,9 +25876,9 @@ } }, "node_modules/scratch-blocks": { - "version": "0.1.0-prerelease.20230507065123", - "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20230507065123.tgz", - "integrity": "sha512-ESFkBv3EyyjXQbfgnbOXMCkLtlTzR1K+mPUANobSWim/evedSowx3l6uuqGVumEbo/9K2ullKyXQo7LDZDrvOw==", + "version": "0.2.0-prerelease.20230509131059", + "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.2.0-prerelease.20230509131059.tgz", + "integrity": "sha512-TPrMjmXhOh3AwlLJu3ggkMUwfHsUN8Y0tk7NnrLM02R0qtr32pgOHttBobXDya7R4Pu+HyfGSuxwtuJftb0MsQ==", "dependencies": { "exports-loader": "0.7.0", "google-closure-library": "20190301.0.0", @@ -25930,9 +25904,9 @@ } }, "node_modules/scratch-paint": { - "version": "1.1.54", - "resolved": "https://registry.npmjs.org/scratch-paint/-/scratch-paint-1.1.54.tgz", - "integrity": "sha512-23fylk1yn74TEpthDX1wGpnVGLRDNuQALhTD+Hrq5b0yfWDW1U+Ane6MuA6zOR2noFRcby357rvCdKNXU9p6zQ==", + "version": "1.2.0-beta.1", + "resolved": "https://registry.npmjs.org/scratch-paint/-/scratch-paint-1.2.0-beta.1.tgz", + "integrity": "sha512-YyVBRv5+ahJA02N0KKjalHUyJXKZOvQmpq658gswePsyU/GkcHByJCRQjd0iYWN8nJZlDEZrOExLuh+noTHjuA==", "dependencies": { "@scratch/paper": "0.11.20200728195508", "classnames": "2.2.5", @@ -29898,6 +29872,74 @@ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", "dev": true }, + "node_modules/url-loader": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", + "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "mime-types": "^2.1.27", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "file-loader": "*", + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "file-loader": { + "optional": true + } + } + }, + "node_modules/url-loader/node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/url-loader/node_modules/schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/url-parse": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz", @@ -34909,12 +34951,6 @@ "uri-js": "^4.2.2" } }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, "json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -36784,10 +36820,9 @@ } }, "@types/json-schema": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz", - "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==", - "dev": true + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==" }, "@types/json5": { "version": "0.0.29", @@ -37162,11 +37197,11 @@ } }, "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "requires": { - "fast-deep-equal": "^2.0.1", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" @@ -37179,9 +37214,9 @@ "requires": {} }, "ajv-keywords": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", - "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "requires": {} }, "amdefine": { @@ -37736,43 +37771,12 @@ "schema-utils": "^2.6.5" }, "dependencies": { - "@types/json-schema": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", - "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==", - "dev": true - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "requires": {} - }, "emojis-list": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "dev": true }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, "find-cache-dir": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", @@ -39183,14 +39187,6 @@ "dev": true, "requires": { "mime-db": ">= 1.43.0 < 2" - }, - "dependencies": { - "mime-db": { - "version": "1.45.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", - "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==", - "dev": true - } } }, "compression": { @@ -39389,10 +39385,9 @@ } }, "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", - "dev": true + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" }, "cookie-signature": { "version": "1.0.6", @@ -39446,28 +39441,6 @@ "webpack-sources": "^1.4.3" }, "dependencies": { - "@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==" - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "requires": {} - }, "array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -39516,11 +39489,6 @@ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -41888,6 +41856,12 @@ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", "dev": true }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "dev": true + }, "qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", @@ -42082,9 +42056,9 @@ } }, "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-glob": { "version": "3.2.6", @@ -42250,12 +42224,43 @@ } }, "file-loader": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-2.0.0.tgz", - "integrity": "sha512-YCsBfd1ZGCyonOKLxPiKPdu+8ld9HAaMEvJewzz+b2eTF7uL5Zm/HdBF6FjCrpCMRq25Mi0U1gl4pwn2TlH7hQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dev": true, "requires": { - "loader-utils": "^1.0.2", - "schema-utils": "^1.0.0" + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "dependencies": { + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } } }, "file-type": { @@ -46936,16 +46941,16 @@ "dev": true }, "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "requires": { - "mime-db": "1.40.0" + "mime-db": "1.52.0" } }, "mimic-fn": { @@ -51459,9 +51464,9 @@ } }, "scratch-blocks": { - "version": "0.1.0-prerelease.20230507065123", - "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20230507065123.tgz", - "integrity": "sha512-ESFkBv3EyyjXQbfgnbOXMCkLtlTzR1K+mPUANobSWim/evedSowx3l6uuqGVumEbo/9K2ullKyXQo7LDZDrvOw==", + "version": "0.2.0-prerelease.20230509131059", + "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.2.0-prerelease.20230509131059.tgz", + "integrity": "sha512-TPrMjmXhOh3AwlLJu3ggkMUwfHsUN8Y0tk7NnrLM02R0qtr32pgOHttBobXDya7R4Pu+HyfGSuxwtuJftb0MsQ==", "requires": { "exports-loader": "0.7.0", "google-closure-library": "20190301.0.0", @@ -51483,9 +51488,9 @@ } }, "scratch-paint": { - "version": "1.1.54", - "resolved": "https://registry.npmjs.org/scratch-paint/-/scratch-paint-1.1.54.tgz", - "integrity": "sha512-23fylk1yn74TEpthDX1wGpnVGLRDNuQALhTD+Hrq5b0yfWDW1U+Ane6MuA6zOR2noFRcby357rvCdKNXU9p6zQ==", + "version": "1.2.0-beta.1", + "resolved": "https://registry.npmjs.org/scratch-paint/-/scratch-paint-1.2.0-beta.1.tgz", + "integrity": "sha512-YyVBRv5+ahJA02N0KKjalHUyJXKZOvQmpq658gswePsyU/GkcHByJCRQjd0iYWN8nJZlDEZrOExLuh+noTHjuA==", "requires": { "@scratch/paper": "0.11.20200728195508", "classnames": "2.2.5", @@ -54639,6 +54644,47 @@ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", "dev": true }, + "url-loader": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", + "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "mime-types": "^2.1.27", + "schema-utils": "^3.0.0" + }, + "dependencies": { + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, "url-parse": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz", diff --git a/package.json b/package.json index a263fb453..1d815fbe0 100644 --- a/package.json +++ b/package.json @@ -39,11 +39,11 @@ "cat-blocks": "npm:scratch-blocks@0.1.0-prerelease.20220318143026", "classnames": "2.2.6", "computed-style-to-inline-style": "3.0.0", + "cookie": "0.5.0", "copy-webpack-plugin": "6.4.1", "core-js": "2.5.7", "css-loader": "^1.0.0", "es6-object-assign": "1.1.0", - "file-loader": "2.0.0", "get-float-time-domain-data": "0.1.0", "get-user-media-promise": "1.1.4", "immutable": "3.8.2", @@ -81,9 +81,9 @@ "redux": "3.7.2", "redux-throttle": "0.1.1", "scratch-audio": "0.1.0-prerelease.20221123180128", - "scratch-blocks": "0.1.0-prerelease.20230507065123", + "scratch-blocks": "0.2.0-prerelease.20230509131059", "scratch-l10n": "3.15.20230507032520", - "scratch-paint": "1.1.54", + "scratch-paint": "1.2.0-beta.1", "scratch-render": "0.1.0-prerelease.20230318150639", "scratch-render-fonts": "1.0.0-prerelease.20221102164332", "scratch-storage": "2.2.1", @@ -122,6 +122,7 @@ "eslint-plugin-import": "2.23.4", "eslint-plugin-jest": "22.17.0", "eslint-plugin-react": "7.24.0", + "file-loader": "6.2.0", "gh-pages": "3.2.3", "html-webpack-plugin": "3.2.0", "husky": "8.0.1", @@ -136,6 +137,7 @@ "selenium-webdriver": "3.6.0", "semantic-release": "19.0.5", "uglifyjs-webpack-plugin": "1.3.0", + "url-loader": "4.1.1", "web-audio-test-api": "0.5.2", "webpack": "4.46.0", "webpack-cli": "3.3.12", diff --git a/release.config.js b/release.config.js index d4156eb15..416c2f5a1 100644 --- a/release.config.js +++ b/release.config.js @@ -8,6 +8,11 @@ module.exports = { { name: 'hotfix/REPLACE', // replace with actual hotfix branch name channel: 'hotfix' + }, + { + name: 'beta', + channel: 'beta', + prerelease: true } ] }; diff --git a/src/components/action-menu/action-menu.css b/src/components/action-menu/action-menu.css index 14dbb5df0..03be98cac 100644 --- a/src/components/action-menu/action-menu.css +++ b/src/components/action-menu/action-menu.css @@ -19,7 +19,7 @@ $more-button-size: 2.25rem; justify-content: center; cursor: pointer; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - background: $motion-primary; + background: $looks-secondary; outline: none; border: none; transition: background-color 0.2s; @@ -45,14 +45,14 @@ button::-moz-focus-inner { border-radius: 100%; width: $main-button-size; height: $main-button-size; - box-shadow: 0 0 0 4px $motion-transparent; + box-shadow: 0 0 0 4px $looks-transparent; z-index: $z-index-add-button; transition: transform, box-shadow 0.5s; } .main-button:hover { transform: scale(1.1); - box-shadow: 0 0 0 6px $motion-transparent; + box-shadow: 0 0 0 6px $looks-transparent; } .main-icon { @@ -72,7 +72,7 @@ button::-moz-focus-inner { */ overflow-y: hidden; - background: $motion-tertiary; + background: $looks-secondary-dark; border-top-left-radius: $more-button-size; border-top-right-radius: $more-button-size; width: $more-button-size; @@ -115,7 +115,7 @@ button::-moz-focus-inner { .more-button { width: $more-button-size; height: $more-button-size; - background: $motion-tertiary; + background: $looks-secondary-dark; } .more-icon { diff --git a/src/components/asset-panel/selector.css b/src/components/asset-panel/selector.css index 6ff5510da..712bdf342 100644 --- a/src/components/asset-panel/selector.css +++ b/src/components/asset-panel/selector.css @@ -20,7 +20,7 @@ align-items: center; justify-content: space-around; padding: 0.75rem 0; - color: $motion-primary; + color: $looks-secondary; text-align: center; background: none; } diff --git a/src/components/audio-trimmer/audio-trimmer.css b/src/components/audio-trimmer/audio-trimmer.css index f3ce9fa42..9583558df 100644 --- a/src/components/audio-trimmer/audio-trimmer.css +++ b/src/components/audio-trimmer/audio-trimmer.css @@ -41,7 +41,7 @@ $hover-scale: 1.25; } .selection-background { - background: $motion-primary; + background: $looks-secondary; opacity: 0.5; } @@ -64,7 +64,7 @@ $hover-scale: 1.25; } .selector .trim-line { - border: 1px solid $motion-tertiary; + border: 1px solid $looks-secondary; } .playhead-container { @@ -83,7 +83,7 @@ $hover-scale: 1.25; */ width: 100%; height: 100%; - border-left: 1px solid $motion-primary; + border-left: 1px solid $looks-secondary; border-top: none; border-bottom: none; border-right: none; @@ -118,7 +118,7 @@ $hover-scale: 1.25; } .trimmer .trim-handle { - filter: hue-rotate(150deg); + filter: hue-rotate(-240deg) brightness(1.35); } .trim-handle img { diff --git a/src/components/audio-trimmer/icon--handle.svg b/src/components/audio-trimmer/icon--handle.svg index dad1ea751..69eb54a7e 100644 --- a/src/components/audio-trimmer/icon--handle.svg +++ b/src/components/audio-trimmer/icon--handle.svg @@ -8,9 +8,9 @@ - + - + diff --git a/src/components/backpack/backpack.css b/src/components/backpack/backpack.css index 25bc42d45..98a35a5e8 100644 --- a/src/components/backpack/backpack.css +++ b/src/components/backpack/backpack.css @@ -88,7 +88,7 @@ } .more { - background: $motion-primary; + background: $looks-secondary; color: $ui-white; border: none; outline: none; diff --git a/src/components/browser-modal/browser-modal.css b/src/components/browser-modal/browser-modal.css index 2f4145bf6..5fa57bbec 100644 --- a/src/components/browser-modal/browser-modal.css +++ b/src/components/browser-modal/browser-modal.css @@ -60,10 +60,10 @@ } .button-row button { - border: 1px solid $motion-primary; + border: 1px solid $looks-secondary; border-radius: 0.25rem; padding: 0.5rem 2rem; - background: $motion-primary; + background: $looks-secondary; color: white; font-weight: bold; font-size: 0.875rem; @@ -77,6 +77,6 @@ } .faq-link { - color: $motion-primary; + color: $looks-secondary; text-decoration: none; } diff --git a/src/components/cards/card.css b/src/components/cards/card.css index ad712e339..523323fff 100644 --- a/src/components/cards/card.css +++ b/src/components/cards/card.css @@ -214,7 +214,7 @@ } .deck-name { - color: $motion-primary; + color: $looks-secondary; font-weight: bold; font-size: 0.85rem; margin: .625rem 0px; @@ -260,7 +260,7 @@ .see-all-button { cursor: pointer; padding: 0.5rem 1rem; - background-color: $motion-primary; + background-color: $looks-secondary; color: white; font-weight: bold; border-radius: 0.25rem; diff --git a/src/components/close-button/close-button.css b/src/components/close-button/close-button.css index 48a18f88d..2a023e290 100644 --- a/src/components/close-button/close-button.css +++ b/src/components/close-button/close-button.css @@ -28,7 +28,7 @@ .small { width: 0.825rem; height: 0.825rem; - background-color: $motion-primary; + background-color: $looks-secondary; color: $ui-white; } diff --git a/src/components/connection-modal/connection-modal.css b/src/components/connection-modal/connection-modal.css index fde45e015..a97c4355d 100644 --- a/src/components/connection-modal/connection-modal.css +++ b/src/components/connection-modal/connection-modal.css @@ -80,8 +80,8 @@ border-radius: 0.25rem; font-weight: 600; font-size: 0.85rem; - background: $motion-primary; - border: $motion-primary; + background: $looks-secondary; + border: $looks-secondary; color: white; cursor: pointer; } @@ -172,9 +172,9 @@ right: -15px; left: -15px; padding: 5px 5px; - background-color: $motion-primary; + background-color: $looks-secondary; border-radius: 100%; - box-shadow: 0px 0px 0px 4px $motion-transparent; + box-shadow: 0px 0px 0px 4px $looks-transparent; /* animation: pulse-blue-ring 1s infinite ease-in-out alternate; */ animation: wiggle 0.5s infinite ease-in-out alternate; @@ -182,7 +182,7 @@ @keyframes pulse-blue-ring { 100% { - box-shadow: 0px 0px 0px 8px $motion-light-transparent; + box-shadow: 0px 0px 0px 8px $looks-light-transparent; } } @@ -208,9 +208,9 @@ .bluetooth-centered-icon { position: absolute; padding: 5px 5px; - background-color: $motion-primary; + background-color: $looks-secondary; border-radius: 100%; - box-shadow: 0px 0px 0px 2px $motion-transparent; + box-shadow: 0px 0px 0px 2px $looks-transparent; } .peripheral-tile-widgets { @@ -220,7 +220,7 @@ .activityArea { height: 165px; - background-color: $motion-light-transparent; + background-color: $looks-light-transparent; display: flex; justify-content: center; align-items: center; @@ -297,7 +297,7 @@ .connection-button { padding: 0.6rem 0.75rem; border-radius: 0.5rem; - background: $motion-primary; + background: $looks-secondary; color: white; font-weight: 600; font-size: 0.85rem; @@ -309,7 +309,7 @@ } .connection-button:disabled { - background: $motion-transparent; + background: $looks-transparent; } .segmented-button { @@ -390,7 +390,7 @@ display: flex; padding: 0.25rem 0.1rem; border-radius: 1rem; - background: $motion-light-transparent; + background: $looks-light-transparent; } .dots-holder-success { @@ -409,11 +409,11 @@ } .inactive-step-dot { - background: $motion-transparent; + background: $looks-transparent; } .active-step-dot { - background: $motion-primary; + background: $looks-secondary; } .success-dot { diff --git a/src/components/context-menu/context-menu.css b/src/components/context-menu/context-menu.css index 7f21c92f7..5548e066b 100644 --- a/src/components/context-menu/context-menu.css +++ b/src/components/context-menu/context-menu.css @@ -25,7 +25,7 @@ } .menu-item:hover { - background: $motion-primary; + background: $looks-secondary; color: white; } diff --git a/src/components/crash-message/crash-message.css b/src/components/crash-message/crash-message.css index 605f7fdab..daac90c53 100644 --- a/src/components/crash-message/crash-message.css +++ b/src/components/crash-message/crash-message.css @@ -10,12 +10,18 @@ justify-content: center; align-items: center; } + .body { width: 35%; color: white; text-align: center; } +/* scratch-www was overriding the colors */ +.body p, .body h2 { + color: inherit; +} + .reloadButton { border: 1px solid $motion-primary; border-radius: 0.25rem; diff --git a/src/components/custom-procedures/custom-procedures.css b/src/components/custom-procedures/custom-procedures.css index 96cc55d15..d54fd7d82 100644 --- a/src/components/custom-procedures/custom-procedures.css +++ b/src/components/custom-procedures/custom-procedures.css @@ -53,8 +53,8 @@ } .option-card:hover { - border: 2px solid $motion-primary; - box-shadow: 0px 0px 0px 4px $motion-transparent; + border: 2px solid $looks-secondary; + box-shadow: 0px 0px 0px 4px $looks-transparent; } .option-card + .option-card { @@ -92,8 +92,8 @@ } .button-row button.ok-button { - background: $motion-primary; - border: $motion-primary; + background: $looks-secondary; + border: $looks-secondary; color: white; } diff --git a/src/components/delete-button/delete-button.css b/src/components/delete-button/delete-button.css index 6abca7ec0..692a9d352 100644 --- a/src/components/delete-button/delete-button.css +++ b/src/components/delete-button/delete-button.css @@ -18,8 +18,8 @@ overflow: hidden; /* Mask the icon animation */ width: 1.75rem; height: 1.75rem; - box-shadow: 0px 0px 0px 2px $motion-transparent; - background-color: $motion-primary; + box-shadow: 0px 0px 0px 2px $looks-transparent; + background-color: $looks-secondary; color: $ui-white; border-radius: 50%; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; diff --git a/src/components/direction-picker/dial.css b/src/components/direction-picker/dial.css index ce9b7daea..43ab7c7d0 100644 --- a/src/components/direction-picker/dial.css +++ b/src/components/direction-picker/dial.css @@ -35,7 +35,7 @@ $dial-size: 40px; } .gauge-path { - fill: $motion-transparent; - stroke: $motion-primary; + fill: $looks-transparent; + stroke: $looks-secondary; stroke-width: 1px; } diff --git a/src/components/direction-picker/direction-picker.css b/src/components/direction-picker/direction-picker.css index 501ff0217..e39721317 100644 --- a/src/components/direction-picker/direction-picker.css +++ b/src/components/direction-picker/direction-picker.css @@ -1,32 +1,4 @@ -@import "../../css/colors.css"; - .button-row { - display: flex; - flex-direction: row; justify-content: center; - -} - -.icon-button { - margin: 0.25rem; - border: none; - background: none; - outline: none; - cursor: pointer; - user-select: none; -} - -.icon-button:active > img { - width: 20px; - height: 20px; - transform: scale(1.15); -} - -.icon-button > img { - transition: transform 0.1s; - filter: grayscale(100%); -} - -.icon-button.active > img { - filter: none; + margin-bottom: 1rem; } diff --git a/src/components/direction-picker/direction-picker.jsx b/src/components/direction-picker/direction-picker.jsx index 84a15db7a..3f06f3dc4 100644 --- a/src/components/direction-picker/direction-picker.jsx +++ b/src/components/direction-picker/direction-picker.jsx @@ -1,12 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; import Popover from 'react-popover'; import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; import Label from '../forms/label.jsx'; import Input from '../forms/input.jsx'; import BufferedInputHOC from '../forms/buffered-input-hoc.jsx'; +import ToggleButtons from '../toggle-buttons/toggle-buttons.jsx'; import Dial from './dial.jsx'; import styles from './direction-picker.css'; @@ -62,44 +62,29 @@ const DirectionPicker = props => ( direction={props.direction} onChange={props.onChangeDirection} /> -
- - - -
+ } isOpen={props.popoverOpen} diff --git a/src/components/direction-picker/icon--all-around.svg b/src/components/direction-picker/icon--all-around.svg index 2412c0b23..8ace45b7c 100644 --- a/src/components/direction-picker/icon--all-around.svg +++ b/src/components/direction-picker/icon--all-around.svg @@ -5,6 +5,6 @@ Created with Sketch. - + - \ No newline at end of file + diff --git a/src/components/direction-picker/icon--dial.svg b/src/components/direction-picker/icon--dial.svg index d4aa8ed82..e2109dfb6 100644 --- a/src/components/direction-picker/icon--dial.svg +++ b/src/components/direction-picker/icon--dial.svg @@ -1 +1 @@ -dial-face \ No newline at end of file +dial-face diff --git a/src/components/direction-picker/icon--dont-rotate.svg b/src/components/direction-picker/icon--dont-rotate.svg index 4796c03af..222054e82 100644 --- a/src/components/direction-picker/icon--dont-rotate.svg +++ b/src/components/direction-picker/icon--dont-rotate.svg @@ -5,6 +5,6 @@ Created with Sketch. - + - \ No newline at end of file + diff --git a/src/components/direction-picker/icon--handle.svg b/src/components/direction-picker/icon--handle.svg index 8e5fee6e0..ea901a7a4 100644 --- a/src/components/direction-picker/icon--handle.svg +++ b/src/components/direction-picker/icon--handle.svg @@ -1 +1 @@ -01_Dial Elements \ No newline at end of file +01_Dial Elements diff --git a/src/components/direction-picker/icon--left-right.svg b/src/components/direction-picker/icon--left-right.svg index 4525bd6e9..8565562d8 100644 --- a/src/components/direction-picker/icon--left-right.svg +++ b/src/components/direction-picker/icon--left-right.svg @@ -4,11 +4,9 @@ left-right-active Created with Sketch. - - - - - - + + + + - \ No newline at end of file + diff --git a/src/components/drag-layer/drag-layer.css b/src/components/drag-layer/drag-layer.css index 926c0ee9f..cb8885374 100644 --- a/src/components/drag-layer/drag-layer.css +++ b/src/components/drag-layer/drag-layer.css @@ -29,7 +29,7 @@ margin-top: -50%; padding: 0.25rem; - border: 2px solid $motion-primary; + border: 2px solid $looks-secondary; background: $ui-white; border-radius: 0.5rem; diff --git a/src/components/filter/filter.css b/src/components/filter/filter.css index fffef4812..ad8ed36f4 100644 --- a/src/components/filter/filter.css +++ b/src/components/filter/filter.css @@ -35,7 +35,7 @@ } .filter:focus-within { - box-shadow: 0 0 0 .25rem $motion-transparent; + box-shadow: 0 0 0 .25rem $looks-transparent; } /* diff --git a/src/components/filter/icon--filter.svg b/src/components/filter/icon--filter.svg index 400bd6234..c60cc9172 100644 --- a/src/components/filter/icon--filter.svg +++ b/src/components/filter/icon--filter.svg @@ -1,3 +1,3 @@ - + diff --git a/src/components/forms/input.css b/src/components/forms/input.css index ee1010dbc..ce0ab17b7 100644 --- a/src/components/forms/input.css +++ b/src/components/forms/input.css @@ -32,12 +32,12 @@ } .input-form:hover { - border-color: $motion-primary; + border-color: $looks-secondary; } .input-form:focus { - border-color: $motion-primary; - box-shadow: 0 0 0 0.25rem $motion-transparent; + border-color: $looks-secondary; + box-shadow: 0 0 0 0.25rem $looks-transparent; } .input-small { diff --git a/src/components/green-flag/green-flag.css b/src/components/green-flag/green-flag.css index aa4eb536d..3ca52f61e 100644 --- a/src/components/green-flag/green-flag.css +++ b/src/components/green-flag/green-flag.css @@ -11,9 +11,9 @@ } .green-flag:hover { - background-color: $motion-light-transparent; + background-color: $looks-light-transparent; } .green-flag.is-active { - background-color: $motion-transparent; + background-color: $looks-transparent; } diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css index eeb3989a1..2002e5bb2 100644 --- a/src/components/gui/gui.css +++ b/src/components/gui/gui.css @@ -124,7 +124,7 @@ .tab.is-selected { height: 90%; - color: $motion-primary; + color: $looks-secondary; background-color: $ui-white; z-index: 4; /* Make sure selected is always above */ } @@ -236,9 +236,9 @@ left: 0; right: 0; z-index: $z-index-extension-button; - background: $motion-primary; + background: $looks-secondary; - border: 1px solid $motion-primary; + border: 1px solid $looks-secondary; box-sizing: content-box; /* To match scratch-block vertical toolbox borders */ } diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 477a91213..f48e5a6ec 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -33,6 +33,7 @@ import TelemetryModal from '../telemetry-modal/telemetry-modal.jsx'; import layout, {STAGE_SIZE_MODES} from '../../lib/layout-constants'; import {resolveStageSize} from '../../lib/screen-utils'; +import {themeMap} from '../../lib/themes'; import styles from './gui.css'; import addExtensionIcon from './icon--extensions.svg'; @@ -68,6 +69,7 @@ const GUIComponent = props => { blocksTabVisible, cardsVisible, canChangeLanguage, + canChangeTheme, canCreateNew, canEditTitle, canManageFiles, @@ -119,6 +121,7 @@ const GUIComponent = props => { stageSizeMode, targetIsStage, telemetryModalVisible, + theme, tipsLibraryVisible, vm, ...componentProps @@ -214,6 +217,7 @@ const GUIComponent = props => { authorThumbnailUrl={authorThumbnailUrl} authorUsername={authorUsername} canChangeLanguage={canChangeLanguage} + canChangeTheme={canChangeTheme} canCreateCopy={canCreateCopy} canCreateNew={canCreateNew} canEditTitle={canEditTitle} @@ -303,14 +307,15 @@ const GUIComponent = props => { @@ -379,6 +384,7 @@ GUIComponent.propTypes = { blocksTabVisible: PropTypes.bool, blocksId: PropTypes.string, canChangeLanguage: PropTypes.bool, + canChangeTheme: PropTypes.bool, canCreateCopy: PropTypes.bool, canCreateNew: PropTypes.bool, canEditTitle: PropTypes.bool, @@ -428,6 +434,7 @@ GUIComponent.propTypes = { stageSizeMode: PropTypes.oneOf(Object.keys(STAGE_SIZE_MODES)), targetIsStage: PropTypes.bool, telemetryModalVisible: PropTypes.bool, + theme: PropTypes.string, tipsLibraryVisible: PropTypes.bool, vm: PropTypes.instanceOf(VM).isRequired }; @@ -437,6 +444,7 @@ GUIComponent.defaultProps = { basePath: './', blocksId: 'original', canChangeLanguage: true, + canChangeTheme: true, canCreateNew: false, canEditTitle: false, canManageFiles: true, @@ -456,8 +464,9 @@ GUIComponent.defaultProps = { const mapStateToProps = state => ({ // This is the button's mode, as opposed to the actual current state + blocksId: state.scratchGui.timeTravel.year.toString(), stageSizeMode: state.scratchGui.stageSize.stageSize, - blocksId: state.scratchGui.timeTravel.year.toString() + theme: state.scratchGui.theme.theme }); export default injectIntl(connect( diff --git a/src/components/gui/icon--code.svg b/src/components/gui/icon--code.svg index 390e683b1..c06998055 100644 --- a/src/components/gui/icon--code.svg +++ b/src/components/gui/icon--code.svg @@ -5,6 +5,6 @@ Created with Sketch. - + - \ No newline at end of file + diff --git a/src/components/gui/icon--costumes.svg b/src/components/gui/icon--costumes.svg index de50ce39b..5111126f0 100644 --- a/src/components/gui/icon--costumes.svg +++ b/src/components/gui/icon--costumes.svg @@ -5,6 +5,6 @@ Created with Sketch. - + - \ No newline at end of file + diff --git a/src/components/gui/icon--sounds.svg b/src/components/gui/icon--sounds.svg index 0de67c537..d05c06309 100644 --- a/src/components/gui/icon--sounds.svg +++ b/src/components/gui/icon--sounds.svg @@ -5,6 +5,6 @@ Created with Sketch. - + - \ No newline at end of file + diff --git a/src/components/icon-button/icon-button.css b/src/components/icon-button/icon-button.css index 002c817ae..5e6b4845e 100644 --- a/src/components/icon-button/icon-button.css +++ b/src/components/icon-button/icon-button.css @@ -7,7 +7,7 @@ cursor: pointer; font-size: 0.75rem; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - color: $motion-primary; + color: $looks-secondary; border-radius: 0.5rem; } @@ -26,5 +26,5 @@ } .container:active { - background-color: $motion-light-transparent; + background-color: $looks-light-transparent; } diff --git a/src/components/language-selector/language-selector.css b/src/components/language-selector/language-selector.css index 013e340dd..d5fb68618 100644 --- a/src/components/language-selector/language-selector.css +++ b/src/components/language-selector/language-selector.css @@ -4,7 +4,7 @@ /* Position the language select over the language icon, and make it transparent */ .language-select { position: absolute; - width: $language-selector-width; + width: 100%; height: $menu-bar-height; opacity: 0; user-select: none; diff --git a/src/components/library-item/library-item.css b/src/components/library-item/library-item.css index d4edd136d..1a7d15933 100644 --- a/src/components/library-item/library-item.css +++ b/src/components/library-item/library-item.css @@ -29,7 +29,7 @@ .library-item:hover { border-width: 2px; - border-color: $motion-primary; + border-color: $looks-secondary; } .hidden { diff --git a/src/components/library/library.css b/src/components/library/library.css index 51287daa7..df13ec5f7 100644 --- a/src/components/library/library.css +++ b/src/components/library/library.css @@ -25,7 +25,7 @@ justify-content: flex-start; align-items: center; height: calc($library-filter-bar-height + 2rem); /* padding */ - background-color: $motion-transparent; + background-color: $looks-transparent; padding: 0 1rem; font-size: .875rem; } diff --git a/src/components/menu-bar/check.svg b/src/components/menu-bar/check.svg new file mode 100644 index 000000000..afe11c097 --- /dev/null +++ b/src/components/menu-bar/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/menu-bar/menu-bar.css b/src/components/menu-bar/menu-bar.css index f2b401385..d35070da0 100644 --- a/src/components/menu-bar/menu-bar.css +++ b/src/components/menu-bar/menu-bar.css @@ -27,7 +27,7 @@ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: $menu-bar-standard-font-size; font-weight: bold; - background-color: $motion-primary; + background-color: $looks-secondary; color: $ui-white; } @@ -54,16 +54,28 @@ .language-icon { height: 1.5rem; vertical-align: middle; + margin: 0 .125rem 0 0; } -.language-caret { - margin: 0 .125rem; +[dir="rtl"] .language-icon { + margin: 0 0 0 .125rem; } .language-menu { box-sizing: border-box; display: inline-flex; - width: $language-selector-width; +} + +.language-label { + max-width: 7rem; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin: 0 .5rem 0 .25rem; +} + +[dir="rtl"] .language-label { + margin: 0 .25rem 0 .5rem; } .menu-bar-item { @@ -105,20 +117,17 @@ } .file-group .menu-bar-item { + margin: 0 .25rem; padding: 0 0.75rem; } -.menu-bar-item.language-menu { - padding: 0 0.5rem; -} - .menu-bar-menu { margin-top: $menu-bar-height; z-index: $z-index-menu-bar; } .feedback-link { - color: $motion-primary; + color: $looks-secondary; text-decoration: none; } @@ -160,6 +169,7 @@ } .account-info-group .menu-bar-item { + margin: 0 .25rem; padding: 0 0.75rem; } @@ -169,7 +179,11 @@ } .help-icon { - margin-right: 0.35rem; + margin: 0 .25rem 0 0; +} + +[dir="rtl"] .help-icon { + margin: 0 0 0 .25rem; } .account-nav-menu, .mystuff-button { @@ -226,3 +240,13 @@ margin: 0.5rem; vertical-align: middle; } + +@media only screen and (max-width: 1024px) { + .language-label, .tutorials-label { + display: none; + } + + .help-icon { + margin-right: 0; + } +} diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx index 5aba0ffaf..5ed80abee 100644 --- a/src/components/menu-bar/menu-bar.jsx +++ b/src/components/menu-bar/menu-bar.jsx @@ -8,6 +8,7 @@ import bowser from 'bowser'; import React from 'react'; import VM from 'scratch-vm'; +import locales from 'scratch-l10n'; import Box from '../box/box.jsx'; import Button from '../button/button.jsx'; @@ -28,6 +29,7 @@ import SB3Downloader from '../../containers/sb3-downloader.jsx'; import DeletionRestorer from '../../containers/deletion-restorer.jsx'; import TurboMode from '../../containers/turbo-mode.jsx'; import MenuBarHOC from '../../containers/menu-bar-hoc.jsx'; +import ThemeMenu from './theme-menu.jsx'; import {openTipsLibrary} from '../../reducers/modals'; import {setPlayer} from '../../reducers/mode'; @@ -67,10 +69,14 @@ import { openLoginMenu, closeLoginMenu, loginMenuOpen, + openThemeMenu, + closeThemeMenu, + themeMenuOpen, openModeMenu, closeModeMenu, modeMenuOpen } from '../../reducers/menus'; +import {setTheme} from '../../reducers/theme.js'; import collectMetadata from '../../lib/collect-metadata'; @@ -91,6 +97,7 @@ import prehistoricLogo from './prehistoric-logo.svg'; import oldtimeyLogo from './oldtimey-logo.svg'; import sharedMessages from '../../lib/shared-messages'; +import {persistTheme} from '../../lib/themes/themePersistance'; const ariaMessages = defineMessages({ language: { @@ -176,6 +183,7 @@ class MenuBar extends React.Component { constructor (props) { super(props); bindAll(this, [ + 'handleChangeTheme', 'handleClickNew', 'handleClickRemix', 'handleClickSave', @@ -302,6 +310,11 @@ class MenuBar extends React.Component { this.props.onClickLanguage(e); } } + handleChangeTheme (theme) { + this.props.onChangeTheme(theme); + this.props.onRequestCloseTheme(); + persistTheme(theme); + } restoreOptionMessage (deletedItem) { switch (deletedItem) { case 'Sprite': @@ -449,18 +462,26 @@ class MenuBar extends React.Component { {(this.props.canChangeLanguage) && (
-
- - -
+ + {locales[this.props.currentLocale].name} +
)} + {(this.props.canChangeTheme) && ()} + {(this.props.canManageFiles) && (
)}
- -
- - -
- {this.props.canEditTitle ? (
) : [])}
+ +
+
+ + + + +
+
{/* show the proper UI in the account menu, given whether the user is @@ -856,6 +880,7 @@ MenuBar.propTypes = { authorUsername: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), autoUpdateProject: PropTypes.func, canChangeLanguage: PropTypes.bool, + canChangeTheme: PropTypes.bool, canCreateCopy: PropTypes.bool, canCreateNew: PropTypes.bool, canEditTitle: PropTypes.bool, @@ -865,6 +890,7 @@ MenuBar.propTypes = { canShare: PropTypes.bool, className: PropTypes.string, confirmReadyToReplaceProject: PropTypes.func, + currentLocale: PropTypes.string.isRequired, editMenuOpen: PropTypes.bool, enableCommunity: PropTypes.bool, fileMenuOpen: PropTypes.bool, @@ -878,13 +904,13 @@ MenuBar.propTypes = { locale: PropTypes.string.isRequired, loginMenuOpen: PropTypes.bool, logo: PropTypes.string, + onChangeTheme: PropTypes.func, modeMenuOpen: PropTypes.bool, modeNow: PropTypes.bool, mode220022BC: PropTypes.bool, mode1920: PropTypes.bool, mode1990: PropTypes.bool, mode2020: PropTypes.bool, - onClickAbout: PropTypes.oneOfType([ PropTypes.func, // button mode: call this callback when the About button is clicked PropTypes.arrayOf( // menu mode: list of items in the About menu @@ -917,6 +943,8 @@ MenuBar.propTypes = { onRequestCloseFile: PropTypes.func, onRequestCloseLanguage: PropTypes.func, onRequestCloseLogin: PropTypes.func, + onRequestCloseTheme: PropTypes.func, + onRequestOpenTheme: PropTypes.func, onRequestCloseMode: PropTypes.func, onSeeCommunity: PropTypes.func, onShare: PropTypes.func, @@ -927,6 +955,8 @@ MenuBar.propTypes = { sessionExists: PropTypes.bool, shouldSaveBeforeTransition: PropTypes.func, showComingSoon: PropTypes.bool, + theme: PropTypes.string, + themeMenuOpen: PropTypes.bool, userOwnsProject: PropTypes.bool, username: PropTypes.string, vm: PropTypes.instanceOf(VM).isRequired @@ -943,6 +973,7 @@ const mapStateToProps = (state, ownProps) => { return { aboutMenuOpen: aboutMenuOpen(state), accountMenuOpen: accountMenuOpen(state), + currentLocale: state.locales.locale, fileMenuOpen: fileMenuOpen(state), editMenuOpen: editMenuOpen(state), isRtl: state.locales.isRtl, @@ -954,6 +985,8 @@ const mapStateToProps = (state, ownProps) => { modeMenuOpen: modeMenuOpen(state), projectTitle: state.scratchGui.projectTitle, sessionExists: state.session && typeof state.session.session !== 'undefined', + theme: state.scratchGui.theme.theme, + themeMenuOpen: themeMenuOpen(state), username: user ? user.username : null, userOwnsProject: ownProps.authorUsername && user && (ownProps.authorUsername === user.username), @@ -983,11 +1016,14 @@ const mapDispatchToProps = dispatch => ({ onRequestCloseMode: () => dispatch(closeModeMenu()), onRequestOpenAbout: () => dispatch(openAboutMenu()), onRequestCloseAbout: () => dispatch(closeAboutMenu()), + onRequestOpenTheme: () => dispatch(openThemeMenu()), + onRequestCloseTheme: () => dispatch(closeThemeMenu()), onClickNew: needSave => dispatch(requestNewProject(needSave)), onClickRemix: () => dispatch(remixProject()), onClickSave: () => dispatch(manualUpdateProject()), onClickSaveAsCopy: () => dispatch(saveProjectAsCopy()), onSeeCommunity: () => dispatch(setPlayer(true)), + onChangeTheme: theme => dispatch(setTheme(theme)), onSetTimeTravelMode: mode => dispatch(setTimeTravel(mode)) }); diff --git a/src/components/menu-bar/theme-menu.css b/src/components/menu-bar/theme-menu.css new file mode 100644 index 000000000..7c79945c2 --- /dev/null +++ b/src/components/menu-bar/theme-menu.css @@ -0,0 +1,52 @@ +@import "../../css/colors.css"; + +.theme-label { + max-width: 7rem; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin: 0 .5rem 0 .25rem; +} + +[dir="rtl"] .theme-label { + margin: 0 .25rem 0 .5rem; +} + +.theme-icon { + height: 1.5rem; + vertical-align: middle; + margin: 0 .25rem 0 0; +} + +[dir="rtl"] .theme-icon { + margin: 0 0 0 .25rem; +} + +.theme-option { + display: flex; + align-items: center; + gap: .5rem; +} + +.theme-active { + background-color: $ui-black-transparent; +} + +.check { + margin: 0 .25rem 0 0; + visibility: hidden; +} + +[dir="rtl"] .check { + margin: 0 0 0 .25rem; +} + +.selected { + visibility: visible; +} + +@media only screen and (max-width: 1024px) { + .theme-label { + display: none; + } +} diff --git a/src/components/menu-bar/theme-menu.jsx b/src/components/menu-bar/theme-menu.jsx new file mode 100644 index 000000000..afd58aa53 --- /dev/null +++ b/src/components/menu-bar/theme-menu.jsx @@ -0,0 +1,105 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {defineMessages, FormattedMessage, injectIntl, intlShape} from 'react-intl'; +import classNames from 'classnames'; + +import MenuBarMenu from './menu-bar-menu.jsx'; +import {DEFAULT_THEME, HIGH_CONTRAST_THEME, themeMap} from '../../lib/themes'; +import {MenuItem, MenuSection} from '../menu/menu.jsx'; + +import dropdownCaret from './dropdown-caret.svg'; +import check from './check.svg'; + +import styles from './theme-menu.css'; +import menuBarStyles from './menu-bar.css'; + +const messages = defineMessages({ + colorTheme: { + id: 'gui.theme.colorTheme', + defaultMessage: 'Color theme', + description: 'label for selecting the site\'s color theme' + } +}); + +const ThemeMenuItem = props => { + const themeInfo = themeMap[props.theme]; + + return ( + +
+ + + +
+
); +}; + +ThemeMenuItem.propTypes = { + isRtl: PropTypes.bool, + isSelected: PropTypes.bool, + onClick: PropTypes.func, + theme: PropTypes.string +}; + +const ThemeMenu = props => { + const enabledThemes = [DEFAULT_THEME, HIGH_CONTRAST_THEME]; + const themeInfo = themeMap[props.theme]; + + return (
+ {props.intl.formatMessage(messages.colorTheme)} + + + + + + + {enabledThemes.map(enabledTheme => ( + props.onChange(enabledTheme)} + theme={enabledTheme} + />) + )} + + +
); +}; + +ThemeMenu.propTypes = { + className: PropTypes.string, + intl: intlShape.isRequired, + isRtl: PropTypes.bool, + onChange: PropTypes.func, + onRequestClose: PropTypes.func, + onRequestOpen: PropTypes.func, + theme: PropTypes.string, + themeMenuOpen: PropTypes.bool +}; + +export default injectIntl(ThemeMenu); diff --git a/src/components/menu/menu.css b/src/components/menu/menu.css index c48f0d25d..e5d0a66ef 100644 --- a/src/components/menu/menu.css +++ b/src/components/menu/menu.css @@ -4,7 +4,7 @@ position: absolute; border: 1px solid $ui-black-transparent; border-radius: 0 0 8px 8px; - background-color: $motion-primary; + background-color: $looks-secondary; padding: 0; margin: 0; min-width: 186px; diff --git a/src/components/modal/modal.css b/src/components/modal/modal.css index 1ed2425ce..6beaaf8f6 100644 --- a/src/components/modal/modal.css +++ b/src/components/modal/modal.css @@ -67,7 +67,7 @@ $sides: 20rem; box-sizing: border-box; width: 100%; - background-color: $motion-primary; + background-color: $looks-secondary; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 1rem; diff --git a/src/components/monitor/default-monitor.jsx b/src/components/monitor/default-monitor.jsx index 992014775..3368519dd 100644 --- a/src/components/monitor/default-monitor.jsx +++ b/src/components/monitor/default-monitor.jsx @@ -10,7 +10,10 @@ const DefaultMonitor = ({categoryColor, label, value}) => (
{value}
@@ -19,7 +22,10 @@ const DefaultMonitor = ({categoryColor, label, value}) => ( ); DefaultMonitor.propTypes = { - categoryColor: PropTypes.string.isRequired, + categoryColor: PropTypes.shape({ + background: PropTypes.string.isRequired, + text: PropTypes.string.isRequired + }).isRequired, label: PropTypes.string.isRequired, value: PropTypes.oneOfType([ PropTypes.string, diff --git a/src/components/monitor/large-monitor.jsx b/src/components/monitor/large-monitor.jsx index 5101ad0a5..64ac32640 100644 --- a/src/components/monitor/large-monitor.jsx +++ b/src/components/monitor/large-monitor.jsx @@ -6,7 +6,10 @@ const LargeMonitor = ({categoryColor, value}) => (
{value}
@@ -14,7 +17,10 @@ const LargeMonitor = ({categoryColor, value}) => ( ); LargeMonitor.propTypes = { - categoryColor: PropTypes.string, + categoryColor: PropTypes.shape({ + background: PropTypes.string.isRequired, + text: PropTypes.string.isRequired + }).isRequired, value: PropTypes.oneOfType([ PropTypes.string, PropTypes.number diff --git a/src/components/monitor/list-monitor-scroller.jsx b/src/components/monitor/list-monitor-scroller.jsx index cfcd6a248..ea0de244e 100644 --- a/src/components/monitor/list-monitor-scroller.jsx +++ b/src/components/monitor/list-monitor-scroller.jsx @@ -41,7 +41,10 @@ class ListMonitorScroller extends React.Component {
{this.props.draggable && this.props.activeIndex === index ? ( @@ -51,6 +54,7 @@ class ListMonitorScroller extends React.Component { autoComplete={false} className={classNames(styles.listInput, 'no-drag')} spellCheck={false} + style={{color: this.props.categoryColor.text}} type="text" value={this.props.activeValue} onBlur={this.props.onDeactivate} @@ -97,7 +101,10 @@ class ListMonitorScroller extends React.Component { ListMonitorScroller.propTypes = { activeIndex: PropTypes.number, activeValue: PropTypes.string, - categoryColor: PropTypes.string, + categoryColor: PropTypes.shape({ + background: PropTypes.string.isRequired, + text: PropTypes.string.isRequired + }).isRequired, draggable: PropTypes.bool, height: PropTypes.number, onActivate: PropTypes.func, diff --git a/src/components/monitor/list-monitor.jsx b/src/components/monitor/list-monitor.jsx index e3422e5b3..4bd9a2563 100644 --- a/src/components/monitor/list-monitor.jsx +++ b/src/components/monitor/list-monitor.jsx @@ -54,7 +54,10 @@ const ListMonitor = ({draggable, label, width, height, value, onResizeMouseDown, ListMonitor.propTypes = { activeIndex: PropTypes.number, - categoryColor: PropTypes.string.isRequired, + categoryColor: PropTypes.shape({ + background: PropTypes.string.isRequired, + text: PropTypes.string.isRequired + }).isRequired, draggable: PropTypes.bool.isRequired, height: PropTypes.number, label: PropTypes.string.isRequired, diff --git a/src/components/monitor/monitor.css b/src/components/monitor/monitor.css index 0947db638..a4011457a 100644 --- a/src/components/monitor/monitor.css +++ b/src/components/monitor/monitor.css @@ -42,7 +42,6 @@ align-items: center; min-width: 40px; text-align: center; - color: white; margin: 0 5px; border-radius: calc($space / 2); padding: 0 2px; @@ -55,7 +54,6 @@ min-width: 3rem; padding: 0.1rem 0.25rem; text-align: center; - color: white; font-size: 1rem; white-space: pre-wrap; transform: translateZ(0); /* Fixes flickering in Safari */ @@ -115,7 +113,6 @@ .list-value { min-width: 40px; text-align: left; - color: white; margin: 0 3px; border-radius: calc($space / 2); border: 1px solid $ui-black-transparent; @@ -161,7 +158,6 @@ padding: 3px 5px; border: 0; background: none; - color: $ui-white; outline: none; font-size: 0.75rem; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; @@ -174,7 +170,6 @@ padding: 0; padding-right: 5px; cursor: pointer; - color: $ui-white; } .add-button { diff --git a/src/components/monitor/monitor.jsx b/src/components/monitor/monitor.jsx index ddb2f2e57..d9ceb9269 100644 --- a/src/components/monitor/monitor.jsx +++ b/src/components/monitor/monitor.jsx @@ -10,17 +10,19 @@ import DefaultMonitor from './default-monitor.jsx'; import LargeMonitor from './large-monitor.jsx'; import SliderMonitor from '../../containers/slider-monitor.jsx'; import ListMonitor from '../../containers/list-monitor.jsx'; +import {getColorsForTheme} from '../../lib/themes/index.js'; import styles from './monitor.css'; -const categories = { - data: '#FF8C1A', - sensing: '#5CB1D6', - sound: '#CF63CF', - looks: '#9966FF', - motion: '#4C97FF', - list: '#FC662C', - extension: '#0FBD8C' +// Map category name to color name used in scratch-blocks Blockly.Colours +const categoryColorMap = { + data: 'data', + sensing: 'sensing', + sound: 'sounds', + looks: 'looks', + motion: 'motion', + list: 'data_lists', + extension: 'pen' }; const modes = { @@ -30,6 +32,14 @@ const modes = { list: ListMonitor }; +const getCategoryColor = (theme, category) => { + const colors = getColorsForTheme(theme); + return { + background: colors[categoryColorMap[category]].primary, + text: colors.text + }; +}; + const MonitorComponent = props => ( ( onDoubleClick={props.mode === 'list' || !props.draggable ? null : props.onNextMode} > {React.createElement(modes[props.mode], { - categoryColor: categories[props.category], + categoryColor: getCategoryColor(props.theme, props.category), ...props })} @@ -122,12 +132,10 @@ const MonitorComponent = props => ( ); -MonitorComponent.categories = categories; - const monitorModes = Object.keys(modes); MonitorComponent.propTypes = { - category: PropTypes.oneOf(Object.keys(categories)), + category: PropTypes.oneOf(Object.keys(categoryColorMap)), componentRef: PropTypes.func.isRequired, draggable: PropTypes.bool.isRequired, label: PropTypes.string.isRequired, @@ -140,7 +148,8 @@ MonitorComponent.propTypes = { onSetModeToDefault: PropTypes.func, onSetModeToLarge: PropTypes.func, onSetModeToSlider: PropTypes.func, - onSliderPromptOpen: PropTypes.func + onSliderPromptOpen: PropTypes.func, + theme: PropTypes.string.isRequired }; MonitorComponent.defaultProps = { diff --git a/src/components/monitor/slider-monitor.jsx b/src/components/monitor/slider-monitor.jsx index 0271b140a..2bda8f593 100644 --- a/src/components/monitor/slider-monitor.jsx +++ b/src/components/monitor/slider-monitor.jsx @@ -12,7 +12,10 @@ const SliderMonitor = ({categoryColor, isDiscrete, label, min, max, value, onSli
{value}
@@ -33,7 +36,10 @@ const SliderMonitor = ({categoryColor, isDiscrete, label, min, max, value, onSli ); SliderMonitor.propTypes = { - categoryColor: PropTypes.string.isRequired, + categoryColor: PropTypes.shape({ + background: PropTypes.string.isRequired, + text: PropTypes.string.isRequired + }).isRequired, isDiscrete: PropTypes.bool, label: PropTypes.string.isRequired, max: PropTypes.number, diff --git a/src/components/prompt/prompt.css b/src/components/prompt/prompt.css index c12afc6b7..71fb7fac7 100644 --- a/src/components/prompt/prompt.css +++ b/src/components/prompt/prompt.css @@ -64,8 +64,8 @@ } .button-row button.ok-button { - background: $motion-primary; - border: $motion-primary; + background: $looks-secondary; + border: $looks-secondary; color: white; } diff --git a/src/components/question/question.css b/src/components/question/question.css index a0f8116ed..ca5c65913 100644 --- a/src/components/question/question.css +++ b/src/components/question/question.css @@ -34,7 +34,7 @@ border-radius: 100%; color: white; - background: $motion-primary; + background: $looks-secondary; } [dir="ltr"] .question-submit-button { @@ -60,7 +60,7 @@ } .question-input > input:focus { - box-shadow: 0px 0px 0px 3px $motion-transparent; + box-shadow: 0px 0px 0px 3px $looks-transparent; } .question-submit-button-icon { diff --git a/src/components/record-modal/icon--back.svg b/src/components/record-modal/icon--back.svg index 47d09bc7f..aff0c4f68 100644 --- a/src/components/record-modal/icon--back.svg +++ b/src/components/record-modal/icon--back.svg @@ -5,7 +5,7 @@ Created with Sketch. - + diff --git a/src/components/record-modal/icon--play.svg b/src/components/record-modal/icon--play.svg index 9494fccb8..14ddbbaf2 100644 --- a/src/components/record-modal/icon--play.svg +++ b/src/components/record-modal/icon--play.svg @@ -18,7 +18,7 @@ - + diff --git a/src/components/record-modal/icon--stop-playback.svg b/src/components/record-modal/icon--stop-playback.svg index bf7bb64be..c72c363e3 100644 --- a/src/components/record-modal/icon--stop-playback.svg +++ b/src/components/record-modal/icon--stop-playback.svg @@ -18,11 +18,11 @@ - - + + - \ No newline at end of file + diff --git a/src/components/record-modal/record-modal.css b/src/components/record-modal/record-modal.css index 41fc90a96..e59df3e91 100644 --- a/src/components/record-modal/record-modal.css +++ b/src/components/record-modal/record-modal.css @@ -47,7 +47,7 @@ } .playing-text { - color: $motion-primary; + color: $looks-secondary; } .recording-text { @@ -89,12 +89,12 @@ border: 1px solid $ui-black-transparent; font-weight: 600; font-size: 0.85rem; - color: $motion-primary; + color: $looks-secondary; } .button-row button.ok-button { - background: $motion-primary; - border: $motion-primary; + background: $looks-secondary; + border: $looks-secondary; color: white; } @@ -106,7 +106,8 @@ text-align: center; } -.record-button { +/* Increase specificity to make sure this wins out over normalize.css */ +svg.record-button { overflow: visible; } diff --git a/src/components/slider-prompt/slider-prompt.css b/src/components/slider-prompt/slider-prompt.css index c1ae4854a..146b11551 100644 --- a/src/components/slider-prompt/slider-prompt.css +++ b/src/components/slider-prompt/slider-prompt.css @@ -41,8 +41,8 @@ } .button-row button.ok-button { - background: $motion-primary; - border: $motion-primary; + background: $looks-secondary; + border: $looks-secondary; color: white; } diff --git a/src/components/sound-editor/icon--copy-to-new.svg b/src/components/sound-editor/icon--copy-to-new.svg index d5f462117..4d228a177 100644 --- a/src/components/sound-editor/icon--copy-to-new.svg +++ b/src/components/sound-editor/icon--copy-to-new.svg @@ -4,6 +4,6 @@ Sound/General/Copy to New Created with Sketch. - + - \ No newline at end of file + diff --git a/src/components/sound-editor/icon--copy.svg b/src/components/sound-editor/icon--copy.svg index 7bac62c1b..b905e4342 100644 --- a/src/components/sound-editor/icon--copy.svg +++ b/src/components/sound-editor/icon--copy.svg @@ -4,11 +4,11 @@ Sound/General/Copy Created with Sketch. - + - + - \ No newline at end of file + diff --git a/src/components/sound-editor/icon--delete.svg b/src/components/sound-editor/icon--delete.svg index 8f662201e..c7e096962 100644 --- a/src/components/sound-editor/icon--delete.svg +++ b/src/components/sound-editor/icon--delete.svg @@ -11,8 +11,8 @@ - + - \ No newline at end of file + diff --git a/src/components/sound-editor/icon--paste.svg b/src/components/sound-editor/icon--paste.svg index 53ca104c1..51027762f 100644 --- a/src/components/sound-editor/icon--paste.svg +++ b/src/components/sound-editor/icon--paste.svg @@ -4,6 +4,6 @@ Sound/General/Paste Created with Sketch. - + - \ No newline at end of file + diff --git a/src/components/sound-editor/icon--redo.svg b/src/components/sound-editor/icon--redo.svg index fdc3b1780..c32f94494 100644 --- a/src/components/sound-editor/icon--redo.svg +++ b/src/components/sound-editor/icon--redo.svg @@ -5,7 +5,7 @@ Created with Sketch. - + diff --git a/src/components/sound-editor/icon--undo.svg b/src/components/sound-editor/icon--undo.svg index 54ff88ee4..44c9517b7 100644 --- a/src/components/sound-editor/icon--undo.svg +++ b/src/components/sound-editor/icon--undo.svg @@ -5,7 +5,7 @@ Created with Sketch. - + diff --git a/src/components/sound-editor/sound-editor.css b/src/components/sound-editor/sound-editor.css index 7a791e6ab..e2e38a020 100644 --- a/src/components/sound-editor/sound-editor.css +++ b/src/components/sound-editor/sound-editor.css @@ -79,9 +79,24 @@ $border-radius: 0.25rem; border: 1px solid $ui-black-transparent; cursor: pointer; font-size: 0.85rem; - transition: 0.2s; user-select: none; margin: 0px; + position: relative; +} + +.button:active { + background: $looks-transparent; +} + +.button:focus::before { + content: ""; + position: absolute; + top: -1px; + right: -1px; + bottom: -1px; + left: -1px; + border: 1px solid $looks-secondary; + z-index: 1; } .button > img { @@ -95,7 +110,7 @@ $border-radius: 0.25rem; height: 3rem; width: 3rem; outline: none; - background: $motion-primary; + background: $looks-secondary; border-radius: 100%; border: 4px solid $ui-white-dim; cursor: pointer; @@ -191,33 +206,27 @@ $border-radius: 0.25rem; border-radius: 0; } -[dir="ltr"] .button-group .button { - border-left: none; -} -[dir="rtl"] .button-group .button { +.button:not(:last-child) { border-right: none; } -[dir="ltr"] .button-group .button:last-of-type { - border-top-right-radius: $border-radius; - border-bottom-right-radius: $border-radius; -} - -[dir="ltr"] .button-group .button:first-of-type { - border-left: 1px solid $ui-black-transparent; - border-top-left-radius: $border-radius; - border-bottom-left-radius: $border-radius; -} - -[dir="rtl"] .button-group .button:last-of-type { - border-top-left-radius: $border-radius; - border-bottom-left-radius: $border-radius; -} - -[dir="rtl"] .button-group .button:first-of-type { +[dir="rtl"] .button:not(:last-child) { + border-left: none; border-right: 1px solid $ui-black-transparent; - border-top-right-radius: $border-radius; - border-bottom-right-radius: $border-radius; +} + +.button-group .button:first-child, +[dir="rtl"] .button-group .button:last-child, +.button-group .button:first-child:focus::before, +[dir="rtl"] .button-group .button:last-child:focus::before { + border-radius: $form-radius 0 0 $form-radius; +} + +.button-group .button:last-child, +[dir="rtl"] .button-group .button:first-child, +.button-group .button:last-child:focus::before, +[dir="rtl"] .button-group .button:first-child:focus::before { + border-radius: 0 $form-radius $form-radius 0; } .button:disabled > img { diff --git a/src/components/spinner/spinner.css b/src/components/spinner/spinner.css index 16d323f5f..48601b034 100644 --- a/src/components/spinner/spinner.css +++ b/src/components/spinner/spinner.css @@ -83,8 +83,8 @@ } .spinner.primary { - border-color: $motion-transparent; + border-color: $looks-transparent; } .spinner.primary::after { - border-top-color: $motion-primary; + border-top-color: $looks-secondary; } diff --git a/src/components/sprite-info/icon--hide.svg b/src/components/sprite-info/icon--hide.svg index 2fd66b830..347b4db5b 100644 --- a/src/components/sprite-info/icon--hide.svg +++ b/src/components/sprite-info/icon--hide.svg @@ -5,10 +5,10 @@ Created with Sketch. - + - \ No newline at end of file + diff --git a/src/components/sprite-info/icon--show.svg b/src/components/sprite-info/icon--show.svg index fe54ed568..d6fd9280d 100644 --- a/src/components/sprite-info/icon--show.svg +++ b/src/components/sprite-info/icon--show.svg @@ -5,11 +5,11 @@ Created with Sketch. - + - \ No newline at end of file + diff --git a/src/components/sprite-info/sprite-info.css b/src/components/sprite-info/sprite-info.css index 93ca30f40..364df9fce 100644 --- a/src/components/sprite-info/sprite-info.css +++ b/src/components/sprite-info/sprite-info.css @@ -47,70 +47,6 @@ user-select: none; } -.icon { - width: 100%; - height: 100%; -} - -/* @todo: refactor radio divs to input */ -.radio-wrapper { - white-space: nowrap; /* make sure visibilty buttons don't wrap */ -} - -.radio { - filter: saturate(0); - cursor: pointer; -} -.radio.is-active { - filter: none; -} -.radio.is-disabled { - cursor: default; -} - -.radio-first { - border: 1px solid $ui-black-transparent; -} - -[dir="ltr"] .radio-first { - border-top-left-radius: $form-radius; - border-bottom-left-radius: $form-radius; -} - -[dir="rtl"] .radio-first { - border-top-right-radius: $form-radius; - border-bottom-right-radius: $form-radius; -} - -.radio-first:focus { - border-color: $motion-primary; - box-shadow: inset 0 0 0 -2px $ui-black-transparent; -} - -.radio-last { - border-bottom: 1px solid $ui-black-transparent; - border-top: 1px solid $ui-black-transparent; -} - -[dir="ltr"] .radio-last { - border-right: 1px solid $ui-black-transparent; - border-left: 1px solid $ui-black-transparent; - border-top-right-radius: $form-radius; - border-bottom-right-radius: $form-radius; -} - -[dir="rtl"] .radio-last { - border-left: 1px solid $ui-black-transparent; - border-right: 1px solid $ui-black-transparent; - border-top-left-radius: $form-radius; - border-bottom-left-radius: $form-radius; -} - -.radio-last:focus { - border-color: $motion-primary; - box-shadow: inset 0 0 0 -2px $ui-black-transparent; -} - .icon { width: 100%; height: 100%; @@ -126,7 +62,7 @@ } .rotation-select:focus { - border-color: $motion-primary; + border-color: $looks-secondary; box-shadow: inset 0 0 0 1px $ui-black-transparent); } diff --git a/src/components/sprite-info/sprite-info.jsx b/src/components/sprite-info/sprite-info.jsx index 09c94d016..6f0fcd1db 100644 --- a/src/components/sprite-info/sprite-info.jsx +++ b/src/components/sprite-info/sprite-info.jsx @@ -19,6 +19,7 @@ import xIcon from './icon--x.svg'; import yIcon from './icon--y.svg'; import showIcon from './icon--show.svg'; import hideIcon from './icon--hide.svg'; +import ToggleButtons from '../toggle-buttons/toggle-buttons.jsx'; const BufferedInput = BufferedInputHOC(Input); @@ -27,6 +28,16 @@ const messages = defineMessages({ id: 'gui.SpriteInfo.spritePlaceholder', defaultMessage: 'Name', description: 'Placeholder text for sprite name' + }, + showSpriteAction: { + id: 'gui.SpriteInfo.showSpriteAction', + defaultMessage: 'Show sprite', + description: 'Tooltip for show sprite button' + }, + hideSpriteAction: { + id: 'gui.SpriteInfo.hideSpriteAction', + defaultMessage: 'Hide sprite', + description: 'Tooltip for hide sprite button' } }); @@ -185,46 +196,23 @@ class SpriteInfo extends React.Component { /> : null } -
-
- -
-
- -
-
+
) : ( - +
+ +
); header = ( @@ -110,40 +112,24 @@ const StageHeaderComponent = function (props) { [] ) : (
-
- -
-
- -
+
); header = ( diff --git a/src/components/stage-selector/stage-selector.css b/src/components/stage-selector/stage-selector.css index 742898bd3..f93a0c344 100644 --- a/src/components/stage-selector/stage-selector.css +++ b/src/components/stage-selector/stage-selector.css @@ -26,12 +26,12 @@ $header-height: calc($stage-menu-height - 2px); .stage-selector.is-selected { border-top-left-radius: .625rem; border-top-right-radius: .625rem; - border-color: $motion-primary; - box-shadow: 0px 0px 0px 4px $motion-transparent; + border-color: $looks-secondary; + box-shadow: 0px 0px 0px 4px $looks-transparent; } .stage-selector:hover { - border-color: $motion-primary; + border-color: $looks-secondary; } .header { @@ -60,7 +60,7 @@ $header-height: calc($stage-menu-height - 2px); } .stage-selector.is-selected .header { - background-color: $motion-primary; + background-color: $looks-secondary; } .stage-selector.is-selected .header-title { diff --git a/src/components/stage/stage.css b/src/components/stage/stage.css index cb8b4f630..a799e05ce 100644 --- a/src/components/stage/stage.css +++ b/src/components/stage/stage.css @@ -108,8 +108,8 @@ stage doesn't "spill" out from under its rounded corners. instead, shift these o } .frame { - background: $motion-transparent; - border: 2px solid $motion-primary; + background: $looks-transparent; + border: 2px solid $looks-secondary; border-radius: 0.5rem; animation-name: flash; animation-duration: 0.75s; diff --git a/src/components/stop-all/stop-all.css b/src/components/stop-all/stop-all.css index da0bcc7b3..ab89a5c70 100644 --- a/src/components/stop-all/stop-all.css +++ b/src/components/stop-all/stop-all.css @@ -10,7 +10,7 @@ } .stop-all:hover { - background-color: $motion-light-transparent; + background-color: $looks-light-transparent; } .stop-all { diff --git a/src/components/tag-button/tag-button.css b/src/components/tag-button/tag-button.css index b77004513..83c9ee9ca 100644 --- a/src/components/tag-button/tag-button.css +++ b/src/components/tag-button/tag-button.css @@ -3,7 +3,7 @@ .tag-button { padding: .625rem 1rem; - background: $motion-primary; + background: $looks-secondary; border-radius: 1.375rem; color: $ui-white; height: $library-filter-bar-height; diff --git a/src/components/telemetry-modal/telemetry-modal.css b/src/components/telemetry-modal/telemetry-modal.css index 73f2d6a1c..d94f2f00c 100644 --- a/src/components/telemetry-modal/telemetry-modal.css +++ b/src/components/telemetry-modal/telemetry-modal.css @@ -35,7 +35,7 @@ $ui-gray: hsla(0, 0%, 95%, 1); .illustration { width: 100%; height: 123px; - background-color: $motion-primary; + background-color: $looks-secondary; background-image: url('./telemetry-modal-header.png'); background-size: cover; } @@ -46,7 +46,7 @@ $ui-gray: hsla(0, 0%, 95%, 1); } .privacy-policy-link { - color: $motion-primary; + color: $looks-secondary; text-decoration: none; } @@ -92,14 +92,14 @@ $ui-gray: hsla(0, 0%, 95%, 1); .radio-buttons input[type="radio"]:checked { transition: all .25s ease; background-color: $ui-white; - border: 1px solid $motion-primary; + border: 1px solid $looks-secondary; } .radio-buttons input[type="radio"]:checked::after { display: block; margin: 0.125rem; border-radius: 50%; - background-color: $motion-primary; + background-color: $looks-secondary; width: .625rem; height: .625rem; content: ""; @@ -127,11 +127,11 @@ $ui-gray: hsla(0, 0%, 95%, 1); } .button-row button { - border: 1px solid $motion-primary; + border: 1px solid $looks-secondary; border-radius: 0.25rem; padding: 0.5rem 1.5rem; color: white; - background: $motion-primary; + background: $looks-secondary; font-weight: bold; font-size: .875rem; cursor: pointer; @@ -139,7 +139,7 @@ $ui-gray: hsla(0, 0%, 95%, 1); .button-row button:hover { background: $extensions-primary; - box-shadow: 0 0 0 6px $motion-transparent; + box-shadow: 0 0 0 6px $looks-transparent; } .button-row button:disabled { diff --git a/src/components/toggle-buttons/toggle-buttons.css b/src/components/toggle-buttons/toggle-buttons.css new file mode 100644 index 000000000..d5b54e51b --- /dev/null +++ b/src/components/toggle-buttons/toggle-buttons.css @@ -0,0 +1,85 @@ +@import "../../css/units.css"; +@import "../../css/colors.css"; + +.row { + display: flex; + flex-direction: row; +} + +.button { + margin: 0; + border: 1px solid $ui-black-transparent; + background: none; + outline: none; + cursor: pointer; + user-select: none; + position: relative; + width: 34px; + height: 34px; + display: flex; + justify-content: center; + align-items: center; +} + +.button > img { + display: block; + transition: transform 0.1s; + filter: grayscale(100%) opacity(.5); +} + +.button:not(:last-child) { + border-right: none; +} + +[dir="rtl"] .button:not(:last-child) { + border-left: none; + border-right: 1px solid $ui-black-transparent; +} + +.button:focus::before { + content: ""; + position: absolute; + top: -1px; + right: -1px; + bottom: -1px; + left: -1px; + border: 1px solid $looks-secondary; +} + +/* round corners for first and last buttons */ + +.button:first-child, +[dir="rtl"] .button:last-child, +.button:first-child:focus::before, +[dir="rtl"] .button:last-child:focus::before { + border-radius: $form-radius 0 0 $form-radius; +} + +.button:last-child, +[dir="rtl"] .button:first-child, +.button:last-child:focus::before, +[dir="rtl"] .button:first-child:focus::before { + border-radius: 0 $form-radius $form-radius 0; +} + +/* selected button styling */ + +.button[aria-pressed='true'] { + background-color: $looks-light-transparent; +} + +.button[aria-pressed='true'] > img { + filter: none; +} + +/* pressed button styling */ + +.button:active { + background-color: $looks-transparent; +} + +/* disabled styling */ + +.disabled .button { + cursor: default; +} diff --git a/src/components/toggle-buttons/toggle-buttons.jsx b/src/components/toggle-buttons/toggle-buttons.jsx new file mode 100644 index 000000000..f09f60e86 --- /dev/null +++ b/src/components/toggle-buttons/toggle-buttons.jsx @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import styles from './toggle-buttons.css'; + +const ToggleButtons = ({buttons, className, disabled}) => ( +
+ {buttons.map((button, index) => ( + + ))} +
+); + +ToggleButtons.propTypes = { + buttons: PropTypes.arrayOf(PropTypes.shape({ + title: PropTypes.string.isRequired, + handleClick: PropTypes.func.isRequired, + icon: PropTypes.string.isRequired, + iconClassName: PropTypes.string, + isSelected: PropTypes.bool + })), + className: PropTypes.string, + disabled: PropTypes.bool +}; + +ToggleButtons.defaultProps = { + disabled: false +}; + +export default ToggleButtons; diff --git a/src/components/webgl-modal/webgl-modal.css b/src/components/webgl-modal/webgl-modal.css index 85fcc0d01..43327f211 100644 --- a/src/components/webgl-modal/webgl-modal.css +++ b/src/components/webgl-modal/webgl-modal.css @@ -29,7 +29,7 @@ .illustration { width: 100%; height: 208px; - background-color: $motion-primary; + background-color: $looks-secondary; background-image: url('./unsupported.png'); background-size: cover; } @@ -54,10 +54,10 @@ } .button-row button { - border: 1px solid $motion-primary; + border: 1px solid $looks-secondary; border-radius: 0.25rem; padding: 0.5rem 2rem; - background: $motion-primary; + background: $looks-secondary; color: white; font-weight: bold; font-size: 0.875rem; @@ -70,6 +70,6 @@ } .faq-link { - color: $motion-primary; + color: $looks-secondary; text-decoration: none; } diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 616d2e090..750be83fc 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -18,6 +18,8 @@ import {BLOCKS_DEFAULT_SCALE, STAGE_DISPLAY_SIZES} from '../lib/layout-constants import DropAreaHOC from '../lib/drop-area-hoc.jsx'; import DragConstants from '../lib/drag-constants'; import defineDynamicBlock from '../lib/define-dynamic-block'; +import {DEFAULT_THEME, getColorsForTheme, themeMap} from '../lib/themes'; +import {injectExtensionBlockTheme, injectExtensionCategoryTheme} from '../lib/themes/blockHelpers'; import {connect} from 'react-redux'; import {updateToolbox} from '../reducers/toolbox'; @@ -100,7 +102,7 @@ class Blocks extends React.Component { const workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options, - {rtl: this.props.isRtl, toolbox: this.props.toolboxXML} + {rtl: this.props.isRtl, toolbox: this.props.toolboxXML, colours: getColorsForTheme(this.props.theme)} ); this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig); @@ -353,11 +355,15 @@ class Blocks extends React.Component { const stageCostumes = stage.getCostumes(); const targetCostumes = target.getCostumes(); const targetSounds = target.getSounds(); - const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML(target); + const dynamicBlocksXML = injectExtensionCategoryTheme( + this.props.vm.runtime.getBlocksXML(target), + this.props.theme + ); return makeToolboxXML(false, target.isStage, target.id, dynamicBlocksXML, targetCostumes[targetCostumes.length - 1].name, stageCostumes[stageCostumes.length - 1].name, - targetSounds.length > 0 ? targetSounds[targetSounds.length - 1].name : '' + targetSounds.length > 0 ? targetSounds[targetSounds.length - 1].name : '', + getColorsForTheme(this.props.theme) ); } catch { return null; @@ -436,7 +442,7 @@ class Blocks extends React.Component { if (blockInfo.info && blockInfo.info.isDynamic) { dynamicBlocksInfo.push(blockInfo); } else if (blockInfo.json) { - staticBlocksJson.push(blockInfo.json); + staticBlocksJson.push(injectExtensionBlockTheme(blockInfo.json, this.props.theme)); } // otherwise it's a non-block entry such as '---' }); @@ -627,22 +633,11 @@ Blocks.propTypes = { wheel: PropTypes.bool, startScale: PropTypes.number }), - colours: PropTypes.shape({ - workspace: PropTypes.string, - flyout: PropTypes.string, - toolbox: PropTypes.string, - toolboxSelected: PropTypes.string, - scrollbar: PropTypes.string, - scrollbarHover: PropTypes.string, - insertionMarker: PropTypes.string, - insertionMarkerOpacity: PropTypes.number, - fieldShadow: PropTypes.string, - dragShadowOpacity: PropTypes.number - }), comments: PropTypes.bool, collapse: PropTypes.bool }), stageSize: PropTypes.oneOf(Object.keys(STAGE_DISPLAY_SIZES)).isRequired, + theme: PropTypes.oneOf(Object.keys(themeMap)), toolboxXML: PropTypes.string, updateMetrics: PropTypes.func, updateToolboxState: PropTypes.func, @@ -664,18 +659,6 @@ Blocks.defaultOptions = { length: 2, colour: '#ddd' }, - colours: { - workspace: '#F9F9F9', - flyout: '#F9F9F9', - toolbox: '#FFFFFF', - toolboxSelected: '#E9EEF2', - scrollbar: '#CECDCE', - scrollbarHover: '#CECDCE', - insertionMarker: '#000000', - insertionMarkerOpacity: 0.2, - fieldShadow: 'rgba(255, 255, 255, 0.3)', - dragShadowOpacity: 0.6 - }, comments: true, collapse: false, sounds: false @@ -683,7 +666,8 @@ Blocks.defaultOptions = { Blocks.defaultProps = { isVisible: true, - options: Blocks.defaultOptions + options: Blocks.defaultOptions, + theme: DEFAULT_THEME }; const mapStateToProps = state => ({ diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index 0081c83c5..55055f0f6 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -36,6 +36,7 @@ import storage from '../lib/storage'; import vmListenerHOC from '../lib/vm-listener-hoc.jsx'; import vmManagerHOC from '../lib/vm-manager-hoc.jsx'; import cloudManagerHOC from '../lib/cloud-manager-hoc.jsx'; +import systemPreferencesHOC from '../lib/system-preferences-hoc.jsx'; import GUIComponent from '../components/gui/gui.jsx'; import {setIsScratchDesktop} from '../lib/isScratchDesktop.js'; @@ -185,7 +186,8 @@ const WrappedGui = compose( vmListenerHOC, vmManagerHOC, SBFileUploaderHOC, - cloudManagerHOC + cloudManagerHOC, + systemPreferencesHOC )(ConnectedGUI); WrappedGui.setAppElement = ReactModal.setAppElement; diff --git a/src/containers/monitor.jsx b/src/containers/monitor.jsx index b952a1836..f45c2d1ef 100644 --- a/src/containers/monitor.jsx +++ b/src/containers/monitor.jsx @@ -9,6 +9,7 @@ import {addMonitorRect, getInitialPosition, resizeMonitorRect, removeMonitorRect import {getVariable, setVariableValue} from '../lib/variable-utils'; import importCSV from '../lib/import-csv'; import downloadBlob from '../lib/download-blob'; +import {DEFAULT_THEME} from '../lib/themes'; import SliderPrompt from './slider-prompt.jsx'; import {connect} from 'react-redux'; @@ -213,6 +214,7 @@ class Monitor extends React.Component { min={this.props.min} mode={this.props.mode} targetId={this.props.targetId} + theme={this.props.theme} width={this.props.width} onDragEnd={this.handleDragEnd} onExport={isList ? this.handleExport : null} @@ -250,6 +252,7 @@ Monitor.propTypes = { resizeMonitorRect: PropTypes.func.isRequired, spriteName: PropTypes.string, // eslint-disable-line react/no-unused-prop-types targetId: PropTypes.string, + theme: PropTypes.string, toolboxXML: PropTypes.string, // eslint-disable-line react/no-unused-prop-types value: PropTypes.oneOfType([ PropTypes.string, @@ -264,8 +267,12 @@ Monitor.propTypes = { x: PropTypes.number, y: PropTypes.number }; +Monitor.defaultProps = { + theme: DEFAULT_THEME +}; const mapStateToProps = state => ({ monitorLayout: state.scratchGui.monitorLayout, + theme: state.scratchGui.theme.theme, // render on toolbox updates since changes to the blocks could affect monitor labels, i.e. updated locale toolboxXML: state.scratchGui.toolbox.toolboxXML, vm: state.scratchGui.vm diff --git a/src/containers/sprite-info.jsx b/src/containers/sprite-info.jsx index fe0a7267e..2499ed104 100644 --- a/src/containers/sprite-info.jsx +++ b/src/containers/sprite-info.jsx @@ -9,9 +9,7 @@ class SpriteInfo extends React.Component { super(props); bindAll(this, [ 'handleClickVisible', - 'handleClickNotVisible', - 'handlePressVisible', - 'handlePressNotVisible' + 'handleClickNotVisible' ]); } handleClickVisible (e) { @@ -22,26 +20,12 @@ class SpriteInfo extends React.Component { e.preventDefault(); this.props.onChangeVisibility(false); } - handlePressVisible (e) { - if (e.key === ' ' || e.key === 'Enter') { - e.preventDefault(); - this.props.onChangeVisibility(true); - } - } - handlePressNotVisible (e) { - if (e.key === ' ' || e.key === 'Enter') { - e.preventDefault(); - this.props.onChangeVisibility(false); - } - } render () { return ( ); } diff --git a/src/css/colors.css b/src/css/colors.css index 6f38510e2..13a861788 100644 --- a/src/css/colors.css +++ b/src/css/colors.css @@ -16,8 +16,11 @@ $text-primary-transparent: hsla(225, 15%, 40%, 0.75); $motion-primary: hsla(215, 100%, 65%, 1); /* #4C97FF */ $motion-tertiary: hsla(215, 60%, 50%, 1); /* #3373CC */ -$motion-transparent: hsla(215, 100%, 65%, 0.35); /* 35% transparent version of motion-primary */ -$motion-light-transparent: hsla(215, 100%, 65%, 0.15); /* 15% transparent version of motion-primary */ + +$looks-secondary: hsla(260, 60%, 60%, 1); /* #855CD6 */ +$looks-transparent: hsla(260, 60%, 60%, 0.35); /* 35% transparent version of looks-tertiary */ +$looks-light-transparent: hsla(260, 60%, 60%, 0.15); /* 15% transparent version of looks-tertiary */ +$looks-secondary-dark: hsla(260, 42%, 51%, 1); /* #714EB6 */ $red-primary: hsla(20, 100%, 55%, 1); /* #FF661A */ $red-tertiary: hsla(20, 100%, 45%, 1); /* #E64D00 */ diff --git a/src/css/units.css b/src/css/units.css index 0b00f3234..587385fc0 100644 --- a/src/css/units.css +++ b/src/css/units.css @@ -6,7 +6,6 @@ $space: 0.5rem; $sprites-per-row: 5; $menu-bar-height: 3rem; -$language-selector-width: 3rem; $sprite-info-height: 6rem; $stage-menu-height: 2.75rem; diff --git a/src/lib/blocks.js b/src/lib/blocks.js index bb9a94db3..4412f1ad4 100644 --- a/src/lib/blocks.js +++ b/src/lib/blocks.js @@ -23,6 +23,7 @@ export default function (vm, useCatBlocks) { colour: colors.secondary, colourSecondary: colors.secondary, colourTertiary: colors.tertiary, + colourQuaternary: colors.quaternary, outputShape: ScratchBlocks.OUTPUT_SHAPE_ROUND }; }; @@ -42,6 +43,7 @@ export default function (vm, useCatBlocks) { colour: colors.primary, colourSecondary: colors.secondary, colourTertiary: colors.tertiary, + colourQuaternary: colors.quaternary, extensions: ['shape_hat'] }; }; @@ -68,6 +70,7 @@ export default function (vm, useCatBlocks) { colour: ScratchBlocks.Colours.sensing.primary, colourSecondary: ScratchBlocks.Colours.sensing.secondary, colourTertiary: ScratchBlocks.Colours.sensing.tertiary, + colourQuaternary: ScratchBlocks.Colours.sensing.quaternary, outputShape: ScratchBlocks.OUTPUT_SHAPE_ROUND }; }; diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js index 0b618979d..8d356e442 100644 --- a/src/lib/make-toolbox-xml.js +++ b/src/lib/make-toolbox-xml.js @@ -1,17 +1,19 @@ import ScratchBlocks from 'scratch-blocks'; +import {defaultColors} from './themes'; const categorySeparator = ''; const blockSeparator = ''; // At default scale, about 28px /* eslint-disable no-unused-vars */ -const motion = function (isInitialSetup, isStage, targetId) { +const motion = function (isInitialSetup, isStage, targetId, colors) { const stageSelected = ScratchBlocks.ScratchMsgs.translate( 'MOTION_STAGE_SELECTED', 'Stage selected: no motion blocks' ); + // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + ${isStage ? ` ` : ` @@ -151,11 +153,12 @@ const xmlEscape = function (unsafe) { }); }; -const looks = function (isInitialSetup, isStage, targetId, costumeName, backdropName) { +const looks = function (isInitialSetup, isStage, targetId, costumeName, backdropName, colors) { const hello = ScratchBlocks.ScratchMsgs.translate('LOOKS_HELLO', 'Hello!'); const hmm = ScratchBlocks.ScratchMsgs.translate('LOOKS_HMM', 'Hmm...'); + // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + ${isStage ? '' : ` @@ -288,9 +291,10 @@ const looks = function (isInitialSetup, isStage, targetId, costumeName, backdrop `; }; -const sound = function (isInitialSetup, isStage, targetId, soundName) { +const sound = function (isInitialSetup, isStage, targetId, soundName, colors) { + // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + @@ -343,9 +347,10 @@ const sound = function (isInitialSetup, isStage, targetId, soundName) { `; }; -const events = function (isInitialSetup, isStage) { +const events = function (isInitialSetup, isStage, targetId, colors) { + // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + @@ -382,9 +387,14 @@ const events = function (isInitialSetup, isStage) { `; }; -const control = function (isInitialSetup, isStage) { +const control = function (isInitialSetup, isStage, targetId, colors) { + // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + @@ -429,10 +439,15 @@ const control = function (isInitialSetup, isStage) { `; }; -const sensing = function (isInitialSetup, isStage) { +const sensing = function (isInitialSetup, isStage, targetId, colors) { const name = ScratchBlocks.ScratchMsgs.translate('SENSING_ASK_TEXT', 'What\'s your name?'); + // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + ${isStage ? '' : ` @@ -504,12 +519,17 @@ const sensing = function (isInitialSetup, isStage) { `; }; -const operators = function (isInitialSetup) { +const operators = function (isInitialSetup, isStage, targetId, colors) { const apple = ScratchBlocks.ScratchMsgs.translate('OPERATORS_JOIN_APPLE', 'apple'); const banana = ScratchBlocks.ScratchMsgs.translate('OPERATORS_JOIN_BANANA', 'banana'); const letter = ScratchBlocks.ScratchMsgs.translate('OPERATORS_LETTEROF_APPLE', 'a'); + // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + @@ -691,25 +711,27 @@ const operators = function (isInitialSetup) { `; }; -const variables = function () { +const variables = function (isInitialSetup, isStage, targetId, colors) { + // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` `; }; -const myBlocks = function () { +const myBlocks = function (isInitialSetup, isStage, targetId, colors) { + // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` `; @@ -732,10 +754,11 @@ const xmlClose = ''; * @param {?string} costumeName - The name of the default selected costume dropdown. * @param {?string} backdropName - The name of the default selected backdrop dropdown. * @param {?string} soundName - The name of the default selected sound dropdown. + * @param {?object} colors - The colors for the theme. * @returns {string} - a ScratchBlocks-style XML document for the contents of the toolbox. */ const makeToolboxXML = function (isInitialSetup, isStage = true, targetId, categoriesXML = [], - costumeName = '', backdropName = '', soundName = '') { + costumeName = '', backdropName = '', soundName = '', colors = defaultColors) { isStage = isInitialSetup || isStage; const gap = [categorySeparator]; @@ -753,15 +776,16 @@ const makeToolboxXML = function (isInitialSetup, isStage = true, targetId, categ } // return `undefined` }; - const motionXML = moveCategory('motion') || motion(isInitialSetup, isStage, targetId); - const looksXML = moveCategory('looks') || looks(isInitialSetup, isStage, targetId, costumeName, backdropName); - const soundXML = moveCategory('sound') || sound(isInitialSetup, isStage, targetId, soundName); - const eventsXML = moveCategory('event') || events(isInitialSetup, isStage, targetId); - const controlXML = moveCategory('control') || control(isInitialSetup, isStage, targetId); - const sensingXML = moveCategory('sensing') || sensing(isInitialSetup, isStage, targetId); - const operatorsXML = moveCategory('operators') || operators(isInitialSetup, isStage, targetId); - const variablesXML = moveCategory('data') || variables(isInitialSetup, isStage, targetId); - const myBlocksXML = moveCategory('procedures') || myBlocks(isInitialSetup, isStage, targetId); + const motionXML = moveCategory('motion') || motion(isInitialSetup, isStage, targetId, colors.motion); + const looksXML = moveCategory('looks') || + looks(isInitialSetup, isStage, targetId, costumeName, backdropName, colors.looks); + const soundXML = moveCategory('sound') || sound(isInitialSetup, isStage, targetId, soundName, colors.sounds); + const eventsXML = moveCategory('event') || events(isInitialSetup, isStage, targetId, colors.event); + const controlXML = moveCategory('control') || control(isInitialSetup, isStage, targetId, colors.control); + const sensingXML = moveCategory('sensing') || sensing(isInitialSetup, isStage, targetId, colors.sensing); + const operatorsXML = moveCategory('operators') || operators(isInitialSetup, isStage, targetId, colors.operators); + const variablesXML = moveCategory('data') || variables(isInitialSetup, isStage, targetId, colors.data); + const myBlocksXML = moveCategory('procedures') || myBlocks(isInitialSetup, isStage, targetId, colors.more); const everything = [ xmlOpen, diff --git a/src/lib/system-preferences-hoc.jsx b/src/lib/system-preferences-hoc.jsx new file mode 100644 index 000000000..c38cd9f37 --- /dev/null +++ b/src/lib/system-preferences-hoc.jsx @@ -0,0 +1,50 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {connect} from 'react-redux'; + +import {setTheme} from '../reducers/theme'; +import {detectTheme} from './themes/themePersistance'; + +// Dark mode is not yet supported +// const prefersDarkQuery = '(prefers-color-scheme: dark)'; +const prefersHighContrastQuery = '(prefers-contrast: more)'; + +const systemPreferencesHOC = function (WrappedComponent) { + class SystemPreferences extends React.Component { + componentDidMount () { + this.preferencesListener = () => this.props.onSetTheme(detectTheme()); + + this.highContrastMatchMedia = window.matchMedia(prefersHighContrastQuery); + this.highContrastMatchMedia.addEventListener('change', this.preferencesListener); + } + + componentWillUnmount () { + this.highContrastMatchMedia.removeEventListener('change', this.preferencesListener); + } + + render () { + const { + /* eslint-disable no-unused-vars */ + onSetTheme, + /* eslint-enable no-unused-vars */ + ...props + } = this.props; + return ; + } + } + + SystemPreferences.propTypes = { + onSetTheme: PropTypes.func + }; + + const mapDispatchToProps = dispatch => ({ + onSetTheme: theme => dispatch(setTheme(theme)) + }); + + return connect( + null, + mapDispatchToProps + )(SystemPreferences); +}; + +export default systemPreferencesHOC; diff --git a/src/lib/themes/blockHelpers.js b/src/lib/themes/blockHelpers.js new file mode 100644 index 000000000..b201241ee --- /dev/null +++ b/src/lib/themes/blockHelpers.js @@ -0,0 +1,104 @@ +import {DEFAULT_THEME, getColorsForTheme, themeMap} from '.'; + +const getBlockIconURI = extensionIcons => { + if (!extensionIcons) return null; + + return extensionIcons.blockIconURI || extensionIcons.menuIconURI; +}; + +const getCategoryIconURI = extensionIcons => { + if (!extensionIcons) return null; + + return extensionIcons.menuIconURI || extensionIcons.blockIconURI; +}; + +// scratch-blocks colours has a pen property that scratch-gui uses for all extensions +const getExtensionColors = theme => getColorsForTheme(theme).pen; + +/** + * Applies extension color theme to categories. + * No changes are applied if called with the default theme, allowing extensions to provide their own colors. + * These colors are not seen if the category provides a blockIconURI. + * @param {Array.} dynamicBlockXML - XML for each category of extension blocks, returned from getBlocksXML + * in the vm runtime. + * @param {string} theme - Theme name + * @returns {Array.} Dynamic block XML updated with colors. + */ +const injectExtensionCategoryTheme = (dynamicBlockXML, theme) => { + // Don't do any manipulation for the default theme + if (theme === DEFAULT_THEME) return dynamicBlockXML; + + const extensionColors = getExtensionColors(theme); + const extensionIcons = themeMap[theme].extensions; + const parser = new DOMParser(); + const serializer = new XMLSerializer(); + + return dynamicBlockXML.map(extension => { + const dom = parser.parseFromString(extension.xml, 'text/xml'); + + dom.documentElement.setAttribute('colour', extensionColors.primary); + // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. + dom.documentElement.setAttribute('secondaryColour', extensionColors.tertiary); + + const categoryIconURI = getCategoryIconURI(extensionIcons[extension.id]); + if (categoryIconURI) { + dom.documentElement.setAttribute('iconURI', categoryIconURI); + } + + return { + ...extension, + xml: serializer.serializeToString(dom) + }; + }); +}; + +const injectBlockIcons = (blockInfoJson, theme) => { + // Block icons are the first element of `args0` + if (!blockInfoJson.args0 || blockInfoJson.args0.length < 1 || + blockInfoJson.args0[0].type !== 'field_image') return blockInfoJson; + + const extensionIcons = themeMap[theme].extensions; + const extensionId = blockInfoJson.type.substring(0, blockInfoJson.type.indexOf('_')); + const blockIconURI = getBlockIconURI(extensionIcons[extensionId]); + + if (!blockIconURI) return blockInfoJson; + + return { + ...blockInfoJson, + args0: blockInfoJson.args0.map((value, index) => { + if (index !== 0) return value; + + return { + ...value, + src: blockIconURI + }; + }) + }; +}; + +/** + * Applies extension color theme to static block json. + * No changes are applied if called with the default theme, allowing extensions to provide their own colors. + * @param {object} blockInfoJson - Static block json + * @param {string} theme - Theme name + * @returns {object} Block info json with updated colors. The original blockInfoJson is not modified. + */ +const injectExtensionBlockTheme = (blockInfoJson, theme) => { + // Don't do any manipulation for the default theme + if (theme === DEFAULT_THEME) return blockInfoJson; + + const extensionColors = getExtensionColors(theme); + + return { + ...injectBlockIcons(blockInfoJson, theme), + colour: extensionColors.primary, + colourSecondary: extensionColors.secondary, + colourTertiary: extensionColors.tertiary, + colourQuaternary: extensionColors.quaternary + }; +}; + +export { + injectExtensionBlockTheme, + injectExtensionCategoryTheme +}; diff --git a/src/lib/themes/dark/__mocks__/index.js b/src/lib/themes/dark/__mocks__/index.js new file mode 100644 index 000000000..6d1b817db --- /dev/null +++ b/src/lib/themes/dark/__mocks__/index.js @@ -0,0 +1,22 @@ +const blockColors = { + motion: { + primary: '#AAAAAA' + }, + pen: { + primary: '#FFFFFF', + secondary: '#EEEEEE', + tertiary: '#DDDDDD' + }, + text: '#BBBBBB' +}; + +const extensions = { + pen: { + blockIconURI: 'darkPenIcon' + } +}; + +export { + blockColors, + extensions +}; diff --git a/src/lib/themes/dark/index.js b/src/lib/themes/dark/index.js new file mode 100644 index 000000000..d1e865163 --- /dev/null +++ b/src/lib/themes/dark/index.js @@ -0,0 +1,84 @@ +const blockColors = { + motion: { + primary: '#0F1E33', + secondary: '#4C4C4C', + tertiary: '#4C97FF', + quaternary: '#4C97FF' + }, + looks: { + primary: '#1E1433', + secondary: '#4C4C4C', + tertiary: '#9966FF', + quaternary: '#9966FF' + }, + sounds: { + primary: '#291329', + secondary: '#4C4C4C', + tertiary: '#CF63CF', + quaternary: '#CF63CF' + }, + control: { + primary: '#332205', + secondary: '#4C4C4C', + tertiary: '#FFAB19', + quaternary: '#FFAB19' + }, + event: { + primary: '#332600', + secondary: '#4C4C4C', + tertiary: '#FFBF00', + quaternary: '#FFBF00' + }, + sensing: { + primary: '#12232A', + secondary: '#4C4C4C', + tertiary: '#5CB1D6', + quaternary: '#5CB1D6' + }, + pen: { + primary: '#03251C', + secondary: '#4C4C4C', + tertiary: '#0fBD8C', + quaternary: '#0fBD8C' + }, + operators: { + primary: '#112611', + secondary: '#4C4C4C', + tertiary: '#59C059', + quaternary: '#59C059' + }, + data: { + primary: '#331C05', + secondary: '#4C4C4C', + tertiary: '#FF8C1A', + quaternary: '#FF8C1A' + }, + data_lists: { + primary: '#331405', + secondary: '#4C4C4C', + tertiary: '#FF661A', + quaternary: '#FF661A' + }, + more: { + primary: '#331419', + secondary: '#4C4C4C', + tertiary: '#FF6680', + quaternary: '#FF6680' + }, + text: 'rgba(255, 255, 255, .7)', + textFieldText: '#E5E5E5', + workspace: '#121212', + toolboxSelected: '#4C4C4C', + toolboxText: '#E5E5E5', + toolbox: '#121212', + flyout: '#121212', + textField: '#4C4C4C', + menuHover: 'rgba(255, 255, 255, 0.3)' +}; + +const extensions = {}; + +export { + blockColors, + extensions +}; diff --git a/src/lib/themes/default/__mocks__/index.js b/src/lib/themes/default/__mocks__/index.js new file mode 100644 index 000000000..d3b3bc94b --- /dev/null +++ b/src/lib/themes/default/__mocks__/index.js @@ -0,0 +1,18 @@ +const blockColors = { + motion: { + primary: '#111111', + secondary: '#222222', + tertiary: '#333333' + }, + pen: { + primary: '#121212', + secondary: '#232323', + tertiary: '#343434' + }, + text: '#444444', + workspace: '#555555' +}; + +export { + blockColors +}; diff --git a/src/lib/themes/default/icon.svg b/src/lib/themes/default/icon.svg new file mode 100644 index 000000000..8f54974f6 --- /dev/null +++ b/src/lib/themes/default/icon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/lib/themes/default/index.js b/src/lib/themes/default/index.js new file mode 100644 index 000000000..3a754dca3 --- /dev/null +++ b/src/lib/themes/default/index.js @@ -0,0 +1,105 @@ +const blockColors = { + motion: { + primary: '#4C97FF', + secondary: '#4280D7', + tertiary: '#3373CC', + quaternary: '#3373CC' + }, + looks: { + primary: '#9966FF', + secondary: '#855CD6', + tertiary: '#774DCB', + quaternary: '#774DCB' + }, + sounds: { + primary: '#CF63CF', + secondary: '#C94FC9', + tertiary: '#BD42BD', + quaternary: '#BD42BD' + }, + control: { + primary: '#FFAB19', + secondary: '#EC9C13', + tertiary: '#CF8B17', + quaternary: '#CF8B17' + }, + event: { + primary: '#FFBF00', + secondary: '#E6AC00', + tertiary: '#CC9900', + quaternary: '#CC9900' + }, + sensing: { + primary: '#5CB1D6', + secondary: '#47A8D1', + tertiary: '#2E8EB8', + quaternary: '#2E8EB8' + }, + pen: { + primary: '#0fBD8C', + secondary: '#0DA57A', + tertiary: '#0B8E69', + quaternary: '#0B8E69' + }, + operators: { + primary: '#59C059', + secondary: '#46B946', + tertiary: '#389438', + quaternary: '#389438' + }, + data: { + primary: '#FF8C1A', + secondary: '#FF8000', + tertiary: '#DB6E00', + quaternary: '#DB6E00' + }, + // This is not a new category, but rather for differentiation + // between lists and scalar variables. + data_lists: { + primary: '#FF661A', + secondary: '#FF5500', + tertiary: '#E64D00', + quaternary: '#E64D00' + }, + more: { + primary: '#FF6680', + secondary: '#FF4D6A', + tertiary: '#FF3355', + quaternary: '#FF3355' + }, + text: '#FFFFFF', + workspace: '#F9F9F9', + toolboxHover: '#4C97FF', + toolboxSelected: '#E9EEF2', + toolboxText: '#575E75', + toolbox: '#FFFFFF', + flyout: '#F9F9F9', + scrollbar: '#CECDCE', + scrollbarHover: '#CECDCE', + textField: '#FFFFFF', + textFieldText: '#575E75', + insertionMarker: '#000000', + insertionMarkerOpacity: 0.2, + dragShadowOpacity: 0.6, + stackGlow: '#FFF200', + stackGlowSize: 4, + stackGlowOpacity: 1, + replacementGlow: '#FFFFFF', + replacementGlowSize: 2, + replacementGlowOpacity: 1, + colourPickerStroke: '#FFFFFF', + // CSS colours: support RGBA + fieldShadow: 'rgba(255, 255, 255, 0.3)', + dropDownShadow: 'rgba(0, 0, 0, .3)', + numPadBackground: '#547AB2', + numPadBorder: '#435F91', + numPadActiveBackground: '#435F91', + numPadText: 'white', // Do not use hex here, it cannot be inlined with data-uri SVG + valueReportBackground: '#FFFFFF', + valueReportBorder: '#AAAAAA', + menuHover: 'rgba(0, 0, 0, 0.2)' +}; + +export { + blockColors +}; diff --git a/src/lib/themes/high-contrast/blocks-media/comment-arrow-down.svg b/src/lib/themes/high-contrast/blocks-media/comment-arrow-down.svg new file mode 100644 index 000000000..9753f40b0 --- /dev/null +++ b/src/lib/themes/high-contrast/blocks-media/comment-arrow-down.svg @@ -0,0 +1,10 @@ + + + + dropdown-caret-up + Created with Sketch. + + + + + diff --git a/src/lib/themes/high-contrast/blocks-media/comment-arrow-up.svg b/src/lib/themes/high-contrast/blocks-media/comment-arrow-up.svg new file mode 100644 index 000000000..a118883f4 --- /dev/null +++ b/src/lib/themes/high-contrast/blocks-media/comment-arrow-up.svg @@ -0,0 +1,10 @@ + + + + dropdown-caret-down + Created with Sketch. + + + + + diff --git a/src/lib/themes/high-contrast/blocks-media/delete-x.svg b/src/lib/themes/high-contrast/blocks-media/delete-x.svg new file mode 100644 index 000000000..46d898a9a --- /dev/null +++ b/src/lib/themes/high-contrast/blocks-media/delete-x.svg @@ -0,0 +1,10 @@ + + + + delete-x + Created with Sketch. + + + + + diff --git a/src/lib/themes/high-contrast/blocks-media/dropdown-arrow.svg b/src/lib/themes/high-contrast/blocks-media/dropdown-arrow.svg new file mode 100644 index 000000000..585e5cc4f --- /dev/null +++ b/src/lib/themes/high-contrast/blocks-media/dropdown-arrow.svg @@ -0,0 +1 @@ +dropdown-arrow \ No newline at end of file diff --git a/src/lib/themes/high-contrast/blocks-media/eyedropper.svg b/src/lib/themes/high-contrast/blocks-media/eyedropper.svg new file mode 100644 index 000000000..c7a7cfc9a --- /dev/null +++ b/src/lib/themes/high-contrast/blocks-media/eyedropper.svg @@ -0,0 +1,22 @@ + + + + Artboard + Created with Sketch. + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/lib/themes/high-contrast/blocks-media/icons/arrow_button.svg b/src/lib/themes/high-contrast/blocks-media/icons/arrow_button.svg new file mode 100644 index 000000000..5fa8ffee2 --- /dev/null +++ b/src/lib/themes/high-contrast/blocks-media/icons/arrow_button.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/lib/themes/high-contrast/blocks-media/repeat.svg b/src/lib/themes/high-contrast/blocks-media/repeat.svg new file mode 100644 index 000000000..70d02b909 --- /dev/null +++ b/src/lib/themes/high-contrast/blocks-media/repeat.svg @@ -0,0 +1,13 @@ + + + + +repeat + + diff --git a/src/lib/themes/high-contrast/blocks-media/rotate-left.svg b/src/lib/themes/high-contrast/blocks-media/rotate-left.svg new file mode 100644 index 000000000..eb120afa4 --- /dev/null +++ b/src/lib/themes/high-contrast/blocks-media/rotate-left.svg @@ -0,0 +1 @@ +rotate-clockwise \ No newline at end of file diff --git a/src/lib/themes/high-contrast/blocks-media/rotate-right.svg b/src/lib/themes/high-contrast/blocks-media/rotate-right.svg new file mode 100644 index 000000000..960c00bbe --- /dev/null +++ b/src/lib/themes/high-contrast/blocks-media/rotate-right.svg @@ -0,0 +1 @@ +rotate-counter-clockwise \ No newline at end of file diff --git a/src/lib/themes/high-contrast/blocks-media/zoom-in.svg b/src/lib/themes/high-contrast/blocks-media/zoom-in.svg new file mode 100644 index 000000000..6395eb2d4 --- /dev/null +++ b/src/lib/themes/high-contrast/blocks-media/zoom-in.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/lib/themes/high-contrast/blocks-media/zoom-out.svg b/src/lib/themes/high-contrast/blocks-media/zoom-out.svg new file mode 100644 index 000000000..3dc33cd6e --- /dev/null +++ b/src/lib/themes/high-contrast/blocks-media/zoom-out.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/lib/themes/high-contrast/blocks-media/zoom-reset.svg b/src/lib/themes/high-contrast/blocks-media/zoom-reset.svg new file mode 100644 index 000000000..2e358b0d7 --- /dev/null +++ b/src/lib/themes/high-contrast/blocks-media/zoom-reset.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/lib/themes/high-contrast/extensions/musicIcon.svg b/src/lib/themes/high-contrast/extensions/musicIcon.svg new file mode 100644 index 000000000..34cf4e4f3 --- /dev/null +++ b/src/lib/themes/high-contrast/extensions/musicIcon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/lib/themes/high-contrast/extensions/penIcon.svg b/src/lib/themes/high-contrast/extensions/penIcon.svg new file mode 100644 index 000000000..d4436d58c --- /dev/null +++ b/src/lib/themes/high-contrast/extensions/penIcon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/lib/themes/high-contrast/extensions/text2speechIcon.svg b/src/lib/themes/high-contrast/extensions/text2speechIcon.svg new file mode 100644 index 000000000..2606c9cfb --- /dev/null +++ b/src/lib/themes/high-contrast/extensions/text2speechIcon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/lib/themes/high-contrast/extensions/translateIcon.svg b/src/lib/themes/high-contrast/extensions/translateIcon.svg new file mode 100644 index 000000000..e1fafc5be --- /dev/null +++ b/src/lib/themes/high-contrast/extensions/translateIcon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/lib/themes/high-contrast/extensions/videoSensingIcon.svg b/src/lib/themes/high-contrast/extensions/videoSensingIcon.svg new file mode 100644 index 000000000..95c2e90db --- /dev/null +++ b/src/lib/themes/high-contrast/extensions/videoSensingIcon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/lib/themes/high-contrast/icon.svg b/src/lib/themes/high-contrast/icon.svg new file mode 100644 index 000000000..4bdf2fa04 --- /dev/null +++ b/src/lib/themes/high-contrast/icon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/lib/themes/high-contrast/index.js b/src/lib/themes/high-contrast/index.js new file mode 100644 index 000000000..8a5dd9415 --- /dev/null +++ b/src/lib/themes/high-contrast/index.js @@ -0,0 +1,110 @@ +import musicIcon from './extensions/musicIcon.svg'; +import penIcon from './extensions/penIcon.svg'; +import text2speechIcon from './extensions/text2speechIcon.svg'; +import translateIcon from './extensions/translateIcon.svg'; +import videoSensingIcon from './extensions/videoSensingIcon.svg'; + +const blockColors = { + motion: { + primary: '#80B5FF', + secondary: '#B3D2FF', + tertiary: '#3373CC', + quaternary: '#CCE1FF' + }, + looks: { + primary: '#CCB3FF', + secondary: '#DDCCFF', + tertiary: '#774DCB', + quaternary: '#EEE5FF' + }, + sounds: { + primary: '#E19DE1', + secondary: '#FFB3FF', + tertiary: '#BD42BD', + quaternary: '#FFCCFF' + + }, + control: { + primary: '#FFBE4C', + secondary: '#FFDA99', + tertiary: '#CF8B17', + quaternary: '#FFE3B3' + }, + event: { + primary: '#FFD966', + secondary: '#FFECB3', + tertiary: '#CC9900', + quaternary: '#FFF2CC' + }, + sensing: { + primary: '#85C4E0', + secondary: '#AED8EA', + tertiary: '#2E8EB8', + quaternary: '#C2E2F0' + }, + pen: { + primary: '#13ECAF', + secondary: '#75F0CD', + tertiary: '#0B8E69', + quaternary: '#A3F5DE' + }, + operators: { + primary: '#7ECE7E', + secondary: '#B5E3B5', + tertiary: '#389438', + quaternary: '#DAF1DA' + }, + data: { + primary: '#FFA54C', + secondary: '#FFCC99', + tertiary: '#DB6E00', + quaternary: '#FFE5CC' + }, + // This is not a new category, but rather for differentiation + // between lists and scalar variables. + data_lists: { + primary: '#FF9966', + secondary: '#FFCAB0', // I don't think this is used, b/c we don't have any droppable fields in list blocks + tertiary: '#E64D00', + quaternary: '#FFDDCC' + }, + more: { + primary: '#FF99AA', + secondary: '#FFCCD5', + tertiary: '#FF3355', + quaternary: '#FFE5EA' + }, + text: '#000000', + textFieldText: '#000000', // Text inside of inputs e.g. 90 in [point in direction (90)] + toolboxText: '#000000', // Toolbox text, color picker text (used to be #575E75) + // The color that the category menu label (e.g. 'motion', 'looks', etc.) changes to on hover + toolboxHover: '#3373CC', + insertionMarker: '#000000', + insertionMarkerOpacity: 0.2, + fieldShadow: 'rgba(255, 255, 255, 0.3)', + dragShadowOpacity: 0.6, + menuHover: 'rgba(255, 255, 255, 0.3)' +}; + +const extensions = { + music: { + blockIconURI: musicIcon + }, + pen: { + blockIconURI: penIcon + }, + text2speech: { + blockIconURI: text2speechIcon + }, + translate: { + blockIconURI: translateIcon + }, + videoSensing: { + blockIconURI: videoSensingIcon + } +}; + +export { + blockColors, + extensions +}; diff --git a/src/lib/themes/index.js b/src/lib/themes/index.js new file mode 100644 index 000000000..673d453e7 --- /dev/null +++ b/src/lib/themes/index.js @@ -0,0 +1,81 @@ +import defaultsDeep from 'lodash.defaultsdeep'; +import {defineMessages} from 'react-intl'; + +import { + blockColors as darkModeBlockColors, + extensions as darkModeExtensions +} from './dark'; +import { + blockColors as highContrastBlockColors, + extensions as highContrastExtensions +} from './high-contrast'; +import {blockColors as defaultColors} from './default'; + +import defaultIcon from './default/icon.svg'; +import highContrastIcon from './high-contrast/icon.svg'; + +const DEFAULT_THEME = 'default'; +const HIGH_CONTRAST_THEME = 'high-contrast'; +const DARK_THEME = 'dark'; + +const mergeWithDefaults = colors => defaultsDeep({}, colors, defaultColors); + +const messages = defineMessages({ + [DEFAULT_THEME]: { + id: 'gui.theme.default', + defaultMessage: 'Original', + description: 'label for original theme' + }, + [DARK_THEME]: { + id: 'gui.theme.dark', + defaultMessage: 'Dark', + description: 'label for dark mode theme' + }, + [HIGH_CONTRAST_THEME]: { + id: 'gui.theme.highContrast', + defaultMessage: 'High Contrast', + description: 'label for high theme' + } +}); + +const themeMap = { + [DEFAULT_THEME]: { + blocksMediaFolder: 'blocks-media/default', + colors: defaultColors, + extensions: {}, + label: messages[DEFAULT_THEME], + icon: defaultIcon + }, + [DARK_THEME]: { + blocksMediaFolder: 'blocks-media/default', + colors: mergeWithDefaults(darkModeBlockColors), + extensions: darkModeExtensions, + label: messages[DARK_THEME] + }, + [HIGH_CONTRAST_THEME]: { + blocksMediaFolder: 'blocks-media/high-contrast', + colors: mergeWithDefaults(highContrastBlockColors), + extensions: highContrastExtensions, + label: messages[HIGH_CONTRAST_THEME], + icon: highContrastIcon + } +}; + +const getColorsForTheme = theme => { + const themeInfo = themeMap[theme]; + + if (!themeInfo) { + throw new Error(`Undefined theme ${theme}`); + } + + return themeInfo.colors; +}; + +export { + DEFAULT_THEME, + DARK_THEME, + HIGH_CONTRAST_THEME, + defaultColors, + getColorsForTheme, + themeMap +}; diff --git a/src/lib/themes/themePersistance.js b/src/lib/themes/themePersistance.js new file mode 100644 index 000000000..c94ff337a --- /dev/null +++ b/src/lib/themes/themePersistance.js @@ -0,0 +1,44 @@ +import cookie from 'cookie'; + +import {DEFAULT_THEME, HIGH_CONTRAST_THEME} from '.'; + +const PREFERS_HIGH_CONTRAST_QUERY = '(prefers-contrast: more)'; +const COOKIE_KEY = 'scratchtheme'; + +// Dark mode isn't enabled yet +const isValidTheme = theme => [DEFAULT_THEME, HIGH_CONTRAST_THEME].includes(theme); + +const systemPreferencesTheme = () => { + if (window.matchMedia && window.matchMedia(PREFERS_HIGH_CONTRAST_QUERY).matches) return HIGH_CONTRAST_THEME; + + return DEFAULT_THEME; +}; + +const detectTheme = () => { + const obj = cookie.parse(document.cookie) || {}; + const themeCookie = obj.scratchtheme; + + if (isValidTheme(themeCookie)) return themeCookie; + + // No cookie set. Fall back to system preferences + return systemPreferencesTheme(); +}; + +const persistTheme = theme => { + if (!isValidTheme(theme)) { + throw new Error(`Invalid theme: ${theme}`); + } + + if (systemPreferencesTheme() === theme) { + // Clear the cookie to represent using the system preferences + document.cookie = `${COOKIE_KEY}=`; + return; + } + + document.cookie = `${COOKIE_KEY}=${theme}`; +}; + +export { + detectTheme, + persistTheme +}; diff --git a/src/reducers/gui.js b/src/reducers/gui.js index bf9dda8fd..c2076df2f 100644 --- a/src/reducers/gui.js +++ b/src/reducers/gui.js @@ -21,6 +21,7 @@ import fontsLoadedReducer, {fontsLoadedInitialState} from './fonts-loaded'; import restoreDeletionReducer, {restoreDeletionInitialState} from './restore-deletion'; import stageSizeReducer, {stageSizeInitialState} from './stage-size'; import targetReducer, {targetsInitialState} from './targets'; +import themeReducer, {themeInitialState} from './theme'; import timeoutReducer, {timeoutInitialState} from './timeout'; import timeTravelReducer, {timeTravelInitialState} from './time-travel'; import toolboxReducer, {toolboxInitialState} from './toolbox'; @@ -56,6 +57,7 @@ const guiInitialState = { fontsLoaded: fontsLoadedInitialState, restoreDeletion: restoreDeletionInitialState, targets: targetsInitialState, + theme: themeInitialState, timeout: timeoutInitialState, timeTravel: timeTravelInitialState, toolbox: toolboxInitialState, @@ -156,6 +158,7 @@ const guiReducer = combineReducers({ fontsLoaded: fontsLoadedReducer, restoreDeletion: restoreDeletionReducer, targets: targetReducer, + theme: themeReducer, timeout: timeoutReducer, timeTravel: timeTravelReducer, toolbox: toolboxReducer, diff --git a/src/reducers/menus.js b/src/reducers/menus.js index b756a9e41..c265e1e5c 100644 --- a/src/reducers/menus.js +++ b/src/reducers/menus.js @@ -7,6 +7,7 @@ const MENU_FILE = 'fileMenu'; const MENU_EDIT = 'editMenu'; const MENU_LANGUAGE = 'languageMenu'; const MENU_LOGIN = 'loginMenu'; +const MENU_THEME = 'themeMenu'; const MENU_MODE = 'modeMenu'; @@ -17,6 +18,7 @@ const initialState = { [MENU_EDIT]: false, [MENU_LANGUAGE]: false, [MENU_LOGIN]: false, + [MENU_THEME]: false, [MENU_MODE]: false }; @@ -61,11 +63,13 @@ const languageMenuOpen = state => state.scratchGui.menus[MENU_LANGUAGE]; const openLoginMenu = () => openMenu(MENU_LOGIN); const closeLoginMenu = () => closeMenu(MENU_LOGIN); const loginMenuOpen = state => state.scratchGui.menus[MENU_LOGIN]; +const openThemeMenu = () => openMenu(MENU_THEME); +const closeThemeMenu = () => closeMenu(MENU_THEME); +const themeMenuOpen = state => state.scratchGui.menus[MENU_THEME]; const openModeMenu = () => openMenu(MENU_MODE); const closeModeMenu = () => closeMenu(MENU_MODE); const modeMenuOpen = state => state.scratchGui.menus[MENU_MODE]; - export { reducer as default, initialState as menuInitialState, @@ -87,6 +91,9 @@ export { openLoginMenu, closeLoginMenu, loginMenuOpen, + openThemeMenu, + closeThemeMenu, + themeMenuOpen, openModeMenu, closeModeMenu, modeMenuOpen diff --git a/src/reducers/theme.js b/src/reducers/theme.js new file mode 100644 index 000000000..4330f94da --- /dev/null +++ b/src/reducers/theme.js @@ -0,0 +1,27 @@ +import {detectTheme} from '../lib/themes/themePersistance'; + +const SET_THEME = 'scratch-gui/theme/SET_THEME'; + +const initialState = { + theme: detectTheme() +}; + +const reducer = (state = initialState, action) => { + switch (action.type) { + case SET_THEME: + return {...state, theme: action.theme}; + default: + return state; + } +}; + +const setTheme = theme => ({ + type: SET_THEME, + theme +}); + +export { + reducer as default, + initialState as themeInitialState, + setTheme +}; diff --git a/test/helpers/selenium-helper.js b/test/helpers/selenium-helper.js index 7ce274322..2e314e09c 100644 --- a/test/helpers/selenium-helper.js +++ b/test/helpers/selenium-helper.js @@ -48,6 +48,7 @@ class SeleniumHelper { reportedValue: '*[@class="blocklyDropDownContent"]', soundsTab: "*[@id='react-tabs-5']", spriteTile: '*[starts-with(@class,"react-contextmenu-wrapper")]', + menuBar: '*[contains(@class,"menu-bar_menu-bar_")]', monitors: '*[starts-with(@class,"stage_monitor-wrapper")]', contextMenu: '*[starts-with(@class,"react-contextmenu")]' }; diff --git a/test/integration/menu-bar.test.js b/test/integration/menu-bar.test.js index c83f1022d..4c6b1fd4e 100644 --- a/test/integration/menu-bar.test.js +++ b/test/integration/menu-bar.test.js @@ -97,4 +97,31 @@ describe('Menu bar settings', () => { .accept(); await findByText('project1-sprite'); }); + + test('Theme picker shows themes', async () => { + await loadUri(uri); + await clickXpath('//img[@alt="Color theme"]'); + + await findByText('Original', scope.menuBar); + await findByText('High Contrast', scope.menuBar); + }); + + test('Theme picker switches to high contrast', async () => { + await loadUri(uri); + await clickXpath('//img[@alt="Color theme"]'); + await clickText('High Contrast', scope.menuBar); + + // There is a tiny delay for the color theme to be applied to the categories. + await driver.wait(async () => { + const motionCategoryDiv = await findByXpath( + '//div[contains(@class, "scratchCategoryMenuItem") and ' + + 'contains(@class, "scratchCategoryId-motion")]/*[1]'); + const color = await motionCategoryDiv.getCssValue('background-color'); + + // Documentation for getCssValue says it depends on how the browser + // returns the value. Locally I am seeing 'rgba(128, 181, 255, 1)', + // but this is a bit flexible just in case. + return /128,\s?181,\s?255/.test(color) || color.includes('80B5FF'); + }, 5000, 'Motion category color does not match high contrast theme'); + }); }); diff --git a/test/integration/stage-size.test.js b/test/integration/stage-size.test.js index 72de9c5bb..ca2359050 100644 --- a/test/integration/stage-size.test.js +++ b/test/integration/stage-size.test.js @@ -35,10 +35,10 @@ describe('Loading scratch gui', () => { await clickText('delete', scope.spriteTile); // Go to small stage mode - await clickXpath('//img[@alt="Switch to small stage"]'); + await clickXpath('//button[@title="Switch to small stage"]'); // Confirm app still working - await clickXpath('//img[@alt="Switch to large stage"]'); + await clickXpath('//button[@title="Switch to large stage"]'); const logs = await getLogs(); await expect(logs).toEqual([]); diff --git a/test/unit/components/menu-bar.test.jsx b/test/unit/components/menu-bar.test.jsx index 6e3f1bd09..f2ae91dec 100644 --- a/test/unit/components/menu-bar.test.jsx +++ b/test/unit/components/menu-bar.test.jsx @@ -3,12 +3,12 @@ import {mountWithIntl} from '../../helpers/intl-helpers'; import MenuBar from '../../../src/components/menu-bar/menu-bar'; import {menuInitialState} from '../../../src/reducers/menus'; import {LoadingState} from '../../../src/reducers/project-state'; +import {DEFAULT_THEME} from '../../../src/lib/themes'; import configureStore from 'redux-mock-store'; import {Provider} from 'react-redux'; import VM from 'scratch-vm'; - describe('MenuBar Component', () => { const store = configureStore()({ locales: { @@ -20,6 +20,9 @@ describe('MenuBar Component', () => { projectState: { loadingState: LoadingState.NOT_LOADED }, + theme: { + theme: DEFAULT_THEME + }, timeTravel: { year: 'NOW' }, diff --git a/test/unit/components/monitor-list.test.jsx b/test/unit/components/monitor-list.test.jsx index 051632ea4..b9576d464 100644 --- a/test/unit/components/monitor-list.test.jsx +++ b/test/unit/components/monitor-list.test.jsx @@ -1,9 +1,10 @@ import React from 'react'; -import {mountWithIntl} from '../../helpers/intl-helpers.jsx'; -import MonitorList from '../../../src/components/monitor-list/monitor-list.jsx'; import {OrderedMap} from 'immutable'; import configureStore from 'redux-mock-store'; import {Provider} from 'react-redux'; +import {mountWithIntl} from '../../helpers/intl-helpers.jsx'; +import MonitorList from '../../../src/components/monitor-list/monitor-list.jsx'; +import {DEFAULT_THEME} from '../../../src/lib/themes'; describe('MonitorListComponent', () => { const store = configureStore()({scratchGui: { @@ -11,6 +12,9 @@ describe('MonitorListComponent', () => { monitors: {}, savedMonitorPositions: {} }, + theme: { + theme: DEFAULT_THEME + }, toolbox: { toolboxXML: '' }, diff --git a/test/unit/components/monitor.test.jsx b/test/unit/components/monitor.test.jsx new file mode 100644 index 000000000..e52fa603a --- /dev/null +++ b/test/unit/components/monitor.test.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import {shallow} from 'enzyme'; +import DefaultMonitor from '../../../src/components/monitor/default-monitor'; +import Monitor from '../../../src/components/monitor/monitor'; +import {DARK_THEME, DEFAULT_THEME} from '../../../src/lib/themes'; + +jest.mock('../../../src/lib/themes/default'); +jest.mock('../../../src/lib/themes/dark'); + +describe('Monitor Component', () => { + test('it selects the correct colors based on default theme', () => { + const noop = () => {}; + + const wrapper = shallow(); + + const defaultMonitor = wrapper.find(DefaultMonitor); + + // selects colors from mock value in src/lib/themes/__mocks__/default-colors.js + expect(defaultMonitor.props().categoryColor).toEqual({background: '#111111', text: '#444444'}); + }); + + test('it selects the correct colors based on dark mode theme', () => { + const noop = () => {}; + + const wrapper = shallow(); + + const defaultMonitor = wrapper.find(DefaultMonitor); + + // selects colors from mock value in src/lib/themes/__mocks__/dark-mode.js + expect(defaultMonitor.props().categoryColor).toEqual({background: '#AAAAAA', text: '#BBBBBB'}); + }); +}); diff --git a/test/unit/components/toggle-buttons.test.jsx b/test/unit/components/toggle-buttons.test.jsx new file mode 100644 index 000000000..547852461 --- /dev/null +++ b/test/unit/components/toggle-buttons.test.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import {shallow} from 'enzyme'; +import ToggleButtons from '../../../src/components/toggle-buttons/toggle-buttons'; + +describe('ToggleButtons', () => { + test('renders multiple buttons', () => { + const component = shallow( {}, + icon: 'Button 1 icon' + }, + { + title: 'Button 2', + handleClick: () => {}, + icon: 'Button 2 icon' + } + ]} + />); + + const buttons = component.find('button'); + + expect(buttons).toHaveLength(2); + expect(buttons.get(0).props.title).toBe('Button 1'); + expect(buttons.get(1).props.title).toBe('Button 2'); + }); + + test('calls correct click handler', () => { + const onClick1 = jest.fn(); + const onClick2 = jest.fn(); + const component = shallow(); + const button2 = component.find('button[title="Button 2"]'); + button2.simulate('click'); + + expect(onClick2).toHaveBeenCalled(); + expect(onClick1).not.toHaveBeenCalled(); + }); +}); diff --git a/test/unit/util/themes.test.js b/test/unit/util/themes.test.js new file mode 100644 index 000000000..a815f9c59 --- /dev/null +++ b/test/unit/util/themes.test.js @@ -0,0 +1,165 @@ +import { + DARK_THEME, + defaultColors, + DEFAULT_THEME, + getColorsForTheme, + HIGH_CONTRAST_THEME +} from '../../../src/lib/themes'; +import {injectExtensionBlockTheme, injectExtensionCategoryTheme} from '../../../src/lib/themes/blockHelpers'; +import {detectTheme, persistTheme} from '../../../src/lib/themes/themePersistance'; + +jest.mock('../../../src/lib/themes/default'); +jest.mock('../../../src/lib/themes/dark'); + +describe('themes', () => { + let serializeToString; + + describe('core functionality', () => { + test('provides the default theme colors', () => { + expect(defaultColors.motion.primary).toEqual('#111111'); + }); + + test('returns the dark mode', () => { + const colors = getColorsForTheme(DARK_THEME); + + expect(colors.motion.primary).toEqual('#AAAAAA'); + }); + + test('uses default theme colors when not specified', () => { + const colors = getColorsForTheme(DARK_THEME); + + expect(colors.motion.secondary).toEqual('#222222'); + }); + }); + + describe('block helpers', () => { + beforeEach(() => { + serializeToString = jest.fn(() => 'mocked xml'); + + global.XMLSerializer = () => ({ + serializeToString + }); + }); + + test('updates extension block colors based on theme', () => { + const blockInfoJson = { + type: 'dummy_block', + colour: '#0FBD8C', + colourSecondary: '#0DA57A', + colourTertiary: '#0B8E69' + }; + + const updated = injectExtensionBlockTheme(blockInfoJson, DARK_THEME); + + expect(updated).toEqual({ + type: 'dummy_block', + colour: '#FFFFFF', + colourSecondary: '#EEEEEE', + colourTertiary: '#DDDDDD' + }); + // The original value was not modified + expect(blockInfoJson.colour).toBe('#0FBD8C'); + }); + + test('updates extension block icon based on theme', () => { + const blockInfoJson = { + type: 'pen_block', + args0: [ + { + type: 'field_image', + src: 'original' + } + ], + colour: '#0FBD8C', + colourSecondary: '#0DA57A', + colourTertiary: '#0B8E69' + }; + + const updated = injectExtensionBlockTheme(blockInfoJson, DARK_THEME); + + expect(updated).toEqual({ + type: 'pen_block', + args0: [ + { + type: 'field_image', + src: 'darkPenIcon' + } + ], + colour: '#FFFFFF', + colourSecondary: '#EEEEEE', + colourTertiary: '#DDDDDD' + }); + // The original value was not modified + expect(blockInfoJson.args0[0].src).toBe('original'); + }); + + test('bypasses updates if using the default theme', () => { + const blockInfoJson = { + type: 'dummy_block', + colour: '#0FBD8C', + colourSecondary: '#0DA57A', + colourTertiary: '#0B8E69' + }; + + const updated = injectExtensionBlockTheme(blockInfoJson, DEFAULT_THEME); + + expect(updated).toEqual({ + type: 'dummy_block', + colour: '#0FBD8C', + colourSecondary: '#0DA57A', + colourTertiary: '#0B8E69' + }); + }); + + test('updates extension category based on theme', () => { + const dynamicBlockXML = [ + { + id: 'pen', + xml: '' + } + ]; + + injectExtensionCategoryTheme(dynamicBlockXML, DARK_THEME); + + // XMLSerializer is not available outside the browser. + // Verify the mocked XMLSerializer.serializeToString is called with updated colors. + expect(serializeToString.mock.calls[0][0].documentElement.getAttribute('colour')).toBe('#FFFFFF'); + expect(serializeToString.mock.calls[0][0].documentElement.getAttribute('secondaryColour')).toBe('#DDDDDD'); + expect(serializeToString.mock.calls[0][0].documentElement.getAttribute('iconURI')).toBe('darkPenIcon'); + }); + }); + + describe('theme persistance', () => { + test('returns the theme stored in a cookie', () => { + window.document.cookie = `scratchtheme=${HIGH_CONTRAST_THEME}`; + + const theme = detectTheme(); + + expect(theme).toEqual(HIGH_CONTRAST_THEME); + }); + + test('returns the system theme when no cookie', () => { + window.document.cookie = 'scratchtheme='; + + const theme = detectTheme(); + + expect(theme).toEqual(DEFAULT_THEME); + }); + + test('persists theme to cookie', () => { + window.document.cookie = 'scratchtheme='; + + persistTheme(HIGH_CONTRAST_THEME); + + expect(window.document.cookie).toEqual(`scratchtheme=${HIGH_CONTRAST_THEME}`); + }); + + test('clears theme when matching system preferences', () => { + window.document.cookie = `scratchtheme=${HIGH_CONTRAST_THEME}`; + + persistTheme(DEFAULT_THEME); + + expect(window.document.cookie).toEqual('scratchtheme='); + }); + }); +}); diff --git a/webpack.config.js b/webpack.config.js index 815333b96..6fe6f36e1 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -88,7 +88,25 @@ const base = { }) ] }, - plugins: [] + plugins: [ + new CopyWebpackPlugin({ + patterns: [ + { + from: 'node_modules/scratch-blocks/media', + to: 'static/blocks-media/default' + }, + { + from: 'node_modules/scratch-blocks/media', + to: 'static/blocks-media/high-contrast' + }, + { + from: 'src/lib/themes/high-contrast/blocks-media', + to: 'static/blocks-media/high-contrast', + force: true + } + ] + }) + ] }; if (!process.env.CI) { @@ -113,8 +131,9 @@ module.exports = [ rules: base.module.rules.concat([ { test: /\.(svg|png|wav|mp3|gif|jpg)$/, - loader: 'file-loader', + loader: 'url-loader', options: { + limit: 2048, outputPath: 'static/assets/' } } @@ -166,14 +185,6 @@ module.exports = [ } ] }), - new CopyWebpackPlugin({ - patterns: [ - { - from: 'node_modules/scratch-blocks/media', - to: 'static/blocks-media' - } - ] - }), new CopyWebpackPlugin({ patterns: [ { @@ -215,8 +226,9 @@ module.exports = [ rules: base.module.rules.concat([ { test: /\.(svg|png|wav|mp3|gif|jpg)$/, - loader: 'file-loader', + loader: 'url-loader', options: { + limit: 2048, outputPath: 'static/assets/', publicPath: `${STATIC_PATH}/assets/` } @@ -224,14 +236,6 @@ module.exports = [ ]) }, plugins: base.plugins.concat([ - new CopyWebpackPlugin({ - patterns: [ - { - from: 'node_modules/scratch-blocks/media', - to: 'static/blocks-media' - } - ] - }), new CopyWebpackPlugin({ patterns: [ {