diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 92f6d4bb96..cbe7625420 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -78,6 +78,10 @@ jobs: POST_ID=$(wp-env run tests-cli wp post create wp-content/plugins/Stackable/e2e/config/post-content.txt --post_title="Existing Blocks" --post_status=publish --porcelain) echo "WP_TEST_POSTID=$POST_ID" >> $GITHUB_ENV continue-on-error: true + - name: Disable guided tours + run: | + TOUR_STATES='["design-system", "editor", "design-library", "blocks"]' + wp-env run tests-cli wp option update stackable_guided_tour_states "$TOUR_STATES" --format=json - name: Run playwright tests id: run-playwright-tests env: diff --git a/package-lock.json b/package-lock.json index e909d1c5f8..258772fc75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,9 @@ "license": "GPL-3.0", "dependencies": { "@wordpress/dom-ready": "^3.2.3", - "@wordpress/icons": "^6.1.1", + "@wordpress/icons": "^10.27.0", "bigpicture": "^2.5.3", + "canvas-confetti": "^1.9.3", "classnames": "^2.2.6", "color-rgba": "^2.2.3", "compare-versions": "^3.6.0", @@ -19,6 +20,7 @@ "deepmerge": "^3.3.0", "fast-deep-equal": "^3.1.3", "glightbox": "^3.2.0", + "html-to-image": "^1.11.13", "is-dark-color": "^1.2.0", "md5": "^2.3.0", "prop-types": "^15.7.2", @@ -1836,11 +1838,11 @@ } }, "node_modules/@babel/runtime": { - "version": "7.16.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz", - "integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz", + "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==", "dependencies": { - "regenerator-runtime": "^0.13.4" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" @@ -1859,6 +1861,11 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/runtime/node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/@babel/template": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.0.tgz", @@ -4964,9 +4971,9 @@ "dev": true }, "node_modules/@types/prop-types": { - "version": "15.7.4", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", - "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==" + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==" }, "node_modules/@types/q": { "version": "1.5.5", @@ -4975,21 +4982,23 @@ "dev": true }, "node_modules/@types/react": { - "version": "16.14.21", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.21.tgz", - "integrity": "sha512-rY4DzPKK/4aohyWiDRHS2fotN5rhBSK6/rz1X37KzNna9HJyqtaGAbq9fVttrEPWF5ywpfIP1ITL8Xi2QZn6Eg==", + "version": "17.0.87", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.87.tgz", + "integrity": "sha512-wpg9AbtJ6agjA+BKYmhG6dRWEU/2DHYwMzCaBzsz137ft6IyuqZ5fI4ic1DWL4DrI03Zy78IyVE6ucrXl0mu4g==", + "dev": true, "dependencies": { "@types/prop-types": "*", - "@types/scheduler": "*", + "@types/scheduler": "^0.16", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "16.9.14", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.14.tgz", - "integrity": "sha512-FIX2AVmPTGP30OUJ+0vadeIFJJ07Mh1m+U0rxfgyW34p3rTlXI+nlenvAxNn4BP36YyI9IJ/+UJ7Wu22N1pI7A==", - "dependencies": { - "@types/react": "^16" + "version": "17.0.26", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.26.tgz", + "integrity": "sha512-Z+2VcYXJwOqQ79HreLU/1fyQ88eXSSFh6I3JdrEHQIfYSI0kCQpTGvOrbE6jFGGYXKsHuwY9tBa/w5Uo6KzrEg==", + "dev": true, + "peerDependencies": { + "@types/react": "^17.0.0" } }, "node_modules/@types/responselike": { @@ -5002,9 +5011,10 @@ } }, "node_modules/@types/scheduler": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", + "dev": true }, "node_modules/@types/source-list-map": { "version": "0.1.2", @@ -5736,6 +5746,20 @@ "node": ">=12" } }, + "node_modules/@wordpress/block-editor/node_modules/@wordpress/icons": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-6.3.0.tgz", + "integrity": "sha512-Vliw7QsFuTsrA05GZov4i3PQiLQOGO97PR2keUeY53fVZdeoJKv/nfDqOZxZCIts5jR2Mfje6P6hc/KlurxsKg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.16.0", + "@wordpress/element": "^4.1.1", + "@wordpress/primitives": "^3.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@wordpress/block-serialization-default-parser": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/@wordpress/block-serialization-default-parser/-/block-serialization-default-parser-4.2.3.tgz", @@ -5841,6 +5865,20 @@ "reakit-utils": "^0.15.1" } }, + "node_modules/@wordpress/components/node_modules/@wordpress/icons": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-6.3.0.tgz", + "integrity": "sha512-Vliw7QsFuTsrA05GZov4i3PQiLQOGO97PR2keUeY53fVZdeoJKv/nfDqOZxZCIts5jR2Mfje6P6hc/KlurxsKg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.16.0", + "@wordpress/element": "^4.1.1", + "@wordpress/primitives": "^3.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@wordpress/compose": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/@wordpress/compose/-/compose-5.0.6.tgz", @@ -6008,26 +6046,38 @@ } }, "node_modules/@wordpress/element": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-4.0.4.tgz", - "integrity": "sha512-GbYVSZrHitOmupQCjb7cSlewVigXHorpZUBpvWnkU3rhyh1tF/N9qve3fgg7Q3s2szjtTP+eEutB+4mmkwHQOA==", + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-4.20.0.tgz", + "integrity": "sha512-Ou7EoGtGe4FUL6fKALINXJLKoSfyWTBJzkJfN2HzSgM1wira9EuWahl8MQN0HAUaWeOoDqMKPvnglfS+kC8JLA==", + "dev": true, "dependencies": { "@babel/runtime": "^7.16.0", - "@types/react": "^16.9.0", - "@types/react-dom": "^16.9.0", - "@wordpress/escape-html": "^2.2.3", - "lodash": "^4.17.21", - "react": "^17.0.1", - "react-dom": "^17.0.1" + "@types/react": "^17.0.37", + "@types/react-dom": "^17.0.11", + "@wordpress/escape-html": "^2.22.0", + "change-case": "^4.1.2", + "is-plain-object": "^5.0.0", + "react": "^17.0.2", + "react-dom": "^17.0.2" }, "engines": { "node": ">=12" } }, + "node_modules/@wordpress/element/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@wordpress/escape-html": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-2.2.3.tgz", - "integrity": "sha512-nYIwT8WzHfAzjjwHLiwDQWrzn4/gUNr5zud465XQszM2cAItN2wnC26/ovSpPomDGkvjcG0YltgnSqc1T62olA==", + "version": "2.58.0", + "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-2.58.0.tgz", + "integrity": "sha512-9YJXMNfzkrhHEVP1jFEhgijbZqW8Mt3NHIMZjIQoWtBf7QE86umpYpGGBXzYC0YlpGTRGzZTBwYaqFKxjeaSgA==", + "dev": true, "dependencies": { "@babel/runtime": "^7.16.0" }, @@ -6153,16 +6203,121 @@ } }, "node_modules/@wordpress/icons": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-6.1.1.tgz", - "integrity": "sha512-UaFAOF8hqlEhjTm5kba0JwSDDeEgPSJToDJNADoz8jkxt22kEG5ACi9IaS0BRIy1X7kR6QaCE394v9+GkToE+g==", + "version": "10.27.0", + "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-10.27.0.tgz", + "integrity": "sha512-KeOz3aLtd7p+cA287gmGzpC9kIO1lxPBn/lDPkXfc8oz482XqNJKohdW/7ZMlEWx1uEcZUI+g3vfSA+gKDgjUQ==", "dependencies": { - "@babel/runtime": "^7.16.0", - "@wordpress/element": "^4.0.4", - "@wordpress/primitives": "^3.0.4" + "@babel/runtime": "7.25.7", + "@wordpress/element": "^6.27.0", + "@wordpress/primitives": "^4.27.0" }, "engines": { - "node": ">=12" + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/icons/node_modules/@types/react": { + "version": "18.3.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", + "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@wordpress/icons/node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@wordpress/icons/node_modules/@wordpress/element": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-6.27.0.tgz", + "integrity": "sha512-gHk4B0J0f7bEsDoUBdTm22vPQwmEWLZxyaojgRyx1ncE2IyktfmubD/q2NIcMEKh7p+Jq3ZUwzPcpchpvkH2mA==", + "dependencies": { + "@babel/runtime": "7.25.7", + "@types/react": "^18.2.79", + "@types/react-dom": "^18.2.25", + "@wordpress/escape-html": "^3.27.0", + "change-case": "^4.1.2", + "is-plain-object": "^5.0.0", + "react": "^18.3.0", + "react-dom": "^18.3.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/icons/node_modules/@wordpress/escape-html": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-3.27.0.tgz", + "integrity": "sha512-1LBB/xOFBUySSmVpd2nFwIZ8fVnP8dLNFl0wLprHVLtW6ZcdykO2ITY9bkaHu2lZ9HLRgHL7A/3R7MsJ1azYkg==", + "dependencies": { + "@babel/runtime": "7.25.7" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/icons/node_modules/@wordpress/primitives": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-4.27.0.tgz", + "integrity": "sha512-ZIhpB4ZmZwMSsrELx4mzhRvxAoqgk8sSE3PaRt/ue4GXFoRRQgI3RVCwEdiNPcsQXId9lOQIhAJNDt5Wa0Fbgg==", + "dependencies": { + "@babel/runtime": "7.25.7", + "@wordpress/element": "^6.27.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/@wordpress/icons/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@wordpress/icons/node_modules/react": { + "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" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@wordpress/icons/node_modules/react-dom": { + "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.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/@wordpress/icons/node_modules/scheduler": { + "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" } }, "node_modules/@wordpress/is-shallow-equal": { @@ -6295,18 +6450,100 @@ } }, "node_modules/@wordpress/primitives": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-3.0.4.tgz", - "integrity": "sha512-yu3BEpr09vpPM0QOYGm5Kmwo/tfo7u7Ez4hN5+AL2dT53VNr3QOmDo0Ym7sewI7+GgU18H4VkAi1QOydrc4vDw==", + "version": "3.56.0", + "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-3.56.0.tgz", + "integrity": "sha512-NXBq1ODjl6inMWx/l7KCbATcjdoeIOqYeL9i9alqdAfWeKx1EH9PIvKWylIkqZk7erXxCxldiRkuyjTtwjNBxw==", + "dev": true, "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/element": "^4.0.4", - "classnames": "^2.3.1" + "@wordpress/element": "^5.35.0", + "clsx": "^2.1.1" }, "engines": { "node": ">=12" } }, + "node_modules/@wordpress/primitives/node_modules/@types/react": { + "version": "18.3.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", + "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@wordpress/primitives/node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@wordpress/primitives/node_modules/@wordpress/element": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-5.35.0.tgz", + "integrity": "sha512-puswpGcIdS+0A2g28uHriMkZqqRCmzFczue5Tk99VNtzBdehyk7Ae+DZ4xw5yT6GqYai8NTqv6MRwCB78uh5Mw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.16.0", + "@types/react": "^18.2.79", + "@types/react-dom": "^18.2.25", + "@wordpress/escape-html": "^2.58.0", + "change-case": "^4.1.2", + "is-plain-object": "^5.0.0", + "react": "^18.3.0", + "react-dom": "^18.3.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@wordpress/primitives/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@wordpress/primitives/node_modules/react": { + "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==", + "dev": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@wordpress/primitives/node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dev": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/@wordpress/primitives/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/@wordpress/priority-queue": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@wordpress/priority-queue/-/priority-queue-2.2.3.tgz", @@ -9574,7 +9811,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", - "dev": true, "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" @@ -9650,11 +9886,19 @@ } ] }, + "node_modules/canvas-confetti": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.3.tgz", + "integrity": "sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==", + "funding": { + "type": "donate", + "url": "https://www.paypal.me/kirilvatev" + } + }, "node_modules/capital-case": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", - "dev": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", @@ -9706,7 +9950,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", - "dev": true, "dependencies": { "camel-case": "^4.1.2", "capital-case": "^1.0.4", @@ -10352,6 +10595,14 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -10787,7 +11038,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", - "dev": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", @@ -12409,7 +12659,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dev": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -17099,7 +17348,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", - "dev": true, "dependencies": { "capital-case": "^1.0.4", "tslib": "^2.0.3" @@ -17195,6 +17443,11 @@ "node": ">=8" } }, + "node_modules/html-to-image": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", + "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==" + }, "node_modules/htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -23989,7 +24242,8 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "node_modules/lodash-es": { "version": "4.17.21", @@ -24269,7 +24523,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dev": true, "dependencies": { "tslib": "^2.0.3" } @@ -25450,7 +25703,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" @@ -26555,7 +26807,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", - "dev": true, "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -26666,7 +26917,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "dev": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -26685,7 +26935,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", - "dev": true, "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -29423,7 +29672,8 @@ "node_modules/regenerator-runtime": { "version": "0.13.9", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", + "dev": true }, "node_modules/regenerator-transform": { "version": "0.14.5", @@ -30427,7 +30677,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", - "dev": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", @@ -30679,7 +30928,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", - "dev": true, "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -32890,8 +33138,7 @@ "node_modules/tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -33355,7 +33602,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", - "dev": true, "dependencies": { "tslib": "^2.0.3" } @@ -33364,7 +33610,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", - "dev": true, "dependencies": { "tslib": "^2.0.3" } @@ -35828,11 +36073,18 @@ } }, "@babel/runtime": { - "version": "7.16.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz", - "integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz", + "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==", "requires": { - "regenerator-runtime": "^0.13.4" + "regenerator-runtime": "^0.14.0" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + } } }, "@babel/runtime-corejs3": { @@ -38290,9 +38542,9 @@ "dev": true }, "@types/prop-types": { - "version": "15.7.4", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", - "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==" + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==" }, "@types/q": { "version": "1.5.5", @@ -38301,22 +38553,22 @@ "dev": true }, "@types/react": { - "version": "16.14.21", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.21.tgz", - "integrity": "sha512-rY4DzPKK/4aohyWiDRHS2fotN5rhBSK6/rz1X37KzNna9HJyqtaGAbq9fVttrEPWF5ywpfIP1ITL8Xi2QZn6Eg==", + "version": "17.0.87", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.87.tgz", + "integrity": "sha512-wpg9AbtJ6agjA+BKYmhG6dRWEU/2DHYwMzCaBzsz137ft6IyuqZ5fI4ic1DWL4DrI03Zy78IyVE6ucrXl0mu4g==", + "dev": true, "requires": { "@types/prop-types": "*", - "@types/scheduler": "*", + "@types/scheduler": "^0.16", "csstype": "^3.0.2" } }, "@types/react-dom": { - "version": "16.9.14", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.14.tgz", - "integrity": "sha512-FIX2AVmPTGP30OUJ+0vadeIFJJ07Mh1m+U0rxfgyW34p3rTlXI+nlenvAxNn4BP36YyI9IJ/+UJ7Wu22N1pI7A==", - "requires": { - "@types/react": "^16" - } + "version": "17.0.26", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.26.tgz", + "integrity": "sha512-Z+2VcYXJwOqQ79HreLU/1fyQ88eXSSFh6I3JdrEHQIfYSI0kCQpTGvOrbE6jFGGYXKsHuwY9tBa/w5Uo6KzrEg==", + "dev": true, + "requires": {} }, "@types/responselike": { "version": "1.0.0", @@ -38328,9 +38580,10 @@ } }, "@types/scheduler": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", + "dev": true }, "@types/source-list-map": { "version": "0.1.2", @@ -38927,6 +39180,19 @@ "redux-multi": "^0.1.12", "rememo": "^3.0.0", "traverse": "^0.6.6" + }, + "dependencies": { + "@wordpress/icons": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-6.3.0.tgz", + "integrity": "sha512-Vliw7QsFuTsrA05GZov4i3PQiLQOGO97PR2keUeY53fVZdeoJKv/nfDqOZxZCIts5jR2Mfje6P6hc/KlurxsKg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.16.0", + "@wordpress/element": "^4.1.1", + "@wordpress/primitives": "^3.1.1" + } + } } }, "@wordpress/block-serialization-default-parser": { @@ -39017,6 +39283,19 @@ "reakit": "^1.3.8", "rememo": "^3.0.0", "uuid": "^8.3.0" + }, + "dependencies": { + "@wordpress/icons": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-6.3.0.tgz", + "integrity": "sha512-Vliw7QsFuTsrA05GZov4i3PQiLQOGO97PR2keUeY53fVZdeoJKv/nfDqOZxZCIts5jR2Mfje6P6hc/KlurxsKg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.16.0", + "@wordpress/element": "^4.1.1", + "@wordpress/primitives": "^3.1.1" + } + } } }, "@wordpress/compose": { @@ -39145,23 +39424,34 @@ } }, "@wordpress/element": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-4.0.4.tgz", - "integrity": "sha512-GbYVSZrHitOmupQCjb7cSlewVigXHorpZUBpvWnkU3rhyh1tF/N9qve3fgg7Q3s2szjtTP+eEutB+4mmkwHQOA==", + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-4.20.0.tgz", + "integrity": "sha512-Ou7EoGtGe4FUL6fKALINXJLKoSfyWTBJzkJfN2HzSgM1wira9EuWahl8MQN0HAUaWeOoDqMKPvnglfS+kC8JLA==", + "dev": true, "requires": { "@babel/runtime": "^7.16.0", - "@types/react": "^16.9.0", - "@types/react-dom": "^16.9.0", - "@wordpress/escape-html": "^2.2.3", - "lodash": "^4.17.21", - "react": "^17.0.1", - "react-dom": "^17.0.1" + "@types/react": "^17.0.37", + "@types/react-dom": "^17.0.11", + "@wordpress/escape-html": "^2.22.0", + "change-case": "^4.1.2", + "is-plain-object": "^5.0.0", + "react": "^17.0.2", + "react-dom": "^17.0.2" + }, + "dependencies": { + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true + } } }, "@wordpress/escape-html": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-2.2.3.tgz", - "integrity": "sha512-nYIwT8WzHfAzjjwHLiwDQWrzn4/gUNr5zud465XQszM2cAItN2wnC26/ovSpPomDGkvjcG0YltgnSqc1T62olA==", + "version": "2.58.0", + "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-2.58.0.tgz", + "integrity": "sha512-9YJXMNfzkrhHEVP1jFEhgijbZqW8Mt3NHIMZjIQoWtBf7QE86umpYpGGBXzYC0YlpGTRGzZTBwYaqFKxjeaSgA==", + "dev": true, "requires": { "@babel/runtime": "^7.16.0" } @@ -39248,13 +39538,93 @@ } }, "@wordpress/icons": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-6.1.1.tgz", - "integrity": "sha512-UaFAOF8hqlEhjTm5kba0JwSDDeEgPSJToDJNADoz8jkxt22kEG5ACi9IaS0BRIy1X7kR6QaCE394v9+GkToE+g==", + "version": "10.27.0", + "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-10.27.0.tgz", + "integrity": "sha512-KeOz3aLtd7p+cA287gmGzpC9kIO1lxPBn/lDPkXfc8oz482XqNJKohdW/7ZMlEWx1uEcZUI+g3vfSA+gKDgjUQ==", "requires": { - "@babel/runtime": "^7.16.0", - "@wordpress/element": "^4.0.4", - "@wordpress/primitives": "^3.0.4" + "@babel/runtime": "7.25.7", + "@wordpress/element": "^6.27.0", + "@wordpress/primitives": "^4.27.0" + }, + "dependencies": { + "@types/react": { + "version": "18.3.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", + "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", + "requires": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "requires": {} + }, + "@wordpress/element": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-6.27.0.tgz", + "integrity": "sha512-gHk4B0J0f7bEsDoUBdTm22vPQwmEWLZxyaojgRyx1ncE2IyktfmubD/q2NIcMEKh7p+Jq3ZUwzPcpchpvkH2mA==", + "requires": { + "@babel/runtime": "7.25.7", + "@types/react": "^18.2.79", + "@types/react-dom": "^18.2.25", + "@wordpress/escape-html": "^3.27.0", + "change-case": "^4.1.2", + "is-plain-object": "^5.0.0", + "react": "^18.3.0", + "react-dom": "^18.3.0" + } + }, + "@wordpress/escape-html": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-3.27.0.tgz", + "integrity": "sha512-1LBB/xOFBUySSmVpd2nFwIZ8fVnP8dLNFl0wLprHVLtW6ZcdykO2ITY9bkaHu2lZ9HLRgHL7A/3R7MsJ1azYkg==", + "requires": { + "@babel/runtime": "7.25.7" + } + }, + "@wordpress/primitives": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-4.27.0.tgz", + "integrity": "sha512-ZIhpB4ZmZwMSsrELx4mzhRvxAoqgk8sSE3PaRt/ue4GXFoRRQgI3RVCwEdiNPcsQXId9lOQIhAJNDt5Wa0Fbgg==", + "requires": { + "@babel/runtime": "7.25.7", + "@wordpress/element": "^6.27.0", + "clsx": "^2.1.1" + } + }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + }, + "react": { + "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==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + } + }, + "scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "requires": { + "loose-envify": "^1.1.0" + } + } } }, "@wordpress/is-shallow-equal": { @@ -39352,13 +39722,83 @@ "dev": true }, "@wordpress/primitives": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-3.0.4.tgz", - "integrity": "sha512-yu3BEpr09vpPM0QOYGm5Kmwo/tfo7u7Ez4hN5+AL2dT53VNr3QOmDo0Ym7sewI7+GgU18H4VkAi1QOydrc4vDw==", + "version": "3.56.0", + "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-3.56.0.tgz", + "integrity": "sha512-NXBq1ODjl6inMWx/l7KCbATcjdoeIOqYeL9i9alqdAfWeKx1EH9PIvKWylIkqZk7erXxCxldiRkuyjTtwjNBxw==", + "dev": true, "requires": { "@babel/runtime": "^7.16.0", - "@wordpress/element": "^4.0.4", - "classnames": "^2.3.1" + "@wordpress/element": "^5.35.0", + "clsx": "^2.1.1" + }, + "dependencies": { + "@types/react": { + "version": "18.3.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", + "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "requires": {} + }, + "@wordpress/element": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-5.35.0.tgz", + "integrity": "sha512-puswpGcIdS+0A2g28uHriMkZqqRCmzFczue5Tk99VNtzBdehyk7Ae+DZ4xw5yT6GqYai8NTqv6MRwCB78uh5Mw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.16.0", + "@types/react": "^18.2.79", + "@types/react-dom": "^18.2.25", + "@wordpress/escape-html": "^2.58.0", + "change-case": "^4.1.2", + "is-plain-object": "^5.0.0", + "react": "^18.3.0", + "react-dom": "^18.3.0" + } + }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true + }, + "react": { + "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==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + } + }, + "scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0" + } + } } }, "@wordpress/priority-queue": { @@ -41837,7 +42277,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", - "dev": true, "requires": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" @@ -41886,11 +42325,15 @@ "integrity": "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==", "dev": true }, + "canvas-confetti": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.3.tgz", + "integrity": "sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==" + }, "capital-case": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", - "dev": true, "requires": { "no-case": "^3.0.4", "tslib": "^2.0.3", @@ -41933,7 +42376,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", - "dev": true, "requires": { "camel-case": "^4.1.2", "capital-case": "^1.0.4", @@ -42437,6 +42879,11 @@ } } }, + "clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -42824,7 +43271,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", - "dev": true, "requires": { "no-case": "^3.0.4", "tslib": "^2.0.3", @@ -44124,7 +44570,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dev": true, "requires": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -47826,7 +48271,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", - "dev": true, "requires": { "capital-case": "^1.0.4", "tslib": "^2.0.3" @@ -47910,6 +48354,11 @@ "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==", "dev": true }, + "html-to-image": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", + "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==" + }, "htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -53136,7 +53585,8 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "lodash-es": { "version": "4.17.21", @@ -53368,7 +53818,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dev": true, "requires": { "tslib": "^2.0.3" } @@ -54288,7 +54737,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, "requires": { "lower-case": "^2.0.2", "tslib": "^2.0.3" @@ -55125,7 +55573,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", - "dev": true, "requires": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -55214,7 +55661,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "dev": true, "requires": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -55230,7 +55676,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", - "dev": true, "requires": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -57245,7 +57690,8 @@ "regenerator-runtime": { "version": "0.13.9", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", + "dev": true }, "regenerator-transform": { "version": "0.14.5", @@ -58029,7 +58475,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", - "dev": true, "requires": { "no-case": "^3.0.4", "tslib": "^2.0.3", @@ -58232,7 +58677,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", - "dev": true, "requires": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -59984,8 +60428,7 @@ "tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "tsutils": { "version": "3.21.0", @@ -60329,7 +60772,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", - "dev": true, "requires": { "tslib": "^2.0.3" } @@ -60338,7 +60780,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", - "dev": true, "requires": { "tslib": "^2.0.3" } diff --git a/package.json b/package.json index c6bcda7292..42972e305a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stackable", - "version": "3.18.1", + "version": "3.19.0", "private": true, "description": "Blocks for everyone", "author": "Benjamin Intal of Gambit", @@ -15,8 +15,9 @@ "bugs": "https://wordpress.org/support/plugin/stackable-ultimate-gutenberg-blocks", "dependencies": { "@wordpress/dom-ready": "^3.2.3", - "@wordpress/icons": "^6.1.1", + "@wordpress/icons": "^10.27.0", "bigpicture": "^2.5.3", + "canvas-confetti": "^1.9.3", "classnames": "^2.2.6", "color-rgba": "^2.2.3", "compare-versions": "^3.6.0", @@ -24,6 +25,7 @@ "deepmerge": "^3.3.0", "fast-deep-equal": "^3.1.3", "glightbox": "^3.2.0", + "html-to-image": "^1.11.13", "is-dark-color": "^1.2.0", "md5": "^2.3.0", "prop-types": "^15.7.2", diff --git a/plugin.php b/plugin.php index 684cd03810..8757e2fe8f 100644 --- a/plugin.php +++ b/plugin.php @@ -6,7 +6,7 @@ * Author: Gambit Technologies, Inc * Author URI: http://gambit.ph * Text Domain: stackable-ultimate-gutenberg-blocks - * Version: 3.18.1 + * Version: 3.19.0 * * @package Stackable * @fs_premium_only /freemius.php, /freemius/ @@ -46,7 +46,7 @@ function stackable_multiple_plugins_check() { defined( 'STACKABLE_SHOW_PRO_NOTICES' ) || define( 'STACKABLE_SHOW_PRO_NOTICES', true ); defined( 'STACKABLE_BUILD' ) || define( 'STACKABLE_BUILD', 'free' ); -defined( 'STACKABLE_VERSION' ) || define( 'STACKABLE_VERSION', '3.18.1' ); +defined( 'STACKABLE_VERSION' ) || define( 'STACKABLE_VERSION', '3.19.0' ); defined( 'STACKABLE_FILE' ) || define( 'STACKABLE_FILE', __FILE__ ); defined( 'STACKABLE_I18N' ) || define( 'STACKABLE_I18N', 'stackable-ultimate-gutenberg-blocks' ); // Plugin slug. defined( 'STACKABLE_DESIGN_LIBRARY_URL' ) || define( 'STACKABLE_DESIGN_LIBRARY_URL', 'https://stackable-files.pages.dev' ); // Design Library CDN URL @@ -311,6 +311,7 @@ function is_frontend() { /** * Welcome screen. */ +require_once( plugin_dir_path( __FILE__ ) . 'src/welcome/getting-started.php' ); if ( is_admin() ) { require_once( plugin_dir_path( __FILE__ ) . 'src/welcome/index.php' ); require_once( plugin_dir_path( __FILE__ ) . 'src/welcome/news.php' ); diff --git a/src/block-components/alignment/edit.js b/src/block-components/alignment/edit.js index f14abd06da..33a4c87690 100644 --- a/src/block-components/alignment/edit.js +++ b/src/block-components/alignment/edit.js @@ -201,6 +201,7 @@ export const Edit = memo( props => { selector: ', .stk-block-columns:has( > .stk-inner-blocks > * > * > [data-type="stackable/column"] > * > .stk-%s)', highlight: 'outline-second-offset', } } + className="ugb-column-align-control" /> } { props.hasRowAlignment && diff --git a/src/block-components/block-div/edit.js b/src/block-components/block-div/edit.js index 40c4c1cca9..0c87b233bc 100644 --- a/src/block-components/block-div/edit.js +++ b/src/block-components/block-div/edit.js @@ -90,6 +90,7 @@ export const Edit = memo( props => { checked={ hasBackground } onChange={ hasBackground => setAttributes( { hasBackground } ) } initialOpen={ initialOpen === 'background' } + className="ugb-block-background-panel" > { const [ isLibraryOpen, setIsLibraryOpen ] = useState( false ) const [ isDialogOpen, setIsDialogOpen ] = useState( DIALOG_OPTIONS.CLOSE ) + const [ isInserting, setIsInserting ] = useState( false ) const designsRef = useRef( [] ) const disabledBlocksRef = useRef( [] ) @@ -167,17 +169,17 @@ const Edit = props => { // For blocks with variations, do not remove the uniqueId // since that will prompt the layout picker to show. const hasVariations = !! getBlockType( blockName ) && getBlockVariations( blockName ).length > 0 - if ( ! hasVariations && block.attributes.uniqueId ) { + if ( ! hasVariations && block.attributes?.uniqueId ) { delete block.attributes.uniqueId } - const customAttributes = block.attributes.customAttributes + const customAttributes = block.attributes?.customAttributes const isDesignLibraryDevMode = devMode && localStorage.getItem( 'stk__design_library__dev_mode' ) === '1' if ( ! isDesignLibraryDevMode ) { const indexToDelete = customAttributes?.findIndex( attribute => attribute[ 0 ] === 'stk-design-library__bg-target' ) if ( customAttributes && indexToDelete !== -1 ) { - block.attributes.customAttributes.splice( indexToDelete, 1 ) + block.attributes?.customAttributes.splice( indexToDelete, 1 ) } } @@ -237,11 +239,14 @@ const Edit = props => { } const addDesigns = async substituteBlocks => { + setIsInserting( true ) const { getBlockRootClientId } = select( 'core/block-editor' ) const parentClientId = getBlockRootClientId( clientId ) if ( ! designsRef.current?.length ) { console.error( 'Design library selection failed: No designs found' ) // eslint-disable-line no-console + setIsInserting( false ) + return } const designs = designsRef.current @@ -264,6 +269,7 @@ const Edit = props => { } if ( ! blocks.length ) { + setIsInserting( false ) return } @@ -284,6 +290,8 @@ const Edit = props => { if ( callbackRef.current ) { callbackRef.current() } + + setIsInserting( false ) } const onClickTertiary = () => { @@ -395,7 +403,10 @@ const Edit = props => { setIsDialogOpen( DIALOG_OPTIONS.CLOSE ) } + onRequestClose={ () => { + setIsDialogOpen( DIALOG_OPTIONS.CLOSE ) + setIsInserting( false ) + } } > { isDialogOpen === DIALOG_OPTIONS.REMOVE_BLOCKS && <> @@ -405,53 +416,55 @@ const Edit = props => {

- + + - - + > + { __( 'Cancel', i18n ) } + + } } { isDialogOpen === DIALOG_OPTIONS.DISABLED_BLOCKS && <> @@ -463,30 +476,33 @@ const Edit = props => {

{ __( 'These blocks can be enabled in the Stackable settings page. Do you want to keep the disabled blocks or substitute them with other Stackable or core blocks?', i18n ) }

- - - + { isInserting ? : <> + + + + } + }
diff --git a/src/components/color-scheme-preview/index.js b/src/components/color-scheme-preview/index.js index 6bab3122a1..035a56ad0b 100644 --- a/src/components/color-scheme-preview/index.js +++ b/src/components/color-scheme-preview/index.js @@ -1,5 +1,21 @@ +import { i18n } from 'stackable' + import { Button, BaseControl } from '@wordpress/components' +import { __ } from '@wordpress/i18n' + import classnames from 'classnames' + +export const COLOR_SCHEME_PROPERTY_LABELS = { + backgroundColor: __( 'Background Color', i18n ), + headingColor: __( 'Heading Color', i18n ), + textColor: __( 'Text Color', i18n ), + linkColor: __( 'Link Color', i18n ), + accentColor: __( 'Accent Color', i18n ), + buttonBackgroundColor: __( 'Button Color', i18n ), + buttonTextColor: __( 'Button Text Color', i18n ), + buttonOutlineColor: __( 'Button Outline Color', i18n ), +} + export const DEFAULT_COLOR_SCHEME_COLORS = { backgroundColor: { desktop: '' }, headingColor: { desktop: '' }, @@ -11,6 +27,17 @@ export const DEFAULT_COLOR_SCHEME_COLORS = { buttonOutlineColor: { desktop: '' }, } +export const ALTERNATE_COLOR_SCHEME_COLORS = { + backgroundColor: { desktop: '#0f0e17' }, + headingColor: { desktop: '#fffffe' }, + textColor: { desktop: '#fffffe' }, + linkColor: { desktop: '#f00069' }, + accentColor: { desktop: '#f00069' }, + buttonBackgroundColor: { desktop: '#f00069' }, + buttonTextColor: { desktop: '#fffffe' }, + buttonOutlineColor: { desktop: '#fffffe' }, +} + const NOOP = () => {} const ColorSchemePreview = ( { diff --git a/src/components/design-library-list/use-preview-renderer.js b/src/components/design-library-list/use-preview-renderer.js index 05f4f066de..c4aba688f9 100644 --- a/src/components/design-library-list/use-preview-renderer.js +++ b/src/components/design-library-list/use-preview-renderer.js @@ -30,6 +30,7 @@ import { } from '@wordpress/element' import { useSelect } from '@wordpress/data' import { serialize } from '@wordpress/blocks' +import { cleanSerializedBlock } from '~stackable/util' const DEFAULT_CONTENT = { ...DEFAULT } @@ -169,7 +170,7 @@ export const usePreviewRenderer = ( preview = replaceImages( preview ) - const cleanedBlock = preview.replace( //g, '' ) // removes comment + const cleanedBlock = cleanSerializedBlock( preview ) // removes comment setBlocks( { parsed: parsedBlocks, diff --git a/src/components/design-library-list/util.js b/src/components/design-library-list/util.js index 82e5f301bd..98e16014af 100644 --- a/src/components/design-library-list/util.js +++ b/src/components/design-library-list/util.js @@ -7,8 +7,9 @@ import { import { parse, serialize } from '@wordpress/blocks' import { select } from '@wordpress/data' +import { META_SEPARATORS } from '~stackable/block/posts/util' -const DEFAULT_CONTENT = { ...DEFAULT } +export const DEFAULT_CONTENT = { ...DEFAULT } const PARSER = new DOMParser() export const cleanParse = content => { @@ -237,7 +238,7 @@ export const parseDisabledBlocks = parsedBlocks => { const IMAGE_STORAGE = cdnUrl.replace( /\/$/, '' ) + '/library-v4/images/' -export const addPlaceholderForPostsBlock = ( content, postsPlaceholder, defaultValues ) => { +export const addPlaceholderForPostsBlock = ( content, postsPlaceholder, defaultValues, img = null ) => { const remainingPosts = [ ...postsPlaceholder ] // Normalize special characters @@ -260,6 +261,7 @@ export const addPlaceholderForPostsBlock = ( content, postsPlaceholder, defaultV const numItems = attrs.numberOfItems ?? 6 const width = attrs.imageWidth ? attrs.imageWidth + ( attrs.imageWidthUnit ?? 'px' ) : 'auto' + const separator = META_SEPARATORS[ attrs.metaSeparator ?? 'dot' ] // Get the post template inside the block const templateMatch = innerHtml.match( /([\s\S]*?)/ ) @@ -268,17 +270,35 @@ export const addPlaceholderForPostsBlock = ( content, postsPlaceholder, defaultV } const template = templateMatch[ 1 ].trim() - const currentPosts = remainingPosts.splice( 0, numItems ) // Slice the posts for this block - const renderedPosts = currentPosts.map( ( post, index ) => - template + let currentPosts + if ( numItems <= remainingPosts.length ) { + currentPosts = remainingPosts.slice( 0, numItems ) + } else { + const needed = numItems + const postsToUse = [ + ...remainingPosts, + ...Array.from( + { length: needed - remainingPosts.length }, + ( _, i ) => postsPlaceholder[ i % postsPlaceholder.length ] // reuse placeholders if numberOfItems > 6 + ), + ] + currentPosts = postsToUse.slice( 0, numItems ) + } + + const renderedPosts = currentPosts.map( ( post, index ) => { + const imgSrc = img ?? `${ IMAGE_STORAGE }stk-design-library-image-${ index + 1 }.jpeg` + return template .replace( /!#title!#/g, post.title_placeholder ) .replace( /!#excerpt!#/g, post.text_placeholder ) + .replace( /!#authorName!#/g, 'John Doe' ) + .replaceAll( /!#metaSeparator!#/g, separator ) + .replace( /!#commentsNum!#/g, '3 comments' ) .replace( /!#date!#/g, 'March 1, 2025' ) .replace( /!#readmoreText!#/g, defaultValues[ 'post-btn_placeholder' ] ) .replace( /!#category!#/g, defaultValues.tag_placeholder ) - .replace( /img class="stk-img"/g, `img class="stk-img" src="${ IMAGE_STORAGE }stk-design-library-image-${ index + 1 }.jpeg" width="${ width }" style="width: ${ width } !important;"` ) - ).join( '\n' ) + .replace( /img class="stk-img"/g, `img class="stk-img" src="${ imgSrc }" width="${ width }" style="width: ${ width } !important;"` ) + } ).join( '\n' ) // Replace just the template portion, keep rest of the block const updatedInnerHtml = innerHtml.replace( diff --git a/src/components/guided-modal-tour/editor.scss b/src/components/guided-modal-tour/editor.scss new file mode 100644 index 0000000000..1bbde732e9 --- /dev/null +++ b/src/components/guided-modal-tour/editor.scss @@ -0,0 +1,236 @@ +.ugb-tour-modal--overlay { + z-index: 1000002; + background-color: transparent !important; + pointer-events: none; +} + +.ugb-tour-modal { + pointer-events: all; + position: absolute; + --offset-x: 0px; + --offset-y: 0px; + --left: 50%; + --top: 50%; + left: var(--left); + top: var(--top); + margin-left: var(--offset-x); + margin-top: var(--offset-y); + overflow: visible; + border-radius: 16px; + + --wp-admin-theme-color: #f00069; + --wp-admin-theme-color-darker-10: #e0003c; + --wp-admin-theme-color-darker-20: #cb0044; + + // Smoothly transition moving top & left. + transition: + max-width 0.4s cubic-bezier(0.4, 0, 0.2, 1), + left 0.2s cubic-bezier(0.4, 0, 0.2, 1), + top 0.2s cubic-bezier(0.4, 0, 0.2, 1), + margin-left 0.2s cubic-bezier(0.4, 0, 0.2, 1), + margin-top 0.2s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.4s ease-in-out, + transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.2s ease-in-out; + will-change: left, top, max-width; + + display: none; + &.ugb-tour-modal--visible { + display: block !important; + opacity: 0 !important; + transform: scale(0.4); + } + &.ugb-tour-modal--visible-delayed { + opacity: 1 !important; + transform: scale(1); + } + + .components-modal__header-heading { + line-height: 1.2; + } + + .components-button { + border-radius: 4px; + } + + .components-modal__content { + padding: 2em; + margin: 0; + position: relative; + overflow: visible; + z-index: 1; + box-shadow: 0 22px 200px 4px #0005; + border-radius: 16px; + // border: 1px solid #f00069ad; + } + .components-modal__header { + position: relative; + padding: 0; + margin-bottom: 8px; + height: auto; + line-height: 1.2; + } + .ugb-tour-modal__footer { + margin-top: 16px; + justify-content: flex-end; + } + + .ugb-tour-modal__help { + position: relative; + background: #f4fbff; + padding: 8px 12px; + padding-inline-start: 30px; + border-radius: 8px; + border: 1px solid rgba(0, 0, 0, 0.1); + margin-block: 16px; + svg { + vertical-align: text-top; + margin-inline-end: 8px; + margin-top: 1px; + position: absolute; + inset-inline-start: 9px; + } + } + + .ugb-tour-modal__cta { + width: 100%; + justify-content: center; + margin: 16px 0 8px; + } + + &.ugb-tour-modal--right, + &.ugb-tour-modal--left, + &.ugb-tour-modal--top, + &.ugb-tour-modal--bottom { + .components-modal__content { + box-shadow: rgba(0, 0, 0, 0.2) -20px 22px 60px -4px; + &::after { + content: ""; + position: absolute; + top: 50%; + left: -10px; + width: 30px; + height: 30px; + transform: translateY(-50%) rotate(45deg); + border-radius: 4px; + background-color: #fff; + z-index: -1; + } + } + } + &.ugb-tour-modal--left { + .components-modal__content { + box-shadow: rgba(0, 0, 0, 0.2) 20px 22px 60px -4px; + &::after { + left: auto; + right: -10px; + } + } + } + &.ugb-tour-modal--left-top { + .components-modal__content { + &::after { + top: 30px; + } + } + } + &.ugb-tour-modal--top { + .components-modal__content { + box-shadow: rgba(0, 0, 0, 0.2) 0px 22px 60px -4px; + &::after { + top: auto; + left: 50%; + bottom: -10px; + transform: translateX(-50%) rotate(45deg); + } + } + } + &.ugb-tour-modal--top-right { + .components-modal__content { + &::after { + left: auto; + right: 16px; + } + } + } + &.ugb-tour-modal--bottom { + .components-modal__content { + box-shadow: rgba(0, 0, 0, 0.2) 0px -22px 60px -4px; + &::after { + left: 50%; + top: -10px; + transform: translateX(-50%) rotate(45deg); + } + } + } +} + +.ugb-tour-modal__steps { + display: flex; + gap: 6px; + margin-inline-end: auto; +} +.ugb-tour-modal__step { + width: 8px; + height: 8px; + border-radius: 20px; + background-color: #e1e1e1; + // cursor: pointer; + padding: 0 !important; + margin: 0 !important; + + &--active { + background: #f00069; + width: 24px; + border-radius: 20px; + } + + // &:hover { + // background-color: #aaa; + // } +} + +.ugb-tour-modal__glow { + position: absolute; + z-index: 1000001; + box-shadow: 0 0 20px #f00069; + border-radius: 8px; + pointer-events: none; + animation: tour-modal-glow 0.7s infinite alternate; + mix-blend-mode: multiply; + will-change: transform, box-shadow; + transition: opacity 0.2s ease-in-out; + opacity: 1; + &.ugb-tour-modal__glow--hidden { + opacity: 0; + } +} + +.ugb-tour-modal__glow--medium, +.ugb-tour-modal__glow--large { + animation: tour-modal-glow-small 0.7s infinite alternate; +} + +// Animation keyframes to grow the box-shadow like it's glowing +@keyframes tour-modal-glow { + 0% { + box-shadow: 0 0 20px #ff2283, 0 0 5px #f00069; + transform: scaleX(1) scaleY(1); + } + 100% { + box-shadow: 0 0 50px #ff2283, 0 0 5px #f00069; + transform: scaleX(1.05) scaleY(1.12); + } +} + +// Animation keyframes for small glow +@keyframes tour-modal-glow-small { + 0% { + box-shadow: 0 0 20px #ff2283, 0 0 5px #f00069; + transform: scaleX(1) scaleY(1); + } + 100% { + box-shadow: 0 0 50px #ff2283, 0 0 5px #f00069; + transform: scaleX(1.02) scaleY(1.02); + } +} diff --git a/src/components/guided-modal-tour/index.js b/src/components/guided-modal-tour/index.js new file mode 100644 index 0000000000..cb1226c6e7 --- /dev/null +++ b/src/components/guided-modal-tour/index.js @@ -0,0 +1,605 @@ +/** + * Internal dependencies + */ +import { TOUR_STEPS } from './tour-steps' +import { + setActiveTour, + clearActiveTour, + isTourActive, + getActiveTourId, + addTourStateListener, +} from './util' + +/** + * External dependencies + */ +import { + i18n, + guidedTourStates, +} from 'stackable' +import classNames from 'classnames' +import confetti from 'canvas-confetti' + +/** + * WordPress dependencies + */ +import { + Modal, Flex, Button, +} from '@wordpress/components' +import { models } from '@wordpress/api' +import { __ } from '@wordpress/i18n' +import { + Icon, arrowRight, arrowLeft, info, +} from '@wordpress/icons' +import { + useEffect, useState, useCallback, useRef, useMemo, +} from '@wordpress/element' + +const NOOP = () => {} + +// The main tour component. +const GuidedModalTour = props => { + const { + tourId = '', // This is the ID of the tour, this will be used to store the tour state in the database and to get the steps. + } = props + + // On mount, check if the tour has been completed, if so, don't show it. + const [ isDone, setIsDone ] = useState( guidedTourStates.includes( tourId ) ) + + // We need this to prevent the tour from being shown again if it's just completed. + const [ justCompleted, setJustCompleted ] = useState( false ) + + // Check if another tour is already active + const [ isAnotherTourActive, setIsAnotherTourActive ] = useState( isTourActive() && getActiveTourId() !== tourId ) + + // Listen for tour state changes + useEffect( () => { + const removeListener = addTourStateListener( activeId => { + setIsAnotherTourActive( activeId !== null && activeId !== tourId ) + } ) + return removeListener + }, [ tourId ] ) + + const { + steps = [], + condition = null, + hasConfetti = true, + initialize = NOOP, + } = TOUR_STEPS[ tourId ] + + if ( justCompleted ) { + return null + } + + // If another tour is already active, don't show this tour + if ( isAnotherTourActive ) { + return null + } + + // If there is a condition, check if it's met, if not, don't show the tour. + // condition can be true, false, or null. true will show the tour (even if + // it's already done), false will not show the tour, null will show the tour + // only once (normal behavior). + const conditionResult = condition ? condition() : null + if ( conditionResult === false ) { + return null + } else if ( conditionResult === null ) { + if ( isDone ) { + return null + } + } + + if ( ! steps.length ) { + return null + } + + return { + setIsDone( true ) + setJustCompleted( true ) + + // Clear the active tour + clearActiveTour() + + // Update the stackable_guided_tour_states setting + if ( ! guidedTourStates.includes( tourId ) ) { + // eslint-disable-next-line camelcase + const settings = new models.Settings( { stackable_guided_tour_states: [ ...guidedTourStates, tourId ] } ) + settings.save() + } + + // Soft update the global variable to prevent the tour from being shown again. + guidedTourStates.push( tourId ) + + // Remove the "tour" GET parameter from the URL so conditions won't get triggered again. + const url = new URL( window.location.href ) + url.searchParams.delete( 'tour' ) + window.history.replaceState( null, '', url.toString() ) + } } + /> +} + +const ModalTour = props => { + const { + tourId, + steps, + onClose = NOOP, + hasConfetti = true, + initialize = NOOP, + } = props + + const [ currentStep, setCurrentStep ] = useState( 0 ) + const [ isVisible, setIsVisible ] = useState( false ) + const [ isVisibleDelayed, setIsVisibleDelayed ] = useState( false ) + const [ forceRefresh, setForceRefresh ] = useState( 0 ) + const [ isTransitioning, setIsTransitioning ] = useState( false ) + const [ direction, setDirection ] = useState( 'forward' ) + const modalRef = useRef( null ) + const glowElementRef = useRef( null ) + + const { + title, + description, + help = null, // If provided, a help text will be shown below the description. + ctaLabel = null, // If provided, a button will be shown with this label. + ctaOnClick = NOOP, // This will be called when the button is clicked, we will move to the next step after. + size = 'small', // Size of the modal. Can be 'small', 'medium', 'large'. + anchor = null, // This is a selector for the element to anchor the modal to. Defaults to middle of the screen. + position = 'center', // This is the position to place the modal relative to the anchor. Can be 'left', 'right', 'top', 'bottom', 'center'. + offsetX = '0px', // This is the X offset of the modal relative to the anchor. + offsetY = '0px', // This is the Y offset of the modal relative to the anchor. + showNext = true, // If true, a "Next" button will be shown. + nextEventTarget = null, // If provided, this is a selector for the element to trigger the next event if there is one. + nextEvent = 'click', // This is the event to listen for to trigger the next step. + glowTarget = null, // If provided, this is a selector for the element to glow when the step is active. + // eslint-disable-next-line no-unused-vars + preStep = NOOP, // If provided, this is a function to run before the step is shown. + // eslint-disable-next-line no-unused-vars + postStep = NOOP, // If provided, this is a function to run after the step is shown. + skipIf = NOOP, // If provided, this is a function to check if the step should be skipped. + } = steps[ currentStep ] + + useEffect( () => { + setTimeout( () => { + initialize() + }, 50 ) + }, [ initialize ] ) + + // Set active tour when modal becomes visible + useEffect( () => { + if ( isVisible ) { + setActiveTour( tourId ) + } + }, [ isVisible, tourId ] ) + + // Clear active tour when component unmounts + useEffect( () => { + return () => { + if ( getActiveTourId() === tourId ) { + clearActiveTour() + } + } + }, [ tourId ] ) + + // While the modal is visible, just keep on force refreshing the modal in an interval to make sure the modal is always in the correct position. + useEffect( () => { + let interval + if ( isVisible && ! isTransitioning ) { + interval = setInterval( () => { + setForceRefresh( forceRefresh => forceRefresh + 1 ) + }, 500 ) + } + return () => clearInterval( interval ) + }, [ isVisible, isVisibleDelayed, isTransitioning ] ) + + // Create a stable function reference for the event listener + const handleNextEvent = useCallback( () => { + // Hide modal during transition + setIsVisible( false ) + setIsVisibleDelayed( false ) + setIsTransitioning( true ) + setDirection( 'forward' ) + + // If at the last step, just close + if ( currentStep === steps.length - 1 ) { + steps[ currentStep ]?.postStep?.( currentStep ) + if ( hasConfetti ) { + throwConfetti() + } + onClose() + return + } + + setTimeout( () => { + setCurrentStep( currentStep => { + setTimeout( () => { + steps[ currentStep ]?.postStep?.( currentStep ) + }, 50 ) + const nextStep = currentStep + 1 + setTimeout( () => { + steps[ nextStep ]?.preStep?.( nextStep ) + }, 50 ) + return nextStep + } ) + + // Show modal after 200ms delay + setTimeout( () => { + setIsVisible( true ) + setTimeout( () => { + setIsVisibleDelayed( true ) + setIsTransitioning( false ) + }, 150 ) + }, 200 ) + }, 100 ) + + setTimeout( () => { + setForceRefresh( forceRefresh => forceRefresh + 1 ) + }, 350 ) + setTimeout( () => { + setForceRefresh( forceRefresh => forceRefresh + 1 ) + }, 650 ) + }, [ currentStep, steps, hasConfetti ] ) + + const handleBackEvent = useCallback( () => { + // Hide modal during transition + setIsVisible( false ) + setIsVisibleDelayed( false ) + setIsTransitioning( true ) + setDirection( 'backward' ) + + setTimeout( () => { + setCurrentStep( currentStep => { + // steps[ currentStep ]?.postStep?.( currentStep ) + const nextStep = currentStep - 1 + steps[ nextStep ]?.preStep?.( nextStep ) + return nextStep + } ) + + // Show modal after 200ms delay + setTimeout( () => { + setIsVisible( true ) + setTimeout( () => { + setIsVisibleDelayed( true ) + setIsTransitioning( false ) + }, 150 ) + }, 200 ) + }, 100 ) + + setTimeout( () => { + setForceRefresh( forceRefresh => forceRefresh + 1 ) + }, 350 ) + setTimeout( () => { + setForceRefresh( forceRefresh => forceRefresh + 1 ) + }, 650 ) + }, [ currentStep, steps ] ) + + // If we just moved to this step, even before showing it check if we should skip it, if so, move to the next/prev step. + useEffect( () => { + if ( skipIf() ) { + if ( direction === 'forward' ) { + handleNextEvent() + } else { + handleBackEvent() + } + } + }, [ currentStep, direction ] ) + + // Show modal after 1 second delay + useEffect( () => { + const timer = setTimeout( () => { + setIsVisible( true ) + setTimeout( () => { + setIsVisibleDelayed( true ) + }, 150 ) + }, 1050 ) + + return () => clearTimeout( timer ) + }, [] ) + + useEffect( () => { + let clickListener = null + + if ( nextEventTarget ) { + if ( nextEvent === 'click' || nextEvent === 'mousedown' || nextEvent === 'mouseup' ) { + clickListener = event => { + // Check if the event target matches the selector or is inside an element that matches + if ( + event.target.matches( nextEventTarget ) || + event.target.closest( nextEventTarget ) + ) { + handleNextEvent() + } + } + // Use ownerDocument instead of document directly + const doc = modalRef.current?.ownerDocument || document + doc.addEventListener( nextEvent, clickListener ) + } else { + const elements = document.querySelectorAll( nextEventTarget ) + for ( let i = 0; i < elements.length; i++ ) { + elements[ i ].addEventListener( nextEvent, handleNextEvent ) + } + } + } + + return () => { + if ( nextEventTarget ) { + if ( ( nextEvent === 'click' || nextEvent === 'mousedown' || nextEvent === 'mouseup' ) && clickListener ) { + // Use ownerDocument instead of document directly + const doc = modalRef.current?.ownerDocument || document + doc.removeEventListener( nextEvent, clickListener ) + } else { + const elements = document.querySelectorAll( nextEventTarget ) + for ( let i = 0; i < elements.length; i++ ) { + elements[ i ].removeEventListener( nextEvent, handleNextEvent ) + } + } + } + } + }, [ currentStep, nextEventTarget, nextEvent, handleNextEvent ] ) + + // Create the glow element while this component is mounted. + useEffect( () => { + // Create the element. + const element = document.createElement( 'div' ) + element.className = `ugb-tour-modal__glow ugb-tour-modal__glow--hidden` + document.body.appendChild( element ) + + // Keep track of the element. + glowElementRef.current = element + + return () => { + glowElementRef.current = null + element.remove() + } + }, [] ) + + // These are the X and Y offsets of the modal relative to the anchor. This will be + const [ modalOffsetX, modalOffsetY ] = useMemo( () => { + if ( ! modalRef.current ) { + return [ '', '' ] // This is for the entire screen. + } + + const modalRect = modalRef.current.querySelector( '.ugb-tour-modal' ).getBoundingClientRect() + const defaultOffset = [ `${ ( window.innerWidth / 2 ) - ( modalRect.width / 2 ) }px`, `${ ( window.innerHeight / 2 ) - ( modalRect.height / 2 ) }px` ] + + if ( ! anchor ) { + return defaultOffset // This is for the entire screen. + } + + // Based on the anchor and position, calculate the X and Y offsets of the modal relative to the anchor. + // We have the modalRef.current which we can use to get the modal's bounding client rect. + const anchorRect = document.querySelector( anchor )?.getBoundingClientRect() + + if ( ! anchorRect ) { + return defaultOffset + } + + switch ( position ) { + case 'left': + // Left, middle + return [ `${ anchorRect.left - modalRect.width - 16 }px`, `${ anchorRect.top + ( anchorRect.height / 2 ) - ( modalRect.height / 2 ) }px` ] + case 'left-top': + return [ `${ anchorRect.left - modalRect.width - 16 }px`, `${ anchorRect.top + 16 }px` ] + case 'left-bottom': + return [ `${ anchorRect.left - modalRect.width - 16 }px`, `${ anchorRect.bottom - modalRect.height - 16 }px` ] + case 'right': + // Right, middle + return [ `${ anchorRect.right + 16 }px`, `${ anchorRect.top + ( anchorRect.height / 2 ) - ( modalRect.height / 2 ) }px` ] + case 'right-top': + return [ `${ anchorRect.right + 16 }px`, `${ anchorRect.top + 16 }px` ] + case 'right-bottom': + return [ `${ anchorRect.right + 16 }px`, `${ anchorRect.bottom - modalRect.height - 16 }px` ] + case 'top': + // Center, top + return [ `${ anchorRect.left + ( anchorRect.width / 2 ) - ( modalRect.width / 2 ) }px`, `${ anchorRect.top - modalRect.height - 16 }px` ] + case 'top-left': + return [ `${ anchorRect.left + 16 }px`, `${ anchorRect.top - modalRect.height - 16 }px` ] + case 'top-right': + return [ `${ anchorRect.right - modalRect.width - 16 }px`, `${ anchorRect.top - modalRect.height - 16 }px` ] + case 'bottom': + // Center, bottom + return [ `${ anchorRect.left + ( anchorRect.width / 2 ) - ( modalRect.width / 2 ) }px`, `${ anchorRect.bottom + 16 }px` ] + case 'bottom-left': + return [ `${ anchorRect.left + 16 }px`, `${ anchorRect.bottom + 16 }px` ] + case 'bottom-right': + return [ `${ anchorRect.right - modalRect.width - 16 }px`, `${ anchorRect.bottom + 16 }px` ] + case 'center': + return [ `${ anchorRect.left + ( anchorRect.width / 2 ) - ( modalRect.width / 2 ) }px`, `${ anchorRect.top + ( anchorRect.height / 2 ) - ( modalRect.height / 2 ) }px` ] + case 'center-top': + return [ `${ anchorRect.left + ( anchorRect.width / 2 ) - ( modalRect.width / 2 ) }px`, `${ anchorRect.top + 16 }px` ] + case 'center-bottom': + return [ `${ anchorRect.left + ( anchorRect.width / 2 ) - ( modalRect.width / 2 ) }px`, `${ anchorRect.bottom - modalRect.height - 16 }px` ] + default: + return defaultOffset + } + }, [ anchor, position, modalRef.current, isVisible, isVisibleDelayed, isTransitioning, forceRefresh ] ) + + // If we have a glow target, create a new element in the body, placed on the top of the target, below the modal. + useEffect( () => { + if ( glowTarget && isVisibleDelayed ) { + // Get the top, left, width, and height of the target. + const target = document.querySelector( glowTarget ) + if ( target ) { + const targetRect = target.getBoundingClientRect() + + // Estimate the size of the glow target based on the size of the target. + const glowTargetSize = targetRect.width > 300 || targetRect.height > 200 ? 'large' + : targetRect.width > 300 || targetRect.height > 100 ? 'medium' + : 'small' + + // Create the element. + if ( glowElementRef.current ) { + glowElementRef.current.className = `ugb-tour-modal__glow ugb-tour-modal__glow--${ glowTargetSize }` + glowElementRef.current.style.top = `${ targetRect.top - 8 }px` + glowElementRef.current.style.left = `${ targetRect.left - 8 }px` + glowElementRef.current.style.width = `${ targetRect.width + 16 }px` + glowElementRef.current.style.height = `${ targetRect.height + 16 }px` + } + } + } else if ( glowElementRef.current ) { + glowElementRef.current.className = `ugb-tour-modal__glow ugb-tour-modal__glow--hidden` + } + }, [ glowTarget, currentStep, isVisible, isVisibleDelayed, isTransitioning, forceRefresh ] ) + + // When unmounted, do not call onClose. So we need to do this handler on our own. + useEffect( () => { + const handleHeaderClick = () => { + onClose() + } + if ( modalRef.current ) { + modalRef.current.querySelector( '.components-modal__header' ).addEventListener( 'click', handleHeaderClick ) + } + return () => { + if ( modalRef.current ) { + modalRef.current.querySelector( '.components-modal__header' ).removeEventListener( 'click', handleHeaderClick ) + } + } + }, [ modalRef.current, onClose ] ) + + if ( ! isVisible ) { + return null + } + + return ( + + + { description } + { help && ( +
+ + { help } +
+ ) } + { ctaLabel && ( + + ) } + + + { currentStep > 0 && ( + + ) } + { showNext && ( + + ) } + +
+ ) +} + +const throwConfetti = () => { + confetti( { + particleCount: 50, + angle: 60, + spread: 70, + origin: { x: 0 }, + zIndex: 100000, + disableForReducedMotion: true, + } ) + confetti( { + particleCount: 50, + angle: 120, + spread: 70, + origin: { x: 1 }, + zIndex: 100000, + disableForReducedMotion: true, + } ) + setTimeout( () => { + confetti( { + particleCount: 50, + angle: -90, + spread: 90, + origin: { y: -0.3 }, + zIndex: 100000, + disableForReducedMotion: true, + } ) + }, 150 ) +} + +const Steps = props => { + const { + numSteps = 3, + currentStep = 0, + } = props + + if ( numSteps === 1 ) { + return null + } + + return ( +
+ { Array.from( { length: numSteps } ).map( ( _, index ) => { + const classes = classNames( [ + 'ugb-tour-modal__step', + currentStep === index && 'ugb-tour-modal__step--active', + ] ) + + return ( +
+ ) + } ) } +
+ ) +} + +export default GuidedModalTour diff --git a/src/components/guided-modal-tour/tour-steps.js b/src/components/guided-modal-tour/tour-steps.js new file mode 100644 index 0000000000..bb5acc8516 --- /dev/null +++ b/src/components/guided-modal-tour/tour-steps.js @@ -0,0 +1,6 @@ +// Import all tour files from the tours directory +import { tours } from './tours/index.js' + +export const TOUR_STEPS = { + ...tours, +} diff --git a/src/components/guided-modal-tour/tours/README.md b/src/components/guided-modal-tour/tours/README.md new file mode 100644 index 0000000000..eda38d8ae2 --- /dev/null +++ b/src/components/guided-modal-tour/tours/README.md @@ -0,0 +1,159 @@ +# Guided Modal Tour Documentation + +This directory contains individual tour configurations for the Stackable guided modal tour system. Each tour is defined in its own JavaScript file and automatically imported into the main tour system. + +## How It Works + +The tour system uses `require.context()` to automatically discover and import all `.js` files in this directory. Each tour's ID (`tourId`) is derived from its filename in kebab-case (e.g., `design-library.js` becomes `design-library`). Each tour file should export a named export corresponding to the tour's purpose. + +## Tour Structure + +Each tour file should export an object with the following structure: + +```javascript +import { __ } from '@wordpress/i18n' +import { i18n } from 'stackable' +import { createInterpolateElement } from '@wordpress/element' + +export const tourName = { + // Tour-level properties + hasConfetti: false, + condition: () => { /* condition logic */ }, + initialize: () => { /* optional initialization */ }, + steps: [ + // Array of step objects + ] +} +``` + +## Tour-Level Properties + +### `steps` (array) +The array of step objects that define the tour flow. + +### `hasConfetti` (boolean) +If `true`, confetti is shown on the last step. Default is `true`. + +### `condition` (function) +A function that returns: +- `true` - Show the tour (even if it's already been completed) +- `false` - Do not show the tour +- `null` - Show the tour only once (default behavior) + +### `initialize` (function, optional) +A function called when the tour starts. Useful for setting up initial state or content. + +## Step Properties + +Each step in a tour is an object with the following possible properties: + +### `title` (string) +The title text displayed at the top of the modal. + +### `description` (string|ReactNode) +The main content or instructions for the step. + +### `help` (string|ReactNode, optional) +If provided, a help text will be shown below the description. + +### `size` (string, optional) +The size of the modal. Can be: +- `'small'` (default) +- `'medium'` +- `'large'` + +### `anchor` (string, optional) +A CSS selector for the element to which the modal should be anchored. If not provided, modal is centered. + +### `position` (string, optional) +The position of the modal relative to the anchor. Can be: +- `'left'` +- `'right'` +- `'top'` +- `'bottom'` +- `'center'` (default) + +### `offsetX` (number, optional) +X-axis offset in pixels for fine-tuning the modal's position relative to the anchor. + +### `offsetY` (number, optional) +Y-axis offset in pixels for fine-tuning the modal's position relative to the anchor. + +### `ctaLabel` (string, optional) +If provided, a call-to-action button will be shown with this label. + +### `ctaOnClick` (function, optional) +Function to call when the CTA button is clicked. The tour will move to the next step after this is called. + +### `showNext` (boolean, optional) +If `true`, a "Next" button is shown. Default is `true`. + +### `nextEventTarget` (string, optional) +A CSS selector for an element. If provided, the tour will wait for the specified event on this element before moving to the next step. + +### `nextEvent` (string, optional) +The event name to listen for on `nextEventTarget` (e.g., 'click'). Default is 'click'. + +### `glowTarget` (string, optional) +A CSS selector for an element to highlight/glow during this step. + +### `preStep` (function, optional) +Function called before the step is displayed. Useful for setup or preparation. + +### `postStep` (function, optional) +Function called after the step is completed. Useful for cleanup or triggering actions. + +### `skipIf` (function, optional) +Function that returns `true` if this step should be skipped. Useful for conditional steps. + +## Example Tour + +```javascript +import { __ } from '@wordpress/i18n' +import { i18n } from 'stackable' +import { createInterpolateElement } from '@wordpress/element' + +export const exampleTour = { + hasConfetti: false, + condition: () => { + // Only show if there's a specific URL parameter + return window?.location?.search?.includes('tour=example') ? true : null + }, + steps: [ + { + title: 'Welcome', + description: 'This is the first step.', + size: 'medium', + anchor: '.my-element', + position: 'bottom', + offsetX: 10, + offsetY: 0, + ctaLabel: 'Get Started', + ctaOnClick: () => { console.log('CTA clicked!') }, + showNext: false, + nextEventTarget: '.my-button', + nextEvent: 'click', + glowTarget: '.my-element', + }, + { + title: 'Second Step', + description: 'This is the second step.', + help: createInterpolateElement( + 'Click the Continue button to proceed.', + { strong: } + ), + anchor: '.another-element', + position: 'right', + nextEventTarget: '.continue-button', + glowTarget: '.another-element', + } + ] +} +``` + +## Creating New Tours + +1. Create a new `.js` file in this directory +2. Import the necessary dependencies (`__`, `i18n`, `createInterpolateElement`) +3. Export a named export with your tour configuration +4. The tour will be automatically discovered and included in the tour system diff --git a/src/components/guided-modal-tour/tours/blocks.js b/src/components/guided-modal-tour/tours/blocks.js new file mode 100644 index 0000000000..dd4d2b74b7 --- /dev/null +++ b/src/components/guided-modal-tour/tours/blocks.js @@ -0,0 +1,289 @@ +import { __ } from '@wordpress/i18n' +import { i18n } from 'stackable' +import { dispatch, select } from '@wordpress/data' +import { createInterpolateElement } from '@wordpress/element' + +export const blocks = { + condition: () => { // If provided, true will show the tour (even if it's already done), false will not show the tour, null will show the tour only once. + // Force show the tour if there is a GET parameter tour=blocks + return window?.location?.search?.includes( 'tour=blocks' ) + }, + initialize: () => { + // Add some default content that we will select + + const blockObject = wp.blocks.createBlock( + 'stackable/columns', + { + uniqueId: '1dbe04e', + blockMargin: { bottom: '' }, + align: 'full', + }, + [ + wp.blocks.createBlock( + 'stackable/column', + { + uniqueId: 'f957abc', + hasContainer: true, + columnSpacing: { + top: '', right: '', bottom: '', left: '', + }, + }, + [ + wp.blocks.createBlock( + 'stackable/heading', + { + uniqueId: 'a8ebea7', + // Retain our text + text: 'Explore the World with Us', + textTag: 'h2', + } + ), + wp.blocks.createBlock( + 'stackable/text', + { + uniqueId: '57e76a1', + // Retain our text + text: 'Discover breathtaking destinations, plan your next adventure, and make unforgettable memories with our travel guides and tips.', + } + ), + wp.blocks.createBlock( + 'stackable/button-group', + { uniqueId: 'e063798' }, + [ + wp.blocks.createBlock( + 'stackable/button', + { + uniqueId: '5d04ca8', + // Retain our text + text: 'Start your journey', + url: '', + } + ), + ] + ), + ] + ), + wp.blocks.createBlock( + 'stackable/column', + { + uniqueId: '3dcffca', + hasContainer: true, + containerBackgroundMediaExternalUrl: 'https://picsum.photos/id/177/500/700.jpg', + containerHeight: '500', + }, + [] + ), + ] + ) + + // Delete all blocks + // const allBlocks = select( 'core/block-editor' ).getBlocks() + // dispatch( 'core/block-editor' ).removeBlocks( allBlocks.map( block => block.clientId ) ) + + // Insert our block + dispatch( 'core/block-editor' ).insertBlocks( [ blockObject ], 0 ) + + // Select the inner columns block for the tour + dispatch( 'core/block-editor' ).selectBlock( blockObject.innerBlocks[ 0 ].clientId ) + }, + steps: [ + { + title: '👋 ' + __( 'Welcome to Your Stackable Blocks', i18n ), + description: __( 'This inspector is contains all the settings for this block, let\'s explore it!', i18n ), + help: createInterpolateElement( __( 'If you\'re familiar with page builders, then you\'ll feel right at home.', i18n ), { + strong: , + } ), + size: 'medium', + anchor: '.ugb--has-panel-tabs', + position: 'left', + glowTarget: '.ugb--has-panel-tabs', + preStep: () => { + // Open the inspector sidebar + dispatch( 'core/edit-post' ).openGeneralSidebar( 'edit-post/block' ) + }, + }, + { + title: __( 'The Layout Tab', i18n ), + description: __( 'Stackable blocks normally have 3 tabs, each with different settings. The Layout Tab contains layout-related options like flex controls, spacing and margins.', i18n ), + help: createInterpolateElement( __( 'Open the Layout Tab to continue.', i18n ), { + strong: , + } ), + anchor: '.edit-post-sidebar__panel-tab.ugb-tab--layout', + position: 'left', + glowTarget: '.edit-post-sidebar__panel-tab.ugb-tab--layout', + nextEventTarget: '.edit-post-sidebar__panel-tab.ugb-tab--layout', + preStep: () => { + // Open the inspector sidebar + dispatch( 'core/edit-post' ).openGeneralSidebar( 'edit-post/block' ) + + // Make sure the Inner Column is selected. + const block = select( 'core/block-editor' ).getSelectedBlock() + if ( block?.name !== 'stackable/column' ) { + // Look for the first "stackable/columns" block + const allBlocks = select( 'core/block-editor' ).getBlocks() + const columnsBlock = allBlocks.find( block => block.name === 'stackable/columns' ) + if ( columnsBlock ) { + dispatch( 'core/block-editor' ).selectBlock( columnsBlock.innerBlocks[ 0 ].clientId ) + } + } + + setTimeout( () => { + // Click the tab + document.querySelector( '.edit-post-sidebar__panel-tab.ugb-tab--layout:not(.is-active)' )?.click() + }, 100 ) + }, + }, + { + title: __( 'Try Changing Alignments', i18n ), + description: __( 'Let\'s try changing this option and see how it affects our block.', i18n ), + help: createInterpolateElement( __( 'Pick Center or End Column Alignment to continue.', i18n ), { + strong: , + } ), + anchor: '.ugb-column-align-control', + position: 'left', + glowTarget: '.ugb-column-align-control', + nextEventTarget: '.ugb-column-align-control .stk-control-content button', + preStep: () => { + // Open the inspector sidebar + dispatch( 'core/edit-post' ).openGeneralSidebar( 'edit-post/block' ) + + // Make sure the Inner Column is selected. + const block = select( 'core/block-editor' ).getSelectedBlock() + if ( block?.name !== 'stackable/column' ) { + // Look for the first "stackable/columns" block + const allBlocks = select( 'core/block-editor' ).getBlocks() + const columnsBlock = allBlocks.find( block => block.name === 'stackable/columns' ) + if ( columnsBlock ) { + dispatch( 'core/block-editor' ).selectBlock( columnsBlock.innerBlocks[ 0 ].clientId ) + } + } + + setTimeout( () => { + // Click the tab + document.querySelector( '.edit-post-sidebar__panel-tab.ugb-tab--layout:not(.is-active)' )?.click() + }, 100 ) + + setTimeout( () => { + document.querySelector( '.ugb-panel--layout:not(.is-opened)' )?.click() + }, 150 ) + }, + }, + { + title: __( 'The Style Tab', i18n ), + description: __( 'Let\'s try to add a background to the main Columns block. The Style Tab contains style-related options like backgrounds, color, borders and typography.', i18n ), + help: createInterpolateElement( __( 'Click the Style Tab to continue.', i18n ), { + strong: , + } ), + anchor: '.edit-post-sidebar__panel-tab.ugb-tab--style', + position: 'left', + glowTarget: '.edit-post-sidebar__panel-tab.ugb-tab--style', + nextEventTarget: '.edit-post-sidebar__panel-tab.ugb-tab--style', + preStep: () => { + // Open the inspector sidebar + dispatch( 'core/edit-post' ).openGeneralSidebar( 'edit-post/block' ) + + // Look for the first "stackable/columns" block + const allBlocks = select( 'core/block-editor' ).getBlocks() + const columnsBlock = allBlocks.find( block => block.name === 'stackable/columns' ) + if ( columnsBlock ) { + dispatch( 'core/block-editor' ).selectBlock( columnsBlock.clientId ) + } + }, + postStep: () => { + // Click the tab + document.querySelector( '.edit-post-sidebar__panel-tab.ugb-tab--style:not(.is-active)' )?.click() + }, + }, + { + title: __( 'Try Enabling Backgrounds', i18n ), + description: __( 'Let\'s try turning on the background for our section and see how it affects our block.', i18n ), + help: createInterpolateElement( __( 'Toggle ON the Background to continue.', i18n ), { + strong: , + } ), + anchor: '.ugb-block-background-panel .components-panel__body-title', + position: 'left', + glowTarget: '.ugb-block-background-panel .components-panel__body-title', + nextEventTarget: '.ugb-block-background-panel .components-panel__body-title input[type="checkbox"]', + nextEvent: 'mousedown', + preStep: () => { + // Open the inspector sidebar + dispatch( 'core/edit-post' ).openGeneralSidebar( 'edit-post/block' ) + + // Look for the first "stackable/columns" block + const allBlocks = select( 'core/block-editor' ).getBlocks() + const columnsBlock = allBlocks.find( block => block.name === 'stackable/columns' ) + if ( columnsBlock ) { + dispatch( 'core/block-editor' ).selectBlock( columnsBlock.clientId ) + } + + setTimeout( () => { + // Click the tab + document.querySelector( '.edit-post-sidebar__panel-tab.ugb-tab--style:not(.is-active)' )?.click() + }, 100 ) + }, + // postStep: () => { + // setTimeout( () => { + // const checkbox = document.querySelector( '.ugb-block-background-panel .components-panel__body-title input[type="checkbox"]' ) + // if ( checkbox && checkbox.value !== 'on' ) { + // checkbox.click() + // } + // }, 100 ) + // }, + }, + { + title: __( 'The Advanced Tab', i18n ), + description: __( 'Lastly, the Advanced Tab contains all other options like z-index, transforms, conditional display and class names.', i18n ), + help: createInterpolateElement( __( 'Click the Advanced Tab to continue.', i18n ), { + strong: , + } ), + anchor: '.edit-post-sidebar__panel-tab.ugb-tab--advanced', + position: 'left', + glowTarget: '.edit-post-sidebar__panel-tab.ugb-tab--advanced', + nextEventTarget: '.edit-post-sidebar__panel-tab.ugb-tab--advanced', + preStep: () => { + // Open the inspector sidebar + dispatch( 'core/edit-post' ).openGeneralSidebar( 'edit-post/block' ) + + // Look for the first "stackable/columns" block + const allBlocks = select( 'core/block-editor' ).getBlocks() + const columnsBlock = allBlocks.find( block => block.name === 'stackable/columns' ) + if ( columnsBlock ) { + dispatch( 'core/block-editor' ).selectBlock( columnsBlock.clientId ) + } + }, + postStep: () => { + // Click the tab + document.querySelector( '.edit-post-sidebar__panel-tab.ugb-tab--advanced:not(.is-active)' )?.click() + }, + }, + { + title: __( 'Consistent Options Everywhere', i18n ), + description: __( 'Once you get the hang of these settings, you\'ll spot them in almost every Stackable block. This makes it easy and familiar to build any design you want.', i18n ), + anchor: '.ugb--has-panel-tabs', + size: 'medium', + position: 'left', + preStep: () => { + // Open the inspector sidebar + dispatch( 'core/edit-post' ).openGeneralSidebar( 'edit-post/block' ) + + // Look for the first "stackable/columns" block + const allBlocks = select( 'core/block-editor' ).getBlocks() + const columnsBlock = allBlocks.find( block => block.name === 'stackable/columns' ) + if ( columnsBlock ) { + dispatch( 'core/block-editor' ).selectBlock( columnsBlock.clientId ) + } + }, + }, + { + title: __( 'One Last Thing…', i18n ), + description: createInterpolateElement( __( 'You can also check the Stackable Design System to globally style all blocks. This saves a ton of time!', i18n ), { + strong: , + } ), + anchor: '[aria-controls="stackable-global-settings:sidebar"]', + position: 'left-top', + offsetY: '-30px', + offsetX: '-8px', + glowTarget: '[aria-controls="stackable-global-settings:sidebar"]', + }, + ], +} diff --git a/src/components/guided-modal-tour/tours/design-library.js b/src/components/guided-modal-tour/tours/design-library.js new file mode 100644 index 0000000000..7387f83961 --- /dev/null +++ b/src/components/guided-modal-tour/tours/design-library.js @@ -0,0 +1,125 @@ +import { __ } from '@wordpress/i18n' +import { i18n } from 'stackable' +import { createInterpolateElement } from '@wordpress/element' + +export const designLibrary = { + condition: () => { // If provided, true will show the tour (even if it's already done), false will not show the tour, null will show the tour only once. + // Force show the tour if there is a GET parameter tour=design-library + return window?.location?.search?.includes( 'tour=design-library' ) ? true : null + }, + initialize: () => { + // Make sure the patterns tab is selected + document.querySelector( '.ugb-modal-design-library button[value="patterns"]:not(.is-primary)' )?.click() + }, + steps: [ + { + title: '👋 ' + __( 'Welcome to Your Design Library', i18n ), + description: __( 'These are hundreds of pre-built designs that are style-matched to your block theme. You can insert one or more patterns to quickly build your page.', i18n ), + help: createInterpolateElement( __( 'Pick one of the designs to continue.', i18n ), { + strong: , + } ), + size: 'medium', + nextEventTarget: '.ugb-design-library-item', + nextEvent: 'mouseup', + offsetX: '-400px', + postStep: () => { + // Make sure the first one (or at least there is one) that's toggled + if ( ! document.querySelector( '.ugb-design-library-item.ugb--is-toggled' ) ) { + document.querySelector( '.ugb-design-library-item' )?.click() + } + }, + }, + { + title: __( 'Pick Styling Options', i18n ), + description: __( 'Optionally, you can turn on backgrounds, change color schemes, to customize the library in real-time.', i18n ), + help: createInterpolateElement( __( 'Toggle the Section Background to continue.', i18n ), { + strong: , + } ), + anchor: '.ugb-modal-design-library__enable-background', + position: 'right', + nextEventTarget: '.ugb-modal-design-library__enable-background', + glowTarget: '.ugb-modal-design-library__enable-background', + postStep: () => { + const el = document.querySelector( '.ugb-modal-design-library__enable-background input' ) + // If the input is not checked, click the button. + if ( el && ! el.checked ) { + el.click() + } + }, + }, + { + title: __( 'Change Color Schemes', i18n ), + description: __( 'Awesome! Your designs now have a background. Try out the available color schemes below. You can also create your own later!', i18n ), + help: createInterpolateElement( __( 'Pick a Color Scheme to continue.', i18n ), { + strong: , + } ), + anchor: '.ugb-design-library__color-scheme-popover', + position: 'top', + nextEventTarget: '.ugb-design-library__color-scheme-popover .ugb-modal-design-library__stk-color-scheme', + glowTarget: '.ugb-design-library__color-scheme-popover .ugb-modal-design-library__stk-color-scheme:last-of-type', + preStep: () => { + // Let's make sure the background scheme is open. + if ( ! document.querySelector( '.ugb-design-library__color-scheme-popover' ) ) { + document.querySelector( '.ugb-modal-design-library__background-scheme .ugb-modal-design-library__stk-color-scheme' )?.click() + } + }, + postStep: () => { + document.querySelector( '.ugb-design-library__color-scheme-popover .ugb-modal-design-library__stk-color-scheme:last-of-type' )?.click() + document.querySelector( '.ugb-modal-design-library__color-scheme-close-button' )?.click() + }, + }, + { + title: __( 'Patterns and Full-Pages', i18n ), + description: __( 'Great! Your entire library is now styled. Aside from patterns, Stackable also provides you with full-page layouts.', i18n ), + help: createInterpolateElement( __( 'Click the Next to continue.', i18n ), { + strong: , + } ), + anchor: '.stk-design-library-tabs .components-button-group', + position: 'bottom', + nextEventTarget: '.stk-design-library-tabs .components-button-group', + glowTarget: '.stk-design-library-tabs .components-button-group', + preStep: () => { + // Disable for now the pages tab + const pagesButton = document.querySelector( '.stk-design-library-tabs button[value="pages"]' ) + if ( pagesButton ) { + pagesButton.disabled = true + } + }, + postStep: () => { + // Enable the pages tab + const pagesButton = document.querySelector( '.stk-design-library-tabs button[value="pages"]' ) + if ( pagesButton ) { + pagesButton.disabled = false + } + }, + }, + { + title: __( 'Let\'s Insert Our Pattern', i18n ), + description: __( 'Now let\'s insert our pattern into our page.', i18n ), + help: createInterpolateElement( __( 'Click on Add Designs to continue.', i18n ), { + strong: , + } ), + anchor: '.ugb-modal-design-library__add-multi', + position: 'top-right', + nextEventTarget: '.ugb-modal-design-library__add-multi', + glowTarget: '.ugb-modal-design-library__add-multi', + preStep: () => { + // Make sure the patterns tab is selected + document.querySelector( '.ugb-modal-design-library button[value="patterns"]:not(.is-primary)' )?.click() + + // Make sure the first one (or at least there is one) that's toggled + if ( ! document.querySelector( '.ugb-design-library-item.ugb--is-toggled' ) ) { + document.querySelector( '.ugb-design-library-item' )?.click() + } + }, + postStep: () => { + setTimeout( () => { + // If the design library is still open, click the add button. + if ( document.querySelector( '.ugb-modal-design-library' ) ) { + document.querySelector( '.ugb-modal-design-library__add-multi' )?.click() + } + }, 100 ) + }, + }, + ], +} diff --git a/src/components/guided-modal-tour/tours/design-system-picker.js b/src/components/guided-modal-tour/tours/design-system-picker.js new file mode 100644 index 0000000000..43e1746f95 --- /dev/null +++ b/src/components/guided-modal-tour/tours/design-system-picker.js @@ -0,0 +1,15 @@ +import { __ } from '@wordpress/i18n' +import { i18n } from 'stackable' + +export const designSystemPicker = { + condition: () => { // If provided, true will show the tour (even if it's already done), false will not show the tour, null will show the tour only once. + // Force show the tour if there is a GET parameter tour=design-system-picker + return window?.location?.search?.includes( 'tour=design-system-picker' ) ? true : null + }, + steps: [ + { + title: '👋 ' + __( 'Welcome to The Design System Picker', i18n ), + description: '', // Not yet available. + }, + ], +} diff --git a/src/components/guided-modal-tour/tours/design-system.js b/src/components/guided-modal-tour/tours/design-system.js new file mode 100644 index 0000000000..5ab781bfe0 --- /dev/null +++ b/src/components/guided-modal-tour/tours/design-system.js @@ -0,0 +1,137 @@ +import { __ } from '@wordpress/i18n' +import { i18n } from 'stackable' +import { createInterpolateElement } from '@wordpress/element' + +export const designSystem = { + condition: () => { // If provided, true will show the tour (even if it's already done), false will not show the tour, null will show the tour only once. + // Force show the tour if there is a GET parameter tour=design-system + return window?.location?.search?.includes( 'tour=design-system' ) ? true : null + }, + steps: [ + { + title: '👋 ' + __( 'Welcome to Your Design System', i18n ), + description: __( 'Design once, apply everywhere! Set global styles so every block across your site looks and feels unified.', i18n ), + size: 'medium', + anchor: '.interface-interface-skeleton__sidebar', + position: 'left-top', + // glowTarget: '.interface-interface-skeleton__sidebar', + }, + { + title: __( 'Use The Style Guide', i18n ), + description: __( 'You can use the Style Guide to see how your complete design system looks.', i18n ), + help: createInterpolateElement( __( 'Click the Preview Design System button to continue.', i18n ), { + strong: , + } ), + anchor: '.ugb-global-settings__preview-button', + position: 'left', + nextEventTarget: '.ugb-global-settings__preview-button', + glowTarget: '.ugb-global-settings__preview-button', + postStep: () => { + // Open the style guide if it's not open. + if ( ! document.querySelector( '.ugb-style-guide-popover' ) ) { + document.querySelector( '.ugb-global-settings__preview-button' )?.click() + } + }, + }, + { + title: __( 'This is Your Style Guide', i18n ), + description: __( 'This Style Guide shows a live preview of your entire design system showing how the different design elements look. This updates based on your current settings.', i18n ), + help: __( 'Scroll down to explore', i18n ), + size: 'medium', + anchor: '.ugb-style-guide-popover > .components-popover__content', + position: 'center', + // glowTarget: '.interface-interface-skeleton__sidebar', + }, + { + title: __( 'Customize Your Design System', i18n ), + description: __( 'These settings are applied to all blocks across your entire site. They are used as defaults for your blocks, but you can override them on a per block basis.', i18n ), + help: createInterpolateElement( __( 'Open the Global Typography panel to continue.', i18n ), { + strong: , + } ), + anchor: '.ugb-global-typography__panel .components-panel__body-title', + position: 'left', + glowTarget: '.ugb-global-typography__panel .components-panel__body-toggle', + nextEventTarget: '.ugb-global-typography__panel .components-panel__body-toggle', + skipIf: () => { + return document.querySelector( '.ugb-global-typography__panel' )?.classList.contains( 'is-opened' ) + }, + postStep: () => { + // Open the typography panel if it's not open. + if ( document.querySelector( '.ugb-global-typography__panel:not(.is-opened)' ) ) { + document.querySelector( '.ugb-global-typography__panel .components-panel__body-toggle' )?.click() + } + }, + }, + { + title: __( 'Try Changing Your Font Pair', i18n ), + description: __( 'Try changing the font pair to see how it looks in the Style Guide.', i18n ), + help: createInterpolateElement( __( 'Pick another Preset Font Pair to continue.', i18n ), { + strong: , + } ), + anchor: '.ugb-global-settings-font-pair-control:not(.ugb-global-settings-font-pair__selected)', + position: 'left', + glowTarget: '.ugb-global-settings-font-pair-control:not(.ugb-global-settings-font-pair__selected)', + nextEventTarget: '.ugb-global-settings-font-pair__container [role="button"]', + preStep: () => { + // Make sure that the typography panel is open. + if ( document.querySelector( '.ugb-global-typography__panel:not(.is-opened)' ) ) { + document.querySelector( '.ugb-global-typography__panel .components-panel__body-title' )?.click() + } + const el = document.querySelector( '.ugb-global-settings-font-pair__container' ) + if ( el ) { + // Scroll this to the top. + el.scrollTo( { + top: 0, + behavior: 'auto', + } ) + } + }, + }, + { + title: __( 'Whoa! Your Site Updated', i18n ), + description: __( 'Did you see that? Your site has been updated with the new font pair. You can see the changes in the Style Guide as well!', i18n ), + help: __( 'Other parts below the style guide also updated!', i18n ), + anchor: '.ugb-style-guide__color-container', + position: 'top', + glowTarget: '.ugb-style-guide__color-container', + preStep: () => { + const el = document.querySelector( '.ugb-style-guide-popover > .components-popover__content' ) + // Scroll this to the top. + if ( el ) { + el.scrollTo( { + top: 0, + behavior: 'smooth', + } ) + } + }, + }, + { + title: __( 'Share Your Style Guide', i18n ), + description: __( 'You can easily share your design system with others by exporting your Style Guide as an image. This is perfect for sharing with clients, teammates, or for documentation.', i18n ), + anchor: '.ugb-style-guide__print-button', + position: 'bottom', + glowTarget: '.ugb-style-guide__print-button', + preStep: () => { + const el = document.querySelector( '.ugb-style-guide-popover > .components-popover__content' ) + // Scroll this to the top. + if ( el ) { + el.scrollTo( { + top: 0, + behavior: 'smooth', + } ) + } + }, + }, + { + title: __( 'You Did It!', i18n ), + description: __( 'That\'s it for the tour! Click the X to close the Style Guide. Your new styles are now live on your site. 🎉', i18n ), + anchor: '.ugb-style-guide-popover__close-button', + position: 'bottom', + glowTarget: '.ugb-style-guide-popover__close-button', + nextEventTarget: '.ugb-style-guide-popover__close-button', + postStep: () => { + document.querySelector( '.ugb-style-guide-popover__close-button' )?.click() + }, + }, + ], +} diff --git a/src/components/guided-modal-tour/tours/editor.js b/src/components/guided-modal-tour/tours/editor.js new file mode 100644 index 0000000000..1d6e2a68e6 --- /dev/null +++ b/src/components/guided-modal-tour/tours/editor.js @@ -0,0 +1,27 @@ +import { __ } from '@wordpress/i18n' +import { i18n, guidedTourStates } from 'stackable' +import { createInterpolateElement } from '@wordpress/element' + +export const editor = { + hasConfetti: false, + condition: () => { // If provided, true will show the tour (even if it's already done), false will not show the tour, null will show the tour only once. + // Do not show the tour if there is a GET parameter that shows another tour. + return window?.location?.search?.includes( 'tour=' ) ? false + : guidedTourStates.includes( 'design-library' ) ? false : null + }, + steps: [ + { + title: '👋 ' + __( 'Welcome to Stackable', i18n ), + description: __( 'We\'re excited to have you here. Let\'s get you started by opening the Design Library.', i18n ), + help: createInterpolateElement( __( 'Click the Design Library button to continue.', i18n ), { + strong: , + } ), + // size: 'medium', + anchor: '.ugb-insert-library-button', + position: 'bottom', + nextEventTarget: '.ugb-insert-library-button', + glowTarget: '.ugb-insert-library-button', + showNext: false, + }, + ], +} diff --git a/src/components/guided-modal-tour/tours/index.js b/src/components/guided-modal-tour/tours/index.js new file mode 100644 index 0000000000..e47c7ab697 --- /dev/null +++ b/src/components/guided-modal-tour/tours/index.js @@ -0,0 +1,34 @@ +// This file automatically imports all tour files from the tours directory +// and exports them as a single object for use in TOUR_STEPS + +// Dynamically import all tour files from the tours directory +const tourContext = require.context( './', false, /\.js$/ ) +const tours = {} + +// Import all tour files and populate the tours object using the filename as the key +tourContext.keys().forEach( fileName => { + // Skip this index.js file itself + if ( fileName === './index.js' ) { + return + } + + // Import the tour module + const tourModule = tourContext( fileName ) + + // Use the filename (without extension) as the key + const tourName = fileName.replace( './', '' ).replace( '.js', '' ) + + // Prefer default export, fallback to first named export if available + if ( tourModule.default ) { + tours[ tourName ] = tourModule.default + } else { + // If no default export, use the first named export (if any) + const namedExports = Object.keys( tourModule ).filter( name => name !== 'default' ) + if ( namedExports.length > 0 ) { + tours[ tourName ] = tourModule[ namedExports[ 0 ] ] + } + } +} ) + +// Export all tours as a single object +export { tours } diff --git a/src/components/guided-modal-tour/tours/site-kit.js b/src/components/guided-modal-tour/tours/site-kit.js new file mode 100644 index 0000000000..93b10fdfcc --- /dev/null +++ b/src/components/guided-modal-tour/tours/site-kit.js @@ -0,0 +1,15 @@ +import { __ } from '@wordpress/i18n' +import { i18n } from 'stackable' + +export const siteKit = { + condition: () => { // If provided, true will show the tour (even if it's already done), false will not show the tour, null will show the tour only once. + // Force show the tour if there is a GET parameter tour=site-kit + return window?.location?.search?.includes( 'tour=site-kit' ) ? true : null + }, + steps: [ + { + title: '👋 ' + __( 'Welcome to Site Kits', i18n ), + description: '', // Not yet available. + }, + ], +} diff --git a/src/components/guided-modal-tour/util.js b/src/components/guided-modal-tour/util.js new file mode 100644 index 0000000000..a8f2f34b7b --- /dev/null +++ b/src/components/guided-modal-tour/util.js @@ -0,0 +1,53 @@ +/** + * Global tour state management utilities + * + * This module provides functions to manage the global state of guided modal tours, + * ensuring that only one tour can be active at a time. + */ + +// Global tour state +let activeTourId = null +const tourStateListeners = new Set() + +/** + * Set the currently active tour + * + * @param {string} tourId - The ID of the tour to set as active + */ +export const setActiveTour = tourId => { + activeTourId = tourId + tourStateListeners.forEach( listener => listener( tourId ) ) +} + +/** + * Clear the currently active tour + */ +export const clearActiveTour = () => { + activeTourId = null + tourStateListeners.forEach( listener => listener( null ) ) +} + +/** + * Check if any tour is currently active + * + * @return {boolean} True if a tour is active, false otherwise + */ +export const isTourActive = () => activeTourId !== null + +/** + * Get the currently active tour ID + * + * @return {string|null} The active tour ID or null if no tour is active + */ +export const getActiveTourId = () => activeTourId + +/** + * Add a listener for tour state changes + * + * @param {Function} listener - Function to call when tour state changes + * @return {Function} Function to remove the listener + */ +export const addTourStateListener = listener => { + tourStateListeners.add( listener ) + return () => tourStateListeners.delete( listener ) +} diff --git a/src/components/index.js b/src/components/index.js index 89ab886b80..7a4e47d490 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -56,6 +56,8 @@ export { default as AdminSelectSetting } from './admin-select-setting' export { default as TaxonomyControl } from './taxonomy-control' export { default as Tooltip } from './tooltip' export { default as BlockStyles } from './block-styles' +export { default as GuidedModalTour } from './guided-modal-tour' +export { default as StyleGuide, StyleGuidePopover } from './style-guide' // V2 only Components, for deprecation export { default as BlockContainer } from './block-container' @@ -123,5 +125,7 @@ export { default as ColorSchemePreview, ColorSchemePresetPicker, DEFAULT_COLOR_SCHEME_COLORS, + ALTERNATE_COLOR_SCHEME_COLORS, + COLOR_SCHEME_PROPERTY_LABELS, } from './color-scheme-preview' export { ColorSchemesHelp } from './color-schemes-help' diff --git a/src/components/modal-design-library/modal.js b/src/components/modal-design-library/modal.js index f6cd9938e2..bc9e4e364f 100644 --- a/src/components/modal-design-library/modal.js +++ b/src/components/modal-design-library/modal.js @@ -6,6 +6,7 @@ import BlockList from './block-list' import Button from '../button' import AdvancedToolbarControl from '../advanced-toolbar-control' import DesignLibraryList from '~stackable/components/design-library-list' +import { GuidedModalTour } from '~stackable/components' import { getDesigns, filterDesigns } from '~stackable/design-library' /** @@ -213,6 +214,9 @@ export const ModalDesignLibrary = props => { onRequestClose={ props.onClose } >
+ + +