diff --git a/.storybook/main.ts b/.storybook/main.ts index 152787e..dbab319 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,4 +1,5 @@ import type { StorybookConfig } from '@storybook/react-vite'; +import remarkGfm from 'remark-gfm'; const config: StorybookConfig = { "stories": [ @@ -7,7 +8,16 @@ const config: StorybookConfig = { ], "addons": [ "@chromatic-com/storybook", - "@storybook/addon-docs", + { + name: '@storybook/addon-docs', + options: { + mdxPluginOptions: { + mdxCompileOptions: { + remarkPlugins: [remarkGfm], + }, + }, + }, + }, "@storybook/addon-a11y", "@storybook/addon-vitest" ], diff --git a/package-lock.json b/package-lock.json index 4445b01..e239f80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,6 +57,7 @@ "lint-staged": "^16.1.5", "playwright": "^1.56.1", "prettier": "3.6.2", + "remark-gfm": "^4.0.1", "start-server-and-test": "^2.0.13", "storybook": "^9.1.17", "tsc-alias": "^1.8.16", @@ -4983,6 +4984,16 @@ "@types/deep-eql": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -5017,6 +5028,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/mdx": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", @@ -5024,6 +5045,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.3.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", @@ -5061,6 +5089,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.40.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz", @@ -6267,6 +6302,17 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -6465,6 +6511,17 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -6499,6 +6556,17 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -7060,6 +7128,20 @@ "dev": true, "license": "MIT" }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -7152,6 +7234,20 @@ "node": ">=8" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -8167,6 +8263,13 @@ "node": ">=12.0.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -9356,6 +9459,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -10335,6 +10451,17 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -10399,6 +10526,17 @@ "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", "dev": true }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -10409,171 +10547,975 @@ "node": ">= 0.4" } }, - "node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 8" + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "dev": true, "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, "engines": { - "node": ">=8.6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8.6" + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", "dev": true, "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" }, - "engines": { - "node": ">= 0.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mimic-fn": { + "node_modules/mdast-util-gfm-footnote": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=4" + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, - "engines": { - "node": "*" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", "dev": true, "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "dev": true, "license": "MIT", "dependencies": { - "minipass": "^7.1.2" + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" }, - "engines": { - "node": ">= 18" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", "dev": true, "license": "MIT", "dependencies": { - "minimist": "^1.2.6" + "@types/mdast": "^4.0.0" }, - "bin": { - "mkdirp": "bin/cmd.js" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mri": { + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", @@ -11803,6 +12745,58 @@ "node": ">=6" } }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", @@ -13110,6 +14104,17 @@ "node": ">=0.6" } }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -13419,6 +14424,85 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -13537,6 +14621,36 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "7.1.12", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", @@ -14136,6 +15250,17 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index 46a67eb..8642fb5 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "lint-staged": "^16.1.5", "playwright": "^1.56.1", "prettier": "3.6.2", + "remark-gfm": "^4.0.1", "start-server-and-test": "^2.0.13", "storybook": "^9.1.17", "tsc-alias": "^1.8.16", diff --git a/spec/components/Carousel/Carousel.test.tsx b/spec/components/Carousel/Carousel.test.tsx index e29a134..e9269f9 100644 --- a/spec/components/Carousel/Carousel.test.tsx +++ b/spec/components/Carousel/Carousel.test.tsx @@ -4,6 +4,7 @@ import { describe, test, expect, vi, afterEach, beforeEach } from 'vitest'; import CioCarousel from '@/components/carousel'; import { Product } from '@/types/productCardTypes'; import { CarouselRenderProps } from '@/types/carouselTypes'; +import { CIO_EVENTS } from '@/utils/events'; const mockProducts: Product[] = [ { @@ -596,4 +597,101 @@ describe('Carousel component', () => { expect(carousel).toBeInTheDocument(); }); }); + + describe('Pub-Sub Events', () => { + afterEach(() => { + cleanup(); + }); + + test('dispatches carousel.next event on root element when next button is clicked', () => { + const { container } = render(); + + const el = container.querySelector('[data-slot="carousel"]')!; + const listener = vi.fn(); + el.addEventListener(CIO_EVENTS.carousel.next, listener); + + const nextButton = screen.getByRole('button', { name: /next/i }); + fireEvent.click(nextButton); + + expect(listener).toHaveBeenCalledTimes(1); + const event = listener.mock.calls[0][0] as CustomEvent; + expect(event.detail.direction).toBe('next'); + + el.removeEventListener(CIO_EVENTS.carousel.next, listener); + }); + + test('dispatches carousel.previous event on root element when previous button is clicked', () => { + const { container } = render(); + + const el = container.querySelector('[data-slot="carousel"]')!; + const listener = vi.fn(); + el.addEventListener(CIO_EVENTS.carousel.previous, listener); + + const prevButton = screen.getByRole('button', { name: /previous/i }); + fireEvent.click(prevButton); + + expect(listener).toHaveBeenCalledTimes(1); + const event = listener.mock.calls[0][0] as CustomEvent; + expect(event.detail.direction).toBe('previous'); + + el.removeEventListener(CIO_EVENTS.carousel.previous, listener); + }); + + test('events bubble up so window listeners still work', () => { + const listener = vi.fn(); + window.addEventListener(CIO_EVENTS.carousel.next, listener); + + render(); + const nextButton = screen.getByRole('button', { name: /next/i }); + fireEvent.click(nextButton); + + expect(listener).toHaveBeenCalledTimes(1); + + window.removeEventListener(CIO_EVENTS.carousel.next, listener); + }); + + test('carousel navigation still works alongside event dispatch', () => { + render(); + + // Buttons should still be present and clickable without errors + const nextButton = screen.getByRole('button', { name: /next/i }); + const prevButton = screen.getByRole('button', { name: /previous/i }); + + expect(() => fireEvent.click(nextButton)).not.toThrow(); + expect(() => fireEvent.click(prevButton)).not.toThrow(); + + // Products should still be rendered + expect(screen.getByText('Product 1')).toBeInTheDocument(); + }); + + test('two carousels: events do not cross-pollinate', () => { + render( + <> +
+ +
+
+ +
+ , + ); + + const wrapper1 = screen.getByTestId('wrapper-1'); + const wrapper2 = screen.getByTestId('wrapper-2'); + const listener1 = vi.fn(); + const listener2 = vi.fn(); + wrapper1.addEventListener(CIO_EVENTS.carousel.next, listener1); + wrapper2.addEventListener(CIO_EVENTS.carousel.next, listener2); + + // Click next on the first carousel only + const nextButtons = screen.getAllByRole('button', { name: /next/i }); + fireEvent.click(nextButtons[0]); + + expect(listener1).toHaveBeenCalledTimes(1); + expect(listener2).not.toHaveBeenCalled(); + + wrapper1.removeEventListener(CIO_EVENTS.carousel.next, listener1); + wrapper2.removeEventListener(CIO_EVENTS.carousel.next, listener2); + }); + }); }); diff --git a/spec/components/product-card/product-card.test.tsx b/spec/components/product-card/product-card.test.tsx index 897dafc..ae0eb28 100644 --- a/spec/components/product-card/product-card.test.tsx +++ b/spec/components/product-card/product-card.test.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { render, screen, fireEvent, cleanup } from '@testing-library/react'; import { describe, test, expect, vi, afterEach } from 'vitest'; import ProductCard from '@/components/product-card'; +import { CIO_EVENTS } from '@/utils/events'; const mockProductData = { product: { @@ -81,7 +82,7 @@ describe('ProductCard component', () => { render(); fireEvent.click(screen.getByText('Add to Cart')); expect(mockOnAddToCart).toHaveBeenCalledTimes(1); - expect(mockOnAddToCart).toHaveBeenCalledWith(expect.any(Object)); + expect(mockOnAddToCart).toHaveBeenCalledWith(expect.any(Object), mockProductData.product); }); test('calls onAddToWishlist when wishlist button is clicked', () => { @@ -89,7 +90,7 @@ describe('ProductCard component', () => { render(); fireEvent.click(screen.getByRole('button', { name: /add to wishlist/i })); expect(mockOnAddToWishlist).toHaveBeenCalledTimes(1); - expect(mockOnAddToWishlist).toHaveBeenCalledWith(expect.any(Object)); + expect(mockOnAddToWishlist).toHaveBeenCalledWith(expect.any(Object), mockProductData.product); }); test('calls onProductClick when product card is clicked', () => { @@ -103,6 +104,7 @@ describe('ProductCard component', () => { ); fireEvent.click(screen.getByTestId('product-card')); expect(mockOnProductClick).toHaveBeenCalledTimes(1); + expect(mockOnProductClick).toHaveBeenCalledWith(mockProductData.product); }); test('does not render add to cart button when onAddToCart is not provided', () => { @@ -393,4 +395,230 @@ describe('ProductCard component', () => { expect(badgeElement).not.toBeInTheDocument(); }); }); + + describe('Pub-Sub Events', () => { + afterEach(() => { + cleanup(); + }); + + test('dispatches productCard.click event on root element with correct product detail', () => { + render(); + + const el = screen.getByTestId('product-card'); + const listener = vi.fn(); + el.addEventListener(CIO_EVENTS.productCard.click, listener); + + fireEvent.click(el); + + expect(listener).toHaveBeenCalledTimes(1); + const event = listener.mock.calls[0][0] as CustomEvent; + expect(event.detail.product).toEqual(mockProductData.product); + + el.removeEventListener(CIO_EVENTS.productCard.click, listener); + }); + + test('dispatches productCard.click event AND calls onProductClick callback', () => { + const mockOnProductClick = vi.fn(); + render( + , + ); + + const el = screen.getByTestId('product-card'); + const listener = vi.fn(); + el.addEventListener(CIO_EVENTS.productCard.click, listener); + + fireEvent.click(el); + + expect(listener).toHaveBeenCalledTimes(1); + expect(mockOnProductClick).toHaveBeenCalledTimes(1); + expect(mockOnProductClick).toHaveBeenCalledWith(mockProductData.product); + + el.removeEventListener(CIO_EVENTS.productCard.click, listener); + }); + + test('events bubble up so window listeners still work', () => { + const listener = vi.fn(); + window.addEventListener(CIO_EVENTS.productCard.click, listener); + + render(); + fireEvent.click(screen.getByTestId('product-card')); + + expect(listener).toHaveBeenCalledTimes(1); + + window.removeEventListener(CIO_EVENTS.productCard.click, listener); + }); + + test('dispatches productCard.click event even without onProductClick prop', () => { + render(); + + const el = screen.getByTestId('product-card'); + const listener = vi.fn(); + el.addEventListener(CIO_EVENTS.productCard.click, listener); + + fireEvent.click(el); + + expect(listener).toHaveBeenCalledTimes(1); + + el.removeEventListener(CIO_EVENTS.productCard.click, listener); + }); + + test('dispatches productCard.conversion event on root element on add-to-cart click', () => { + const mockOnAddToCart = vi.fn(); + render(); + + const el = screen.getByTestId('product-card'); + const listener = vi.fn(); + el.addEventListener(CIO_EVENTS.productCard.conversion, listener); + + fireEvent.click(screen.getByText('Add to Cart')); + + expect(listener).toHaveBeenCalledTimes(1); + const event = listener.mock.calls[0][0] as CustomEvent; + expect(event.detail.product).toEqual(mockProductData.product); + expect(mockOnAddToCart).toHaveBeenCalledTimes(1); + expect(mockOnAddToCart).toHaveBeenCalledWith(expect.any(Object), mockProductData.product); + + el.removeEventListener(CIO_EVENTS.productCard.conversion, listener); + }); + + test('clicking add-to-cart does NOT also dispatch productCard.click', () => { + render(); + + const el = screen.getByTestId('product-card'); + const clickListener = vi.fn(); + const conversionListener = vi.fn(); + el.addEventListener(CIO_EVENTS.productCard.click, clickListener); + el.addEventListener(CIO_EVENTS.productCard.conversion, conversionListener); + + fireEvent.click(screen.getByText('Add to Cart')); + + expect(conversionListener).toHaveBeenCalledTimes(1); + expect(clickListener).not.toHaveBeenCalled(); + + el.removeEventListener(CIO_EVENTS.productCard.click, clickListener); + el.removeEventListener(CIO_EVENTS.productCard.conversion, conversionListener); + }); + + test('clicking add-to-cart does NOT call onProductClick callback', () => { + const mockOnProductClick = vi.fn(); + render( + , + ); + fireEvent.click(screen.getByText('Add to Cart')); + expect(mockOnProductClick).not.toHaveBeenCalled(); + }); + + test('dispatches productCard.wishlist event on root element on wishlist click', () => { + const mockOnAddToWishlist = vi.fn(); + render(); + + const el = screen.getByTestId('product-card'); + const listener = vi.fn(); + el.addEventListener(CIO_EVENTS.productCard.wishlist, listener); + + fireEvent.click(screen.getByRole('button', { name: /add to wishlist/i })); + + expect(listener).toHaveBeenCalledTimes(1); + const event = listener.mock.calls[0][0] as CustomEvent; + expect(event.detail.product).toEqual(mockProductData.product); + expect(mockOnAddToWishlist).toHaveBeenCalledTimes(1); + expect(mockOnAddToWishlist).toHaveBeenCalledWith(expect.any(Object), mockProductData.product); + + el.removeEventListener(CIO_EVENTS.productCard.wishlist, listener); + }); + + test('clicking wishlist button does NOT call onProductClick callback', () => { + const mockOnProductClick = vi.fn(); + render( + , + ); + fireEvent.click(screen.getByRole('button', { name: /add to wishlist/i })); + expect(mockOnProductClick).not.toHaveBeenCalled(); + }); + + test('clicking wishlist button does NOT dispatch productCard.click event', () => { + const clickListener = vi.fn(); + window.addEventListener(CIO_EVENTS.productCard.click, clickListener); + + render(); + fireEvent.click(screen.getByRole('button', { name: /add to wishlist/i })); + + expect(clickListener).not.toHaveBeenCalled(); + + window.removeEventListener(CIO_EVENTS.productCard.click, clickListener); + }); + + test('dispatches productCard.imageEnter on root element on mouseEnter of image section', () => { + render(); + + const el = screen.getByTestId('product-card'); + const listener = vi.fn(); + el.addEventListener(CIO_EVENTS.productCard.imageEnter, listener); + + const imageSection = el.querySelector('.cio-product-card-image-section')!; + fireEvent.mouseEnter(imageSection); + + expect(listener).toHaveBeenCalledTimes(1); + const event = listener.mock.calls[0][0] as CustomEvent; + expect(event.detail.product).toEqual(mockProductData.product); + + el.removeEventListener(CIO_EVENTS.productCard.imageEnter, listener); + }); + + test('dispatches productCard.imageLeave on root element on mouseLeave of image section', () => { + render(); + + const el = screen.getByTestId('product-card'); + const listener = vi.fn(); + el.addEventListener(CIO_EVENTS.productCard.imageLeave, listener); + + const imageSection = el.querySelector('.cio-product-card-image-section')!; + fireEvent.mouseLeave(imageSection); + + expect(listener).toHaveBeenCalledTimes(1); + const event = listener.mock.calls[0][0] as CustomEvent; + expect(event.detail.product).toEqual(mockProductData.product); + + el.removeEventListener(CIO_EVENTS.productCard.imageLeave, listener); + }); + + test('two product cards: events do not cross-pollinate', () => { + const product2 = { ...mockProductData, product: { ...mockProductData.product, id: 'product-2', name: 'Product 2' } }; + + render( + <> +
+ +
+
+ +
+ , + ); + + const wrapper1 = screen.getByTestId('wrapper-1'); + const wrapper2 = screen.getByTestId('wrapper-2'); + const listener1 = vi.fn(); + const listener2 = vi.fn(); + wrapper1.addEventListener(CIO_EVENTS.productCard.click, listener1); + wrapper2.addEventListener(CIO_EVENTS.productCard.click, listener2); + + // Click only the first card + fireEvent.click(screen.getByTestId('card-1')); + + expect(listener1).toHaveBeenCalledTimes(1); + expect(listener2).not.toHaveBeenCalled(); + + wrapper1.removeEventListener(CIO_EVENTS.productCard.click, listener1); + wrapper2.removeEventListener(CIO_EVENTS.productCard.click, listener2); + }); + }); }); diff --git a/spec/utils/events.test.ts b/spec/utils/events.test.ts new file mode 100644 index 0000000..3959c8e --- /dev/null +++ b/spec/utils/events.test.ts @@ -0,0 +1,127 @@ +import { describe, test, expect, vi, afterEach } from 'vitest'; +import { CIO_EVENTS, dispatchCioEvent } from '@/utils/events'; +import type { Product } from '@/types/productCardTypes'; + +const mockProduct: Product = { id: '1', name: 'Test Product' }; + +describe('Event utility', () => { + describe('CIO_EVENTS constants', () => { + test('productCard event names are correct string literals', () => { + expect(CIO_EVENTS.productCard.click).toBe('cio.components.productCard.click'); + expect(CIO_EVENTS.productCard.conversion).toBe('cio.components.productCard.conversion'); + expect(CIO_EVENTS.productCard.imageEnter).toBe('cio.components.productCard.imageEnter'); + expect(CIO_EVENTS.productCard.imageLeave).toBe('cio.components.productCard.imageLeave'); + }); + + test('carousel event names are correct string literals', () => { + expect(CIO_EVENTS.carousel.next).toBe('cio.components.carousel.next'); + expect(CIO_EVENTS.carousel.previous).toBe('cio.components.carousel.previous'); + }); + + test('CIO_EVENTS object is frozen (immutable)', () => { + expect(Object.isFrozen(CIO_EVENTS)).toBe(true); + expect(Object.isFrozen(CIO_EVENTS.productCard)).toBe(true); + expect(Object.isFrozen(CIO_EVENTS.carousel)).toBe(true); + }); + }); + + describe('dispatchCioEvent', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test('dispatches CustomEvent on window with correct event name', () => { + const listener = vi.fn(); + window.addEventListener(CIO_EVENTS.productCard.click, listener); + + const detail = { product: mockProduct }; + dispatchCioEvent(CIO_EVENTS.productCard.click, detail); + + expect(listener).toHaveBeenCalledTimes(1); + const event = listener.mock.calls[0][0] as CustomEvent; + expect(event.type).toBe('cio.components.productCard.click'); + + window.removeEventListener(CIO_EVENTS.productCard.click, listener); + }); + + test('dispatches with correct detail payload', () => { + const listener = vi.fn(); + window.addEventListener(CIO_EVENTS.productCard.click, listener); + + const detail = { product: mockProduct }; + dispatchCioEvent(CIO_EVENTS.productCard.click, detail); + + const event = listener.mock.calls[0][0] as CustomEvent; + expect(event.detail).toEqual(detail); + + window.removeEventListener(CIO_EVENTS.productCard.click, listener); + }); + + test('no-ops without throwing when window is undefined (SSR)', () => { + const originalWindow = globalThis.window; + delete (globalThis as Record).window; + try { + expect(() => { + dispatchCioEvent(CIO_EVENTS.productCard.click, { product: mockProduct }); + }).not.toThrow(); + } finally { + globalThis.window = originalWindow; + } + }); + + test('dispatches on a specific DOM element when target is provided', () => { + const element = document.createElement('div'); + document.body.appendChild(element); + const listener = vi.fn(); + element.addEventListener(CIO_EVENTS.productCard.click, listener); + + dispatchCioEvent(CIO_EVENTS.productCard.click, { product: mockProduct }, element); + + expect(listener).toHaveBeenCalledTimes(1); + const event = listener.mock.calls[0][0] as CustomEvent; + expect(event.detail.product).toEqual(mockProduct); + + element.removeEventListener(CIO_EVENTS.productCard.click, listener); + document.body.removeChild(element); + }); + + test('event bubbles from child element to parent listener', () => { + const parent = document.createElement('div'); + const child = document.createElement('span'); + parent.appendChild(child); + document.body.appendChild(parent); + + const parentListener = vi.fn(); + parent.addEventListener(CIO_EVENTS.productCard.click, parentListener); + + dispatchCioEvent(CIO_EVENTS.productCard.click, { product: mockProduct }, child); + + expect(parentListener).toHaveBeenCalledTimes(1); + + parent.removeEventListener(CIO_EVENTS.productCard.click, parentListener); + document.body.removeChild(parent); + }); + + test('falls back to window when target is null', () => { + const listener = vi.fn(); + window.addEventListener(CIO_EVENTS.productCard.click, listener); + + dispatchCioEvent(CIO_EVENTS.productCard.click, { product: mockProduct }, null); + + expect(listener).toHaveBeenCalledTimes(1); + + window.removeEventListener(CIO_EVENTS.productCard.click, listener); + }); + + test('falls back to window when target is undefined', () => { + const listener = vi.fn(); + window.addEventListener(CIO_EVENTS.productCard.click, listener); + + dispatchCioEvent(CIO_EVENTS.productCard.click, { product: mockProduct }, undefined); + + expect(listener).toHaveBeenCalledTimes(1); + + window.removeEventListener(CIO_EVENTS.productCard.click, listener); + }); + }); +}); diff --git a/src/components/carousel.tsx b/src/components/carousel.tsx index cc57c6f..db3ba91 100644 --- a/src/components/carousel.tsx +++ b/src/components/carousel.tsx @@ -9,7 +9,7 @@ import React, { import useEmblaCarousel from 'embla-carousel-react'; import Autoplay from 'embla-carousel-autoplay'; -import { cn, RenderPropsWrapper } from '@/utils'; +import { cn, RenderPropsWrapper, dispatchCioEvent, CIO_EVENTS } from '@/utils'; import Button from '@/components/button'; import { useCarouselResponsive } from '@/hooks/useCarouselResponsive'; import { useCarouselTweenOpacity } from '@/hooks/useCarouselTweenOpacity'; @@ -128,6 +128,7 @@ function CarouselBase({ api.on('select', onSelect); return () => { + api?.off('reInit', onSelect); api?.off('select', onSelect); }; }, [api, onSelect]); @@ -310,7 +311,25 @@ function CarouselNavButton({ const isPrevious = direction === 'previous'; const canScroll = isPrevious ? canScrollPrev : canScrollNext; - const handleClick = isPrevious ? scrollPrev : scrollNext; + const scrollFn = isPrevious ? scrollPrev : scrollNext; + + const handleClick = useCallback( + (e: React.MouseEvent) => { + const eventName = isPrevious ? CIO_EVENTS.carousel.previous : CIO_EVENTS.carousel.next; + + dispatchCioEvent( + eventName, + { + direction, + }, + e.currentTarget, + ); + + scrollFn?.(); + }, + [isPrevious, direction, scrollFn], + ); + const override = isPrevious ? componentOverrides?.previous?.reactNode : componentOverrides?.next?.reactNode; @@ -347,7 +366,7 @@ function CarouselNext(props: NavButtonProps) { return ; } -// Create compound component with all sub-components attached +// Attach compound components to Carousel Carousel.Content = CarouselContent; Carousel.Item = CarouselItem; Carousel.Previous = CarouselPrevious; diff --git a/src/components/product-card.tsx b/src/components/product-card.tsx index 9f1c7db..2dfcebd 100644 --- a/src/components/product-card.tsx +++ b/src/components/product-card.tsx @@ -1,5 +1,5 @@ -import React, { createContext, useContext } from 'react'; -import { cn, RenderPropsWrapper } from '@/utils'; +import React, { createContext, useCallback, useContext } from 'react'; +import { cn, RenderPropsWrapper, dispatchCioEvent, CIO_EVENTS } from '@/utils'; import { Card, CardContentProps, CardFooterProps } from '@/components/card'; import Button from '@/components/button'; import BadgeComponent from '@/components/badge'; @@ -45,6 +45,18 @@ const WishlistButton: React.FC = (props) => { children, } = props; + const handleWishlistClick = useCallback( + (e: React.MouseEvent) => { + dispatchCioEvent( + CIO_EVENTS.productCard.wishlist, + { product: renderProps.product }, + e.currentTarget, + ); + onAddToWishlist?.(e, renderProps.product); + }, + [renderProps.product, onAddToWishlist], + ); + return ( = (props) => { size='icon' variant='secondary' conversionType='add_to_wishlist' - onClick={onAddToWishlist} + onClick={handleWishlistClick} aria-label={isInWishlist ? 'Remove from wishlist' : 'Add to wishlist'}> = (props) => { // Use props with fallback to context values const imageUrl = props.imageUrl || contextImageUrl; + const handleMouseEnter = useCallback( + (e: React.MouseEvent) => { + dispatchCioEvent( + CIO_EVENTS.productCard.imageEnter, + { product: renderProps.product }, + e.currentTarget, + ); + }, + [renderProps.product], + ); + + const handleMouseLeave = useCallback( + (e: React.MouseEvent) => { + dispatchCioEvent( + CIO_EVENTS.productCard.imageLeave, + { product: renderProps.product }, + e.currentTarget, + ); + }, + [renderProps.product], + ); + return ( -
+
{name = (props) => { children, } = props; + const handleAddToCartClick = useCallback( + (e: React.MouseEvent) => { + dispatchCioEvent( + CIO_EVENTS.productCard.conversion, + { product: renderProps.product }, + e.currentTarget, + ); + onAddToCart?.(e, renderProps.product); + }, + [renderProps.product, onAddToCart], + ); + return ( = (props) => { props.className, )} conversionType='add_to_cart' - onClick={onAddToCart}> + onClick={handleAddToCartClick}> {addToCartText} )} @@ -350,6 +399,21 @@ function ProductCard({ componentOverrides, children, className, ...props }: Prod ...restProps } = props; + const handleProductClick = useCallback( + (e: React.MouseEvent) => { + const target = e.target as HTMLElement; + + // Do not fire if a conversion button (AddToCart / Wishlist) is clicked + if (target.closest('[data-cnstrc-btn]')) { + return; + } + + dispatchCioEvent(CIO_EVENTS.productCard.click, { product }, e.currentTarget); + onProductClick?.(product); + }, + [product, onProductClick], + ); + const renderPropFn = typeof children === 'function' && children; // Default layout when no children provided or render prop function @@ -361,7 +425,7 @@ function ProductCard({ componentOverrides, children, className, ...props }: Prod 'cio-product-card min-w-[176px] max-w-[256px] h-full cursor-pointer border-0', className, )} - onClick={onProductClick} + onClick={handleProductClick} {...getProductCardDataAttributes(product)} {...restProps}> @@ -393,7 +457,7 @@ function ProductCard({ componentOverrides, children, className, ...props }: Prod ); } -// Create compound component with all sub-components attached +// Attach compound components to ProductCard ProductCard.ImageSection = ImageSection; ProductCard.Badge = Badge; ProductCard.WishlistButton = WishlistButton; diff --git a/src/index.ts b/src/index.ts index b310ab0..eeb0fe2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,11 +3,16 @@ export { default as Button } from '@/components/button'; export { default as Badge } from '@/components/badge'; export { default as ProductCard } from '@/components/product-card'; export { default as Carousel } from '@/components/carousel'; -export { RenderPropsWrapper } from '@/utils'; +export { RenderPropsWrapper, CIO_EVENTS, dispatchCioEvent } from '@/utils'; // Hooks // Types +export type { + ProductCardEventDetail, + CarouselNavEventDetail, + CioEventDetailMap, +} from '@/utils/events'; export type { ButtonVariants, ButtonOverrides, ButtonProps } from '@/components/button'; export type { BadgeVariants, BadgeOverrides, BadgeProps } from '@/components/badge'; export * from '@/types'; diff --git a/src/stories/components/Carousel/CarouselEvents.stories.tsx b/src/stories/components/Carousel/CarouselEvents.stories.tsx new file mode 100644 index 0000000..32db193 --- /dev/null +++ b/src/stories/components/Carousel/CarouselEvents.stories.tsx @@ -0,0 +1,90 @@ +import React, { useEffect, useRef, useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import CioCarousel from '../../../components/carousel'; +import { Product } from '../../../types/productCardTypes'; +import { CIO_EVENTS } from '../../../utils/events'; +import { DEMO_IMAGE_URL } from '../../constants'; + +const meta = { + title: 'Components/Carousel', + component: CioCarousel, + parameters: { + layout: 'fullscreen', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const mockProducts: Product[] = Array.from({ length: 10 }, (_, i) => ({ + id: `product-${i + 1}`, + name: `Product ${i + 1}`, + description: `This is a description for product ${i + 1}`, + imageUrl: DEMO_IMAGE_URL, + price: (Math.random() * 100 + 20).toFixed(2), + salePrice: Math.random() > 0.5 ? (Math.random() * 80 + 10).toFixed(2) : undefined, + rating: (Math.random() * 2 + 3).toFixed(1), + reviewsCount: Math.floor(Math.random() * 500 + 10), + tags: ['Tag 1', 'Tag 2'].slice(0, Math.floor(Math.random() * 3)), +})); + +function CarouselEventListeningDemo() { + const wrapperRef = useRef(null); + const [eventLog, setEventLog] = useState([]); + + useEffect(() => { + const el = wrapperRef.current; + if (!el) return; + + const logEvent = (label: string) => (e: Event) => { + const detail = (e as CustomEvent).detail; + setEventLog((prev) => [ + `[${new Date().toLocaleTimeString()}] ${label} — direction: ${detail?.direction}`, + ...prev.slice(0, 49), + ]); + }; + + const handlers = [ + [CIO_EVENTS.carousel.next, logEvent('carousel.next')] as const, + [CIO_EVENTS.carousel.previous, logEvent('carousel.previous')] as const, + ]; + + handlers.forEach(([name, fn]) => el.addEventListener(name, fn)); + return () => handlers.forEach(([name, fn]) => el.removeEventListener(name, fn)); + }, []); + + return ( +
+
+ +
+
+
Event Log
+ {eventLog.length === 0 ? ( +
Click the carousel arrows to see events...
+ ) : ( + eventLog.map((entry, i) => ( +
+ {entry} +
+ )) + )} +
+
+ ); +} + +export const EventListening: Story = { + render: () => , + tags: ['!autodocs', '!dev'], +}; diff --git a/src/stories/components/Carousel/Code Examples - Events.mdx b/src/stories/components/Carousel/Code Examples - Events.mdx new file mode 100644 index 0000000..2b5b646 --- /dev/null +++ b/src/stories/components/Carousel/Code Examples - Events.mdx @@ -0,0 +1,76 @@ +import { Meta, Canvas } from '@storybook/addon-docs/blocks'; +import * as CarouselEventsStories from './CarouselEvents.stories'; + + + +# `Carousel` - Scoped Event Listening + +Listen for navigation events on a `Carousel` instance by wrapping it in a container element, instead of listening on `window`. + +## Why scoped events? + +When multiple `Carousel` instances exist on the same page, listening on `window` means **every** listener fires for **every** carousel. Scoped events solve this: each carousel dispatches events on its own root DOM element with `bubbles: true`, so a listener attached to any ancestor element only receives events from carousels within that subtree. + +## Interactive Demo + +Click the previous/next arrows to see navigation events appear in the log panel below. + + + +## Basic Setup + +```tsx +import { useRef, useEffect } from 'react'; +import CioCarousel from '@constructorio/ui-components/carousel'; +import { CIO_EVENTS } from '@constructorio/ui-components'; + +function MyCarousel({ products }) { + const wrapperRef = useRef(null); + + useEffect(() => { + const el = wrapperRef.current; + if (!el) return; + + const handleNav = (e: Event) => { + const { direction } = (e as CustomEvent).detail; + console.log(`Scrolled ${direction}`); + }; + + el.addEventListener(CIO_EVENTS.carousel.next, handleNav); + el.addEventListener(CIO_EVENTS.carousel.previous, handleNav); + + return () => { + el.removeEventListener(CIO_EVENTS.carousel.next, handleNav); + el.removeEventListener(CIO_EVENTS.carousel.previous, handleNav); + }; + }, []); + + return ( +
+ +
+ ); +} +``` + +## Available Events + +Both events carry a `CarouselNavEventDetail` payload with the scroll direction. + +| Event Name | Constant | Fires When | +| ------------------------------------- | --------------------------------- | ---------------------- | +| `cio.components.carousel.next` | `CIO_EVENTS.carousel.next` | Next button clicked | +| `cio.components.carousel.previous` | `CIO_EVENTS.carousel.previous` | Previous button clicked| + +**Payload type:** `CarouselNavEventDetail` + +```ts +interface CarouselNavEventDetail { + direction: 'next' | 'previous'; +} +``` + +## Notes + +- All events are dispatched with `bubbles: true`, so you can listen on any ancestor element instead of the carousel itself. +- Use `CIO_EVENTS` constants instead of raw strings to avoid typos and get autocomplete. diff --git a/src/stories/components/ProductCard/Code Examples - Component Overrides.mdx b/src/stories/components/ProductCard/Code Examples - Component Overrides.mdx index ef3f17a..4a814ff 100644 --- a/src/stories/components/ProductCard/Code Examples - Component Overrides.mdx +++ b/src/stories/components/ProductCard/Code Examples - Component Overrides.mdx @@ -101,7 +101,7 @@ const addToCartButtonOverride = { reactNode: (props) => ( @@ -144,7 +144,7 @@ const wishlistButtonOverride = { reactNode: (props) => ( diff --git a/src/stories/components/ProductCard/Code Examples - Events.mdx b/src/stories/components/ProductCard/Code Examples - Events.mdx new file mode 100644 index 0000000..3a04c45 --- /dev/null +++ b/src/stories/components/ProductCard/Code Examples - Events.mdx @@ -0,0 +1,74 @@ +import { Meta, Canvas } from '@storybook/addon-docs/blocks'; +import * as ProductCardEventsStories from './ProductCardEvents.stories'; + + + +# `ProductCard` - Scoped Event Listening + +Listen for user-interaction events on a `ProductCard` instance by wrapping it in a container element, instead of listening on `window`. + +## Why scoped events? + +When multiple `ProductCard` instances exist on the same page, listening on `window` means **every** listener fires for **every** card. Scoped events solve this: each card dispatches events on its own root DOM element with `bubbles: true`, so a listener attached to any ancestor element only receives events from cards within that subtree. + +## Interactive Demo + +Click the card, hover the image, or click "Add to Cart" to see events appear in the log panel below. + + + +## Basic Setup + +```tsx +import { useRef, useEffect } from 'react'; +import ProductCard from '@constructorio/ui-components/product-card'; +import { CIO_EVENTS } from '@constructorio/ui-components'; + +function MyProductCard({ product }) { + const wrapperRef = useRef(null); + + useEffect(() => { + const el = wrapperRef.current; + if (!el) return; + + const handleClick = (e: Event) => { + const { product } = (e as CustomEvent).detail; + console.log('Card clicked:', product.name); + }; + + el.addEventListener(CIO_EVENTS.productCard.click, handleClick); + return () => el.removeEventListener(CIO_EVENTS.productCard.click, handleClick); + }, []); + + return ( +
+ +
+ ); +} +``` + +## Available Events + +All events carry a `ProductCardEventDetail` payload containing the `product: Product` for the card that fired. + +| Event Name | Constant | Fires When | +| --------------------------------------------- | ------------------------------------- | ------------------------- | +| `cio.components.productCard.click` | `CIO_EVENTS.productCard.click` | Card is clicked | +| `cio.components.productCard.conversion` | `CIO_EVENTS.productCard.conversion` | Add to Cart is clicked | +| `cio.components.productCard.imageEnter` | `CIO_EVENTS.productCard.imageEnter` | Mouse enters card image | +| `cio.components.productCard.imageLeave` | `CIO_EVENTS.productCard.imageLeave` | Mouse leaves card image | + +**Payload type:** `ProductCardEventDetail` + +```ts +interface ProductCardEventDetail { + product: Product; +} +``` + +## Notes + +- All events are dispatched with `bubbles: true`, so you can listen on any ancestor element instead of the card itself. +- The `detail` payload always contains the `Product` object for the card that fired the event. +- Use `CIO_EVENTS` constants instead of raw strings to avoid typos and get autocomplete. diff --git a/src/stories/components/ProductCard/Code Examples - Render Props.mdx b/src/stories/components/ProductCard/Code Examples - Render Props.mdx index 36fc7f9..59cfd3c 100644 --- a/src/stories/components/ProductCard/Code Examples - Render Props.mdx +++ b/src/stories/components/ProductCard/Code Examples - Render Props.mdx @@ -36,7 +36,7 @@ function CustomOverrideExample() {

{props.priceCurrency}{props.product.price}

-
@@ -90,7 +90,7 @@ function CompactListStyleCard(props) {
diff --git a/src/stories/components/ProductCard/ProductCard.stories.tsx b/src/stories/components/ProductCard/ProductCard.stories.tsx index 5702fa3..f65c24b 100644 --- a/src/stories/components/ProductCard/ProductCard.stories.tsx +++ b/src/stories/components/ProductCard/ProductCard.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import ProductCard from '../../../components/product-card'; import { CompleteCustomOverrideCard, CompactListStyleCard } from './ProductCardVariants'; -import { ProductCardProps } from '../../../types/productCardTypes'; +import { Product, ProductCardProps } from '../../../types/productCardTypes'; import { DEMO_IMAGE_URL } from '../../constants'; const meta = { @@ -114,7 +114,7 @@ export const WithAddToCart: Story = { imageUrl: DEMO_IMAGE_URL, price: '299', }, - onAddToCart: (e) => console.log('Added to cart', e), + onAddToCart: (e, product) => console.log('Added to cart', e, product), }, argTypes: { onAddToCart: { action: 'add to cart clicked' }, @@ -131,7 +131,7 @@ export const WithWishlist: Story = { imageUrl: DEMO_IMAGE_URL, price: '299', }, - onAddToWishlist: (e) => console.log('Added to wishlist', e), + onAddToWishlist: (e, product) => console.log('Added to wishlist', e, product), }, argTypes: { onAddToWishlist: { action: 'add to wishlist clicked' }, @@ -162,7 +162,7 @@ export const CustomAddToCartText: Story = { imageUrl: DEMO_IMAGE_URL, price: '299', }, - onAddToCart: (e) => console.log('Added to cart', e), + onAddToCart: (e, product) => console.log('Added to cart', e, product), addToCartText: 'Buy Now', }, argTypes: { @@ -184,8 +184,8 @@ export const CustomCurrency: Story = { reviewsCount: 89, }, priceCurrency: '€', - onAddToCart: (e) => console.log('Added to cart', e), - onAddToWishlist: (e) => console.log('Added to wishlist', e), + onAddToCart: (e, product) => console.log('Added to cart', e, product), + onAddToWishlist: (e, product) => console.log('Added to wishlist', e, product), }, argTypes: { onAddToCart: { action: 'add to cart clicked' }, @@ -211,8 +211,8 @@ export const FullyFeatured: Story = { badge: 'New', }, priceCurrency: '$', - onAddToCart: (e) => console.log('Added to cart', e), - onAddToWishlist: (e) => console.log('Added to wishlist', e), + onAddToCart: (e, product) => console.log('Added to cart', e, product), + onAddToWishlist: (e, product) => console.log('Added to wishlist', e, product), onProductClick: () => console.log('Product clicked'), addToCartText: 'Add to Cart', }, @@ -238,8 +238,8 @@ export const InWishlist: Story = { }, priceCurrency: '$', isInWishlist: true, - onAddToWishlist: (e) => console.log('Removed from wishlist', e), - onAddToCart: (e) => console.log('Added to cart', e), + onAddToWishlist: (e, product) => console.log('Removed from wishlist', e, product), + onAddToCart: (e, product) => console.log('Added to cart', e, product), }, argTypes: { onAddToCart: { action: 'add to cart clicked' }, @@ -278,8 +278,8 @@ export const CustomBadge: Story = { reviewsCount: 2713, }, priceCurrency: '$', - onAddToCart: (e) => console.log('Added to cart', e), - onAddToWishlist: (e) => console.log('Added to wishlist', e), + onAddToCart: (e, product) => console.log('Added to cart', e, product), + onAddToWishlist: (e, product) => console.log('Added to wishlist', e, product), componentOverrides: { image: { badge: { @@ -316,8 +316,8 @@ export const CustomBadgeCompound: Story = { reviewsCount: 2713, }, priceCurrency: '$', - onAddToCart: (e) => console.log('Added to cart', e), - onAddToWishlist: (e) => console.log('Added to wishlist', e), + onAddToCart: (e, product) => console.log('Added to cart', e, product), + onAddToWishlist: (e, product) => console.log('Added to wishlist', e, product), }, render: (args) => ( @@ -359,8 +359,8 @@ export const CompoundBasic: Story = { price: '299', }, priceCurrency: '$', - onAddToCart: (e) => console.log('Added to cart', e), - onAddToWishlist: (e) => console.log('Added to wishlist', e), + onAddToCart: (e, product) => console.log('Added to cart', e, product), + onAddToWishlist: (e, product) => console.log('Added to wishlist', e, product), }, render: (args) => ( @@ -407,7 +407,9 @@ export const CompoundFullyFeatured: Story = { console.log('Added to wishlist', e)} + onAddToWishlist={(e: React.MouseEvent, product: Product) => + console.log('Added to wishlist', e, product) + } /> New @@ -419,7 +421,9 @@ export const CompoundFullyFeatured: Story = { console.log('Added to cart', e)} + onAddToCart={(e: React.MouseEvent, product: Product) => + console.log('Added to cart', e, product) + } /> @@ -450,8 +454,8 @@ export const CompoundCustomLayout: Story = { tags: ['Premium', 'Fast Shipping'], }, priceCurrency: '$', - onAddToCart: (e) => console.log('Added to cart', e), - onAddToWishlist: (e) => console.log('Added to wishlist', e), + onAddToCart: (e, product) => console.log('Added to cart', e, product), + onAddToWishlist: (e, product) => console.log('Added to wishlist', e, product), }, render: (args) => ( @@ -494,7 +498,7 @@ export const CompoundGridLayout: Story = { description: 'Premium golf pants designed for comfort and performance on the course', }, priceCurrency: '$', - onAddToCart: (e) => console.log('Added to cart', e), + onAddToCart: (e, product) => console.log('Added to cart', e, product), className: 'overflow-hidden max-w-md', }, render: (args) => ( @@ -530,7 +534,7 @@ export const CompoundMinimal: Story = { price: '199', }, priceCurrency: '$', - onAddToCart: (e) => console.log('Added to cart', e), + onAddToCart: (e, product) => console.log('Added to cart', e, product), }, render: (args) => ( @@ -570,8 +574,8 @@ export const CompleteCustomOverride: Story = { tags: ['Premium', 'Limited Edition'], }, priceCurrency: '$', - onAddToCart: (e) => console.log('Added to cart', e), - onAddToWishlist: (e) => console.log('Added to wishlist', e), + onAddToCart: (e, product) => console.log('Added to cart', e, product), + onAddToWishlist: (e, product) => console.log('Added to wishlist', e, product), children: (props: ProductCardProps) => , }, argTypes: { @@ -594,7 +598,7 @@ export const CompactListStyle: Story = { reviewsCount: 156, }, priceCurrency: '$', - onAddToCart: (e) => console.log('Added to cart', e), + onAddToCart: (e, product) => console.log('Added to cart', e, product), children: (props: ProductCardProps) => , }, argTypes: { @@ -651,11 +655,15 @@ const titleOverride = { }; const addToCartButtonOverride = { - reactNode: (props: { onAddToCart?: (e: React.MouseEvent) => void; addToCartText?: string }) => ( + reactNode: (props: { + onAddToCart?: (e: React.MouseEvent, product: Product) => void; + addToCartText?: string; + product: Product; + }) => ( ), @@ -666,7 +674,7 @@ const wishlistButtonOverride = { ), @@ -681,7 +689,7 @@ const footerOverride = { )} @@ -1097,19 +1107,20 @@ export const ComponentOverrideExample: Story = { tags: ['Same day delivery', 'Free assembly'], }, priceCurrency: '$', - onAddToCart: (e) => console.log('Added to cart', e), - onAddToWishlist: (e) => console.log('Added to wishlist', e), + onAddToCart: (e, product) => console.log('Added to cart', e, product), + onAddToWishlist: (e, product) => console.log('Added to wishlist', e, product), addToCartText: 'Add to Cart', componentOverrides: { footer: { addToCartButton: { reactNode: (props: { - onAddToCart?: (e: React.MouseEvent) => void; + onAddToCart?: (e: React.MouseEvent, product: Product) => void; addToCartText?: string; + product: Product; }) => ( ), @@ -1139,8 +1150,8 @@ export const DataAttributesExample: Story = { reviewsCount: 2713, }, priceCurrency: '$', - onAddToCart: (e) => console.log('Added to cart', e), - onAddToWishlist: (e) => console.log('Added to wishlist', e), + onAddToCart: (e, product) => console.log('Added to cart', e, product), + onAddToWishlist: (e, product) => console.log('Added to wishlist', e, product), addToCartText: 'Add to Cart', // @ts-expect-error: Data Attribute 'data-cnstrc-item-id': 'product-123', @@ -1199,7 +1210,9 @@ export const RenderPropsExample: Story = { {renderProps.onAddToCart && ( )} @@ -1220,8 +1233,8 @@ export const RenderPropsExample: Story = { reviewsCount: 2713, }, priceCurrency: '$', - onAddToCart: (e) => console.log('Added to cart', e), - onAddToWishlist: (e) => console.log('Added to wishlist', e), + onAddToCart: (e, product) => console.log('Added to cart', e, product), + onAddToWishlist: (e, product) => console.log('Added to wishlist', e, product), addToCartText: 'Add to Cart', }, argTypes: { diff --git a/src/stories/components/ProductCard/ProductCardEvents.stories.tsx b/src/stories/components/ProductCard/ProductCardEvents.stories.tsx new file mode 100644 index 0000000..128328d --- /dev/null +++ b/src/stories/components/ProductCard/ProductCardEvents.stories.tsx @@ -0,0 +1,91 @@ +import React, { useEffect, useRef, useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import ProductCard from '../../../components/product-card'; +import { CIO_EVENTS } from '../../../utils/events'; +import { DEMO_IMAGE_URL } from '../../constants'; + +const meta = { + title: 'Components/ProductCard', + component: ProductCard, + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +function ProductCardEventListeningDemo() { + const wrapperRef = useRef(null); + const [eventLog, setEventLog] = useState([]); + + useEffect(() => { + const el = wrapperRef.current; + if (!el) return; + + const logEvent = (label: string) => (e: Event) => { + const detail = (e as CustomEvent).detail; + setEventLog((prev) => [ + `[${new Date().toLocaleTimeString()}] ${label} — product: ${detail?.product?.name ?? 'N/A'}`, + ...prev.slice(0, 49), + ]); + }; + + const handlers = [ + [CIO_EVENTS.productCard.click, logEvent('productCard.click')] as const, + [CIO_EVENTS.productCard.conversion, logEvent('productCard.conversion')] as const, + [CIO_EVENTS.productCard.imageEnter, logEvent('productCard.imageEnter')] as const, + [CIO_EVENTS.productCard.imageLeave, logEvent('productCard.imageLeave')] as const, + ]; + + handlers.forEach(([name, fn]) => el.addEventListener(name, fn)); + return () => handlers.forEach(([name, fn]) => el.removeEventListener(name, fn)); + }, []); + + return ( +
+
+ {}} + /> +
+
+
Event Log
+ {eventLog.length === 0 ? ( +
Interact with the card above to see events...
+ ) : ( + eventLog.map((entry, i) => ( +
+ {entry} +
+ )) + )} +
+
+ ); +} + +export const EventListening: Story = { + render: () => , + tags: ['!autodocs', '!dev'], +}; diff --git a/src/stories/components/ProductCard/ProductCardVariants.tsx b/src/stories/components/ProductCard/ProductCardVariants.tsx index a8ee8b3..6b97f03 100644 --- a/src/stories/components/ProductCard/ProductCardVariants.tsx +++ b/src/stories/components/ProductCard/ProductCardVariants.tsx @@ -49,12 +49,12 @@ export const CompleteCustomOverrideCard: React.FC = (props) =>
@@ -91,7 +91,7 @@ export const CompactListStyleCard: React.FC = (props) => (
diff --git a/src/types/productCardTypes.ts b/src/types/productCardTypes.ts index d0ae77a..a8d030d 100644 --- a/src/types/productCardTypes.ts +++ b/src/types/productCardTypes.ts @@ -22,9 +22,9 @@ export interface ProductCardProps extends Omit, 'chi priceCurrency?: string; addToCartText?: string; isInWishlist?: boolean; - onAddToCart?: (e: React.MouseEvent) => void; - onAddToWishlist?: (e: React.MouseEvent) => void; - onProductClick?: () => void; + onAddToCart?: (e: React.MouseEvent, product: Product) => void; + onAddToWishlist?: (e: React.MouseEvent, product: Product) => void; + onProductClick?: (product: Product) => void; children?: RenderPropsChildren; componentOverrides?: ProductCardOverrides; } @@ -48,7 +48,7 @@ export type ProductCardOverrides = ComponentOverrideProps & { // Section component interfaces export interface WishlistButtonProps extends IncludeRenderProps { - onAddToWishlist?: (e: React.MouseEvent) => void; + onAddToWishlist?: (e: React.MouseEvent, product: Product) => void; isInWishlist?: boolean; className?: string; } @@ -89,7 +89,7 @@ export interface DescriptionSectionProps extends IncludeRenderProps { - onAddToCart?: (e: React.MouseEvent) => void; + onAddToCart?: (e: React.MouseEvent, product: Product) => void; addToCartText?: string; className?: string; } diff --git a/src/utils/events.ts b/src/utils/events.ts new file mode 100644 index 0000000..81c0c32 --- /dev/null +++ b/src/utils/events.ts @@ -0,0 +1,64 @@ +import type { Product } from '@/types/productCardTypes'; + +/** + * Canonical event name constants for Constructor.io UI component events. + */ +export const CIO_EVENTS = Object.freeze({ + productCard: Object.freeze({ + click: 'cio.components.productCard.click' as const, + conversion: 'cio.components.productCard.conversion' as const, + imageEnter: 'cio.components.productCard.imageEnter' as const, + imageLeave: 'cio.components.productCard.imageLeave' as const, + wishlist: 'cio.components.productCard.wishlist' as const, + }), + carousel: Object.freeze({ + next: 'cio.components.carousel.next' as const, + previous: 'cio.components.carousel.previous' as const, + }), +}); + +export interface ProductCardEventDetail { + product: Product; +} + +export interface CarouselNavEventDetail { + direction: 'next' | 'previous'; +} + +export interface CioEventDetailMap { + [CIO_EVENTS.productCard.click]: ProductCardEventDetail; + [CIO_EVENTS.productCard.conversion]: ProductCardEventDetail; + [CIO_EVENTS.productCard.imageEnter]: ProductCardEventDetail; + [CIO_EVENTS.productCard.imageLeave]: ProductCardEventDetail; + [CIO_EVENTS.productCard.wishlist]: ProductCardEventDetail; + [CIO_EVENTS.carousel.next]: CarouselNavEventDetail; + [CIO_EVENTS.carousel.previous]: CarouselNavEventDetail; +} + +/** + * Dispatches a typed CustomEvent for the given CIO event name. + * + * This is the primary pub-sub mechanism for Constructor.io UI component events. + * When a `target` element is provided, the event is dispatched on that element + * and bubbles up the DOM tree. Consumers can listen on the component element or + * any ancestor. When no target is provided (or during SSR), falls back to + * dispatching on `window` for backwards compatibility. + * + * @param eventName - A key from {@link CioEventDetailMap} (use `CIO_EVENTS` constants). + * @param detail - The strongly-typed payload for the event. + * @param target - Optional DOM element to dispatch the event on. Falls back to `window`. + */ +export function dispatchCioEvent( + eventName: K, + detail: CioEventDetailMap[K], + target?: EventTarget | null, +): void { + if (typeof window === 'undefined') return; + + const event = new CustomEvent(eventName, { + bubbles: true, // lets consumers listen on any ancestor, not just the dispatching element. + cancelable: true, + detail, + }); + (target || window).dispatchEvent(event); +} diff --git a/src/utils/index.ts b/src/utils/index.ts index a7ae071..df1b658 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,4 @@ /* eslint-disable no-restricted-imports */ export * from './styleHelpers'; +export * from './events'; export { default as RenderPropsWrapper } from './RenderPropsWrapper';