diff --git a/package-lock.json b/package-lock.json index 8ecae424c..3283671b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "bowser": "1.9.4", "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", @@ -10408,10 +10409,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" } @@ -13505,6 +13505,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", @@ -14434,6 +14443,7 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "inBundle": true, + "license": "ISC", "optional": true }, "node_modules/fsevents/node_modules/ansi-regex": { @@ -14441,6 +14451,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "inBundle": true, + "license": "MIT", "optional": true, "engines": { "node": ">=0.10.0" @@ -14451,6 +14462,7 @@ "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "inBundle": true, + "license": "ISC", "optional": true }, "node_modules/fsevents/node_modules/are-we-there-yet": { @@ -14458,6 +14470,7 @@ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "inBundle": true, + "license": "ISC", "optional": true, "dependencies": { "delegates": "^1.0.0", @@ -14469,6 +14482,7 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "inBundle": true, + "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/brace-expansion": { @@ -14476,6 +14490,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "inBundle": true, + "license": "MIT", "optional": true, "dependencies": { "balanced-match": "^1.0.0", @@ -14487,6 +14502,7 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", "inBundle": true, + "license": "ISC", "optional": true }, "node_modules/fsevents/node_modules/code-point-at": { @@ -14494,6 +14510,7 @@ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "inBundle": true, + "license": "MIT", "optional": true, "engines": { "node": ">=0.10.0" @@ -14504,6 +14521,7 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "inBundle": true, + "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/console-control-strings": { @@ -14511,6 +14529,7 @@ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "inBundle": true, + "license": "ISC", "optional": true }, "node_modules/fsevents/node_modules/core-util-is": { @@ -14518,14 +14537,15 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "inBundle": true, + "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", "inBundle": true, + "license": "MIT", "optional": true, "dependencies": { "ms": "^2.1.1" @@ -14536,6 +14556,7 @@ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "inBundle": true, + "license": "MIT", "optional": true, "engines": { "node": ">=4.0.0" @@ -14546,6 +14567,7 @@ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "inBundle": true, + "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/detect-libc": { @@ -14553,6 +14575,7 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "inBundle": true, + "license": "Apache-2.0", "optional": true, "bin": { "detect-libc": "bin/detect-libc.js" @@ -14566,6 +14589,7 @@ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "inBundle": true, + "license": "ISC", "optional": true, "dependencies": { "minipass": "^2.2.1" @@ -14576,6 +14600,7 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "inBundle": true, + "license": "ISC", "optional": true }, "node_modules/fsevents/node_modules/gauge": { @@ -14583,6 +14608,7 @@ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "inBundle": true, + "license": "ISC", "optional": true, "dependencies": { "aproba": "^1.0.3", @@ -14600,6 +14626,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "inBundle": true, + "license": "ISC", "optional": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -14618,6 +14645,7 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "inBundle": true, + "license": "ISC", "optional": true }, "node_modules/fsevents/node_modules/iconv-lite": { @@ -14625,6 +14653,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "inBundle": true, + "license": "MIT", "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -14638,6 +14667,7 @@ "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "inBundle": true, + "license": "ISC", "optional": true, "dependencies": { "minimatch": "^3.0.4" @@ -14648,6 +14678,7 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "inBundle": true, + "license": "ISC", "optional": true, "dependencies": { "once": "^1.3.0", @@ -14659,13 +14690,24 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "inBundle": true, + "license": "ISC", "optional": true }, + "node_modules/fsevents/node_modules/ini": { + "version": "1.3.5", + "inBundle": true, + "license": "ISC", + "optional": true, + "engines": { + "node": "*" + } + }, "node_modules/fsevents/node_modules/is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "inBundle": true, + "license": "MIT", "optional": true, "dependencies": { "number-is-nan": "^1.0.0" @@ -14679,6 +14721,7 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "inBundle": true, + "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/minimatch": { @@ -14686,6 +14729,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "inBundle": true, + "license": "ISC", "optional": true, "dependencies": { "brace-expansion": "^1.1.7" @@ -14699,6 +14743,7 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "inBundle": true, + "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/minipass": { @@ -14706,6 +14751,7 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "inBundle": true, + "license": "ISC", "optional": true, "dependencies": { "safe-buffer": "^5.1.2", @@ -14717,6 +14763,7 @@ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", "inBundle": true, + "license": "MIT", "optional": true, "dependencies": { "minipass": "^2.2.1" @@ -14726,8 +14773,8 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", "inBundle": true, + "license": "MIT", "optional": true, "dependencies": { "minimist": "0.0.8" @@ -14741,6 +14788,7 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "inBundle": true, + "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/needle": { @@ -14748,6 +14796,7 @@ "resolved": "https://registry.npmjs.org/needle/-/needle-2.3.0.tgz", "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==", "inBundle": true, + "license": "MIT", "optional": true, "dependencies": { "debug": "^4.1.0", @@ -14765,8 +14814,8 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz", "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", - "deprecated": "Please upgrade to @mapbox/node-pre-gyp: the non-scoped node-pre-gyp package is deprecated and only the @mapbox scoped package will recieve updates in the future", "inBundle": true, + "license": "BSD-3-Clause", "optional": true, "dependencies": { "detect-libc": "^1.0.2", @@ -14789,6 +14838,7 @@ "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "inBundle": true, + "license": "ISC", "optional": true, "dependencies": { "abbrev": "1", @@ -14803,6 +14853,7 @@ "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", "inBundle": true, + "license": "ISC", "optional": true }, "node_modules/fsevents/node_modules/npm-packlist": { @@ -14810,6 +14861,7 @@ "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz", "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", "inBundle": true, + "license": "ISC", "optional": true, "dependencies": { "ignore-walk": "^3.0.1", @@ -14821,6 +14873,7 @@ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "inBundle": true, + "license": "ISC", "optional": true, "dependencies": { "are-we-there-yet": "~1.1.2", @@ -14834,6 +14887,7 @@ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "inBundle": true, + "license": "MIT", "optional": true, "engines": { "node": ">=0.10.0" @@ -14844,6 +14898,7 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "inBundle": true, + "license": "MIT", "optional": true, "engines": { "node": ">=0.10.0" @@ -14854,6 +14909,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "inBundle": true, + "license": "ISC", "optional": true, "dependencies": { "wrappy": "1" @@ -14864,6 +14920,7 @@ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "inBundle": true, + "license": "MIT", "optional": true, "engines": { "node": ">=0.10.0" @@ -14874,6 +14931,7 @@ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "inBundle": true, + "license": "MIT", "optional": true, "engines": { "node": ">=0.10.0" @@ -14884,6 +14942,7 @@ "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "inBundle": true, + "license": "ISC", "optional": true, "dependencies": { "os-homedir": "^1.0.0", @@ -14895,6 +14954,7 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "inBundle": true, + "license": "MIT", "optional": true, "engines": { "node": ">=0.10.0" @@ -14905,6 +14965,7 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "inBundle": true, + "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/rc": { @@ -14912,6 +14973,7 @@ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "inBundle": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "optional": true, "dependencies": { "deep-extend": "^0.6.0", @@ -14928,6 +14990,7 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "inBundle": true, + "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/readable-stream": { @@ -14935,6 +14998,7 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "inBundle": true, + "license": "MIT", "optional": true, "dependencies": { "core-util-is": "~1.0.0", @@ -14951,6 +15015,7 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "inBundle": true, + "license": "ISC", "optional": true, "dependencies": { "glob": "^7.1.3" @@ -14964,6 +15029,7 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "inBundle": true, + "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/safer-buffer": { @@ -14971,6 +15037,7 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "inBundle": true, + "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/sax": { @@ -14978,6 +15045,7 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "inBundle": true, + "license": "ISC", "optional": true }, "node_modules/fsevents/node_modules/semver": { @@ -14985,6 +15053,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "inBundle": true, + "license": "ISC", "optional": true, "bin": { "semver": "bin/semver" @@ -14995,6 +15064,7 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "inBundle": true, + "license": "ISC", "optional": true }, "node_modules/fsevents/node_modules/signal-exit": { @@ -15002,6 +15072,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "inBundle": true, + "license": "ISC", "optional": true }, "node_modules/fsevents/node_modules/string_decoder": { @@ -15009,6 +15080,7 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "inBundle": true, + "license": "MIT", "optional": true, "dependencies": { "safe-buffer": "~5.1.0" @@ -15019,6 +15091,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "inBundle": true, + "license": "MIT", "optional": true, "dependencies": { "code-point-at": "^1.0.0", @@ -15034,6 +15107,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "inBundle": true, + "license": "MIT", "optional": true, "dependencies": { "ansi-regex": "^2.0.0" @@ -15047,6 +15121,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "inBundle": true, + "license": "MIT", "optional": true, "engines": { "node": ">=0.10.0" @@ -15057,6 +15132,7 @@ "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", "inBundle": true, + "license": "ISC", "optional": true, "dependencies": { "chownr": "^1.1.1", @@ -15076,6 +15152,7 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "inBundle": true, + "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/wide-align": { @@ -15083,6 +15160,7 @@ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "inBundle": true, + "license": "ISC", "optional": true, "dependencies": { "string-width": "^1.0.2 || 2" @@ -15093,6 +15171,7 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "inBundle": true, + "license": "ISC", "optional": true }, "node_modules/fsevents/node_modules/yallist": { @@ -15100,6 +15179,7 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", "inBundle": true, + "license": "ISC", "optional": true }, "node_modules/function-bind": { @@ -39144,10 +39224,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", @@ -41611,6 +41690,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", @@ -42532,6 +42617,11 @@ "bundled": true, "optional": true }, + "ini": { + "version": "1.3.5", + "bundled": true, + "optional": true + }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", diff --git a/package.json b/package.json index abfb05839..ef7070619 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "bowser": "1.9.4", "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", diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 53330d3b7..360c725b7 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -67,6 +67,7 @@ const GUIComponent = props => { blocksTabVisible, cardsVisible, canChangeLanguage, + canChangeTheme, canCreateNew, canEditTitle, canManageFiles, @@ -213,6 +214,7 @@ const GUIComponent = props => { authorThumbnailUrl={authorThumbnailUrl} authorUsername={authorUsername} canChangeLanguage={canChangeLanguage} + canChangeTheme={canChangeTheme} canCreateCopy={canCreateCopy} canCreateNew={canCreateNew} canEditTitle={canEditTitle} @@ -377,6 +379,7 @@ GUIComponent.propTypes = { basePath: PropTypes.string, blocksTabVisible: PropTypes.bool, canChangeLanguage: PropTypes.bool, + canChangeTheme: PropTypes.bool, canCreateCopy: PropTypes.bool, canCreateNew: PropTypes.bool, canEditTitle: PropTypes.bool, @@ -434,6 +437,7 @@ GUIComponent.defaultProps = { backpackVisible: false, basePath: './', canChangeLanguage: true, + canChangeTheme: true, canCreateNew: false, canEditTitle: false, canManageFiles: true, 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/menu-bar/menu-bar.css b/src/components/menu-bar/menu-bar.css index 78c639c15..a5fbf5bac 100644 --- a/src/components/menu-bar/menu-bar.css +++ b/src/components/menu-bar/menu-bar.css @@ -27,7 +27,8 @@ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: $menu-bar-standard-font-size; font-weight: bold; - background-color: $motion-primary; + /* background-color: $motion-primary; */ + background-color: #774DCB; color: $ui-white; } @@ -52,16 +53,17 @@ .language-icon { height: 1.5rem; vertical-align: middle; -} - -.language-caret { - margin: 0 .125rem; + margin-inline-end: .125rem; } .language-menu { box-sizing: border-box; display: inline-flex; - width: $language-selector-width; +} + +.language-label { + margin-inline-start: .25rem; + margin-inline-end: .5rem; } .menu-bar-item { @@ -106,10 +108,6 @@ 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; @@ -224,3 +222,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 126f0d460..b3cb50987 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'; @@ -58,8 +60,12 @@ import { languageMenuOpen, openLoginMenu, closeLoginMenu, - loginMenuOpen + loginMenuOpen, + openThemeMenu, + closeThemeMenu, + themeMenuOpen } from '../../reducers/menus'; +import {setTheme} from '../../reducers/theme.js'; import collectMetadata from '../../lib/collect-metadata'; @@ -76,6 +82,7 @@ import aboutIcon from './icon--about.svg'; import scratchLogo from './scratch-logo.svg'; import sharedMessages from '../../lib/shared-messages'; +import {DEFAULT_THEME, HIGH_CONTRAST_THEME} from '../../lib/themes/index.js'; const ariaMessages = defineMessages({ language: { @@ -161,6 +168,7 @@ class MenuBar extends React.Component { constructor (props) { super(props); bindAll(this, [ + 'handleChangeTheme', 'handleClickNew', 'handleClickRemix', 'handleClickSave', @@ -256,6 +264,19 @@ class MenuBar extends React.Component { this.props.onClickLanguage(e); } } + handleChangeTheme (theme) { + this.props.onChangeTheme(theme); + this.props.onRequestCloseTheme(); + + let systemPreferencesTheme = DEFAULT_THEME; + const prefersHighContrastQuery = '(prefers-contrast: more)'; + if (window.matchMedia(prefersHighContrastQuery).matches) { + systemPreferencesTheme = HIGH_CONTRAST_THEME; + } + + // Remove the cookie if it matches the system preferences. Otherwise store the theme in the cookie. + document.cookie = `scratchtheme=${systemPreferencesTheme === theme ? '' : theme}`; + } restoreOptionMessage (deletedItem) { switch (deletedItem) { case 'Sprite': @@ -402,18 +423,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 @@ -761,6 +792,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, @@ -770,6 +802,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, @@ -782,6 +815,7 @@ MenuBar.propTypes = { locale: PropTypes.string.isRequired, loginMenuOpen: PropTypes.bool, logo: PropTypes.string, + onChangeTheme: PropTypes.func, 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 @@ -812,6 +846,8 @@ MenuBar.propTypes = { onRequestCloseFile: PropTypes.func, onRequestCloseLanguage: PropTypes.func, onRequestCloseLogin: PropTypes.func, + onRequestCloseTheme: PropTypes.func, + onRequestOpenTheme: PropTypes.func, onSeeCommunity: PropTypes.func, onShare: PropTypes.func, onStartSelectingFileUpload: PropTypes.func, @@ -821,6 +857,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 @@ -837,6 +875,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, @@ -847,6 +886,8 @@ const mapStateToProps = (state, ownProps) => { loginMenuOpen: loginMenuOpen(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), @@ -869,11 +910,14 @@ const mapDispatchToProps = dispatch => ({ onRequestCloseLogin: () => dispatch(closeLoginMenu()), 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)) + onSeeCommunity: () => dispatch(setPlayer(true)), + onChangeTheme: theme => dispatch(setTheme(theme)) }); export default compose( diff --git a/src/components/menu-bar/theme-menu.css b/src/components/menu-bar/theme-menu.css new file mode 100644 index 000000000..24fcec18a --- /dev/null +++ b/src/components/menu-bar/theme-menu.css @@ -0,0 +1,28 @@ +@import "../../css/colors.css"; + +.theme-label { + margin-inline-start: .25rem; + margin-inline-end: .5rem; +} + +.theme-icon { + height: 1.5rem; + vertical-align: middle; + margin-inline-end: .25rem; +} + +.theme-option { + display: flex; + align-items: center; + gap: .5rem; +} + +.theme-active { + background-color: $ui-black-transparent; +} + +@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..d9ca0b1a5 --- /dev/null +++ b/src/components/menu-bar/theme-menu.jsx @@ -0,0 +1,91 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {FormattedMessage} 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 styles from './theme-menu.css'; +import menuBarStyles from './menu-bar.css'; + +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 (
+ + + + + + + + {enabledThemes.map(enabledTheme => ( + props.onChange(enabledTheme)} + theme={enabledTheme} + />) + )} + + +
); +}; + +ThemeMenu.propTypes = { + className: PropTypes.string, + isRtl: PropTypes.bool, + onChange: PropTypes.func, + onRequestClose: PropTypes.func, + onRequestOpen: PropTypes.func, + theme: PropTypes.string, + themeMenuOpen: PropTypes.bool +}; + +export default ThemeMenu; diff --git a/src/components/menu/menu.css b/src/components/menu/menu.css index c48f0d25d..d671d1381 100644 --- a/src/components/menu/menu.css +++ b/src/components/menu/menu.css @@ -4,7 +4,8 @@ position: absolute; border: 1px solid $ui-black-transparent; border-radius: 0 0 8px 8px; - background-color: $motion-primary; + /* background-color: $motion-primary; */ + background-color: #774DCB; padding: 0; margin: 0; min-width: 186px; 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/system-preferences-hoc.jsx b/src/lib/system-preferences-hoc.jsx index 0190af4c0..1f2cf6f91 100644 --- a/src/lib/system-preferences-hoc.jsx +++ b/src/lib/system-preferences-hoc.jsx @@ -1,20 +1,17 @@ import React from 'react'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; +import cookie from 'cookie'; + import {DEFAULT_THEME} from './themes'; import {setTheme} from '../reducers/theme'; -const prefersDarkQuery = '(prefers-color-scheme: dark)'; +// Dark mode is not yet supported +// const prefersDarkQuery = '(prefers-color-scheme: dark)'; const prefersHighContrastQuery = '(prefers-contrast: more)'; const getTheme = () => { - const highContrast = window.matchMedia(prefersHighContrastQuery).matches; - - if (highContrast) return 'high-contrast'; - - const dark = window.matchMedia(prefersDarkQuery).matches; - - if (dark) return 'dark-mode'; + if (window.matchMedia(prefersHighContrastQuery).matches) return 'high-contrast'; return DEFAULT_THEME; }; @@ -22,17 +19,22 @@ const getTheme = () => { const systemPreferencesHOC = function (WrappedComponent) { class SystemPreferences extends React.Component { componentDidMount () { - this.props.onSetTheme(getTheme()); - this.preferencesListener = () => this.props.onSetTheme(getTheme()); + // this.props.onSetTheme(getTheme()); + this.preferencesListener = () => { + const obj = cookie.parse(document.cookie) || {}; + const themeCookie = obj.scratchtheme; + + // Only use system preferences if there is not a specified cookie. + if (themeCookie) return; + + this.props.onSetTheme(getTheme()); + } this.highContrastMatchMedia = window.matchMedia(prefersHighContrastQuery); this.highContrastMatchMedia.addEventListener('change', this.preferencesListener); - this.darkMatchMedia = window.matchMedia(prefersDarkQuery); - this.darkMatchMedia.addEventListener('change', this.preferencesListener); } componentWillUnmount () { this.highContrastMatchMedia.removeEventListener('change', this.preferencesListener); - this.darkMatchMedia.removeEventListener('change', this.preferencesListener); } render () { diff --git a/src/lib/themes/high-contrast.svg b/src/lib/themes/high-contrast.svg new file mode 100644 index 000000000..4bdf2fa04 --- /dev/null +++ b/src/lib/themes/high-contrast.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/lib/themes/index.js b/src/lib/themes/index.js index bd1edcb5b..81151406f 100644 --- a/src/lib/themes/index.js +++ b/src/lib/themes/index.js @@ -1,33 +1,73 @@ import defaultsDeep from 'lodash.defaultsdeep'; +import {defineMessages} from 'react-intl'; import darkMode from './dark-mode'; import highContrast from './high-contrast'; import defaultColors from './default-colors'; +import defaultIcon from './standard.svg'; +import highContrastIcon from './high-contrast.svg'; + const DEFAULT_THEME = 'standard'; +const HIGH_CONTRAST_THEME = 'high-contrast'; +const DARK_THEME = 'dark-mode'; + const mergeWithDefaults = colors => defaultsDeep({}, colors, defaultColors); +const messages = defineMessages({ + [DEFAULT_THEME]: { + id: 'gui.theme.default', + defaultMessage: 'Default', + description: 'label for default theme' + }, + [DARK_THEME]: { + id: 'gui.theme.dark', + defaultMessage: 'Dark', + description: 'label for dark mode theme' + }, + [HIGH_CONTRAST_THEME]: { + id: 'gui.theme.highContrast', + defaultMessage: 'High Text Contrast', + description: 'label for high constast theme' + } +}); + const themeMap = { - 'dark-mode': mergeWithDefaults(darkMode), - 'high-contrast': mergeWithDefaults(highContrast), - [DEFAULT_THEME]: defaultColors + [DEFAULT_THEME]: { + colors: defaultColors, + label: messages[DEFAULT_THEME], + icon: defaultIcon + }, + [DARK_THEME]: { + colors: mergeWithDefaults(darkMode), + label: messages[DARK_THEME] + }, + [HIGH_CONTRAST_THEME]: { + colors: mergeWithDefaults(highContrast), + label: messages[HIGH_CONTRAST_THEME], + icon: highContrastIcon + } }; const getColorsForTheme = theme => { - const colors = themeMap[theme]; + const themeInfo = themeMap[theme]; - if (!colors) { + if (!themeInfo) { throw new Error(`Undefined theme ${theme}`); } - return colors; + return themeInfo.colors; }; const themes = Object.keys(themeMap); export { DEFAULT_THEME, + DARK_THEME, + HIGH_CONTRAST_THEME, defaultColors, getColorsForTheme, - themes + messages, + themes, + themeMap }; diff --git a/src/lib/themes/standard.svg b/src/lib/themes/standard.svg new file mode 100644 index 000000000..8f54974f6 --- /dev/null +++ b/src/lib/themes/standard.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/reducers/menus.js b/src/reducers/menus.js index fc3687f5a..c8c4a6945 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 initialState = { @@ -15,7 +16,8 @@ const initialState = { [MENU_FILE]: false, [MENU_EDIT]: false, [MENU_LANGUAGE]: false, - [MENU_LOGIN]: false + [MENU_LOGIN]: false, + [MENU_THEME]: false }; const reducer = function (state, action) { @@ -59,6 +61,9 @@ 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]; export { reducer as default, @@ -80,5 +85,8 @@ export { languageMenuOpen, openLoginMenu, closeLoginMenu, - loginMenuOpen + loginMenuOpen, + openThemeMenu, + closeThemeMenu, + themeMenuOpen }; diff --git a/src/reducers/theme.js b/src/reducers/theme.js index 63ddabc0e..0c8664332 100644 --- a/src/reducers/theme.js +++ b/src/reducers/theme.js @@ -1,9 +1,24 @@ -import {DEFAULT_THEME} from '../lib/themes'; +import cookie from 'cookie'; + +import {DEFAULT_THEME, HIGH_CONTRAST_THEME} from '../lib/themes'; const SET_THEME = 'scratch-gui/theme/SET_THEME'; +const detectTheme = () => { + const obj = cookie.parse(document.cookie) || {}; + const themeCookie = obj.scratchtheme; + + if ([DEFAULT_THEME, HIGH_CONTRAST_THEME].includes(themeCookie)) return themeCookie; + + // No cookie set. Fall back to system preferences + const prefersHighContrastQuery = '(prefers-contrast: more)'; + if (window.matchMedia(prefersHighContrastQuery).matches) return HIGH_CONTRAST_THEME; + + return DEFAULT_THEME; +}; + const initialState = { - theme: DEFAULT_THEME + theme: detectTheme() }; const reducer = (state = initialState, action) => { @@ -25,3 +40,19 @@ export { initialState as themeInitialState, setTheme }; + + +const prefersDarkQuery = '(prefers-color-scheme: dark)'; +const prefersHighContrastQuery = '(prefers-contrast: more)'; + +const getTheme = () => { + const highContrast = window.matchMedia(prefersHighContrastQuery).matches; + + if (highContrast) return 'high-contrast'; + + const dark = window.matchMedia(prefersDarkQuery).matches; + + if (dark) return 'dark-mode'; + + return DEFAULT_THEME; +};