diff --git a/next.config.js b/next.config.js index dbb0599cc44..515f6ef4132 100644 --- a/next.config.js +++ b/next.config.js @@ -5,6 +5,8 @@ const {patchWebpackConfig} = require('next-global-css'); const withTM = require('next-transpile-modules')([ '@gravity-ui/page-constructor', '@gravity-ui/components', + '@gravity-ui/chartkit', + '@gravity-ui/yagr', ]); const plugins = [ diff --git a/package-lock.json b/package-lock.json index 88cdc542ef0..ffa0189c77c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,14 @@ "name": "landing", "version": "0.1.0", "dependencies": { - "@diplodoc/transform": "^4.11.0", + "@diplodoc/transform": "^4.26.0", + "@gravity-ui/chartkit": "^5.10.1", "@gravity-ui/components": "^3.7.0", "@gravity-ui/date-components": "^2.8.0", "@gravity-ui/icons": "^2.10.0", + "@gravity-ui/navigation": "^2.21.0", "@gravity-ui/page-constructor": "^5.2.0", - "@gravity-ui/uikit": "^6.22.0", + "@gravity-ui/uikit": "^6.23.0", "@mdx-js/mdx": "^2.3.0", "@mdx-js/react": "^2.3.0", "@octokit/rest": "^20.1.1", @@ -21,11 +23,12 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "bem-cn-lite": "^4.1.0", + "chroma-js": "^2.4.2", "husky": "^8.0.3", "i18next": "^23.8.3", "javascript-time-ago": "^2.5.9", - "landing-icons": "npm:@gravity-ui/icons@^2.8.1", - "landing-uikit": "npm:@gravity-ui/uikit@^6.17.0", + "landing-icons": "npm:@gravity-ui/icons@^2.10.0", + "landing-uikit": "npm:@gravity-ui/uikit@^6.23.0", "lint-staged": "^14.0.1", "lodash": "^4.17.21", "micromatch": "^4.0.7", @@ -34,7 +37,7 @@ "next-i18next": "^15.2.0", "prismjs": "^1.29.0", "react": "^18.2.0", - "react-dom": "^18.2.0", + "react-dom": "^18.3.1", "react-i18next": "^14.0.5", "react-time-ago": "^7.2.1", "rehype-autolink-headings": "^6.1.1", @@ -43,7 +46,8 @@ "remark-link-rewrite": "^1.0.7", "swiper": "^10.2.0", "typescript": "^5.1.6", - "url": "^0.11.0" + "url": "^0.11.0", + "uuid": "^10.0.0" }, "devDependencies": { "@aws-sdk/client-s3": "^3.614.0", @@ -52,6 +56,7 @@ "@gravity-ui/stylelint-config": "^2.0.0", "@gravity-ui/tsconfig": "^1.0.0", "@svgr/webpack": "^6.5.1", + "@types/chroma-js": "^2.4.4", "@types/jest": "^29.2.4", "@types/lodash": "^4.14.197", "@types/micromatch": "^4.0.7", @@ -60,6 +65,7 @@ "@types/react": "^18.0.26", "@types/react-dom": "^18.0.9", "@types/react-timeago": "^4.1.3", + "@types/uuid": "^10.0.0", "dotenv": "^16.0.3", "eslint": "^8.27.0", "eslint-plugin-testing-library": "^5.9.1", @@ -2927,9 +2933,9 @@ } }, "node_modules/@diplodoc/tabs-extension": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@diplodoc/tabs-extension/-/tabs-extension-2.1.0.tgz", - "integrity": "sha512-e3T6bmE/8K5mR4Kg8bucC1+7DvgCxwu/qJ0B38c2YpFhvT+R+lredULNdHNYLTU9jSvGRygP6bwsqYoNRixkIw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@diplodoc/tabs-extension/-/tabs-extension-3.1.1.tgz", + "integrity": "sha512-FWywClCNtMUQGZAsd6Hr/Cs1TX9aOJ4t9SOR9pIuOVsJwVWXNihdUW7x2yaAT0WwP4bBqXg7GSUQ/DIVIde9Ww==", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" }, @@ -2940,17 +2946,17 @@ } }, "node_modules/@diplodoc/transform": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@diplodoc/transform/-/transform-4.11.0.tgz", - "integrity": "sha512-3JRC/6ISrdhPhZIenwTNAZyUJmD+o3vGRkeh4CD7+/Nh5eAlrcQ4/aRkuNIdFqE5sM1WeZLjKgiMsS0lz5RpZg==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@diplodoc/transform/-/transform-4.26.0.tgz", + "integrity": "sha512-k6TxF3DzWkAdfPnjzfUBR4mNGiE4dQC5Cvk9flGazhX2aNOXetSYU1/Y1voSNNnG8EmTJqXOsTptGMLBxAz/hw==", "dependencies": { - "@diplodoc/tabs-extension": "^2.1.0", - "chalk": "4.1.2", + "@diplodoc/tabs-extension": "^3.0.0", + "chalk": "^4.1.2", "cheerio": "^1.0.0-rc.12", "css": "^3.0.0", "cssfilter": "0.0.10", "get-root-node-polyfill": "1.0.0", - "github-slugger": "1.4.0", + "github-slugger": "^1.5.0", "js-yaml": "^4.1.0", "lodash": "4.17.21", "markdown-it": "^13.0.2", @@ -2958,10 +2964,11 @@ "markdown-it-deflist": "2.1.0", "markdown-it-meta": "0.0.1", "markdown-it-sup": "1.0.0", - "markdownlint": "^0.25.1", + "markdownlint": "^0.32.1", "markdownlint-rule-helpers": "0.17.2", "sanitize-html": "^2.11.0", - "slugify": "1.6.5" + "slugify": "1.6.5", + "svgo": "^3.2.0" }, "peerDependencies": { "highlight.js": "^10.0.3 || ^11" @@ -3023,17 +3030,6 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/@diplodoc/transform/node_modules/entities": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", - "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/@diplodoc/transform/node_modules/htmlparser2": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", @@ -3063,21 +3059,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/@diplodoc/transform/node_modules/markdown-it": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.2.tgz", - "integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==", - "dependencies": { - "argparse": "^2.0.1", - "entities": "~3.0.1", - "linkify-it": "^4.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - }, - "bin": { - "markdown-it": "bin/markdown-it.js" - } - }, "node_modules/@diplodoc/transform/node_modules/sanitize-html": { "version": "2.12.1", "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.12.1.tgz", @@ -3192,6 +3173,65 @@ "node": ">=14" } }, + "node_modules/@gravity-ui/chartkit": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@gravity-ui/chartkit/-/chartkit-5.10.2.tgz", + "integrity": "sha512-2PrnMgPZvcQYLoyD7O7DAoyI+m8nSxqKA1Ibps16Hq2oAaer6TeSB6fCA5Zi1jFZK3AW9VoFutYYrXLqhNO/rQ==", + "dependencies": { + "@bem-react/classname": "^1.6.0", + "@gravity-ui/date-utils": "^2.1.0", + "@gravity-ui/i18n": "^1.0.0", + "@gravity-ui/yagr": "^4.3.0", + "afterframe": "^1.0.2", + "d3": "^7.8.5", + "lodash": "^4.17.21", + "react-split-pane": "^0.1.92", + "tslib": "^2.6.2" + }, + "peerDependencies": { + "@gravity-ui/uikit": "^6.0.0", + "react": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@gravity-ui/chartkit/node_modules/react-dom": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", + "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.19.1" + }, + "peerDependencies": { + "react": "^16.14.0" + } + }, + "node_modules/@gravity-ui/chartkit/node_modules/react-split-pane": { + "version": "0.1.92", + "resolved": "https://registry.npmjs.org/react-split-pane/-/react-split-pane-0.1.92.tgz", + "integrity": "sha512-GfXP1xSzLMcLJI5BM36Vh7GgZBpy+U/X0no+VM3fxayv+p1Jly5HpMofZJraeaMl73b3hvlr+N9zJKvLB/uz9w==", + "dependencies": { + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.4", + "react-style-proptype": "^3.2.2" + }, + "peerDependencies": { + "react": "^16.0.0-0", + "react-dom": "^16.0.0-0" + } + }, + "node_modules/@gravity-ui/chartkit/node_modules/scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, "node_modules/@gravity-ui/components": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/@gravity-ui/components/-/components-3.7.0.tgz", @@ -3299,6 +3339,23 @@ } } }, + "node_modules/@gravity-ui/navigation": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@gravity-ui/navigation/-/navigation-2.21.0.tgz", + "integrity": "sha512-zdbuHaEuKeUao4HAbPiXrn+NaMQ87w+XgnOznZ7RPtE3TfkmlAHVG69Rz51cs3J127xZdn7gUkf1PfiegQl7Vg==", + "dependencies": { + "react-transition-group": "^4.4.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@bem-react/classname": "^1.6.0", + "@gravity-ui/components": "^3.0.0", + "@gravity-ui/icons": "^2.2.0", + "@gravity-ui/uikit": "^6.15.0", + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@gravity-ui/page-constructor": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@gravity-ui/page-constructor/-/page-constructor-5.2.0.tgz", @@ -3365,6 +3422,18 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, + "node_modules/@gravity-ui/page-constructor/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@gravity-ui/prettier-config": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@gravity-ui/prettier-config/-/prettier-config-1.0.1.tgz", @@ -3398,10 +3467,9 @@ "dev": true }, "node_modules/@gravity-ui/uikit": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/@gravity-ui/uikit/-/uikit-6.22.0.tgz", - "integrity": "sha512-F0PZovzGbBB4kG0O1F2dt5JFJjg8MtobY3CkY0yBP3yILuVFU/nnEPISqXe1fxdn4W5arAph5FnnYf1P1sl3wA==", - "license": "MIT", + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/@gravity-ui/uikit/-/uikit-6.23.0.tgz", + "integrity": "sha512-v+1QPL+IdaGxv3tuEUHaRSzK5Hp/XNJv3EQxMIMFSV4ojtvio6sQAlzLIYALGN/IULWRwlfF0cqG/TavOo9yog==", "dependencies": { "@bem-react/classname": "^1.6.0", "@gravity-ui/i18n": "^1.3.0", @@ -3425,6 +3493,17 @@ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/@gravity-ui/yagr": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@gravity-ui/yagr/-/yagr-4.3.0.tgz", + "integrity": "sha512-mi7zj69Oci82xdkK0Hyc7IERntrV12sBCNX+Bf90c9z5bb++3CugvGr+29DrEtKh3UOQfZzEwaK9Pb4wk0S0Cw==", + "dependencies": { + "uplot": "1.6.27" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -4280,6 +4359,19 @@ "node": ">=16.0.0" } }, + "node_modules/@smithy/middleware-retry/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@smithy/middleware-serde": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.3.tgz", @@ -5245,7 +5337,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "dev": true, "engines": { "node": ">=10.13.0" } @@ -5269,6 +5360,12 @@ "integrity": "sha512-0WR3b8eaISjEW7RpZnclONaLFDf7buaowRHdqLp4vLj54AsSAYWfh3DRbfiYJY9XDxMgx1B4sE1Afw2PGpuHOA==", "dev": true }, + "node_modules/@types/chroma-js": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.4.tgz", + "integrity": "sha512-/DTccpHTaKomqussrn+ciEvfW4k6NAHzNzs/sts1TCqg333qNxOhy8TNIoQCmbGG3Tl8KdEhkGAssb1n3mTXiQ==", + "dev": true + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -5539,6 +5636,12 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true + }, "node_modules/@types/yargs": { "version": "17.0.23", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.23.tgz", @@ -6049,6 +6152,11 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/afterframe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/afterframe/-/afterframe-1.0.2.tgz", + "integrity": "sha512-0JeMZI7dIfVs5guqLgidQNV7c6jBC2HO0QNSekAUB82Hr7PdU9QXNAF3kpFkvATvHYDDTGto7FPsRu1ey+aKJQ==" + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -6798,6 +6906,11 @@ "node": ">= 6" } }, + "node_modules/chroma-js": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", + "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" + }, "node_modules/chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -7144,7 +7257,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dev": true, "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" @@ -7198,7 +7310,6 @@ "version": "5.0.5", "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "dev": true, "dependencies": { "css-tree": "~2.2.0" }, @@ -7211,7 +7322,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "dev": true, "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" @@ -7224,14 +7334,391 @@ "node_modules/csso/node_modules/mdn-data": { "version": "2.0.28", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "dev": true + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==" }, "node_modules/csstype": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/dayjs": { "version": "1.11.10", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", @@ -7355,6 +7842,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/deprecation": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", @@ -8875,9 +9370,9 @@ "integrity": "sha512-2REUOV3ue6NmT0QThhfzfYmeSoYpCG73+tL7Ir2C7P+gshRerI05WuIQuhDkE2Zlg5Wc39hc2DHj+pE23mGJvw==" }, "node_modules/github-slugger": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.4.0.tgz", - "integrity": "sha512-w0dzqw/nt51xMVmlaV1+JRzN+oCa1KfcgGEWhxUG16wbdA+Xnt/yoFO8Z8x/V82ZcZ0wy6ln9QDup5avbhiDhQ==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", + "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==" }, "node_modules/glob": { "version": "7.2.3", @@ -9357,6 +9852,17 @@ "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.3.1.tgz", "integrity": "sha512-tvfXskmG/9o+TJ5Fxu54sSO5OkY6d+uMn+K6JiUGLJrwxAVfer+8V3nU8jq3ts9Pe5lXJv4b1N7foIjJ8Iy2Gg==" }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -9454,6 +9960,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -10112,15 +10626,23 @@ }, "node_modules/landing-icons": { "name": "@gravity-ui/icons", - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/@gravity-ui/icons/-/icons-2.8.1.tgz", - "integrity": "sha512-cldaFAN3W2OAzEZBiurD7RsqyqGhS7xoVS9TC4DrOG9bXy8dWUvNEeeOnKgpIvZgAGFlTmg01BK6jMH0IFFSPw==" + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@gravity-ui/icons/-/icons-2.10.0.tgz", + "integrity": "sha512-xS0G4+TM7cD2cCKS4wVc01c4lLe/OreKjm4sHwrOtJWH4EawaRbpkuwtgUDcUvY2EryIcI6lgV+8o714m6lcyQ==", + "peerDependencies": { + "react": "*" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } }, "node_modules/landing-uikit": { "name": "@gravity-ui/uikit", - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/@gravity-ui/uikit/-/uikit-6.17.0.tgz", - "integrity": "sha512-aelbGQh0DxgZ/b3hbjZNBPc417mmoL4P5vq6yYOvUol/4GKimofgs7kOB2IjIVOAaEYivGgoVjmLQxREJfM15A==", + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/@gravity-ui/uikit/-/uikit-6.23.0.tgz", + "integrity": "sha512-v+1QPL+IdaGxv3tuEUHaRSzK5Hp/XNJv3EQxMIMFSV4ojtvio6sQAlzLIYALGN/IULWRwlfF0cqG/TavOo9yog==", "dependencies": { "@bem-react/classname": "^1.6.0", "@gravity-ui/i18n": "^1.3.0", @@ -10732,10 +11254,9 @@ } }, "node_modules/markdown-it": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz", - "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", - "peer": true, + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.2.tgz", + "integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==", "dependencies": { "argparse": "^2.0.1", "entities": "~3.0.1", @@ -10800,7 +11321,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", - "peer": true, "engines": { "node": ">=0.12" }, @@ -10818,14 +11338,26 @@ } }, "node_modules/markdownlint": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.25.1.tgz", - "integrity": "sha512-AG7UkLzNa1fxiOv5B+owPsPhtM4D6DoODhsJgiaNg1xowXovrYgOnLqAgOOFQpWOlHFVQUzjMY5ypNNTeov92g==", + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.32.1.tgz", + "integrity": "sha512-3sx9xpi4xlHlokGyHO9k0g3gJbNY4DI6oNEeEYq5gQ4W7UkiJ90VDAnuDl2U+yyXOUa6BX+0gf69ZlTUGIBp6A==", "dependencies": { - "markdown-it": "12.3.2" + "markdown-it": "13.0.2", + "markdownlint-micromark": "0.1.7" }, "engines": { - "node": ">=12" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/DavidAnson" + } + }, + "node_modules/markdownlint-micromark": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/markdownlint-micromark/-/markdownlint-micromark-0.1.7.tgz", + "integrity": "sha512-BbRPTC72fl5vlSKv37v/xIENSRDYL/7X/XoFzZ740FGEbs9vZerLrIkFRY0rv7slQKxDczToYuMmqQFN61fi4Q==", + "engines": { + "node": ">=16" } }, "node_modules/markdownlint-rule-helpers": { @@ -10836,37 +11368,6 @@ "node": ">=12" } }, - "node_modules/markdownlint/node_modules/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/markdownlint/node_modules/linkify-it": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", - "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", - "dependencies": { - "uc.micro": "^1.0.1" - } - }, - "node_modules/markdownlint/node_modules/markdown-it": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", - "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", - "dependencies": { - "argparse": "^2.0.1", - "entities": "~2.1.0", - "linkify-it": "^3.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - }, - "bin": { - "markdown-it": "bin/markdown-it.js" - } - }, "node_modules/mathml-tag-names": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", @@ -11165,8 +11666,7 @@ "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "dev": true + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" }, "node_modules/mdurl": { "version": "1.0.1", @@ -13014,9 +13514,9 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dependencies": { "loose-envify": "^1.1.0" }, @@ -13055,15 +13555,15 @@ } }, "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dependencies": { "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.3.1" } }, "node_modules/react-fast-compare": { @@ -13113,6 +13613,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, "node_modules/react-monaco-editor": { "version": "0.53.0", "resolved": "https://registry.npmjs.org/react-monaco-editor/-/react-monaco-editor-0.53.0.tgz", @@ -13195,6 +13700,14 @@ "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-style-proptype": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-style-proptype/-/react-style-proptype-3.2.2.tgz", + "integrity": "sha512-ywYLSjNkxKHiZOqNlso9PZByNEY+FTyh3C+7uuziK0xFXu9xzdyfHwg4S9iyiRRoPCR4k2LqaBBsWVmSBwCWYQ==", + "dependencies": { + "prop-types": "^15.5.4" + } + }, "node_modules/react-time-ago": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/react-time-ago/-/react-time-ago-7.2.1.tgz", @@ -13784,6 +14297,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -13807,6 +14325,11 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -13869,6 +14392,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/sanitize-html": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.6.1.tgz", @@ -13938,9 +14466,9 @@ } }, "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "dependencies": { "loose-envify": "^1.1.0" } @@ -14685,15 +15213,15 @@ "dev": true }, "node_modules/svgo": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.0.2.tgz", - "integrity": "sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==", - "dev": true, + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", "dependencies": { "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", - "css-tree": "^2.2.1", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.0.0" }, @@ -14712,7 +15240,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, "engines": { "node": ">= 10" } @@ -15387,6 +15914,11 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uplot": { + "version": "1.6.27", + "resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.27.tgz", + "integrity": "sha512-78U4ss5YeU65kQkOC/QAKiyII+4uo+TYUJJKvuxRzeSpk/s5sjpY1TL0agkmhHBBShpvLtmbHIEiM7+C5lBULg==" + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -15432,9 +15964,9 @@ } }, "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" diff --git a/package.json b/package.json index 6e9b93d8a94..8cf8fcc2dd2 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,14 @@ "private": true, "homepage": "https://gravity-ui.com/", "dependencies": { - "@diplodoc/transform": "^4.11.0", + "@diplodoc/transform": "^4.26.0", + "@gravity-ui/chartkit": "^5.10.1", "@gravity-ui/components": "^3.7.0", "@gravity-ui/date-components": "^2.8.0", "@gravity-ui/icons": "^2.10.0", + "@gravity-ui/navigation": "^2.21.0", "@gravity-ui/page-constructor": "^5.2.0", - "@gravity-ui/uikit": "^6.22.0", + "@gravity-ui/uikit": "^6.23.0", "@mdx-js/mdx": "^2.3.0", "@mdx-js/react": "^2.3.0", "@octokit/rest": "^20.1.1", @@ -17,11 +19,12 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "bem-cn-lite": "^4.1.0", + "chroma-js": "^2.4.2", "husky": "^8.0.3", "i18next": "^23.8.3", "javascript-time-ago": "^2.5.9", - "landing-icons": "npm:@gravity-ui/icons@^2.8.1", - "landing-uikit": "npm:@gravity-ui/uikit@^6.17.0", + "landing-icons": "npm:@gravity-ui/icons@^2.10.0", + "landing-uikit": "npm:@gravity-ui/uikit@^6.23.0", "lint-staged": "^14.0.1", "lodash": "^4.17.21", "micromatch": "^4.0.7", @@ -30,7 +33,7 @@ "next-i18next": "^15.2.0", "prismjs": "^1.29.0", "react": "^18.2.0", - "react-dom": "^18.2.0", + "react-dom": "^18.3.1", "react-i18next": "^14.0.5", "react-time-ago": "^7.2.1", "rehype-autolink-headings": "^6.1.1", @@ -39,7 +42,8 @@ "remark-link-rewrite": "^1.0.7", "swiper": "^10.2.0", "typescript": "^5.1.6", - "url": "^0.11.0" + "url": "^0.11.0", + "uuid": "^10.0.0" }, "devDependencies": { "@aws-sdk/client-s3": "^3.614.0", @@ -48,6 +52,7 @@ "@gravity-ui/stylelint-config": "^2.0.0", "@gravity-ui/tsconfig": "^1.0.0", "@svgr/webpack": "^6.5.1", + "@types/chroma-js": "^2.4.4", "@types/jest": "^29.2.4", "@types/lodash": "^4.14.197", "@types/micromatch": "^4.0.7", @@ -56,6 +61,7 @@ "@types/react": "^18.0.26", "@types/react-dom": "^18.0.9", "@types/react-timeago": "^4.1.3", + "@types/uuid": "^10.0.0", "dotenv": "^16.0.3", "eslint": "^8.27.0", "eslint-plugin-testing-library": "^5.9.1", @@ -73,6 +79,12 @@ "svgo": "^3.0.2", "undici": "^5.14.0" }, + "overrides": { + "react-split-pane": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, "scripts": { "prepare": "husky install", "start": "next dev", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index b2fbe5fab73..af08e007f8e 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -8,6 +8,7 @@ "menu_components": "Components", "menu_design": "Design", "menu_icons": "Icons", + "menu_themes": "Themes", "roadmap_inProgress": "In progress", "navigation_previous": "Previous", "navigation_next": "Next", diff --git a/public/locales/en/component.json b/public/locales/en/component.json index f94dd7cd5ee..2a8ec0011d6 100644 --- a/public/locales/en/component.json +++ b/public/locales/en/component.json @@ -5,6 +5,7 @@ "color-input_validation-format-error": "Incorrect format", "maintainers_one": "Maintainer:", "maintainers_other": "Maintainers:", + "maintainers": "Maintainers:", "theme": "Switch theme", "rtlOn": "Switch RTL on", "rtlOff": "Switch RTL off", diff --git a/public/locales/en/themes.json b/public/locales/en/themes.json new file mode 100644 index 00000000000..6d4b05d103a --- /dev/null +++ b/public/locales/en/themes.json @@ -0,0 +1,35 @@ +{ + "title": "Themes", + "tags_colors": "Colors", + "tags_typography": "Typography", + "tags_borderRadius": "Border Radius", + "tags_preview": "Preview", + "btn_export_theme": "Export theme", + "custom_brand_palette": "Custom Brand Palette", + "page_background": "Page Background", + "brand_color": "Brand Color", + "hide_advanced_settings": "Hide Advanced Settings", + "advanced_settings": "Advanced Settings", + "theme": "Theme", + "basic_palette": "Basic Palette", + "add_color": "Add color", + "color-input_validation-format-error": "Incorrect format", + "palette_colors_description": "Support Colors for various cases and
states", + "dark_theme": "Dark theme", + "light_theme": "Light theme", + "advanced_brand_palette": "Advanced Brand Palette", + "additional_colors": "Additional Colors", + "component_preview": "Component preview", + "is_everything_set": "Is everything set?", + "cancel": "Cancel", + "export_theme_config": "Export theme config", + "radius": "Radius", + "radius_regular": "Regular", + "radius_circled": "Circled", + "radius_squared": "Squared", + "radius_custom": "Custom radius", + "choose_border_radius": "Choose border radius", + "label": "Label", + "input_placeholder": "Content", + "button": "Button" +} diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json index 867886b112e..03868dceecc 100644 --- a/public/locales/ru/common.json +++ b/public/locales/ru/common.json @@ -8,6 +8,7 @@ "menu_components": "Компоненты", "menu_design": "Дизайн", "menu_icons": "Иконки", + "menu_themes": "Темы", "roadmap_inProgress": "В работе", "navigation_previous": "Назад", "navigation_next": "Далее", diff --git a/public/locales/ru/component.json b/public/locales/ru/component.json index 08b64cfba88..e91f7fac739 100644 --- a/public/locales/ru/component.json +++ b/public/locales/ru/component.json @@ -5,6 +5,7 @@ "color-input_validation-format-error": "Неверный формат", "maintainers_one": "Maintainer:", "maintainers_other": "Maintainers:", + "maintainers": "Maintainers:", "theme": "Переключить тему", "rtlOn": "Включить RTL", "rtlOff": "Выключить RTL", diff --git a/public/locales/ru/themes.json b/public/locales/ru/themes.json new file mode 100644 index 00000000000..09a7c078c34 --- /dev/null +++ b/public/locales/ru/themes.json @@ -0,0 +1,35 @@ +{ + "title": "Темы", + "tags_colors": "Цвета", + "tags_typography": "Шрифты", + "tags_borderRadius": "Радиусы границ", + "tags_preview": "Предпоказ", + "color-input_validation-format-error": "Неверный формат", + "btn_export_theme": "Экспортировать тему", + "custom_brand_palette": "Кастомная брендовая палитра", + "page_background": "Цвет фона", + "brand_color": "Брендовый цвет", + "hide_advanced_settings": "Скрыть расширенные настройки", + "advanced_settings": "Расширенные настройки", + "theme": "Тема", + "basic_palette": "Базовая палитра", + "add_color": "Добавить цвет", + "palette_colors_description": "Поддерживаемые цвета для различных случаев и состояний", + "dark_theme": "Тёмная тема", + "light_theme": "Светлая тема", + "advanced_brand_palette": "Расширенная брендовая палитра", + "additional_colors": "Дополнительные цвета", + "component_preview": "Предпросмотр компонентов", + "is_everything_set": "Все настроили?", + "cancel": "Закрыть", + "export_theme_config": "Экспорт конфигурации темы", + "radius": "Радиус", + "radius_regular": "Стандартный", + "radius_circled": "Круглый", + "radius_squared": "Без скругления", + "radius_custom": "Настроить скругление", + "choose_border_radius": "Выберите радиус скругления", + "label": "Ярлык", + "input_placeholder": "Контент", + "button": "Кнопка" +} diff --git a/src/[locale]/themes/index.tsx b/src/[locale]/themes/index.tsx new file mode 100644 index 00000000000..b424ca5f66f --- /dev/null +++ b/src/[locale]/themes/index.tsx @@ -0,0 +1,38 @@ +import {GetStaticPaths, GetStaticProps} from 'next'; +import {useTranslation} from 'next-i18next'; +import React from 'react'; + +import {Layout} from '../../components/Layout/Layout'; +import {Themes} from '../../components/Themes/Themes'; +import {useLocaleRedirect} from '../../hooks/useLocaleRedirect'; +import {getI18nPaths, getI18nProps} from '../../utils/i18next'; + +export const getStaticPaths: GetStaticPaths = async () => { + return { + paths: getI18nPaths(), + fallback: false, + }; +}; + +export const getStaticProps: GetStaticProps = async (ctx) => { + const result = { + props: { + ...(await getI18nProps(ctx, ['themes'])), + }, + }; + return result; +}; + +export const ThemesPage = () => { + useLocaleRedirect(); + + const {t} = useTranslation(); + + return ( + + + + ); +}; + +export default ThemesPage; diff --git a/src/assets/icons/gravity-ui.svg b/src/assets/icons/gravity-ui.svg new file mode 100644 index 00000000000..64ca776a285 --- /dev/null +++ b/src/assets/icons/gravity-ui.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/preview-card-1.png b/src/assets/preview-card-1.png new file mode 100644 index 00000000000..9dd9b39cde6 Binary files /dev/null and b/src/assets/preview-card-1.png differ diff --git a/src/assets/preview-card-2.png b/src/assets/preview-card-2.png new file mode 100644 index 00000000000..af28b8db626 Binary files /dev/null and b/src/assets/preview-card-2.png differ diff --git a/src/assets/preview-card-3.png b/src/assets/preview-card-3.png new file mode 100644 index 00000000000..b5a33662064 Binary files /dev/null and b/src/assets/preview-card-3.png differ diff --git a/src/blocks/Examples/components/Showcase/Showcase.tsx b/src/blocks/Examples/components/Showcase/Showcase.tsx index eb2210fbf5d..4cf7b0cc159 100644 --- a/src/blocks/Examples/components/Showcase/Showcase.tsx +++ b/src/blocks/Examples/components/Showcase/Showcase.tsx @@ -44,11 +44,12 @@ const tableColumns = [ ]; export type ShowcaseProps = { - color: string; + color?: string; theme: Theme; + style?: string; }; -export const Showcase: React.FC = ({color, theme}) => { +export const Showcase: React.FC = ({color, theme, style}) => { const [activeTab, setActiveTab] = React.useState(tabs[0].id); const [tableSelectedIds, setTableSelectedIds] = React.useState(['1']); @@ -67,6 +68,9 @@ export const Showcase: React.FC = ({color, theme}) => { return ( + {style ? ( + + ) : null}
diff --git a/src/components/ClipboardArea/ClipboardArea.scss b/src/components/ClipboardArea/ClipboardArea.scss index 3eda2335970..71e124fa790 100644 --- a/src/components/ClipboardArea/ClipboardArea.scss +++ b/src/components/ClipboardArea/ClipboardArea.scss @@ -7,6 +7,14 @@ $block: '.#{variables.$ns}clipboard-area'; #{$block} { display: flex; + &__popover { + width: 100%; + + .g-popover__handler { + width: 100%; + } + } + &__popup { --g-popup-background-color: #3e3235; --g-popup-border-color: #3e3235; diff --git a/src/components/ClipboardArea/ClipboardArea.tsx b/src/components/ClipboardArea/ClipboardArea.tsx index b2653065bd7..161c5a63013 100644 --- a/src/components/ClipboardArea/ClipboardArea.tsx +++ b/src/components/ClipboardArea/ClipboardArea.tsx @@ -30,6 +30,7 @@ export const ClipboardArea: React.FC = ({ return isNeedPopup ? ( = ({code, tooltipContent, className}) => { + return ( + + {(status) => ( +
+
+
+ {code} +
+
+ +
+
+
+ )} +
+ ); +}; diff --git a/src/components/ColorPickerInput/ColorPickerInput.scss b/src/components/ColorPickerInput/ColorPickerInput.scss deleted file mode 100644 index dbd6883958f..00000000000 --- a/src/components/ColorPickerInput/ColorPickerInput.scss +++ /dev/null @@ -1,20 +0,0 @@ -@use '../../variables.scss'; - -$block: '.#{variables.$ns}color-picker'; - -#{$block} { - &__preview { - width: 16px; - height: 16px; - margin-inline-start: var(--g-spacing-2); - margin-inline-end: var(--g-spacing-1); - border-radius: var(--g-border-radius-xs); - } - - &__input { - width: 100%; - height: 0; - opacity: 0; - border: 1px solid transparent; - } -} diff --git a/src/components/ColorPickerInput/ColorPickerInput.tsx b/src/components/ColorPickerInput/ColorPickerInput.tsx deleted file mode 100644 index d3dea9c30fd..00000000000 --- a/src/components/ColorPickerInput/ColorPickerInput.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import {Palette} from 'landing-icons'; -import {Button, Flex, Icon, TextInput, TextInputProps} from 'landing-uikit'; -import {useTranslation} from 'next-i18next'; -import React, {ChangeEventHandler, useCallback, useRef, useState} from 'react'; - -import {block} from '../../utils'; - -import './ColorPickerInput.scss'; -import {ColorPreview} from './ColorPreview'; -import {NativeColorPicker} from './NativeColorPicker'; -import {hexRegexp, parseRgbStringToHex, rgbRegexp, rgbaRegexp} from './utils'; - -const b = block('color-picker'); - -export interface ColorPickerInputProps { - defaultValue: string; - name?: string; - value?: string; - onChange?: (color: string) => void; - errorMessage?: string; -} - -export const ColorPickerInput = ({ - name, - value, - onChange: onChangeExternal, - defaultValue, - errorMessage, -}: ColorPickerInputProps) => { - const {t} = useTranslation('component'); - - const [color, setColor] = useState(defaultValue); - const [inputValue, setInputValue] = useState(defaultValue); - const [validationError, setValidationError] = useState(); - - const colorInputRef = useRef(null); - - const managedValue = value || inputValue; - - const onChange: ChangeEventHandler = useCallback( - (event) => { - const newValue = event.target.value.replaceAll(' ', ''); - onChangeExternal?.(newValue); - setInputValue(newValue); - setValidationError(undefined); - - if ( - !newValue || - new RegExp(hexRegexp, 'g').test(newValue) || - new RegExp(rgbaRegexp, 'g').test(newValue) - ) { - setColor(newValue); - - return; - } - - if (new RegExp(rgbRegexp, 'g').test(newValue)) { - const hexColor = parseRgbStringToHex(newValue); - - setColor(hexColor); - return; - } - }, - [onChangeExternal], - ); - - const onNativeInputChange: ChangeEventHandler = useCallback((e) => { - const newValue = e.target.value.toUpperCase(); - - setColor(newValue); - setInputValue(newValue); - }, []); - - const onBlur = useCallback(() => { - if ( - !managedValue || - (!new RegExp(hexRegexp, 'g').test(managedValue) && - !new RegExp(rgbRegexp, 'g').test(managedValue) && - !new RegExp(rgbaRegexp, 'g').test(managedValue)) - ) { - setValidationError('invalid'); - } - }, [managedValue]); - - return ( - - } - endContent={ - - } - onBlur={onBlur} - /> - - - ); -}; diff --git a/src/components/ColorPickerInput/ColorPreview.tsx b/src/components/ColorPickerInput/ColorPreview.tsx deleted file mode 100644 index 29b23540590..00000000000 --- a/src/components/ColorPickerInput/ColorPreview.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; - -import {block} from '../../utils'; - -import './ColorPickerInput.scss'; - -export interface ColorPreviewProps { - color: string; -} - -const b = block('color-picker__preview'); - -export const ColorPreview = ({color}: ColorPreviewProps) => { - return
; -}; diff --git a/src/components/ColorPickerInput/NativeColorPicker.tsx b/src/components/ColorPickerInput/NativeColorPicker.tsx deleted file mode 100644 index 2f30307dacc..00000000000 --- a/src/components/ColorPickerInput/NativeColorPicker.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React, {ChangeEventHandler, forwardRef} from 'react'; - -import {block} from '../../utils'; - -import './ColorPickerInput.scss'; - -export interface NativeColorPickerProps { - value: string; - onChange: ChangeEventHandler; -} - -const b = block('color-picker__input'); - -export const NativeColorPicker = forwardRef( - ({value, onChange}, ref) => { - return ; - }, -); diff --git a/src/components/ColorPickerInput/utils.ts b/src/components/ColorPickerInput/utils.ts deleted file mode 100644 index 7b44fa2fd1d..00000000000 --- a/src/components/ColorPickerInput/utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -export const hexRegexp = /^#[a-fA-F0-9]{6}$/; -export const rgbRegexp = /^rgb\((\d{1,3}, ?){2}(\d{1,3})\)$/; -export const rgbaRegexp = /^rgba\((\d{1,3}, ?){3}((0(,|\.)[0-9]{1,2})|1)\)$/; - -const numberRegexp = /\b\d+\b/g; - -export const parseRgbStringToHex = (rgbString: string) => { - let hexColor = '#'; - rgbString.match(new RegExp(numberRegexp, 'g'))?.forEach((val) => { - const hex = Number(val).toString(16); - - hexColor += hex?.length === 1 ? `0${hex}` : hex; - }); - - return hexColor; -}; diff --git a/src/components/Icons/IconDialog/UsageExample/UsageExample.scss b/src/components/Icons/IconDialog/UsageExample/UsageExample.scss index 177fd39ae79..f55d745ffa9 100644 --- a/src/components/Icons/IconDialog/UsageExample/UsageExample.scss +++ b/src/components/Icons/IconDialog/UsageExample/UsageExample.scss @@ -13,45 +13,4 @@ $block: '.#{variables.$ns}icon-usage-example'; margin-bottom: 12px; color: rgba(255, 255, 255, 0.7); } - - &__wrapper { - display: flex; - flex-direction: row; - width: 100%; - background: var(--g-color-base-background); - border-radius: 16px; - padding: 16px 48px 16px 16px; - position: relative; - - &:hover { - cursor: pointer; - - #{$block}__code { - color: #fff; - } - - #{$block}__copy-icon { - color: #fff; - } - } - } - - &__code { - @include pcStyles.text-size(code-2); - flex-grow: 1; - font-family: var(--g-font-family-monospace); - color: rgba(255, 255, 255, 0.7); - margin-right: 12px; - transition: color 0.1s ease-in-out; - - &_copied { - color: #fff; - } - } - - &__copy-button { - position: absolute; - right: 16px; - top: 16px; - } } diff --git a/src/components/Icons/IconDialog/UsageExample/UsageExample.tsx b/src/components/Icons/IconDialog/UsageExample/UsageExample.tsx index 3cf2c8ff4d8..ab9f9ce2fca 100644 --- a/src/components/Icons/IconDialog/UsageExample/UsageExample.tsx +++ b/src/components/Icons/IconDialog/UsageExample/UsageExample.tsx @@ -2,8 +2,7 @@ import {useTranslation} from 'next-i18next'; import React from 'react'; import {block} from '../../../../utils'; -import {ClipboardArea} from '../../../ClipboardArea/ClipboardArea'; -import {ClipboardIcon} from '../../../ClipboardIcon/ClipboardIcon'; +import {CodeExample} from '../../../CodeExample/CodeExample'; import type {IconItem} from '../../types'; import './UsageExample.scss'; @@ -29,29 +28,14 @@ export const UsageExample: React.FC = ({icon, variant}) => {
{variant === 'react' ? t('icons:usage_reactComponent') : t('icons:usage_svg')}
- - {(status) => ( -
-
- {importCode} -
-
- -
-
- )} -
+ />
); }; diff --git a/src/components/Libraries/Libraries.tsx b/src/components/Libraries/Libraries.tsx index 591a8245380..f97f31fe784 100644 --- a/src/components/Libraries/Libraries.tsx +++ b/src/components/Libraries/Libraries.tsx @@ -8,9 +8,9 @@ import starIcon from '../../assets/icons/star.svg'; import versionIcon from '../../assets/icons/version.svg'; import {block, getLibsList} from '../../utils'; import {Link} from '../Link'; +import {TagItem, Tags} from '../Tags/Tags'; import './Libraries.scss'; -import {TagItem, Tags} from './Tags/Tags'; const b = block('libraries'); diff --git a/src/components/Libraries/Tags/Tags.scss b/src/components/Libraries/Tags/Tags.scss deleted file mode 100644 index 6ec0c6b6d73..00000000000 --- a/src/components/Libraries/Tags/Tags.scss +++ /dev/null @@ -1,32 +0,0 @@ -@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; -@use '../../../variables.scss'; - -$block: '.#{variables.$ns}tags'; - -#{$block} { - display: flex; - overflow-x: auto; - - --g-scrollbar-width: 0; - - &__tag { - padding: 11px 24px; - margin-left: 8px; - font-size: 15px; - line-height: 20px; - font-weight: 400; - border-radius: 34px; - border: 1px solid rgba(255, 255, 255, 0.15); - cursor: pointer; - - &:first-child { - margin-left: 0; - } - - &_active { - color: #ffbe5c; - background: rgba(255, 190, 92, 0.1); - border: 1px solid transparent; - } - } -} diff --git a/src/components/Libraries/Tags/Tags.tsx b/src/components/Libraries/Tags/Tags.tsx deleted file mode 100644 index 74c19a98dc9..00000000000 --- a/src/components/Libraries/Tags/Tags.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; - -import {block} from '../../../utils'; - -import './Tags.scss'; - -const b = block('tags'); - -export type TagItem = { - title: string; - value: string; -}; - -type Props = { - value: string; - items: TagItem[]; - onChange: (newValue: string) => void; -}; - -export const Tags: React.FC = ({value, items, onChange}) => { - return ( -
- {items.map((item) => { - return ( -
{ - onChange(item.value); - }} - className={b('tag', {active: item.value === value})} - > - {item.title} -
- ); - })} -
- ); -}; diff --git a/src/components/SelectableCard/SelectableCard.scss b/src/components/SelectableCard/SelectableCard.scss new file mode 100644 index 00000000000..0f25b2429d4 --- /dev/null +++ b/src/components/SelectableCard/SelectableCard.scss @@ -0,0 +1,31 @@ +@use '../../variables.scss'; + +$block: '.#{variables.$ns}selectable-card'; + +#{$block} { + position: relative; + display: flex; + padding: 22px 12px; + height: 80px; + align-items: center; + justify-content: center; + cursor: pointer; + text-align: center; + + &__icon { + position: absolute; + top: 4px; + right: 4px; + color: var(--g-color-base-brand); + } + + &__fake-button { + width: 69px; + height: 28px; + background-color: var(--g-color-base-brand); + display: flex; + align-items: center; + justify-content: center; + user-select: none; + } +} diff --git a/src/components/SelectableCard/SelectableCard.tsx b/src/components/SelectableCard/SelectableCard.tsx new file mode 100644 index 00000000000..2954300d40a --- /dev/null +++ b/src/components/SelectableCard/SelectableCard.tsx @@ -0,0 +1,54 @@ +import {CircleCheckFill} from '@gravity-ui/icons'; +import {Card, type CardProps, DOMProps, Text, TextProps} from '@gravity-ui/uikit'; +import React from 'react'; + +import {block} from '../../utils'; + +import './SelectableCard.scss'; + +const b = block('selectable-card'); + +export type SelecableCardProps = { + /** + * Text to display inside + */ + text: string; + /** + * Flag to show only text without decoration + */ + pureText?: boolean; + /** + * Props for inner Text component + */ + textProps?: TextProps; +} & Pick & + Pick; + +const CardContent = ({ + text, + pureText, + textProps, +}: Pick) => { + const props: Record = pureText + ? {variant: 'body-2'} + : {color: 'inverted-primary', className: b('fake-button')}; + return ( + + {text} + + ); +}; + +export const SelectableCard = ({ + selected, + pureText, + text, + onClick, + className, + textProps, +}: SelecableCardProps) => ( + + + {selected && } + +); diff --git a/src/components/Tags/Tags.scss b/src/components/Tags/Tags.scss new file mode 100644 index 00000000000..edd586f3da4 --- /dev/null +++ b/src/components/Tags/Tags.scss @@ -0,0 +1,16 @@ +@use '../../../node_modules/@gravity-ui/page-constructor/styles/variables' as pcVariables; +@use '../../variables'; + +$block: '.#{variables.$ns}tags'; + +#{$block} { + --g-scrollbar-width: 0; + + overflow-x: auto; + + &__tag { + &_selected { + --g-color-base-selection: var(--g-color-base-warning-light); + } + } +} diff --git a/src/components/Tags/Tags.tsx b/src/components/Tags/Tags.tsx new file mode 100644 index 00000000000..1df68dda9e5 --- /dev/null +++ b/src/components/Tags/Tags.tsx @@ -0,0 +1,44 @@ +import {Button, Flex} from '@gravity-ui/uikit'; +import React from 'react'; + +import {block} from '../../utils'; + +import './Tags.scss'; + +const b = block('tags'); + +export type TagItem = { + title: string; + value: T; +}; + +interface TagsProps { + value: T; + items: TagItem[]; + onChange: (newValue: T) => void; + className?: string; +} + +export function Tags({value, items, onChange, className}: TagsProps) { + return ( + + {items.map((item) => { + return ( + + ); + })} + + ); +} diff --git a/src/components/Themes/Themes.scss b/src/components/Themes/Themes.scss new file mode 100644 index 00000000000..53f417921de --- /dev/null +++ b/src/components/Themes/Themes.scss @@ -0,0 +1,69 @@ +@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; +@use '~@gravity-ui/uikit/styles/mixins' as ukitMixins; +@use '../../variables.scss'; + +$block: '.#{variables.$ns}themes'; + +#{$block} { + margin-block-start: calc(var(--g-spacing-base) * 16); + + &__title { + margin-block-end: var(--g-spacing-6); + + &__text { + @include ukitMixins.text-display-4(); + + @media (max-width: map-get(pcVariables.$gridBreakpoints, 'sm')) { + @include ukitMixins.text-display-2(); + } + } + } + + &__header-actions { + margin-block-end: calc(var(--g-spacing-base) * 8); + justify-content: space-between; + + @media (max-width: map-get(pcVariables.$gridBreakpoints, 'md') - 1) { + flex-direction: column; + justify-content: flex-start; + } + } + + & &__tabs { + display: flex; + overflow: auto; + flex-wrap: wrap; + + @media (max-width: map-get(pcVariables.$gridBreakpoints, 'md') - 1) { + /* stylelint-disable */ + flex-wrap: nowrap !important; + margin: 0 -24px; + padding: 0 24px; + } + } + + &__export-theme-btn { + --g-button-border-radius: 8px; + width: fit-content; + + @media (max-width: map-get(pcVariables.$gridBreakpoints, 'md') - 1) { + margin-top: var(--g-spacing-6); + } + + @media (max-width: map-get(pcVariables.$gridBreakpoints, 'sm')) { + width: 100%; + } + } + + &__export-theme-btn { + border-radius: var(--g-border-radius-m); + } + + &__content { + padding: calc(var(--g-spacing-base) * 12) 0; + + @media (max-width: map-get(pcVariables.$gridBreakpoints, 'sm')) { + padding: calc(var(--g-spacing-base) * 6) 0; + } + } +} diff --git a/src/components/Themes/Themes.tsx b/src/components/Themes/Themes.tsx new file mode 100644 index 00000000000..84e2bcac275 --- /dev/null +++ b/src/components/Themes/Themes.tsx @@ -0,0 +1,102 @@ +import {Col, Grid, Row} from '@gravity-ui/page-constructor'; +import {ArrowUpFromSquare} from 'landing-icons'; +import {Button, Flex, Icon, Text} from 'landing-uikit'; +import {useTranslation} from 'next-i18next'; +import React, {useMemo, useState} from 'react'; + +import {block} from '../../utils'; +import {TagItem, Tags} from '../Tags/Tags'; + +import './Themes.scss'; +import {DEFAULT_THEME} from './lib/constants'; +import {BorderRadiusTab} from './ui/BorderRadiusTab/BorderRadiusTab'; +import {ColorsTab} from './ui/ColorsTab/ColorsTab'; +import {PreviewTab} from './ui/PreviewTab/PreviewTab'; +import {ThemeCreatorContextProvider} from './ui/ThemeCreatorContextProvider'; +import {ThemeExportDialog} from './ui/ThemeExportDialog/ThemeExportDialog'; +import {TypographyTab} from './ui/TypographyTab/TypographyTab'; + +const b = block('themes'); + +enum ThemeTab { + Colors = 'colors', + Typography = 'typography', + BorderRadius = 'borderRadius', + Preview = 'preview', +} + +const tabToComponent: Record = { + [ThemeTab.Colors]: ColorsTab, + [ThemeTab.Typography]: TypographyTab, + [ThemeTab.BorderRadius]: BorderRadiusTab, + [ThemeTab.Preview]: PreviewTab, +}; + +export const Themes = () => { + const {t} = useTranslation('themes'); + + const [isExportDialogVisible, toggleExportDialog] = React.useReducer( + (isOpen) => !isOpen, + false, + ); + + const tags: TagItem[] = useMemo( + () => [ + { + value: ThemeTab.Colors, + title: t('tags_colors'), + }, + { + value: ThemeTab.Typography, + title: t('tags_typography'), + }, + { + value: ThemeTab.BorderRadius, + title: t('tags_borderRadius'), + }, + { + value: ThemeTab.Preview, + title: t('tags_preview'), + }, + ], + [t], + ); + + const [activeTab, setActiveTab] = useState(ThemeTab.Colors); + + const TabComponent = tabToComponent[activeTab]; + + return ( + + + + + {t('title')} + + + + + + + + + {TabComponent ? : null} + + + + + ); +}; diff --git a/src/components/Themes/hooks/index.ts b/src/components/Themes/hooks/index.ts new file mode 100644 index 00000000000..e68e06b27c8 --- /dev/null +++ b/src/components/Themes/hooks/index.ts @@ -0,0 +1,4 @@ +export {useThemeCreator, useThemeCreatorMethods} from './useThemeCreator'; +export {useThemePalette, useThemePaletteColor} from './useThemePalette'; +export {useThemeUtilityColor} from './useThemeUtilityColor'; +export {useThemePrivateColorOptions} from './useThemePrivateColorOptions'; diff --git a/src/components/Themes/hooks/useThemeCreator.ts b/src/components/Themes/hooks/useThemeCreator.ts new file mode 100644 index 00000000000..c7e62a37b0c --- /dev/null +++ b/src/components/Themes/hooks/useThemeCreator.ts @@ -0,0 +1,6 @@ +import React from 'react'; + +import {ThemeCreatorContext, ThemeCreatorMethodsContext} from '../lib/themeCreatorContext'; + +export const useThemeCreator = () => React.useContext(ThemeCreatorContext); +export const useThemeCreatorMethods = () => React.useContext(ThemeCreatorMethodsContext); diff --git a/src/components/Themes/hooks/useThemePalette.ts b/src/components/Themes/hooks/useThemePalette.ts new file mode 100644 index 00000000000..71ffd953664 --- /dev/null +++ b/src/components/Themes/hooks/useThemePalette.ts @@ -0,0 +1,36 @@ +import React from 'react'; + +import {getThemePalette} from '../lib/themeCreatorUtils'; +import type {ThemeVariant} from '../lib/types'; + +import {useThemeCreator, useThemeCreatorMethods} from './useThemeCreator'; + +export const useThemePalette = () => { + const themeState = useThemeCreator(); + return React.useMemo(() => getThemePalette(themeState), [themeState]); +}; + +type UseThemePaletteColorParams = { + token: string; + theme: ThemeVariant; +}; + +export const useThemePaletteColor = ({token, theme}: UseThemePaletteColorParams) => { + const themeState = useThemeCreator(); + const {updateColor} = useThemeCreatorMethods(); + + const value = React.useMemo(() => themeState.palette[theme][token], [themeState, token, theme]); + + const updateValue = React.useCallback( + (newValue: string) => { + updateColor({ + theme, + title: token, + value: newValue, + }); + }, + [token, theme, updateColor], + ); + + return [value, updateValue] as const; +}; diff --git a/src/components/Themes/hooks/useThemePrivateColorOptions.ts b/src/components/Themes/hooks/useThemePrivateColorOptions.ts new file mode 100644 index 00000000000..6aa706b024c --- /dev/null +++ b/src/components/Themes/hooks/useThemePrivateColorOptions.ts @@ -0,0 +1,15 @@ +import React from 'react'; + +import {getThemeColorOptions} from '../lib/themeCreatorUtils'; +import {ThemeVariant} from '../lib/types'; + +import {useThemeCreator} from './useThemeCreator'; + +export const useThemePrivateColorOptions = (themeVariant: ThemeVariant) => { + const themeState = useThemeCreator(); + + return React.useMemo( + () => getThemeColorOptions({themeState, themeVariant}), + [themeState, themeVariant], + ); +}; diff --git a/src/components/Themes/hooks/useThemeUtilityColor.ts b/src/components/Themes/hooks/useThemeUtilityColor.ts new file mode 100644 index 00000000000..2160797535e --- /dev/null +++ b/src/components/Themes/hooks/useThemeUtilityColor.ts @@ -0,0 +1,30 @@ +import React from 'react'; + +import type {ColorsOptions, ThemeVariant} from '../lib/types'; + +import {useThemeCreator, useThemeCreatorMethods} from './useThemeCreator'; + +type UseThemeColorParams = { + name: keyof ColorsOptions; + theme: ThemeVariant; +}; + +export const useThemeUtilityColor = ({name, theme}: UseThemeColorParams) => { + const themeState = useThemeCreator(); + const {changeUtilityColor} = useThemeCreatorMethods(); + + const value = React.useMemo(() => themeState.colors[theme][name], [themeState, name, theme]); + + const updateValue = React.useCallback( + (newValue: string) => { + changeUtilityColor({ + themeVariant: theme, + name, + value: newValue, + }); + }, + [name, theme, changeUtilityColor], + ); + + return [value, updateValue] as const; +}; diff --git a/src/components/Themes/lib/constants.ts b/src/components/Themes/lib/constants.ts new file mode 100644 index 00000000000..86b28a1aefb --- /dev/null +++ b/src/components/Themes/lib/constants.ts @@ -0,0 +1,333 @@ +import {RadiusPresetName, RadiusValue, type ThemeOptions, type ThemeVariant} from './types'; +import {defaultTypographyPreset} from './typography/constants'; + +export const THEME_COLOR_VARIABLE_PREFIX = '--g-color'; + +export const THEME_BORDER_RADIUS_VARIABLE_PREFIX = '--g-border-radius'; + +export const DEFAULT_NEW_COLOR_TITLE = 'New color'; + +export const DEFAULT_BRAND_COLORS = [ + 'rgb(203,255,92)', + 'rgb(0,41,255)', + 'rgb(49,78,60)', + 'rgb(108,145,201)', + 'rgb(255,190,92)', + 'rgb(255,92,92)', +] as const; + +export const TEXT_CONTRAST_COLORS: Record = { + dark: { + white: 'rgb(255, 255, 255)', + black: 'rgba(0, 0, 0, 0.9)', // --g-color-private-black-900 + }, + light: { + white: 'rgb(255, 255, 255)', + black: 'rgba(0, 0, 0, 0.85)', // --g-color-private-black-850 + }, +}; + +export const DEFAULT_PALETTE: ThemeOptions['palette'] = { + light: { + white: 'rgb(255, 255, 255)', + black: 'rgb(0, 0, 0)', + brand: DEFAULT_BRAND_COLORS[0], + orange: 'rgb(255, 119, 0)', + green: 'rgb(59, 201, 53)', + yellow: 'rgb(255, 219, 77)', + red: 'rgb(255, 4, 0)', + blue: 'rgb(82, 130, 255)', + 'cool-grey': 'rgb(107, 132, 153)', + purple: 'rgb(143, 82, 204)', + }, + dark: { + white: 'rgb(255, 255, 255)', + black: 'rgb(0, 0, 0)', + brand: DEFAULT_BRAND_COLORS[0], + orange: 'rgb(200, 99, 12)', + green: 'rgb(91, 181, 87)', + yellow: 'rgb(255, 203, 0)', + red: 'rgb(232, 73, 69)', + blue: 'rgb(82, 130, 255)', + 'cool-grey': 'rgb(96, 128, 156)', + purple: 'rgb(143, 82, 204)', + }, +}; + +export const DEFAULT_PALETTE_TOKENS = new Set(Object.keys(DEFAULT_PALETTE.light)); + +export const DEFAULT_RADIUS: RadiusValue = { + xs: '3', + s: '5', + m: '6', + l: '8', + xl: '10', +}; + +export const RADIUS_PRESETS: Record = { + [RadiusPresetName.Regular]: DEFAULT_RADIUS, + [RadiusPresetName.Circled]: { + xs: '10', + s: '12', + m: '14', + l: '18', + xl: '22', + }, + [RadiusPresetName.Squared]: { + xs: '0', + s: '0', + m: '0', + l: '0', + xl: '0', + }, + [RadiusPresetName.Custom]: DEFAULT_RADIUS, +}; + +// Default colors mappings (values from gravity-ui styles) +// https://github.com/gravity-ui/uikit/tree/main/styles/themes +export const DEFAULT_COLORS: ThemeOptions['colors'] = { + light: { + 'base-background': 'rgb(255,255,255)', + 'base-brand-hover': 'private.brand.600-solid', + 'base-selection': 'private.brand.200', + 'base-selection-hover': 'private.brand.300', + 'line-brand': 'private.brand.600-solid', + 'text-brand': 'private.brand.700-solid', + 'text-brand-heavy': 'private.brand.700-solid', + 'text-brand-contrast': TEXT_CONTRAST_COLORS.light.black, + 'text-link': 'private.brand.600-solid', + 'text-link-hover': 'private.orange.800-solid', + 'text-link-visited': 'private.purple.550-solid', + 'text-link-visited-hover': 'private.purple.800-solid', + }, + dark: { + 'base-background': 'rgb(34,29,34)', + 'base-brand-hover': 'private.brand.650-solid', + 'base-selection': 'private.brand.150', + 'base-selection-hover': 'private.brand.200', + 'line-brand': 'private.brand.600-solid', + 'text-brand': 'private.brand.600-solid', + 'text-brand-heavy': 'private.brand.700-solid', + 'text-brand-contrast': TEXT_CONTRAST_COLORS.dark.black, + 'text-link': 'private.brand.550-solid', + 'text-link-hover': 'private.brand.700-solid', + 'text-link-visited': 'private.purple.700-solid', + 'text-link-visited-hover': 'private.purple.850-solid', + }, +}; + +export const DEFAULT_THEME: ThemeOptions = { + palette: DEFAULT_PALETTE, + colors: DEFAULT_COLORS, + borders: { + preset: RadiusPresetName.Regular, + values: RADIUS_PRESETS[RadiusPresetName.Regular], + }, + typography: defaultTypographyPreset, +}; + +export type BrandPreset = { + brandColor: typeof DEFAULT_BRAND_COLORS[number]; + colors: ThemeOptions['colors']; +}; + +export const BRAND_COLORS_PRESETS: BrandPreset[] = [ + { + brandColor: 'rgb(203,255,92)', + colors: { + light: { + 'base-background': 'rgb(255,255,255)', + 'base-brand-hover': 'private.brand.600-solid', + 'base-selection': 'private.brand.200', + 'base-selection-hover': 'private.brand.300', + 'line-brand': 'private.brand.600-solid', + 'text-brand': 'private.brand.700-solid', + 'text-brand-heavy': 'private.brand.700-solid', + 'text-brand-contrast': TEXT_CONTRAST_COLORS.light.black, + 'text-link': 'private.brand.600-solid', + 'text-link-hover': 'private.orange.800-solid', + 'text-link-visited': 'private.purple.550-solid', + 'text-link-visited-hover': 'private.purple.800-solid', + }, + dark: { + 'base-background': 'rgb(34,29,34)', + 'base-brand-hover': 'private.brand.650-solid', + 'base-selection': 'private.brand.150', + 'base-selection-hover': 'private.brand.200', + 'line-brand': 'private.brand.600-solid', + 'text-brand': 'private.brand.600-solid', + 'text-brand-heavy': 'private.brand.700-solid', + 'text-brand-contrast': TEXT_CONTRAST_COLORS.dark.black, + 'text-link': 'private.brand.550-solid', + 'text-link-hover': 'private.brand.700-solid', + 'text-link-visited': 'private.purple.700-solid', + 'text-link-visited-hover': 'private.purple.850-solid', + }, + }, + }, + { + brandColor: 'rgb(0,41,255)', + colors: { + light: { + 'base-background': 'rgb(255,255,255)', + 'base-brand-hover': 'private.brand.600-solid', + 'base-selection': 'private.brand.200', + 'base-selection-hover': 'private.brand.300', + 'line-brand': 'private.brand.600-solid', + 'text-brand': 'private.brand.700-solid', + 'text-brand-heavy': 'private.brand.700-solid', + 'text-brand-contrast': TEXT_CONTRAST_COLORS.light.white, + 'text-link': 'private.brand.600-solid', + 'text-link-hover': 'private.orange.800-solid', + 'text-link-visited': 'private.purple.550-solid', + 'text-link-visited-hover': 'private.purple.800-solid', + }, + dark: { + 'base-background': 'rgb(34,29,34)', + 'base-brand-hover': 'private.brand.650-solid', + 'base-selection': 'private.brand.150', + 'base-selection-hover': 'private.brand.200', + 'line-brand': 'private.brand.600-solid', + 'text-brand': 'private.brand.600-solid', + 'text-brand-heavy': 'private.brand.700-solid', + 'text-brand-contrast': TEXT_CONTRAST_COLORS.dark.white, + 'text-link': 'private.brand.550-solid', + 'text-link-hover': 'private.brand.700-solid', + 'text-link-visited': 'private.purple.700-solid', + 'text-link-visited-hover': 'private.purple.850-solid', + }, + }, + }, + { + brandColor: 'rgb(49,78,60)', + colors: { + light: { + 'base-background': 'rgb(255,255,255)', + 'base-brand-hover': 'private.brand.600-solid', + 'base-selection': 'private.brand.200', + 'base-selection-hover': 'private.brand.300', + 'line-brand': 'private.brand.600-solid', + 'text-brand': 'private.brand.700-solid', + 'text-brand-heavy': 'private.brand.700-solid', + 'text-brand-contrast': TEXT_CONTRAST_COLORS.light.white, + 'text-link': 'private.brand.600-solid', + 'text-link-hover': 'private.orange.800-solid', + 'text-link-visited': 'private.purple.550-solid', + 'text-link-visited-hover': 'private.purple.800-solid', + }, + dark: { + 'base-background': 'rgb(34,29,34)', + 'base-brand-hover': 'private.brand.650-solid', + 'base-selection': 'private.brand.150', + 'base-selection-hover': 'private.brand.200', + 'line-brand': 'private.brand.600-solid', + 'text-brand': 'private.brand.600-solid', + 'text-brand-heavy': 'private.brand.700-solid', + 'text-brand-contrast': TEXT_CONTRAST_COLORS.dark.white, + 'text-link': 'private.brand.550-solid', + 'text-link-hover': 'private.brand.700-solid', + 'text-link-visited': 'private.purple.700-solid', + 'text-link-visited-hover': 'private.purple.850-solid', + }, + }, + }, + { + brandColor: 'rgb(108,145,201)', + colors: { + light: { + 'base-background': 'rgb(255,255,255)', + 'base-brand-hover': 'private.brand.600-solid', + 'base-selection': 'private.brand.200', + 'base-selection-hover': 'private.brand.300', + 'line-brand': 'private.brand.600-solid', + 'text-brand': 'private.brand.700-solid', + 'text-brand-heavy': 'private.brand.700-solid', + 'text-brand-contrast': TEXT_CONTRAST_COLORS.light.white, + 'text-link': 'private.brand.600-solid', + 'text-link-hover': 'private.orange.800-solid', + 'text-link-visited': 'private.purple.550-solid', + 'text-link-visited-hover': 'private.purple.800-solid', + }, + dark: { + 'base-background': 'rgb(34,29,34)', + 'base-brand-hover': 'private.brand.650-solid', + 'base-selection': 'private.brand.150', + 'base-selection-hover': 'private.brand.200', + 'line-brand': 'private.brand.600-solid', + 'text-brand': 'private.brand.600-solid', + 'text-brand-heavy': 'private.brand.700-solid', + 'text-brand-contrast': TEXT_CONTRAST_COLORS.dark.white, + 'text-link': 'private.brand.550-solid', + 'text-link-hover': 'private.brand.700-solid', + 'text-link-visited': 'private.purple.700-solid', + 'text-link-visited-hover': 'private.purple.850-solid', + }, + }, + }, + { + brandColor: 'rgb(255,190,92)', + colors: { + light: { + 'base-background': 'rgb(255,255,255)', + 'base-brand-hover': 'private.brand.600-solid', + 'base-selection': 'private.brand.200', + 'base-selection-hover': 'private.brand.300', + 'line-brand': 'private.brand.600-solid', + 'text-brand': 'private.brand.700-solid', + 'text-brand-heavy': 'private.brand.700-solid', + 'text-brand-contrast': TEXT_CONTRAST_COLORS.light.black, + 'text-link': 'private.brand.600-solid', + 'text-link-hover': 'private.orange.800-solid', + 'text-link-visited': 'private.purple.550-solid', + 'text-link-visited-hover': 'private.purple.800-solid', + }, + dark: { + 'base-background': 'rgb(34,29,34)', + 'base-brand-hover': 'private.brand.650-solid', + 'base-selection': 'private.brand.150', + 'base-selection-hover': 'private.brand.200', + 'line-brand': 'private.brand.600-solid', + 'text-brand': 'private.brand.600-solid', + 'text-brand-heavy': 'private.brand.700-solid', + 'text-brand-contrast': TEXT_CONTRAST_COLORS.dark.black, + 'text-link': 'private.brand.550-solid', + 'text-link-hover': 'private.brand.700-solid', + 'text-link-visited': 'private.purple.700-solid', + 'text-link-visited-hover': 'private.purple.850-solid', + }, + }, + }, + { + brandColor: 'rgb(255,92,92)', + colors: { + light: { + 'base-background': 'rgb(255,255,255)', + 'base-brand-hover': 'private.brand.600-solid', + 'base-selection': 'private.brand.200', + 'base-selection-hover': 'private.brand.300', + 'line-brand': 'private.brand.600-solid', + 'text-brand': 'private.brand.700-solid', + 'text-brand-heavy': 'private.brand.700-solid', + 'text-brand-contrast': TEXT_CONTRAST_COLORS.light.white, + 'text-link': 'private.brand.600-solid', + 'text-link-hover': 'private.orange.800-solid', + 'text-link-visited': 'private.purple.550-solid', + 'text-link-visited-hover': 'private.purple.800-solid', + }, + dark: { + 'base-background': 'rgb(34,29,34)', + 'base-brand-hover': 'private.brand.650-solid', + 'base-selection': 'private.brand.150', + 'base-selection-hover': 'private.brand.200', + 'line-brand': 'private.brand.600-solid', + 'text-brand': 'private.brand.600-solid', + 'text-brand-heavy': 'private.brand.700-solid', + 'text-brand-contrast': TEXT_CONTRAST_COLORS.dark.white, + 'text-link': 'private.brand.550-solid', + 'text-link-hover': 'private.brand.700-solid', + 'text-link-visited': 'private.purple.700-solid', + 'text-link-visited-hover': 'private.purple.850-solid', + }, + }, + }, +]; diff --git a/src/components/Themes/lib/privateColors/constants.ts b/src/components/Themes/lib/privateColors/constants.ts new file mode 100644 index 00000000000..16569307cb0 --- /dev/null +++ b/src/components/Themes/lib/privateColors/constants.ts @@ -0,0 +1,289 @@ +export const themeXd = { + light: { + white: { + privateSolidVariables: [], + privateVariables: [ + 950, 900, 850, 800, 750, 700, 650, 600, 550, 500, 450, 400, 350, 300, 250, 200, 150, + 100, 70, 50, + ], + colorsMap: { + 50: {a: 0.05, c: 1}, + 70: {a: 0.07, c: 1}, + 100: {a: 0.1, c: 1}, + 150: {a: 0.15, c: 1}, + 200: {a: 0.2, c: 1}, + 250: {a: 0.25, c: 1}, + 300: {a: 0.3, c: 1}, + 350: {a: 0.35, c: 1}, + 400: {a: 0.4, c: 1}, + 450: {a: 0.45, c: 1}, + 500: {a: 0.5, c: 1}, + 550: {a: 0.55, c: 1}, + 600: {a: 0.6, c: 1}, + 650: {a: 0.65, c: 1}, + 700: {a: 0.7, c: 1}, + 750: {a: 0.75, c: 1}, + 800: {a: 0.8, c: 1}, + 850: {a: 0.85, c: 1}, + 900: {a: 0.9, c: 1}, + 950: {a: 0.95, c: 1}, + 1000: {a: 1, c: 1}, + }, + }, + black: { + privateSolidVariables: [ + 950, 900, 850, 800, 750, 700, 650, 600, 550, 500, 450, 400, 350, 300, 250, 200, 150, + 100, 50, + ], + privateVariables: [ + 950, 900, 850, 800, 750, 700, 650, 600, 550, 500, 450, 400, 350, 300, 250, 200, 150, + 100, 50, + ], + colorsMap: { + 50: {a: 0.05, c: -1}, + 70: {a: 0.07, c: -1}, + 100: {a: 0.1, c: -1}, + 150: {a: 0.15, c: -1}, + 200: {a: 0.2, c: -1}, + 250: {a: 0.25, c: -1}, + 300: {a: 0.3, c: -1}, + 350: {a: 0.35, c: -1}, + 400: {a: 0.4, c: -1}, + 450: {a: 0.45, c: -1}, + 500: {a: 0.5, c: -1}, + 550: {a: 0.55, c: -1}, + 600: {a: 0.6, c: -1}, + 650: {a: 0.65, c: -1}, + 700: {a: 0.7, c: -1}, + 750: {a: 0.75, c: -1}, + 800: {a: 0.8, c: -1}, + 850: {a: 0.85, c: -1}, + 900: {a: 0.9, c: -1}, + 950: {a: 0.95, c: -1}, + 1000: {a: 1, c: -1}, + }, + }, + }, + dark: { + white: { + privateSolidVariables: [ + 950, 900, 850, 800, 750, 700, 650, 600, 550, 500, 450, 400, 350, 300, 250, 200, 150, + 100, 70, 50, + ], + privateVariables: [ + 950, 900, 850, 800, 750, 700, 650, 600, 550, 500, 450, 400, 350, 300, 250, 200, 150, + 100, 70, 50, + ], + colorsMap: { + 50: {a: 0.05, c: -1}, + 70: {a: 0.07, c: -1}, + 100: {a: 0.1, c: -1}, + 150: {a: 0.15, c: -1}, + 200: {a: 0.2, c: -1}, + 250: {a: 0.25, c: -1}, + 300: {a: 0.3, c: -1}, + 350: {a: 0.35, c: -1}, + 400: {a: 0.4, c: -1}, + 450: {a: 0.45, c: -1}, + 500: {a: 0.5, c: -1}, + 550: {a: 0.55, c: -1}, + 600: {a: 0.6, c: -1}, + 650: {a: 0.65, c: -1}, + 700: {a: 0.7, c: -1}, + 750: {a: 0.75, c: -1}, + 800: {a: 0.8, c: -1}, + 850: {a: 0.85, c: -1}, + 900: {a: 0.9, c: -1}, + 950: {a: 0.95, c: -1}, + 1000: {a: 1, c: -1}, + }, + }, + black: { + privateSolidVariables: [], + privateVariables: [ + 950, 900, 850, 800, 750, 700, 650, 600, 550, 500, 450, 400, 350, 300, 250, 200, 150, + 100, 50, 20, + ], + colorsMap: { + 20: {a: 0.02, c: 1}, + 50: {a: 0.05, c: 1}, + 100: {a: 0.1, c: 1}, + 150: {a: 0.15, c: 1}, + 200: {a: 0.2, c: 1}, + 250: {a: 0.25, c: 1}, + 300: {a: 0.3, c: 1}, + 350: {a: 0.35, c: 1}, + 400: {a: 0.4, c: 1}, + 450: {a: 0.45, c: 1}, + 500: {a: 0.5, c: 1}, + 550: {a: 0.55, c: 1}, + 600: {a: 0.6, c: 1}, + 650: {a: 0.65, c: 1}, + 700: {a: 0.7, c: 1}, + 750: {a: 0.75, c: 1}, + 800: {a: 0.8, c: 1}, + 850: {a: 0.85, c: 1}, + 900: {a: 0.9, c: 1}, + 950: {a: 0.95, c: 1}, + 1000: {a: 1, c: 1}, + }, + }, + 'White Opaque': { + privateVariables: [150], + colorsMap: { + 50: {a: 0.05, c: 1}, + 70: {a: 0.07, c: 1}, + 100: {a: 0.1, c: 1}, + 150: {a: 0.15, c: 1}, + 200: {a: 0.2, c: 1}, + 250: {a: 0.25, c: 1}, + 300: {a: 0.3, c: 1}, + 350: {a: 0.35, c: 1}, + 400: {a: 0.4, c: 1}, + 450: {a: 0.45, c: 1}, + 500: {a: 0.5, c: 1}, + 550: {a: 0.55, c: 1}, + 600: {a: 0.6, c: 1}, + 650: {a: 0.65, c: 1}, + 700: {a: 0.7, c: 1}, + 750: {a: 0.75, c: 1}, + 800: {a: 0.8, c: 1}, + 850: {a: 0.85, c: 1}, + 900: {a: 0.9, c: 1}, + 950: {a: 0.95, c: 1}, + 1000: {a: 1, c: 1}, + }, + }, + }, + 'light-hc': { + white: { + privateSolidVariables: [], + privateVariables: [ + 950, 900, 850, 800, 750, 700, 650, 600, 550, 500, 450, 400, 350, 300, 250, 200, 150, + 100, 70, 50, + ], + colorsMap: { + 50: {a: 0.05, c: 1}, + 70: {a: 0.07, c: 1}, + 100: {a: 0.1, c: 1}, + 150: {a: 0.15, c: 1}, + 200: {a: 0.2, c: 1}, + 250: {a: 0.25, c: 1}, + 300: {a: 0.3, c: 1}, + 350: {a: 0.35, c: 1}, + 400: {a: 0.4, c: 1}, + 450: {a: 0.45, c: 1}, + 500: {a: 0.5, c: 1}, + 550: {a: 0.55, c: 1}, + 600: {a: 0.6, c: 1}, + 650: {a: 0.65, c: 1}, + 700: {a: 0.7, c: 1}, + 750: {a: 0.75, c: 1}, + 800: {a: 0.8, c: 1}, + 850: {a: 0.85, c: 1}, + 900: {a: 0.9, c: 1}, + 950: {a: 0.95, c: 1}, + 1000: {a: 1, c: 1}, + }, + }, + Black: { + privateSolidVariables: [ + 950, 900, 850, 800, 750, 700, 650, 600, 550, 500, 450, 400, 350, 300, 250, 200, 150, + 100, 50, 20, + ], + privateVariables: [ + 950, 900, 850, 800, 750, 700, 650, 600, 550, 500, 450, 400, 350, 300, 250, 200, 150, + 100, 70, 50, + ], + colorsMap: { + 20: {a: 0.02, c: 1}, + 50: {a: 0.05, c: 1}, + 70: {a: 0.07, c: 1}, + 100: {a: 0.1, c: 1}, + 150: {a: 0.15, c: 1}, + 200: {a: 0.2, c: 1}, + 250: {a: 0.25, c: 1}, + 300: {a: 0.3, c: 1}, + 350: {a: 0.35, c: 1}, + 400: {a: 0.4, c: 1}, + 450: {a: 0.45, c: 1}, + 500: {a: 0.5, c: 1}, + 550: {a: 0.55, c: 1}, + 600: {a: 0.6, c: 1}, + 650: {a: 0.65, c: 1}, + 700: {a: 0.7, c: 1}, + 750: {a: 0.75, c: 1}, + 800: {a: 0.8, c: 1}, + 850: {a: 0.85, c: 1}, + 900: {a: 0.9, c: 1}, + 950: {a: 0.95, c: 1}, + 1000: {a: 1, c: 1}, + }, + }, + }, + 'dark-hc': { + white: { + privateSolidVariables: [ + 950, 900, 850, 800, 750, 700, 650, 600, 550, 500, 450, 400, 350, 300, 250, 200, 150, + 100, 70, 50, + ], + privateVariables: [ + 950, 900, 850, 800, 750, 700, 650, 600, 550, 500, 450, 400, 350, 300, 250, 200, 150, + 100, 70, 50, + ], + colorsMap: { + 50: {a: 0.05, c: 1}, + 70: {a: 0.07, c: 1}, + 100: {a: 0.1, c: 1}, + 150: {a: 0.15, c: 1}, + 200: {a: 0.2, c: 1}, + 250: {a: 0.25, c: 1}, + 300: {a: 0.3, c: 1}, + 350: {a: 0.35, c: 1}, + 400: {a: 0.4, c: 1}, + 450: {a: 0.45, c: 1}, + 500: {a: 0.5, c: 1}, + 550: {a: 0.55, c: 1}, + 600: {a: 0.6, c: 1}, + 650: {a: 0.65, c: 1}, + 700: {a: 0.7, c: 1}, + 750: {a: 0.75, c: 1}, + 800: {a: 0.8, c: 1}, + 850: {a: 0.85, c: 1}, + 900: {a: 0.9, c: 1}, + 950: {a: 0.95, c: 1}, + 1000: {a: 1, c: 1}, + }, + }, + black: { + privateSolidVariables: [], + privateVariables: [ + 950, 900, 850, 800, 750, 700, 650, 600, 550, 500, 450, 400, 350, 300, 250, 200, 150, + 100, 50, 20, + ], + colorsMap: { + 20: {a: 0.02, c: 1}, + 50: {a: 0.05, c: 1}, + 100: {a: 0.1, c: 1}, + 150: {a: 0.15, c: 1}, + 200: {a: 0.2, c: 1}, + 250: {a: 0.25, c: 1}, + 300: {a: 0.3, c: 1}, + 350: {a: 0.35, c: 1}, + 400: {a: 0.4, c: 1}, + 450: {a: 0.45, c: 1}, + 500: {a: 0.5, c: 1}, + 550: {a: 0.55, c: 1}, + 600: {a: 0.6, c: 1}, + 650: {a: 0.65, c: 1}, + 700: {a: 0.7, c: 1}, + 750: {a: 0.75, c: 1}, + 800: {a: 0.8, c: 1}, + 850: {a: 0.85, c: 1}, + 900: {a: 0.9, c: 1}, + 950: {a: 0.95, c: 1}, + 1000: {a: 1, c: 1}, + }, + }, + }, +}; diff --git a/src/components/Themes/lib/privateColors/index.ts b/src/components/Themes/lib/privateColors/index.ts new file mode 100644 index 00000000000..af32a8c8b0d --- /dev/null +++ b/src/components/Themes/lib/privateColors/index.ts @@ -0,0 +1 @@ +export {generatePrivateColors} from './utils'; diff --git a/src/components/Themes/lib/privateColors/utils.ts b/src/components/Themes/lib/privateColors/utils.ts new file mode 100644 index 00000000000..0d27b14c263 --- /dev/null +++ b/src/components/Themes/lib/privateColors/utils.ts @@ -0,0 +1,98 @@ +import chroma from 'chroma-js'; + +import {themeXd} from './constants'; + +const privateSolidVariables = [ + 1000, 950, 900, 850, 800, 750, 700, 650, 600, 500, 450, 400, 350, 300, 250, 200, 150, 100, 50, +]; +const privateVariables = [500, 450, 400, 350, 300, 250, 200, 150, 100, 50]; +const colorsMap = { + 50: {a: 0.1, c: -1}, + 100: {a: 0.15, c: -1}, + 150: {a: 0.2, c: -1}, + 200: {a: 0.3, c: -1}, + 250: {a: 0.4, c: -1}, + 300: {a: 0.5, c: -1}, + 350: {a: 0.6, c: -1}, + 400: {a: 0.7, c: -1}, + 450: {a: 0.8, c: -1}, + 500: {a: 0.9, c: -1}, + 550: {a: 1, c: 1}, + 600: {a: 0.9, c: 1}, + 650: {a: 0.8, c: 1}, + 700: {a: 0.7, c: 1}, + 750: {a: 0.6, c: 1}, + 800: {a: 0.5, c: 1}, + 850: {a: 0.4, c: 1}, + 900: {a: 0.3, c: 1}, + 950: {a: 0.2, c: 1}, + 1000: {a: 0.15, c: 1}, +}; + +type Theme = 'light' | 'dark'; + +type GeneratePrivateColorsArgs = { + theme: Theme; + colorToken: string; + colorValue: string; + lightBg: string; + darkBg: string; +}; + +export const generatePrivateColors = ({ + theme, + colorToken, + colorValue, + lightBg, + darkBg, +}: GeneratePrivateColorsArgs) => { + const privateColors: Record = {}; + + if (!chroma.valid(colorValue)) { + throw Error('Not valid color for chroma'); + } + + let colorsMapInternal = colorsMap; + + if (colorToken === 'white' || colorToken === 'black') { + colorsMapInternal = themeXd[theme][colorToken].colorsMap; + } + + const pallete = Object.entries(colorsMapInternal).reduce((res, [key, {a, c}]) => { + const solidColor = chroma.mix(colorValue, c > 0 ? darkBg : lightBg, 1 - a, 'rgb').css(); + + const alphaColor = chroma(colorValue).alpha(a).css(); + + res[key] = [solidColor, alphaColor]; + + return res; + }, {} as Record); + + let privateSolidVariablesInternal = privateSolidVariables; + let privateVariablesInternal = privateVariables; + + if (colorToken === 'white' || colorToken === 'black') { + privateSolidVariablesInternal = themeXd[theme][colorToken].privateSolidVariables; + privateVariablesInternal = themeXd[theme][colorToken].privateVariables; + } + + // Set 550 Solid Color + privateColors['550-solid'] = chroma(colorValue).css(); + + // Set 50-1000 Solid Colors, except 550 Solid Color + privateSolidVariablesInternal.forEach((varName) => { + privateColors[`${varName}-solid`] = chroma(pallete[varName][0]).css(); + }); + + // Set 50-500 Colors + privateVariablesInternal.forEach((varName) => { + privateColors[`${varName}`] = chroma(pallete[varName][1]).css(); + }); + + if (theme === 'dark' && colorToken === 'white') { + const updatedColor = chroma(pallete[150][0]).alpha(0.95).css(); + privateColors['opaque-150'] = chroma(updatedColor).css(); + } + + return privateColors; +}; diff --git a/src/components/Themes/lib/themeCreatorContext.ts b/src/components/Themes/lib/themeCreatorContext.ts new file mode 100644 index 00000000000..41cb161d590 --- /dev/null +++ b/src/components/Themes/lib/themeCreatorContext.ts @@ -0,0 +1,60 @@ +import noop from 'lodash/noop'; +import {createContext} from 'react'; + +import {BrandPreset, DEFAULT_THEME} from './constants'; +import {initThemeCreator} from './themeCreatorUtils'; +import type { + AddColorToThemeParams, + AddFontFamilyTypeParams, + ChangeRadiusPresetInThemeParams, + ChangeUtilityColorInThemeParams, + RenameColorInThemeParams, + UpdateAdvancedTypographySettingsParams, + UpdateColorInThemeParams, + UpdateCustomRadiusPresetInThemeParams, + UpdateFontFamilyParams, + UpdateFontFamilyTypeTitleParams, +} from './themeCreatorUtils'; +import type {ThemeCreatorState} from './types'; + +export const ThemeCreatorContext = createContext( + initThemeCreator(DEFAULT_THEME), +); + +export interface ThemeCreatorMethodsContextType { + addColor: (params?: AddColorToThemeParams) => void; + updateColor: (params: UpdateColorInThemeParams) => void; + removeColor: (title: string) => void; + renameColor: (params: RenameColorInThemeParams) => void; + changeUtilityColor: (params: ChangeUtilityColorInThemeParams) => void; + applyBrandPreset: (preset: BrandPreset) => void; + changeRadiusPreset: (params: ChangeRadiusPresetInThemeParams) => void; + updateCustomRadiusPreset: (params: UpdateCustomRadiusPresetInThemeParams) => void; + updateFontFamily: (params: UpdateFontFamilyParams) => void; + addFontFamilyType: (params: AddFontFamilyTypeParams) => void; + updateFontFamilyTypeTitle: (params: UpdateFontFamilyTypeTitleParams) => void; + removeFontFamilyType: ({fontType}: {fontType: string}) => void; + updateAdvancedTypographySettings: (params: UpdateAdvancedTypographySettingsParams) => void; + updateAdvancedTypography: () => void; + openMainSettings: () => void; + setAdvancedMode: (enabled: boolean) => void; +} + +export const ThemeCreatorMethodsContext = createContext({ + addColor: noop, + updateColor: noop, + removeColor: noop, + renameColor: noop, + changeUtilityColor: noop, + applyBrandPreset: noop, + changeRadiusPreset: noop, + updateCustomRadiusPreset: noop, + updateFontFamily: noop, + addFontFamilyType: noop, + updateFontFamilyTypeTitle: noop, + removeFontFamilyType: noop, + updateAdvancedTypographySettings: noop, + updateAdvancedTypography: noop, + openMainSettings: noop, + setAdvancedMode: noop, +}); diff --git a/src/components/Themes/lib/themeCreatorExport.ts b/src/components/Themes/lib/themeCreatorExport.ts new file mode 100644 index 00000000000..27f3c21be1a --- /dev/null +++ b/src/components/Themes/lib/themeCreatorExport.ts @@ -0,0 +1,194 @@ +import {DEFAULT_PALETTE, DEFAULT_THEME} from './constants'; +import { + createBorderRadiusPresetForExport, + createFontImportsForExport, + createPrivateColorCssVariable, + createPrivateColorCssVariableFromToken, + createPrivateColorToken, + createTypographyPresetForExport, + createUtilityColorCssVariable, + isPrivateColorToken, +} from './themeCreatorUtils'; +import type {ColorOption, ThemeCreatorState, ThemeVariant} from './types'; + +const COMMON_VARIABLES_TEMPLATE_NAME = '%COMMON_VARIABLES%'; +const LIGHT_THEME_VARIABLES_TEMPLATE_NAME = '%LIGHT_THEME_VARIABLES%'; +const DARK_THEME_VARIABLES_TEMPLATE_NAME = '%DARK_THEME_VARIABLES%'; +const FONTS_TEMPLATE_NAME = '%IMPORT_FONTS%'; + +const SCSS_TEMPLATE = ` +@use '@gravity-ui/uikit/styles/themes'; + +${FONTS_TEMPLATE_NAME} + +.g-root { + @include themes.g-theme-common; + + ${COMMON_VARIABLES_TEMPLATE_NAME} + + &_theme_light { + @include themes.g-theme-light; + + ${LIGHT_THEME_VARIABLES_TEMPLATE_NAME} + } + + &_theme_dark { + @include themes.g-theme-dark; + + ${DARK_THEME_VARIABLES_TEMPLATE_NAME} + } +} +`.trim(); + +export type ExportFormat = 'scss' | 'json'; + +type ExportThemeParams = { + themeState: ThemeCreatorState; + format?: ExportFormat; + ignoreDefaultValues?: boolean; + forPreview?: boolean; +}; + +const isBackgroundColorChanged = (themeState: ThemeCreatorState) => { + return ( + DEFAULT_THEME.colors.dark['base-background'] !== + themeState.colors.dark['base-background'] || + DEFAULT_THEME.colors.light['base-background'] !== themeState.colors.light['base-background'] + ); +}; + +export function exportTheme({ + themeState, + format = 'scss', + ignoreDefaultValues = true, + forPreview = true, +}: ExportThemeParams) { + if (format === 'json') { + throw new Error('Not implemented'); + } + + const {paletteTokens, palette} = themeState; + const backgroundColorChanged = isBackgroundColorChanged(themeState); + + const prepareThemeVariables = (themeVariant: ThemeVariant) => { + let cssVariables = ''; + const privateColors: Record = {}; + + themeState.tokens.forEach((token) => { + // Dont export colors that are equals to default (except brand color) + // Private colors recalculate when background color changes + const valueEqualsToDefault = + DEFAULT_PALETTE[themeVariant][token] === themeState.palette[themeVariant][token] && + token !== 'brand' && + !backgroundColorChanged; + + if (valueEqualsToDefault && ignoreDefaultValues) { + return; + } + + const needExportColor = + backgroundColorChanged || token === 'brand' || !valueEqualsToDefault; + + if (!needExportColor) { + return; + } + + if (paletteTokens[token]?.privateColors[themeVariant]) { + Object.entries(paletteTokens[token].privateColors[themeVariant]!).forEach( + ([privateColorCode, color]) => { + privateColors[createPrivateColorToken(token, privateColorCode)] = color; + cssVariables += `${createPrivateColorCssVariable( + token, + privateColorCode, + )}: ${color}${forPreview ? ' !important' : ''};\n`; + }, + ); + cssVariables += '\n'; + } + }); + + cssVariables += '\n'; + + cssVariables += `${createUtilityColorCssVariable('base-brand')}: ${ + palette[themeVariant].brand + }${forPreview ? ' !important' : ''};\n`; + + Object.entries(themeState.colors[themeVariant]).forEach( + ([colorName, colorOrPrivateToken]) => { + if ( + ignoreDefaultValues && + DEFAULT_THEME.colors[themeVariant][colorName as ColorOption] === + colorOrPrivateToken + ) { + return; + } + + const color = isPrivateColorToken(colorOrPrivateToken) + ? `var(${createPrivateColorCssVariableFromToken(colorOrPrivateToken)})` + : colorOrPrivateToken; + + cssVariables += `${createUtilityColorCssVariable(colorName)}: ${color}${ + forPreview ? ' !important' : '' + };\n`; + }, + ); + + if (forPreview) { + cssVariables += createBorderRadiusPresetForExport({ + borders: themeState.borders, + forPreview, + ignoreDefaultValues, + }); + + cssVariables += createTypographyPresetForExport({ + typography: themeState.typography, + ignoreDefaultValues, + forPreview, + }); + } + + return cssVariables.trim(); + }; + + const prepareCommonThemeVariables = () => { + const borderRadiusVariabels = createBorderRadiusPresetForExport({ + borders: themeState.borders, + forPreview, + ignoreDefaultValues, + }); + + const typographyVariables = createTypographyPresetForExport({ + typography: themeState.typography, + ignoreDefaultValues, + forPreview, + }); + + return borderRadiusVariabels + '\n' + typographyVariables; + }; + + return { + fontImports: createFontImportsForExport(themeState.typography.baseSetting.fontFamilies), + common: prepareCommonThemeVariables(), + light: prepareThemeVariables('light'), + dark: prepareThemeVariables('dark'), + }; +} + +type ExportThemeForDialogParams = Pick; + +export function exportThemeForDialog({themeState, format = 'scss'}: ExportThemeForDialogParams) { + if (format === 'json') { + return 'not implemented'; + } + + const {common, light, dark, fontImports} = exportTheme({ + themeState, + format, + forPreview: false, + }); + + return SCSS_TEMPLATE.replace(FONTS_TEMPLATE_NAME, fontImports) + .replace(COMMON_VARIABLES_TEMPLATE_NAME, common.replaceAll('\n', '\n'.padEnd(5))) + .replace(LIGHT_THEME_VARIABLES_TEMPLATE_NAME, light.replaceAll('\n', '\n'.padEnd(9))) + .replace(DARK_THEME_VARIABLES_TEMPLATE_NAME, dark.replaceAll('\n', '\n'.padEnd(9))); +} diff --git a/src/components/Themes/lib/themeCreatorImport.ts b/src/components/Themes/lib/themeCreatorImport.ts new file mode 100644 index 00000000000..fb5eeccd3c5 --- /dev/null +++ b/src/components/Themes/lib/themeCreatorImport.ts @@ -0,0 +1 @@ +// TODO implement import logic diff --git a/src/components/Themes/lib/themeCreatorUtils.ts b/src/components/Themes/lib/themeCreatorUtils.ts new file mode 100644 index 00000000000..532b44fb8bf --- /dev/null +++ b/src/components/Themes/lib/themeCreatorUtils.ts @@ -0,0 +1,869 @@ +import {TextProps} from 'landing-uikit'; +import capitalize from 'lodash/capitalize'; +import cloneDeep from 'lodash/cloneDeep'; +import kebabCase from 'lodash/kebabCase'; +import lowerCase from 'lodash/lowerCase'; +import {v4 as uuidv4} from 'uuid'; + +import { + BrandPreset, + DEFAULT_NEW_COLOR_TITLE, + DEFAULT_PALETTE_TOKENS, + RADIUS_PRESETS, + THEME_BORDER_RADIUS_VARIABLE_PREFIX, + THEME_COLOR_VARIABLE_PREFIX, +} from './constants'; +import {generatePrivateColors} from './privateColors'; +import type { + BordersOption, + ColorsOptions, + Palette, + PaletteTokens, + PrivateColors, + RadiusValue, + ThemeCreatorState, + ThemeOptions, + ThemeVariant, +} from './types'; +import {CustomFontSelectType, RadiusPresetName, TypographyOptions} from './types'; +import {DefaultFontFamilyType, TextVariants, defaultTypographyPreset} from './typography/constants'; +import { + createFontFamilyVariable, + createFontLinkImport, + createTextFontFamilyVariable, + createTextFontSizeVariable, + createTextFontWeightVariable, + createTextLineHeightVariable, + getCustomFontTypeKey, +} from './typography/utils'; + +function createColorToken(title: string) { + return kebabCase(title); +} + +function createTitleFromToken(token: string) { + return capitalize(lowerCase(token)); +} + +export function createPrivateColorToken(mainColorToken: string, privateColorCode: string) { + return `private.${mainColorToken}.${privateColorCode}`; +} + +export function isPrivateColorToken(privateColorToken?: string) { + if (!privateColorToken) { + return false; + } + + const parts = privateColorToken.split('.'); + + if (parts.length !== 3 || parts[0] !== 'private') { + return false; + } + + return true; +} + +export function parsePrivateColorToken(privateColorToken: string) { + const parts = privateColorToken.split('.'); + + if (parts.length !== 3 || parts[0] !== 'private') { + return undefined; + } + + return { + mainColorToken: parts[1], + privateColorCode: parts[2], + }; +} + +export function createPrivateColorCssVariable(mainColorToken: string, privateColorCode: string) { + return `${THEME_COLOR_VARIABLE_PREFIX}-private-${mainColorToken}-${privateColorCode}`; +} + +export function createPrivateColorCssVariableFromToken(privateColorToken: string) { + const result = parsePrivateColorToken(privateColorToken); + + if (result) { + return createPrivateColorCssVariable(result.mainColorToken, result.privateColorCode); + } + + return ''; +} + +export function createUtilityColorCssVariable(colorName: string) { + return `${THEME_COLOR_VARIABLE_PREFIX}-${colorName}`; +} + +function isManuallyCreatedToken(token: string) { + return !DEFAULT_PALETTE_TOKENS.has(token); +} + +function createNewColorTitle(currentPaletteTokens: PaletteTokens) { + let i = 0; + + // eslint-disable-next-line no-constant-condition + while (true) { + const title = i === 0 ? DEFAULT_NEW_COLOR_TITLE : `${DEFAULT_NEW_COLOR_TITLE} ${i}`; + const token = createColorToken(title); + + if (!currentPaletteTokens[token]) { + return title; + } + + i++; + } +} + +function createPrivateColors({ + themeVariant, + colorToken, + colorValue, + theme, +}: { + colorToken: string; + colorValue: string; + themeVariant: ThemeVariant; + theme: ThemeOptions; +}): PrivateColors { + return generatePrivateColors({ + theme: themeVariant, + colorToken, + colorValue, + lightBg: + themeVariant === 'light' + ? theme.colors.light['base-background'] + : theme.colors.dark['base-background'], + darkBg: + themeVariant === 'light' + ? theme.colors.dark['base-background'] + : theme.colors.light['base-background'], + }); +} + +function createPalleteTokens(theme: ThemeOptions): PaletteTokens { + const {palette} = theme; + const tokens = Object.keys(palette.light); + + return tokens.reduce( + (acc, token) => ({ + ...acc, + [token]: { + title: createTitleFromToken(token), + privateColors: { + light: palette.light[token] + ? createPrivateColors({ + colorToken: token, + colorValue: palette.light[token], + theme, + themeVariant: 'light', + }) + : undefined, + dark: palette.dark[token] + ? createPrivateColors({ + colorToken: token, + colorValue: palette.dark[token], + theme, + themeVariant: 'dark', + }) + : undefined, + }, + }, + }), + {}, + ); +} + +export type UpdateColorInThemeParams = { + /** The title of the color to update. */ + title: string; + /** The theme variant to update. */ + theme: ThemeVariant; + /** The new value of the color. */ + value: string; +}; + +/** + * Updates a color in the given theme state. + * + * @param {ThemeCreatorState} themeState - The current state of the theme. + * @param {UpdateColorInThemeParams} params - The parameters for the color update. + * @returns {ThemeCreatorState} The updated theme state. + */ +export function updateColorInTheme( + themeState: ThemeCreatorState, + params: UpdateColorInThemeParams, +): ThemeCreatorState { + const newThemeState = {...themeState}; + const token = createColorToken(params.title); + + if (params.theme === 'light') { + if (!newThemeState.palette.light[token]) { + newThemeState.palette.light[token] = ''; + } + + newThemeState.palette.light[token] = params.value; + } + + if (params.theme === 'dark') { + if (!newThemeState.palette.dark[token]) { + newThemeState.palette.dark[token] = ''; + } + + newThemeState.palette.dark[token] = params.value; + } + + const privateColors = createPrivateColors({ + colorToken: token, + colorValue: params.value, + theme: newThemeState, + themeVariant: params.theme, + }); + + newThemeState.paletteTokens[token] = { + ...newThemeState.paletteTokens[token], + title: params.title, + privateColors: { + light: + params.theme === 'light' + ? privateColors + : newThemeState.paletteTokens[token]?.privateColors?.light, + dark: + params.theme === 'dark' + ? privateColors + : newThemeState.paletteTokens[token]?.privateColors?.dark, + }, + }; + + const isNewToken = !themeState.paletteTokens[token]; + if (isNewToken) { + newThemeState.tokens.push(token); + } + + return newThemeState; +} + +export type AddColorToThemeParams = + | { + title?: string; + colors?: Partial>; + } + | undefined; + +/** + * Adds a new color to the given theme state. + * + * @param {ThemeCreatorState} themeState - The current state of the theme. + * @param {AddColorToThemeParams} params - The parameters of the adding color. + * @returns {ThemeCreatorState} The updated theme state with the new color added. + */ +export function addColorToTheme( + themeState: ThemeCreatorState, + params: AddColorToThemeParams, +): ThemeCreatorState { + const newThemeState = {...themeState}; + const title = params?.title ?? createNewColorTitle(themeState.paletteTokens); + const token = createColorToken(title); + + if (!themeState.palette.dark[token]) { + newThemeState.palette.dark = { + ...newThemeState.palette.dark, + [token]: '', + }; + } + + if (!themeState.palette.light[token]) { + newThemeState.palette.light = { + ...newThemeState.palette.light, + [token]: '', + }; + } + + if (params?.colors?.dark) { + newThemeState.palette.dark = { + ...newThemeState.palette.dark, + [token]: params.colors.dark, + }; + } + + if (params?.colors?.light) { + newThemeState.palette.light = { + ...newThemeState.palette.light, + [token]: params.colors.light, + }; + } + + newThemeState.paletteTokens = { + ...newThemeState.paletteTokens, + [token]: { + ...newThemeState.paletteTokens[token], + title, + privateColors: { + light: params?.colors?.light + ? createPrivateColors({ + colorToken: token, + colorValue: params.colors.light, + theme: newThemeState, + themeVariant: 'light', + }) + : undefined, + dark: params?.colors?.dark + ? createPrivateColors({ + colorToken: token, + colorValue: params.colors.dark, + theme: newThemeState, + themeVariant: 'dark', + }) + : undefined, + }, + isCustom: true, + }, + }; + + newThemeState.tokens = [...newThemeState.tokens, token]; + + return newThemeState; +} + +export function removeColorFromTheme( + themeState: ThemeCreatorState, + colorTitle: string, +): ThemeCreatorState { + const newThemeState = {...themeState}; + const token = createColorToken(colorTitle); + + delete newThemeState.palette.dark[token]; + delete newThemeState.palette.light[token]; + delete newThemeState.paletteTokens[token]; + + newThemeState.tokens = newThemeState.tokens.filter((t) => t !== token); + + return newThemeState; +} + +export type RenameColorInThemeParams = { + oldTitle: string; + newTitle: string; +}; + +export function renameColorInTheme( + themeState: ThemeCreatorState, + {oldTitle, newTitle}: RenameColorInThemeParams, +): ThemeCreatorState { + const newThemeState = {...themeState}; + const oldToken = createColorToken(oldTitle); + const newToken = createColorToken(newTitle); + + if (newThemeState.paletteTokens[oldToken]) { + newThemeState.paletteTokens[newToken] = { + ...newThemeState.paletteTokens[oldToken], + title: newTitle, + }; + newThemeState.palette.dark[newToken] = newThemeState.palette.dark[oldToken]; + newThemeState.palette.light[newToken] = newThemeState.palette.light[oldToken]; + } + + newThemeState.tokens = newThemeState.tokens.map((token) => + token === oldToken ? newToken : token, + ); + + delete newThemeState.palette.dark[oldToken]; + delete newThemeState.palette.light[oldToken]; + delete newThemeState.paletteTokens[oldToken]; + + return newThemeState; +} + +export type ThemeColorOption = { + token: string; + title: string; + color: string; + privateColors: { + token: string; + title: string; + color: string; + }[]; +}; + +/** + * Generates theme color options from the given palette tokens and theme variant. + * + * @param {Object} params - The parameters for generating theme color options. + * @param {PaletteTokens} params.paletteTokens - The palette tokens to generate options from. + * @param {ThemeVariant} params.themeVariant - The theme variant to filter private colors (light, dark). + * @returns {ThemeColorOption[]} The generated theme color options. + */ +export function getThemeColorOptions({ + themeState, + themeVariant, +}: { + themeState: ThemeCreatorState; + themeVariant: ThemeVariant; +}) { + const {tokens, paletteTokens, palette} = themeState; + + return tokens.reduce((acc, token) => { + if (paletteTokens[token]?.privateColors[themeVariant]) { + return [ + ...acc, + { + token, + color: palette[themeVariant][token], + title: paletteTokens[token].title, + privateColors: Object.entries( + paletteTokens[token].privateColors[themeVariant]!, + ).map(([privateColorCode, color]) => ({ + token: createPrivateColorToken(token, privateColorCode), + title: createPrivateColorCssVariable(token, privateColorCode), + color, + })), + }, + ]; + } + + return acc; + }, []); +} + +export type ChangeUtilityColorInThemeParams = { + themeVariant: ThemeVariant; + name: keyof ColorsOptions; + value: string; +}; + +export function changeUtilityColorInTheme( + themeState: ThemeCreatorState, + {themeVariant, name, value}: ChangeUtilityColorInThemeParams, +): ThemeCreatorState { + const newState = {...themeState}; + newState.colors[themeVariant][name] = value; + + if (name === 'base-background') { + newState.paletteTokens = createPalleteTokens(newState); + } + + return newState; +} + +export function applyBrandPresetToTheme( + themeState: ThemeCreatorState, + {brandColor, colors}: BrandPreset, +): ThemeCreatorState { + let newState = {...themeState}; + + (['light', 'dark'] as const).forEach((theme) => { + newState = updateColorInTheme(newState, { + theme, + title: 'brand', + value: brandColor, + }); + }); + + newState.colors.light = {...colors.light}; + newState.colors.dark = {...colors.dark}; + + return newState; +} + +export function getThemePalette(theme: ThemeCreatorState): Palette { + return theme.tokens.map((token) => { + return { + title: theme.paletteTokens[token]?.title || '', + colors: { + light: theme.palette.light[token], + dark: theme.palette.dark[token], + }, + isCustom: isManuallyCreatedToken(token), + }; + }); +} + +export function initThemeCreator(inputTheme: ThemeOptions): ThemeCreatorState { + const theme = cloneDeep(inputTheme); + const paletteTokens = createPalleteTokens(theme); + + return { + ...theme, + paletteTokens, + tokens: Object.keys(paletteTokens), + showMainSettings: false, + advancedModeEnabled: false, + changesExist: false, + }; +} + +export type ChangeRadiusPresetInThemeParams = { + radiusPresetName: RadiusPresetName; +}; + +export function changeRadiusPresetInTheme( + themeState: ThemeCreatorState, + {radiusPresetName}: ChangeRadiusPresetInThemeParams, +): ThemeCreatorState { + const newBorderValue = { + preset: radiusPresetName, + values: {...RADIUS_PRESETS[radiusPresetName]}, + }; + + return {...themeState, borders: newBorderValue}; +} + +export type UpdateCustomRadiusPresetInThemeParams = {radiusValue: Partial}; + +export function updateCustomRadiusPresetInTheme( + themeState: ThemeCreatorState, + {radiusValue}: UpdateCustomRadiusPresetInThemeParams, +): ThemeCreatorState { + const previousRadiusValues = themeState.borders.values; + const newCustomPresetValues = { + preset: RadiusPresetName.Custom, + values: {...previousRadiusValues, ...radiusValue}, + }; + + return {...themeState, borders: newCustomPresetValues}; +} + +function createBorderRadiusCssVariable(radiusSize: string) { + return `${THEME_BORDER_RADIUS_VARIABLE_PREFIX}-${radiusSize}`; +} + +/** + * Generates ready-to-use in css string with borders variables + * @returns string + */ +export function createBorderRadiusPresetForExport({ + borders, + forPreview, + ignoreDefaultValues, +}: { + borders: BordersOption; + ignoreDefaultValues: boolean; + forPreview: boolean; +}) { + // Don't export radius preset that are equals to default + if (ignoreDefaultValues && borders.preset === RadiusPresetName.Regular) { + return ''; + } + let cssString = ''; + Object.entries(borders.values).forEach(([radiusName, radiusValue]) => { + if (radiusValue) { + cssString += `${createBorderRadiusCssVariable(radiusName)}: ${radiusValue}px${ + forPreview ? ' !important' : '' + };\n`; + } + }); + return cssString; +} + +export type UpdateFontFamilyParams = { + fontType: DefaultFontFamilyType | string; + fontWebsite?: string; + isCustom?: boolean; + customType?: string; + value?: { + title: string; + key: string; + link: string; + alternatives: string[]; + }; +}; + +export function updateFontFamilyInTheme( + themeState: ThemeCreatorState, + {fontType, value, isCustom, fontWebsite, customType}: UpdateFontFamilyParams, +): ThemeCreatorState { + const previousFontFamilySettings = themeState.typography.baseSetting.fontFamilies; + + const newFontFamilySettings = { + ...previousFontFamilySettings, + [fontType]: { + ...previousFontFamilySettings[fontType], + ...(value || {}), + isCustom, + customType: customType || previousFontFamilySettings[fontType].customType, + fontWebsite, + }, + }; + + return { + ...themeState, + typography: { + ...themeState.typography, + baseSetting: { + ...themeState.typography.baseSetting, + fontFamilies: newFontFamilySettings, + }, + }, + }; +} + +export type AddFontFamilyTypeParams = { + title: string; +}; + +export function addFontFamilyTypeInTheme( + themeState: ThemeCreatorState, + {title}: AddFontFamilyTypeParams, +): ThemeCreatorState { + const {customFontFamilyType} = themeState.typography.baseSetting; + const newFontType = `custom-font-type-${uuidv4()}`; + + const newCustomFontFamily = [ + ...customFontFamilyType, + { + value: newFontType, + content: title, + }, + ]; + + return { + ...themeState, + typography: { + ...themeState.typography, + baseSetting: { + ...themeState.typography.baseSetting, + fontFamilies: { + ...themeState.typography.baseSetting.fontFamilies, + [newFontType]: { + isCustom: true, + customType: CustomFontSelectType.GoogleFonts, + title: '', + key: '', + link: '', + alternatives: [], + }, + }, + customFontFamilyType: newCustomFontFamily, + }, + }, + }; +} + +export type UpdateFontFamilyTypeTitleParams = { + title: string; + familyType: string; +}; + +export function updateFontFamilyTypeTitleInTheme( + themeState: ThemeCreatorState, + {title, familyType}: UpdateFontFamilyTypeTitleParams, +): ThemeCreatorState { + const {customFontFamilyType} = themeState.typography.baseSetting; + + const newCustomFontFamily = customFontFamilyType.map((fontFamilyType) => { + return fontFamilyType.value === familyType + ? { + content: title, + value: familyType, + } + : fontFamilyType; + }); + + return { + ...themeState, + typography: { + ...themeState.typography, + baseSetting: { + ...themeState.typography.baseSetting, + customFontFamilyType: newCustomFontFamily, + }, + }, + }; +} + +export function removeFontFamilyTypeFromTheme( + themeState: ThemeCreatorState, + {fontType}: {fontType: string}, +): ThemeCreatorState { + const {customFontFamilyType, fontFamilies} = themeState.typography.baseSetting; + + const {[fontType]: _, ...restFontFamilies} = fontFamilies; + + const newCustomFontFamilyType = customFontFamilyType.filter( + (fontFamily) => fontFamily.value !== fontType, + ); + + const newAdvanced = cloneDeep(themeState.typography.advanced); + + // Reset selected font to default + Object.entries(newAdvanced).forEach(([textVariant, settings]) => { + if (settings.selectedFontFamilyType === fontType) { + newAdvanced[textVariant as TextVariants].selectedFontFamilyType = + defaultTypographyPreset.advanced[ + textVariant as TextVariants + ].selectedFontFamilyType; + } + }); + + return { + ...themeState, + typography: { + ...themeState.typography, + advanced: newAdvanced, + baseSetting: { + ...themeState.typography.baseSetting, + fontFamilies: restFontFamilies, + customFontFamilyType: newCustomFontFamilyType, + }, + }, + }; +} + +export type UpdateAdvancedTypographySettingsParams = { + key: TextVariants; + fontWeight?: number; + selectedFontFamilyType?: string; + sizeKey?: Exclude; + fontSize?: number; + lineHeight?: number; +}; + +export function updateAdvancedTypographySettingsInTheme( + themeState: ThemeCreatorState, + { + key, + fontSize, + selectedFontFamilyType, + sizeKey, + fontWeight, + lineHeight, + }: UpdateAdvancedTypographySettingsParams, +): ThemeCreatorState { + const previousTypographyAdvancedSettings = themeState.typography.advanced; + + const newSizes = sizeKey + ? { + [sizeKey]: { + ...previousTypographyAdvancedSettings[key].sizes[sizeKey], + fontSize: + fontSize ?? previousTypographyAdvancedSettings[key].sizes[sizeKey]?.fontSize, + lineHeight: + lineHeight ?? + previousTypographyAdvancedSettings[key].sizes[sizeKey]?.lineHeight, + }, + } + : {}; + + const newTypographyAdvancedSettings = { + ...previousTypographyAdvancedSettings, + [key]: { + ...previousTypographyAdvancedSettings[key], + fontWeight: fontWeight ?? previousTypographyAdvancedSettings[key].fontWeight, + selectedFontFamilyType: + selectedFontFamilyType ?? + previousTypographyAdvancedSettings[key].selectedFontFamilyType, + sizes: { + ...previousTypographyAdvancedSettings[key].sizes, + ...newSizes, + }, + }, + }; + + return { + ...themeState, + typography: { + ...themeState.typography, + advanced: { + ...newTypographyAdvancedSettings, + }, + }, + }; +} + +export const updateAdvancedTypographyInTheme = ( + themeState: ThemeCreatorState, +): ThemeCreatorState => { + return { + ...themeState, + typography: { + ...themeState.typography, + isAdvancedActive: !themeState.typography.isAdvancedActive, + }, + }; +}; + +export const createFontImportsForExport = ( + fontFamily: TypographyOptions['baseSetting']['fontFamilies'], +) => { + let cssString = ''; + + Object.entries(fontFamily).forEach(([, value]) => { + cssString += `${createFontLinkImport(value.link)}\n`; + }); + + return cssString; +}; + +export const createTypographyPresetForExport = ({ + typography, + forPreview, +}: { + typography: TypographyOptions; + ignoreDefaultValues: boolean; + forPreview: boolean; +}) => { + const {baseSetting, advanced} = typography; + let cssString = ''; + + Object.entries(baseSetting.fontFamilies).forEach(([key, value]) => { + const customFontKey = getCustomFontTypeKey(key, baseSetting.customFontFamilyType); + + cssString += `${createFontFamilyVariable( + customFontKey ? kebabCase(customFontKey) : key, + value.title, + value.alternatives, + forPreview, + )}\n`; + }); + + Object.entries(advanced).forEach(([key, data]) => { + const defaultAdvancedSetting = defaultTypographyPreset.advanced[key as TextVariants]; + + if (defaultAdvancedSetting.selectedFontFamilyType !== data.selectedFontFamilyType) { + const customFontTypeKey = getCustomFontTypeKey( + data.selectedFontFamilyType, + baseSetting.customFontFamilyType, + ); + + cssString += `${createTextFontFamilyVariable( + key as TextVariants, + customFontTypeKey ? kebabCase(customFontTypeKey) : data.selectedFontFamilyType, + forPreview, + )}\n`; + } + if (defaultAdvancedSetting.fontWeight !== data.fontWeight) { + cssString += `${createTextFontWeightVariable( + key as TextVariants, + data.fontWeight, + forPreview, + )}\n`; + cssString += '\n'; + } + + Object.entries(data.sizes).forEach(([sizeKey, sizeData]) => { + if ( + defaultAdvancedSetting.sizes[sizeKey as Exclude] + ?.fontSize !== sizeData.fontSize + ) { + cssString += `${createTextFontSizeVariable( + sizeKey as TextProps['variant'], + sizeData.fontSize, + forPreview, + )}\n`; + } + + if ( + defaultAdvancedSetting.sizes[sizeKey as Exclude] + ?.lineHeight !== sizeData.lineHeight + ) { + cssString += `${createTextLineHeightVariable( + sizeKey as TextProps['variant'], + sizeData.lineHeight, + forPreview, + )}\n`; + cssString += '\n'; + } + }); + }); + + return cssString; +}; diff --git a/src/components/Themes/lib/types.ts b/src/components/Themes/lib/types.ts new file mode 100644 index 00000000000..8a7cc1662b4 --- /dev/null +++ b/src/components/Themes/lib/types.ts @@ -0,0 +1,130 @@ +import {TextProps} from 'landing-uikit'; + +import {DefaultFontFamilyType, TextVariants} from './typography/constants'; + +export type ThemeVariant = 'light' | 'dark'; + +export type PaletteOptions = { + brand: string; + [key: string]: string; +}; + +export type ColorsOptions = { + 'base-background': string; + 'base-brand-hover': string; + 'base-selection': string; + 'base-selection-hover': string; + 'line-brand': string; + 'text-brand': string; + 'text-brand-heavy': string; + 'text-brand-contrast': string; + 'text-link': string; + 'text-link-hover': string; + 'text-link-visited': string; + 'text-link-visited-hover': string; +}; + +export type ColorOption = keyof ColorsOptions; + +export type RadiusSizeName = 'xs' | 's' | 'm' | 'l' | 'xl'; + +export enum RadiusPresetName { + Regular = 'radius_regular', + Circled = 'radius_circled', + Squared = 'radius_squared', + Custom = 'radius_custom', +} + +export type RadiusValue = Record; + +export type BordersOption = { + preset: RadiusPresetName; + values: RadiusValue; +}; + +export enum CustomFontSelectType { + GoogleFonts = 'google-fonts', + Manual = 'manual', +} + +export type TypographyOptions = { + baseSetting: { + defaultFontFamilyType: { + value: DefaultFontFamilyType; + content: string; + }[]; + customFontFamilyType: { + value: string; + content: string; + }[]; + fontFamilies: Record< + string, + { + title: string; + key: string; + link: string; + alternatives: string[]; + isCustom?: boolean; + customType?: string; + fontWebsite?: string; + } + >; + }; + isAdvancedActive: boolean; + advanced: Record< + TextVariants, + { + title: string; + fontWeight: number; + selectedFontFamilyType: DefaultFontFamilyType; + sizes: Partial< + Record< + Exclude, + { + title: string; + fontSize: number; + lineHeight: number; + } + > + >; + } + >; +}; + +export interface ThemeOptions { + /** Values of solid colors, from which private colors are calculated */ + palette: Record; + /** Utility colors that used in components (background, link, brand-text, etc.) */ + colors: Record; + borders: BordersOption; + typography: TypographyOptions; +} + +export type PrivateColors = Record; + +type PaletteToken = { + /** Title that will using in UI */ + title: string; + /** Is color manually created */ + isCustom?: boolean; + /** Auto-generated private colors for each theme variant */ + privateColors: Record; +}; + +export type PaletteTokens = Record; + +export interface ThemeCreatorState extends ThemeOptions { + /** Mapping color tokens to their information (title and private colors) */ + paletteTokens: PaletteTokens; + /** All available palette tokens in theme */ + tokens: string[]; + showMainSettings: boolean; + advancedModeEnabled: boolean; + changesExist: boolean; +} + +export type Palette = { + title: string; + isCustom?: boolean; + colors: Record; +}[]; diff --git a/src/components/Themes/lib/typography/constants.ts b/src/components/Themes/lib/typography/constants.ts new file mode 100644 index 00000000000..0a7dc8f7b7c --- /dev/null +++ b/src/components/Themes/lib/typography/constants.ts @@ -0,0 +1,214 @@ +import {CustomFontSelectType, TypographyOptions} from '../types'; + +export const THEME_FONT_FAMILY_PREFIX = '--g-font-family'; +export const THEME_TEXT_PREFIX = '--g-text'; + +export enum DefaultFontFamilyType { + Sans = 'sans', + Monospace = 'monospace', +} + +export enum TextVariants { + Body = 'body', + Caption = 'caption', + Header = 'header', + Subheader = 'subheader', + Display = 'display', + Code = 'code', +} + +export const FONT_WEIGHTS = [100, 200, 300, 400, 500, 600, 700, 800, 900]; + +export const GOOGLE_FONTS_DOWNLOAD_HOST = 'https://fonts.googleapis.com/css2'; +export const GOOGLE_FONTS_FONT_PREVIEW_HOST = 'https://fonts.google.com/specimen/'; + +export const DEFAULT_FONTS: Record = { + sans: ["'Helvetica Neue'", "'Helvetica'", "'Arial'", 'sans-serif'], + monospace: [ + "'Menlo'", + "'Monaco'", + "'Consolas'", + "'Ubuntu Mono'", + "'Liberation Mono'", + "'DejaVu Sans Mono'", + "'Courier New'", + "'Courier'", + 'monospace', + ], +}; + +export const defaultTypographyPreset: TypographyOptions = { + baseSetting: { + customFontFamilyType: [], + defaultFontFamilyType: [ + {value: DefaultFontFamilyType.Sans, content: 'Sans Font Family'}, + {value: DefaultFontFamilyType.Monospace, content: 'Monospace Font Family'}, + ], + fontFamilies: { + [DefaultFontFamilyType.Sans]: { + title: 'Inter', + key: 'inter', + link: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap', + customType: CustomFontSelectType.GoogleFonts, + alternatives: DEFAULT_FONTS[DefaultFontFamilyType.Sans], + }, + [DefaultFontFamilyType.Monospace]: { + title: 'Roboto Mono', + key: 'roboto_mono', + link: 'https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap', + customType: CustomFontSelectType.GoogleFonts, + alternatives: DEFAULT_FONTS[DefaultFontFamilyType.Monospace], + }, + }, + }, + isAdvancedActive: false, + advanced: { + [TextVariants.Body]: { + title: 'Body Text', + fontWeight: 400, + selectedFontFamilyType: DefaultFontFamilyType.Sans, + sizes: { + 'body-short': { + title: 'Body 1 Short', + fontSize: 13, + lineHeight: 16, + }, + 'body-1': { + title: 'Body 1', + fontSize: 13, + lineHeight: 18, + }, + 'body-2': { + title: 'Body 2', + fontSize: 15, + lineHeight: 20, + }, + 'body-3': { + title: 'Body 3', + fontSize: 17, + lineHeight: 24, + }, + }, + }, + [TextVariants.Caption]: { + title: 'Caption', + fontWeight: 400, + selectedFontFamilyType: DefaultFontFamilyType.Sans, + sizes: { + 'caption-1': { + title: 'Caption 1', + fontSize: 9, + lineHeight: 12, + }, + 'caption-2': { + title: 'Caption 2', + fontSize: 11, + lineHeight: 16, + }, + }, + }, + [TextVariants.Header]: { + title: 'Header', + fontWeight: 600, + selectedFontFamilyType: DefaultFontFamilyType.Sans, + sizes: { + 'header-1': { + title: 'Header 1', + fontSize: 20, + lineHeight: 24, + }, + 'header-2': { + title: 'Header 2', + fontSize: 24, + lineHeight: 28, + }, + }, + }, + [TextVariants.Subheader]: { + title: 'Subheader', + fontWeight: 600, + selectedFontFamilyType: DefaultFontFamilyType.Sans, + sizes: { + 'subheader-1': { + title: 'Subheader 1', + fontSize: 13, + lineHeight: 18, + }, + 'subheader-2': { + title: 'Subheader 2', + fontSize: 15, + lineHeight: 20, + }, + 'subheader-3': { + title: 'Subheader 3', + fontSize: 17, + lineHeight: 24, + }, + }, + }, + [TextVariants.Display]: { + title: 'Display', + fontWeight: 600, + selectedFontFamilyType: DefaultFontFamilyType.Sans, + sizes: { + 'display-1': { + title: 'Display 1', + fontSize: 28, + lineHeight: 36, + }, + 'display-2': { + title: 'Display 2', + fontSize: 32, + lineHeight: 40, + }, + 'display-3': { + title: 'Display 3', + fontSize: 40, + lineHeight: 48, + }, + 'display-4': { + title: 'Display 4', + fontSize: 48, + lineHeight: 52, + }, + }, + }, + [TextVariants.Code]: { + title: 'Code', + fontWeight: 600, + selectedFontFamilyType: DefaultFontFamilyType.Monospace, + sizes: { + 'code-1': { + title: 'Code 1', + fontSize: 12, + lineHeight: 18, + }, + 'code-inline-1': { + title: 'Code Inline 1', + fontSize: 12, + lineHeight: 14, + }, + 'code-2': { + title: 'Code 2', + fontSize: 14, + lineHeight: 20, + }, + 'code-inline-2': { + title: 'Code Inline 2', + fontSize: 14, + lineHeight: 16, + }, + 'code-3': { + title: 'Code 3', + fontSize: 16, + lineHeight: 24, + }, + 'code-inline-3': { + title: 'Code Inline 3', + fontSize: 16, + lineHeight: 20, + }, + }, + }, + }, +}; diff --git a/src/components/Themes/lib/typography/utils.ts b/src/components/Themes/lib/typography/utils.ts new file mode 100644 index 00000000000..bd06d307525 --- /dev/null +++ b/src/components/Themes/lib/typography/utils.ts @@ -0,0 +1,80 @@ +import {TextProps} from 'landing-uikit'; + +import {TypographyOptions} from '../types'; + +import { + GOOGLE_FONTS_DOWNLOAD_HOST, + THEME_FONT_FAMILY_PREFIX, + THEME_TEXT_PREFIX, + TextVariants, +} from './constants'; + +export const createFontLinkImport = (fontLink: string) => { + return `@import url('${fontLink}');`; +}; + +export const createFontFamilyVariable = ( + fontFamilyType: string, + value: string, + alternatives: string[], + forPreview: boolean, +) => { + return `${THEME_FONT_FAMILY_PREFIX}-${fontFamilyType}: '${value}'${ + alternatives.length ? `, ${alternatives.join(', ')}` : '' + }${forPreview ? '!important' : ''};`; +}; + +export const createTextFontWeightVariable = ( + textVariant: TextVariants, + value: number, + forPreview: boolean, +) => { + return `${THEME_TEXT_PREFIX}-${textVariant}-font-weight: ${value}${ + forPreview ? '!important' : '' + };`; +}; + +export const createTextFontFamilyVariable = ( + textVariant: TextVariants, + value: string, + forPreview: boolean, +) => { + return `${THEME_TEXT_PREFIX}-${textVariant}-font-family: var(${THEME_FONT_FAMILY_PREFIX}-${value})${ + forPreview ? '!important' : '' + };`; +}; + +export const createTextFontSizeVariable = ( + variant: TextProps['variant'], + value: number, + forPreview: boolean, +) => { + return `${THEME_TEXT_PREFIX}-${variant}-font-size: ${value}px${ + forPreview ? '!important' : '' + };`; +}; + +export const createTextLineHeightVariable = ( + variant: TextProps['variant'], + value: number, + forPreview: boolean, +) => { + return `${THEME_TEXT_PREFIX}-${variant}-line-height: ${value}px${ + forPreview ? '!important' : '' + };`; +}; + +export const generateGoogleFontDownloadLink = (fontName?: string) => { + if (!fontName) { + return ''; + } + + return `${GOOGLE_FONTS_DOWNLOAD_HOST}?family=${fontName}&display=swap`; +}; + +export const getCustomFontTypeKey = ( + key: string, + customFontFamilyType: TypographyOptions['baseSetting']['customFontFamilyType'], +) => { + return customFontFamilyType.find((setting) => setting.value === key)?.content.toLowerCase(); +}; diff --git a/src/components/Themes/ui/BasicPalette/AddColorButton.scss b/src/components/Themes/ui/BasicPalette/AddColorButton.scss new file mode 100644 index 00000000000..f9dd80f30b3 --- /dev/null +++ b/src/components/Themes/ui/BasicPalette/AddColorButton.scss @@ -0,0 +1,14 @@ +@use '../../../../variables.scss'; +@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; + +$block: '.#{variables.$ns}theme-palette-add-color-button'; + +#{$block} { + --g-button-border-radius: var(--g-spacing-2); + margin-top: var(--g-spacing-4); + width: min-content; + + @media (max-width: map-get(pcVariables.$gridBreakpoints, 'sm')) { + width: 100%; + } +} diff --git a/src/components/Themes/ui/BasicPalette/AddColorButton.tsx b/src/components/Themes/ui/BasicPalette/AddColorButton.tsx new file mode 100644 index 00000000000..d85752b7838 --- /dev/null +++ b/src/components/Themes/ui/BasicPalette/AddColorButton.tsx @@ -0,0 +1,24 @@ +import {Plus} from 'landing-icons'; +import {Button, Icon} from 'landing-uikit'; +import {useTranslation} from 'next-i18next'; +import React from 'react'; + +import {block} from '../../../../utils'; + +import './AddColorButton.scss'; + +const b = block('theme-palette-add-color-button'); + +interface AddColorButtonProps { + onClick: () => void; +} + +export const AddColorButton: React.FC = ({onClick}) => { + const {t} = useTranslation('themes'); + + return ( + + ); +}; diff --git a/src/components/Themes/ui/BasicPalette/BasicPalette.tsx b/src/components/Themes/ui/BasicPalette/BasicPalette.tsx new file mode 100644 index 00000000000..6b2f793bfb9 --- /dev/null +++ b/src/components/Themes/ui/BasicPalette/BasicPalette.tsx @@ -0,0 +1,61 @@ +import {useTranslation} from 'next-i18next'; +import React from 'react'; + +import {useThemeCreatorMethods, useThemePalette} from '../../hooks'; +import {ThemeVariant} from '../../lib/types'; +import {ColorPickerInput} from '../ColorPickerInput/ColorPickerInput'; +import {ThemableSettings} from '../ThemableSettings/ThemableSettings'; +import {ThemableRow} from '../ThemableSettings/types'; +import {ThemeSection} from '../ThemeSection'; + +import {AddColorButton} from './AddColorButton'; +import {PaletteColorEditor} from './PaletteColorEditor'; + +const hiddenColors = new Set(['white', 'black', 'brand']); + +export const BasicPalette = () => { + const {t} = useTranslation('themes'); + + const {addColor, removeColor, updateColor, renameColor} = useThemeCreatorMethods(); + const origPalette = useThemePalette(); + + const palette = React.useMemo( + () => origPalette.filter(({title}) => !hiddenColors.has(title.toLowerCase())), + [origPalette], + ); + + const rows = React.useMemo( + () => + palette.map((paletteColorData) => ({ + id: paletteColorData.title, + title: paletteColorData.title, + renderTitle: () => ( + + ), + render: (currentTheme: ThemeVariant) => ( + + updateColor({theme: currentTheme, title: paletteColorData.title, value}) + } + /> + ), + })), + [palette, removeColor, renameColor], + ); + + return ( + + } + /> + + ); +}; diff --git a/src/components/Themes/ui/BasicPalette/PaletteColorEditor.scss b/src/components/Themes/ui/BasicPalette/PaletteColorEditor.scss new file mode 100644 index 00000000000..0673e5d65ba --- /dev/null +++ b/src/components/Themes/ui/BasicPalette/PaletteColorEditor.scss @@ -0,0 +1,56 @@ +@use '../../../../variables.scss'; +@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; + +$block: '.#{variables.$ns}theme-palette-color-editor'; + +#{$block} { + display: flex; + gap: var(--g-spacing-2); + + @media (max-width: map-get(pcVariables.$gridBreakpoints, 'sm')) { + flex-direction: column-reverse; + } + + &__default-title { + padding: var(--g-spacing-2) 0; + + @media (max-width: map-get(pcVariables.$gridBreakpoints, 'sm')) { + padding: 0; + } + } + + & &__input { + padding-inline-start: 9px; + + @media (max-width: map-get(pcVariables.$gridBreakpoints, 'sm')) { + padding-inline-start: 3px; + } + } + + &__input-title { + display: none; + + @media (max-width: map-get(pcVariables.$gridBreakpoints, 'sm')) { + display: block; + padding-left: var(--g-spacing-2); + padding-top: 1px; + } + } + + &__header { + display: flex; + + @media (max-width: map-get(pcVariables.$gridBreakpoints, 'sm')) { + justify-content: space-between; + align-items: center; + } + } + + &__title { + display: none; + + @media (max-width: map-get(pcVariables.$gridBreakpoints, 'sm')) { + display: block; + } + } +} diff --git a/src/components/Themes/ui/BasicPalette/PaletteColorEditor.tsx b/src/components/Themes/ui/BasicPalette/PaletteColorEditor.tsx new file mode 100644 index 00000000000..aed0838ee2c --- /dev/null +++ b/src/components/Themes/ui/BasicPalette/PaletteColorEditor.tsx @@ -0,0 +1,83 @@ +import {TrashBin} from 'landing-icons'; +import {Button, Icon, Text, TextInput} from 'landing-uikit'; +import debounce from 'lodash/debounce'; +import React from 'react'; + +import {block} from '../../../../utils'; +import {Palette} from '../../lib/types'; + +import './PaletteColorEditor.scss'; + +const b = block('theme-palette-color-editor'); + +interface PaletteColorEditorProps { + paletteColorData: Palette[0]; + onUpdateTitle: (params: {oldTitle: string; newTitle: string}) => void; + onDelete: (title: string) => void; +} + +export const PaletteColorEditor: React.FC = ({ + onDelete, + onUpdateTitle, + paletteColorData, +}) => { + const {title, isCustom} = paletteColorData; + + const [localTitle, setLocalTitle] = React.useState(title); + + React.useEffect(() => { + setLocalTitle(title); + }, [title]); + + const handleDelete = React.useCallback(() => onDelete(title), [onDelete, title]); + + const updateTitle = React.useCallback( + (newTitle: string) => onUpdateTitle({oldTitle: title, newTitle}), + [title, onUpdateTitle], + ); + + const debouncedUpdateTitle = React.useMemo(() => debounce(updateTitle, 500), [updateTitle]); + + const handleUpdateTitle = React.useCallback( + (newTitle: string) => { + setLocalTitle(newTitle); + debouncedUpdateTitle(newTitle); + }, + [debouncedUpdateTitle], + ); + + if (!isCustom) { + return ( +
+ {title} +
+ ); + } + + return ( +
+ + Name: + + } + /> +
+ + New color + + +
+
+ ); +}; diff --git a/src/components/Themes/ui/BorderRadiusTab/BorderCard/BorderCard.tsx b/src/components/Themes/ui/BorderRadiusTab/BorderCard/BorderCard.tsx new file mode 100644 index 00000000000..f26562d7f1c --- /dev/null +++ b/src/components/Themes/ui/BorderRadiusTab/BorderCard/BorderCard.tsx @@ -0,0 +1,32 @@ +import {useTranslation} from 'next-i18next'; +import React, {useCallback} from 'react'; +import {SelectableCard} from 'src/components/SelectableCard/SelectableCard'; +import {RADIUS_PRESETS} from 'src/components/Themes/lib/constants'; +import {RadiusPresetName} from 'src/components/Themes/lib/types'; + +export type BorderCardProps = { + preset: RadiusPresetName; + selected: boolean; + onClick: (preset: RadiusPresetName) => void; +}; + +export const BorderCard = ({selected, preset, onClick}: BorderCardProps) => { + const {t} = useTranslation('themes'); + + const handleClick = useCallback(() => { + onClick(preset); + }, [preset]); + + const displayName = t(preset); + const borderRadiusStyle = {borderRadius: RADIUS_PRESETS[preset]?.m + 'px'}; + + return ( + + ); +}; diff --git a/src/components/Themes/ui/BorderRadiusTab/BorderPresets/BorderPresets.tsx b/src/components/Themes/ui/BorderRadiusTab/BorderPresets/BorderPresets.tsx new file mode 100644 index 00000000000..5af16520186 --- /dev/null +++ b/src/components/Themes/ui/BorderRadiusTab/BorderPresets/BorderPresets.tsx @@ -0,0 +1,49 @@ +import {Col, Row} from 'landing-uikit'; +import {useTranslation} from 'next-i18next'; +import React, {useCallback} from 'react'; +import {ChangeRadiusPresetInThemeParams} from 'src/components/Themes/lib/themeCreatorUtils'; +import {RadiusPresetName} from 'src/components/Themes/lib/types'; + +import {ThemeSection} from '../../ThemeSection'; +import {BorderCard, BorderCardProps} from '../BorderCard/BorderCard'; + +const ColCard = (props: BorderCardProps) => ( + + + +); + +const PRESETS_ORDER = [ + RadiusPresetName.Regular, + RadiusPresetName.Circled, + RadiusPresetName.Squared, + RadiusPresetName.Custom, +]; + +export type BorderPresetsProps = { + selectedPreset: RadiusPresetName; + onClick: (preset: ChangeRadiusPresetInThemeParams) => void; +}; + +export const BorderPresets = ({selectedPreset, onClick}: BorderPresetsProps) => { + const {t} = useTranslation('themes'); + + const handleClick = useCallback((preset: RadiusPresetName) => { + onClick({radiusPresetName: preset}); + }, []); + + return ( + + + {PRESETS_ORDER.map((preset) => ( + + ))} + + + ); +}; diff --git a/src/components/Themes/ui/BorderRadiusTab/BorderRadiusTab.scss b/src/components/Themes/ui/BorderRadiusTab/BorderRadiusTab.scss new file mode 100644 index 00000000000..d67246ae836 --- /dev/null +++ b/src/components/Themes/ui/BorderRadiusTab/BorderRadiusTab.scss @@ -0,0 +1,15 @@ +@use '~@gravity-ui/uikit/styles/themes/_index.scss' as themes; +@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; +@use '../../../../variables.scss'; + +$block: '.#{variables.$ns}border-radius-tab'; + +#{$block} { + @include themes.g-theme-common; //restore default uikit styles for components + + gap: calc(var(--g-spacing-base) * 24); + + @media (max-width: map-get(pcVariables.$gridBreakpoints, 'sm')) { + gap: calc(var(--g-spacing-base) * 12); + } +} diff --git a/src/components/Themes/ui/BorderRadiusTab/BorderRadiusTab.tsx b/src/components/Themes/ui/BorderRadiusTab/BorderRadiusTab.tsx new file mode 100644 index 00000000000..d4e32d9610a --- /dev/null +++ b/src/components/Themes/ui/BorderRadiusTab/BorderRadiusTab.tsx @@ -0,0 +1,34 @@ +import {Flex} from 'landing-uikit'; +import React from 'react'; + +import {block} from '../../../../utils'; +import {useThemeCreator, useThemeCreatorMethods} from '../../hooks'; +import {RadiusPresetName} from '../../lib/types'; +import {ExportThemeSection} from '../ExportThemeSection/ExportThemeSection'; + +import {BorderPresets} from './BorderPresets/BorderPresets'; +import './BorderRadiusTab.scss'; +import {ComponentPreview} from './ComponentPreview/ComponentPreview'; +import {CustomRadius} from './CustomRadius/CustomRadius'; + +const b = block('border-radius-tab'); + +export const BorderRadiusTab = () => { + const themeState = useThemeCreator(); + + const {changeRadiusPreset, updateCustomRadiusPreset} = useThemeCreatorMethods(); + + const preset = themeState.borders.preset; + const values = themeState.borders.values; + + return ( + + + {preset === RadiusPresetName.Custom && ( + + )} + + + + ); +}; diff --git a/src/components/Themes/ui/BorderRadiusTab/ComponentPreview/ComponentPreview.tsx b/src/components/Themes/ui/BorderRadiusTab/ComponentPreview/ComponentPreview.tsx new file mode 100644 index 00000000000..430191079f4 --- /dev/null +++ b/src/components/Themes/ui/BorderRadiusTab/ComponentPreview/ComponentPreview.tsx @@ -0,0 +1,23 @@ +import {useTranslation} from 'next-i18next'; +import React from 'react'; + +import {useThemeCreator} from '../../../hooks'; +import {exportTheme} from '../../../lib/themeCreatorExport'; +import {ThemeSection} from '../../ThemeSection'; +import {Showcase} from '../Showcase/Showcase'; + +export const ComponentPreview = () => { + const {t} = useTranslation('themes'); + const themeState = useThemeCreator(); + + const themeStyles = React.useMemo( + () => exportTheme({themeState, ignoreDefaultValues: false}), + [themeState], + ); + + return ( + + + + ); +}; diff --git a/src/components/Themes/ui/BorderRadiusTab/CustomRadius/CustomRadius.scss b/src/components/Themes/ui/BorderRadiusTab/CustomRadius/CustomRadius.scss new file mode 100644 index 00000000000..ac2e41b4dbf --- /dev/null +++ b/src/components/Themes/ui/BorderRadiusTab/CustomRadius/CustomRadius.scss @@ -0,0 +1,28 @@ +@use '~@gravity-ui/uikit/styles/themes/_index.scss' as themes; +@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; +@use '../../../../../variables.scss'; +@use '../../../../../mixins.scss' as baseMixins; + +$root: '.g-root'; +$block: '.#{variables.$ns}custom-radius'; + +// Workaround for missing theme class in ThemeProvider +$workaroundBlockDarkTheme: &#{$block}_theme_dark; + +#{$block} { + &__px { + margin-inline: 8px; + } + + &__radius-input-row { + align-items: center; + input::-webkit-outer-spin-button, + input::-webkit-inner-spin-button { + appearance: none; + margin: 0; + } + input[type='number'] { + appearance: textfield; + } + } +} diff --git a/src/components/Themes/ui/BorderRadiusTab/CustomRadius/CustomRadius.tsx b/src/components/Themes/ui/BorderRadiusTab/CustomRadius/CustomRadius.tsx new file mode 100644 index 00000000000..3f530187ffb --- /dev/null +++ b/src/components/Themes/ui/BorderRadiusTab/CustomRadius/CustomRadius.tsx @@ -0,0 +1,74 @@ +import {Col, Flex, Row, Text, TextInput} from 'landing-uikit'; +import {useTranslation} from 'next-i18next'; +import React, {useCallback, useMemo} from 'react'; +import {UpdateCustomRadiusPresetInThemeParams} from 'src/components/Themes/lib/themeCreatorUtils'; +import {RadiusSizeName, RadiusValue} from 'src/components/Themes/lib/types'; + +import {block} from '../../../../../utils'; +import {ThemeSection} from '../../ThemeSection'; + +import './CustomRadius.scss'; + +const b = block('custom-radius'); + +type RadiusInputProps = { + radiusSizeName: RadiusSizeName; + onUpdate: (param: UpdateCustomRadiusPresetInThemeParams) => void; + value?: string; +}; + +const RadiusInputRow = ({radiusSizeName, onUpdate, value}: RadiusInputProps) => { + const {t} = useTranslation('themes'); + + const text = useMemo(() => t('radius') + ` ${radiusSizeName.toUpperCase()}`, [radiusSizeName]); + + const handleUpdate = useCallback( + (newValue: string) => { + onUpdate({radiusValue: {[radiusSizeName]: newValue}}); + }, + [radiusSizeName], + ); + + return ( + + + {text} + + + + px + + } + /> + + + ); +}; + +type CustomRadiusProps = { + values: RadiusValue; + onUpdate: (param: UpdateCustomRadiusPresetInThemeParams) => void; +}; + +export const CustomRadius = ({onUpdate, values}: CustomRadiusProps) => { + const {t} = useTranslation('themes'); + + return ( + + + + + + + + + + ); +}; diff --git a/src/components/Themes/ui/BorderRadiusTab/Showcase/Showcase.scss b/src/components/Themes/ui/BorderRadiusTab/Showcase/Showcase.scss new file mode 100644 index 00000000000..cc7e8220669 --- /dev/null +++ b/src/components/Themes/ui/BorderRadiusTab/Showcase/Showcase.scss @@ -0,0 +1,27 @@ +@use '~@gravity-ui/uikit/styles/themes/_index.scss' as themes; +@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; +@use '../../../../../variables.scss'; +@use '../../../../../mixins.scss' as baseMixins; + +$root: '.g-root'; +$block: '.#{variables.$ns}border-radius-showcase'; + +#{$block} { + padding: 40px; + border-radius: 24px; + + &__column-transform { + @media (max-width: (map-get(pcVariables.$gridBreakpoints, 'md') - 1)) { + flex-direction: column; + width: 100%; + } + } + + &__text-input-block { + flex-grow: 1; + @media (max-width: (map-get(pcVariables.$gridBreakpoints, 'lg') - 1)) { + flex-direction: column; + width: 100%; + } + } +} diff --git a/src/components/Themes/ui/BorderRadiusTab/Showcase/Showcase.tsx b/src/components/Themes/ui/BorderRadiusTab/Showcase/Showcase.tsx new file mode 100644 index 00000000000..419414e8f89 --- /dev/null +++ b/src/components/Themes/ui/BorderRadiusTab/Showcase/Showcase.tsx @@ -0,0 +1,127 @@ +import {PencilToLine} from 'landing-icons'; +import { + Button, + Flex, + FlexProps, + Label, + RadioButton, + TextInput, + Theme, + ThemeProvider, +} from 'landing-uikit'; +import type {ButtonProps} from 'landing-uikit'; +import {useTranslation} from 'next-i18next'; +import React, {useMemo} from 'react'; + +import {block} from '../../../../../utils'; + +import './Showcase.scss'; + +const b = block('border-radius-showcase'); + +export type ShowcaseProps = { + color?: string; + theme: Theme; + style?: string; +}; + +type ShowcaseBlockProps = FlexProps & { + text: string; +}; + +const BlockWrapper = (props: FlexProps) => ( + + {props.children} + +); +const LabelBlock = (props: ShowcaseBlockProps) => ( + + + + + +); + +const getIconSize = (size: ButtonProps['size']) => { + switch (size) { + case 'xs': + return 12; + case 'xl': + return 20; + default: + return 16; + } +}; + +const ShowcaseButton = ({size, children}: Pick) => { + const iconSize = getIconSize(size); + return ( + + ); +}; + +const ButtonBlock = (props: ShowcaseBlockProps) => ( + + {props.text} + {props.text} + {props.text} + {props.text} + {props.text} + +); + +const RadioButtonBlock = (props: ShowcaseBlockProps) => { + const radioButtonOptions = useMemo( + () => [ + {value: '1', content: props.text}, + {value: '2', content: props.text}, + ], + [], + ); + return ( + + + + + + + ); +}; +const TextInputBlock = (props: ShowcaseBlockProps) => ( + + + + + + +); + +const borderRadiusShowcaseCn = b(); + +export const Showcase: React.FC = ({color, theme, style}) => { + const {t} = useTranslation('themes'); + + return ( + + {style ? : null} + + + + + + + + + + + ); +}; diff --git a/src/components/Themes/ui/BrandColors/BrandColors.scss b/src/components/Themes/ui/BrandColors/BrandColors.scss new file mode 100644 index 00000000000..e3b7f14e213 --- /dev/null +++ b/src/components/Themes/ui/BrandColors/BrandColors.scss @@ -0,0 +1,65 @@ +@use '../../../../variables.scss'; +@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; + +$block: '.#{variables.$ns}brand-colors'; + +#{$block} { + gap: 32px; + + &__brand-color-picker { + display: flex; + gap: 2px; + overflow: auto; + + @media (max-width: map-get(pcVariables.$gridBreakpoints, 'sm')) { + margin: 0 -24px; + padding: 0 24px; + } + } + + &__color { + display: flex; + align-items: center; + justify-content: center; + border: 2px solid transparent; + border-radius: 12px; + padding: 6px; + cursor: pointer; + + &_custom { + --color-value: conic-gradient( + from 180deg at 50% 50%, + #fa00ff -47.18deg, + #ffd028 46.82deg, + #00e6bd 138.38deg, + #6932de 223.7deg, + #fa00ff 312.82deg, + #ffd028 406.82deg + ); + + gap: 8px; + padding-right: 18px; + border-color: rgba(255, 255, 255, 0.15); + } + + &_selected { + border-color: var(--color-value); + + &#{$block}__color_custom { + border-color: rgba(255, 197, 108, 1); + } + } + + &-inner { + background: var(--color-value); + border-radius: 5px; + height: 32px; + width: 32px; + } + } + + &__switch-button { + --g-button-border-radius: 8px; + width: min-content; + } +} diff --git a/src/components/Themes/ui/BrandColors/BrandColors.tsx b/src/components/Themes/ui/BrandColors/BrandColors.tsx new file mode 100644 index 00000000000..e857956124d --- /dev/null +++ b/src/components/Themes/ui/BrandColors/BrandColors.tsx @@ -0,0 +1,104 @@ +import {Sliders} from 'landing-icons'; +import {Button, Flex, Icon, Text} from 'landing-uikit'; +import React from 'react'; + +import {block} from '../../../../utils'; +import {useThemeCreatorMethods, useThemePaletteColor} from '../../hooks'; +import {BRAND_COLORS_PRESETS} from '../../lib/constants'; +import {ThemeSection} from '../ThemeSection'; + +import './BrandColors.scss'; + +const b = block('brand-colors'); + +interface BrandColorsProps { + showThemeEditButton?: boolean; + onEditThemeClick: () => void; + onSelectCustomColor: () => void; +} + +export const BrandColors: React.FC = ({ + showThemeEditButton, + onEditThemeClick, + onSelectCustomColor, +}) => { + const [customModeEnabled, setCustomMode] = React.useState(false); + + const [lightBrandColor] = useThemePaletteColor({ + token: 'brand', + theme: 'light', + }); + const [darkBrandColor] = useThemePaletteColor({ + token: 'brand', + theme: 'dark', + }); + + const {applyBrandPreset} = useThemeCreatorMethods(); + + const activeColorIndex = React.useMemo(() => { + return BRAND_COLORS_PRESETS.findIndex( + (value) => value.brandColor === lightBrandColor && value.brandColor === darkBrandColor, + ); + }, [lightBrandColor, darkBrandColor]); + + const setBrandPreset = React.useCallback( + (index: number) => { + setCustomMode(false); + + if (activeColorIndex === index) { + return; + } + + applyBrandPreset(BRAND_COLORS_PRESETS[index]); + }, + [activeColorIndex, applyBrandPreset], + ); + + const handleSelectCustomColor = React.useCallback(() => { + setCustomMode(true); + onSelectCustomColor(); + }, [onSelectCustomColor]); + + return ( + + +
+ {BRAND_COLORS_PRESETS.map((value, index) => ( +
setBrandPreset(index)} + > +
+
+ ))} +
+
+ Custom +
+
+ + {showThemeEditButton && ( + + )} + + ); +}; diff --git a/src/components/Themes/ui/ColorPickerInput/ColorPickerInput.scss b/src/components/Themes/ui/ColorPickerInput/ColorPickerInput.scss new file mode 100644 index 00000000000..bce7eb6580e --- /dev/null +++ b/src/components/Themes/ui/ColorPickerInput/ColorPickerInput.scss @@ -0,0 +1,34 @@ +@use '../../../../variables.scss'; + +$block: '.#{variables.$ns}color-picker'; + +#{$block} { + --g-border-radius-xl: 8px; + flex-grow: 1; + position: relative; + + &__text-input { + z-index: 1; + } + + &__preview { + margin-inline-start: var(--g-spacing-2); + margin-inline-end: var(--g-spacing-1); + + &_with-border { + border: 1px solid var(--g-color-line-generic); + } + } + + &__input { + width: 35px; + opacity: 0; + padding: 0; + margin: 0; + border: 0; + position: absolute; + bottom: 0; + right: 0; + z-index: 0; + } +} diff --git a/src/components/Themes/ui/ColorPickerInput/ColorPickerInput.tsx b/src/components/Themes/ui/ColorPickerInput/ColorPickerInput.tsx new file mode 100644 index 00000000000..ee69d733e22 --- /dev/null +++ b/src/components/Themes/ui/ColorPickerInput/ColorPickerInput.tsx @@ -0,0 +1,139 @@ +import {Palette} from 'landing-icons'; +import {Button, Flex, Icon, TextInput, TextInputProps} from 'landing-uikit'; +import debounce from 'lodash/debounce'; +import {useTranslation} from 'next-i18next'; +import React, {ChangeEventHandler, useCallback, useEffect, useRef, useState} from 'react'; + +import {block} from '../../../../utils'; +import {ColorPreview} from '../ColorPreview/ColorPreview'; + +import './ColorPickerInput.scss'; +import {NativeColorPicker} from './NativeColorPicker'; +import {getValidColor, isValidColor} from './utils'; + +const b = block('color-picker'); + +export interface ColorPickerInputProps { + defaultValue: string; + name?: string; + value?: string; + onChange: (color: string) => void; + errorMessage?: string; + size?: TextInputProps['size']; + withBorderInPreview?: boolean; +} + +export const ColorPickerInput = ({ + name, + value, + onChange: onChangeExternal, + defaultValue, + errorMessage, + size = 'l', + withBorderInPreview, +}: ColorPickerInputProps) => { + const {t} = useTranslation('themes'); + + const debouncedExternalChange = React.useMemo( + () => debounce(onChangeExternal, 200), + [onChangeExternal], + ); + + const [color, setColor] = useState(() => { + const validColor = getValidColor(defaultValue); + + return validColor ?? ''; + }); + + const [inputValue, setInputValue] = useState(value ?? defaultValue); + const [validationError, setValidationError] = useState(); + + const colorInputRef = useRef(null); + + const validateAndChangeExternal = React.useCallback( + (newValue: string, formatValueToHex = false) => { + if (!isValidColor(newValue)) { + setValidationError('invalid'); + return; + } + + setValidationError(undefined); + + let formattedValue = newValue; + + if (formatValueToHex) { + const validColor = getValidColor(newValue); + if (validColor !== undefined) { + formattedValue = validColor; + } + } + + setInputValue(formattedValue); + setColor(formattedValue); + debouncedExternalChange(formattedValue); + }, + [debouncedExternalChange], + ); + + const onChange: ChangeEventHandler = useCallback((event) => { + const newValue = event.target.value; + setInputValue(newValue); + setValidationError(undefined); + }, []); + + const onNativeInputChange: ChangeEventHandler = useCallback( + (e) => { + const newValue = e.target.value.toUpperCase(); + setInputValue(newValue); + validateAndChangeExternal(newValue, true); + }, + [validateAndChangeExternal], + ); + + const onBlur = useCallback(() => { + validateAndChangeExternal(inputValue); + }, [inputValue, validateAndChangeExternal]); + + useEffect(() => { + // Dont validate if not initial value + if (!value && !defaultValue) { + return; + } + + validateAndChangeExternal(value ?? defaultValue); + }, [value, defaultValue]); + + return ( + + + } + endContent={ + + } + onBlur={onBlur} + /> + + + ); +}; diff --git a/src/components/Themes/ui/ColorPickerInput/NativeColorPicker.tsx b/src/components/Themes/ui/ColorPickerInput/NativeColorPicker.tsx new file mode 100644 index 00000000000..d38b529d500 --- /dev/null +++ b/src/components/Themes/ui/ColorPickerInput/NativeColorPicker.tsx @@ -0,0 +1,35 @@ +import React, {ChangeEventHandler, forwardRef} from 'react'; + +import {block} from '../../../../utils'; + +import './ColorPickerInput.scss'; +import {getValidColor} from './utils'; + +export interface NativeColorPickerProps { + value: string; + onChange: ChangeEventHandler; +} + +const b = block('color-picker__input'); + +export const NativeColorPicker = forwardRef( + ({value, onChange}, ref) => { + const normalizedValue = React.useMemo(() => { + try { + return getValidColor(value); + } catch (_err) { + return value; + } + }, [value]); + + return ( + + ); + }, +); diff --git a/src/components/Themes/ui/ColorPickerInput/utils.ts b/src/components/Themes/ui/ColorPickerInput/utils.ts new file mode 100644 index 00000000000..c2fa475d8f1 --- /dev/null +++ b/src/components/Themes/ui/ColorPickerInput/utils.ts @@ -0,0 +1,45 @@ +import chroma from 'chroma-js'; + +export const hexRegexp = /^#[a-fA-F0-9]{6}$/; +export const rgbRegexp = /^rgb\((\d{1,3}, ?){2}(\d{1,3})\)$/; +export const rgbaRegexp = /^rgba\((\d{1,3}, ?){3}((0(,|\.)[0-9]{1,2})|1)\)$/; + +const numberRegexp = /\b\d+\b/g; + +export const parseRgbStringToHex = (rgbString: string) => { + let hexColor = '#'; + rgbString.match(new RegExp(numberRegexp, 'g'))?.forEach((val) => { + const hex = Number(val).toString(16); + + hexColor += hex?.length === 1 ? `0${hex}` : hex; + }); + + return hexColor; +}; + +export const isValidColor = (textColor: string) => { + try { + chroma(textColor); + return true; + } catch (_err) { + return false; + } +}; + +export const getValidColor = (textColor: string) => { + const testColor = textColor.replaceAll(' ', ''); + + if ( + !testColor || + new RegExp(hexRegexp, 'g').test(testColor) || + new RegExp(rgbaRegexp, 'g').test(testColor) + ) { + return textColor; + } + + if (new RegExp(rgbRegexp, 'g').test(testColor)) { + return chroma(testColor).hex(); + } + + return undefined; +}; diff --git a/src/components/Themes/ui/ColorPreview/ColorPreview.scss b/src/components/Themes/ui/ColorPreview/ColorPreview.scss new file mode 100644 index 00000000000..3ef56f7f9ba --- /dev/null +++ b/src/components/Themes/ui/ColorPreview/ColorPreview.scss @@ -0,0 +1,35 @@ +@use '../../../../variables.scss'; + +$block: '.#{variables.$ns}color-preview'; + +#{$block} { + --chess: rgb(235, 235, 235); + --surface: rgb(255, 255, 255); + --opacity-pattern: repeating-conic-gradient(var(--chess) 0% 25%, var(--surface) 0% 50%) 50% / + 8px 8px; + + width: 16px; + height: 16px; + border-radius: var(--g-border-radius-xs); + overflow: hidden; + position: relative; + + &__color { + position: relative; + width: 100%; + height: 100%; + } + + &_with-opacity { + &::before { + content: ''; + display: block; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: var(--opacity-pattern); + } + } +} diff --git a/src/components/Themes/ui/ColorPreview/ColorPreview.tsx b/src/components/Themes/ui/ColorPreview/ColorPreview.tsx new file mode 100644 index 00000000000..235b8c69100 --- /dev/null +++ b/src/components/Themes/ui/ColorPreview/ColorPreview.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +import {block} from '../../../../utils'; + +import './ColorPreview.scss'; + +export interface ColorPreviewProps { + color?: string; + className?: string; +} + +const b = block('color-preview'); + +const isColorWithOpacity = (color?: string) => !color || color?.startsWith('rgba'); + +export const ColorPreview = ({color, className}: ColorPreviewProps) => { + return ( +
+
+
+ ); +}; diff --git a/src/components/Themes/ui/ColorsTab/ColorsTab.scss b/src/components/Themes/ui/ColorsTab/ColorsTab.scss new file mode 100644 index 00000000000..a2061ec559c --- /dev/null +++ b/src/components/Themes/ui/ColorsTab/ColorsTab.scss @@ -0,0 +1,12 @@ +@use '../../../../variables.scss'; +@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; + +$block: '.#{variables.$ns}colors-tab'; + +#{$block} { + gap: calc(var(--g-spacing-base) * 24); + + @media (max-width: map-get(pcVariables.$gridBreakpoints, 'sm')) { + gap: calc(var(--g-spacing-base) * 12); + } +} diff --git a/src/components/Themes/ui/ColorsTab/ColorsTab.tsx b/src/components/Themes/ui/ColorsTab/ColorsTab.tsx new file mode 100644 index 00000000000..afaaa2e83a4 --- /dev/null +++ b/src/components/Themes/ui/ColorsTab/ColorsTab.tsx @@ -0,0 +1,116 @@ +import {Flex} from 'landing-uikit'; +import {Trans, useTranslation} from 'next-i18next'; +import React from 'react'; + +import {block} from '../../../../utils'; +import {useThemeCreator, useThemeCreatorMethods} from '../../hooks'; +import {BasicPalette} from '../BasicPalette/BasicPalette'; +import {BrandColors} from '../BrandColors/BrandColors'; +import {ComponentPreview} from '../ComponentPreview/ComponentPreview'; +import {ExportThemeSection} from '../ExportThemeSection/ExportThemeSection'; +import {MainSettings} from '../MainSettings/MainSettings'; +import {EditableColorOption, PrivateColorsSettings} from '../PrivateColorsSettings'; + +import './ColorsTab.scss'; + +const b = block('colors-tab'); + +const ADVANCED_COLORS_OPTIONS: EditableColorOption[] = [ + { + title: 'Hovered Brand Color', + name: 'base-brand-hover', + }, + { + title: 'Brand Text', + name: 'text-brand', + }, + { + title: 'Higher Contrast Brand Text', + name: 'text-brand-heavy', + }, + { + title: 'Brand Line Color', + name: 'line-brand', + }, + { + title: 'Selection Background', + name: 'base-selection', + }, + { + title: 'Hovered Selection Background', + name: 'base-selection-hover', + }, +]; + +const ADDITIONAL_COLORS_OPTIONS: EditableColorOption[] = [ + { + title: 'Link', + name: 'text-link', + }, + { + title: 'Hovered Link', + name: 'text-link-hover', + }, + { + title: 'Visited Link', + name: 'text-link-visited', + }, + { + title: 'Hovered Visited Link', + name: 'text-link-visited-hover', + }, +]; + +export const ColorsTab = () => { + const {t} = useTranslation('themes'); + + const {advancedModeEnabled, showMainSettings} = useThemeCreator(); + const {setAdvancedMode, openMainSettings} = useThemeCreatorMethods(); + + const toggleAdvancedMode = React.useCallback( + () => setAdvancedMode(!advancedModeEnabled), + [setAdvancedMode, advancedModeEnabled], + ); + + const handleSelectCustomColor = React.useCallback(() => { + openMainSettings(); + setAdvancedMode(true); + }, [openMainSettings, setAdvancedMode]); + + return ( + + + {showMainSettings && ( + + )} + {advancedModeEnabled && ( + + + +
+ + } + options={ADVANCED_COLORS_OPTIONS} + /> + +
+ )} + + +
+ ); +}; diff --git a/src/components/Themes/ui/ComponentPreview/ComponentPreview.tsx b/src/components/Themes/ui/ComponentPreview/ComponentPreview.tsx new file mode 100644 index 00000000000..093427eda4a --- /dev/null +++ b/src/components/Themes/ui/ComponentPreview/ComponentPreview.tsx @@ -0,0 +1,37 @@ +import {Flex} from 'landing-uikit'; +import {useTranslation} from 'next-i18next'; +import dynamic from 'next/dynamic'; +import React from 'react'; + +import {useThemeCreator} from '../../hooks'; +import {exportTheme} from '../../lib/themeCreatorExport'; +import {ThemeSection} from '../ThemeSection'; + +const Showcase = dynamic( + () => + import('../../../../blocks/Examples/components/Showcase/Showcase').then( + (res) => res.Showcase, + ), + { + ssr: false, + }, +); + +export const ComponentPreview = () => { + const {t} = useTranslation('themes'); + const themeState = useThemeCreator(); + + const themeStyles = React.useMemo( + () => exportTheme({themeState, ignoreDefaultValues: false}), + [themeState], + ); + + return ( + + + + + + + ); +}; diff --git a/src/components/Themes/ui/ExportThemeSection/ExportThemeSection.scss b/src/components/Themes/ui/ExportThemeSection/ExportThemeSection.scss new file mode 100644 index 00000000000..61663003648 --- /dev/null +++ b/src/components/Themes/ui/ExportThemeSection/ExportThemeSection.scss @@ -0,0 +1,11 @@ +@use '../../../../variables.scss'; + +$block: '.#{variables.$ns}export-theme-section'; + +#{$block} { + --g-button-border-radius: 8px; + + &__export-button { + width: min-content; + } +} diff --git a/src/components/Themes/ui/ExportThemeSection/ExportThemeSection.tsx b/src/components/Themes/ui/ExportThemeSection/ExportThemeSection.tsx new file mode 100644 index 00000000000..170a01ebdec --- /dev/null +++ b/src/components/Themes/ui/ExportThemeSection/ExportThemeSection.tsx @@ -0,0 +1,27 @@ +import {ArrowUpFromSquare} from 'landing-icons'; +import {Button, Icon} from 'landing-uikit'; +import {useTranslation} from 'next-i18next'; +import React from 'react'; + +import {block} from '../../../../utils'; +import {ThemeExportDialog} from '../ThemeExportDialog/ThemeExportDialog'; +import {ThemeSection} from '../ThemeSection'; + +import './ExportThemeSection.scss'; + +const b = block('export-theme-section'); + +export const ExportThemeSection = () => { + const {t} = useTranslation('themes'); + const [isDialogVisible, toggleDialog] = React.useReducer((isOpen) => !isOpen, false); + + return ( + + + + + ); +}; diff --git a/src/components/Themes/ui/MainSettings/MainSettings.scss b/src/components/Themes/ui/MainSettings/MainSettings.scss new file mode 100644 index 00000000000..1acbadbdd64 --- /dev/null +++ b/src/components/Themes/ui/MainSettings/MainSettings.scss @@ -0,0 +1,28 @@ +@use '../../../../variables.scss'; +@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; + +$block: '.#{variables.$ns}main-settings'; + +#{$block} { + --g-button-border-radius: 8px; + --g-text-input-border-radius: 8px; + + &__switch-button { + margin-top: var(--g-spacing-3); + width: min-content; + } + + &__text-card { + width: 50%; + } + + &__text-contrast-title { + height: 80px; + display: flex; + align-items: center; + + @media (max-width: map-get(pcVariables.$gridBreakpoints, 'md')) { + height: auto; + } + } +} diff --git a/src/components/Themes/ui/MainSettings/MainSettings.tsx b/src/components/Themes/ui/MainSettings/MainSettings.tsx new file mode 100644 index 00000000000..a18e7a093d5 --- /dev/null +++ b/src/components/Themes/ui/MainSettings/MainSettings.tsx @@ -0,0 +1,152 @@ +import {Sliders} from 'landing-icons'; +import {Button, Flex, Icon, Text} from 'landing-uikit'; +import {useTranslation} from 'next-i18next'; +import React from 'react'; + +import {block} from '../../../../utils'; +import {SelectableCard} from '../../../SelectableCard/SelectableCard'; +import {useThemePaletteColor, useThemeUtilityColor} from '../../hooks'; +import {TEXT_CONTRAST_COLORS} from '../../lib/constants'; +import type {ColorsOptions, ThemeVariant} from '../../lib/types'; +import {ColorPickerInput} from '../ColorPickerInput/ColorPickerInput'; +import {ThemableSettings} from '../ThemableSettings/ThemableSettings'; +import {ThemableRow} from '../ThemableSettings/types'; +import {ThemeSection} from '../ThemeSection'; + +import './MainSettings.scss'; + +const b = block('main-settings'); + +const BASE_CARD_BUTTON_STYLES = { + borderRadius: '8px', + padding: '10px 16px', + height: 'auto', + width: 'auto', +}; + +interface ThemeUtilityColorEditorProps { + name: keyof ColorsOptions; + theme: ThemeVariant; +} + +const ThemeUtilityColorEditor: React.FC = ({name, theme}) => { + const [color, setColor] = useThemeUtilityColor({ + name, + theme, + }); + + return ( + + ); +}; + +const BrandColorEditor: React.FC<{theme: ThemeVariant}> = ({theme}) => { + const [brandColor, setBrandColor] = useThemePaletteColor({token: 'brand', theme}); + + return ( + + ); +}; + +const TextContrastColorEditor: React.FC<{theme: ThemeVariant}> = ({theme}) => { + const [brandTextColor, setBrandTextColor] = useThemeUtilityColor({ + name: 'text-brand-contrast', + theme, + }); + + const [brandColor] = useThemePaletteColor({token: 'brand', theme}); + + return ( + + setBrandTextColor(TEXT_CONTRAST_COLORS[theme].black)} + textProps={{ + style: { + ...BASE_CARD_BUTTON_STYLES, + color: TEXT_CONTRAST_COLORS[theme].black, + backgroundColor: brandColor, + }, + }} + /> + setBrandTextColor(TEXT_CONTRAST_COLORS[theme].white)} + textProps={{ + style: { + ...BASE_CARD_BUTTON_STYLES, + color: TEXT_CONTRAST_COLORS[theme].white, + backgroundColor: brandColor, + }, + }} + /> + + ); +}; + +interface MainSettingsProps { + advancedModeEnabled: boolean; + toggleAdvancedMode: () => void; +} + +export const MainSettings: React.FC = ({ + advancedModeEnabled, + toggleAdvancedMode, +}) => { + const {t} = useTranslation('themes'); + + const rows = React.useMemo(() => { + return [ + { + id: 'base-background', + title: t('page_background'), + render: (theme) => , + }, + { + id: 'brand', + title: t('brand_color'), + render: (theme) => , + }, + { + id: 'text-brand-contrast', + title: 'Text on Brand', + render: (theme) => , + renderTitle: () => ( +
+ Text on Brand +
+ ), + }, + ]; + }, [t]); + + return ( + + + + {advancedModeEnabled ? t('hide_advanced_settings') : t('advanced_settings')} + + } + /> + + ); +}; diff --git a/src/components/Themes/ui/PreviewTab/CardsPreview/CardsPreview.scss b/src/components/Themes/ui/PreviewTab/CardsPreview/CardsPreview.scss new file mode 100644 index 00000000000..bce0faaed4e --- /dev/null +++ b/src/components/Themes/ui/PreviewTab/CardsPreview/CardsPreview.scss @@ -0,0 +1,26 @@ +@use '../../../../../variables'; +@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; +@use '~@gravity-ui/uikit/styles/themes/_index.scss' as themes; + +$block: '.#{variables.$ns}cards-preview'; + +#{$block} { + width: 100%; + height: 100%; + overflow: auto; + + &__card { + width: 295px; + height: 430px; + padding: var(--g-spacing-1) var(--g-spacing-1) 0; + + &__content { + padding: var(--g-spacing-5) var(--g-spacing-4); + height: 100%; + + &__footer { + margin-block-start: auto; + } + } + } +} diff --git a/src/components/Themes/ui/PreviewTab/CardsPreview/CardsPreview.tsx b/src/components/Themes/ui/PreviewTab/CardsPreview/CardsPreview.tsx new file mode 100644 index 00000000000..049b146692f --- /dev/null +++ b/src/components/Themes/ui/PreviewTab/CardsPreview/CardsPreview.tsx @@ -0,0 +1,59 @@ +import {Card, Flex, Text, User} from 'landing-uikit'; +import React from 'react'; + +import avatar1Asset from '../../../../../assets/avatar-1.png'; +import {block} from '../../../../../utils'; +import {cardData} from '../constants'; + +import './CardsPreview.scss'; + +const b = block('cards-preview'); + +const PreviewCard = ({ + imgSrc, + title, + text, + date, + user, +}: { + imgSrc: string; + title: string; + text: string; + date: string; + user: string; +}) => { + return ( + + + {user} + + + {title} + {text} + + + {date} + + + + + + ); +}; + +export const CardsPreview = ({justify}: {justify: string}) => { + return ( + + Cards + + {cardData.map((card, index) => ( + + ))} + + + ); +}; diff --git a/src/components/Themes/ui/PreviewTab/DashboardsPreview/DashboardPreview.scss b/src/components/Themes/ui/PreviewTab/DashboardsPreview/DashboardPreview.scss new file mode 100644 index 00000000000..6f59ea1c54b --- /dev/null +++ b/src/components/Themes/ui/PreviewTab/DashboardsPreview/DashboardPreview.scss @@ -0,0 +1,18 @@ +@use '../../../../../variables'; +@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; +@use '~@gravity-ui/uikit/styles/themes/_index.scss' as themes; + +$block: '.#{variables.$ns}dashboards-preview'; + +#{$block} { + overflow-y: auto; + + &__card { + flex: 1; + padding: var(--g-spacing-5); + } + + &__dashboard-wrapper { + height: 230px; + } +} diff --git a/src/components/Themes/ui/PreviewTab/DashboardsPreview/DashboardPreview.tsx b/src/components/Themes/ui/PreviewTab/DashboardsPreview/DashboardPreview.tsx new file mode 100644 index 00000000000..10d420d6cca --- /dev/null +++ b/src/components/Themes/ui/PreviewTab/DashboardsPreview/DashboardPreview.tsx @@ -0,0 +1,80 @@ +import ChartKit, {settings} from '@gravity-ui/chartkit'; +import {D3Plugin} from '@gravity-ui/chartkit/d3'; +import {Card, Col, Container, Flex, Row, Text} from 'landing-uikit'; +import React, {PropsWithChildren} from 'react'; + +import {block} from '../../../../../utils'; +import { + areaDashboardData, + barXDashboardData, + dotsDashboardData, + linesDashboardData, + pieDashboardData, +} from '../constants'; + +import './DashboardPreview.scss'; + +interface StyleCardProps extends PropsWithChildren {} + +settings.set({plugins: [D3Plugin]}); + +const b = block('dashboards-preview'); + +const StyledCard = ({children}: StyleCardProps) => { + return ( + + {children} + + ); +}; + +export const DashboardPreview = ({justify}: {justify: string}) => { + return ( + + Dashboard + + + + About + + + A dashboard is a visual representation of key performance indicators + (KPIs) that helps businesses monitor and analyze their performance in + real time. It typically includes graphs, charts, and tables that + summarize data from multiple sources, such as financial reports, + customer surveys, and operational metrics. Dashboards allow businesses + to quickly identify trends and opportunities for improvement, making + them a valuable tool for decision-making and strategy development. + + + + + + {[barXDashboardData, linesDashboardData, areaDashboardData].map( + (data, index) => ( + + +
+ +
+
+ + ), + )} +
+ + {[pieDashboardData, dotsDashboardData].map((data, index) => ( + + +
+ +
+
+ + ))} +
+
+
+
+ ); +}; diff --git a/src/components/Themes/ui/PreviewTab/FormPreview/FormPreview.scss b/src/components/Themes/ui/PreviewTab/FormPreview/FormPreview.scss new file mode 100644 index 00000000000..203467e4b85 --- /dev/null +++ b/src/components/Themes/ui/PreviewTab/FormPreview/FormPreview.scss @@ -0,0 +1,13 @@ +@use '../../../../../variables'; +@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; +@use '~@gravity-ui/uikit/styles/themes/_index.scss' as themes; + +$block: '.#{variables.$ns}form-preview'; + +#{$block} { + overflow: auto; + + &__wrapper { + width: 600px; + } +} diff --git a/src/components/Themes/ui/PreviewTab/FormPreview/FormPreview.tsx b/src/components/Themes/ui/PreviewTab/FormPreview/FormPreview.tsx new file mode 100644 index 00000000000..6fc8e375de0 --- /dev/null +++ b/src/components/Themes/ui/PreviewTab/FormPreview/FormPreview.tsx @@ -0,0 +1,82 @@ +import {FormRow} from '@gravity-ui/components'; +import {ArrowLeft} from 'landing-icons'; +import {Button, Flex, Icon, Select, Text, TextArea} from 'landing-uikit'; +import React from 'react'; + +import {block} from '../../../../../utils'; +import {labels, projects, users} from '../constants'; + +import './FormPreview.scss'; + +const b = block('form-preview'); + +export const FormPreview = ({justify}: {justify: string}) => { + return ( + + + User edit + + + ({ + value: pr, + content: pr, + }))} + disablePortal={true} + /> + or + + + + +