diff --git a/.prettierrc.js b/.prettierrc.js index 9db12ef65..1ada9fa1c 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -5,7 +5,7 @@ module.exports = { proseWrap: 'always', trailingComma: 'none', singleQuote: true, - plugins: ['prettier-plugin-astro', 'prettier-plugin-svelte'], + plugins: ['prettier-plugin-astro'], overrides: [ { files: '*.astro', diff --git a/package.json b/package.json index 2b05edecf..c75d9bf94 100644 --- a/package.json +++ b/package.json @@ -37,15 +37,12 @@ "concurrently": "^9.2.1", "del": "^7.1.0", "eslint": "^9.39.2", - "eslint-plugin-svelte": "^3.14.0", "postcss": "^8.5.6", "prettier": "3.3.3", "prettier-plugin-astro": "^0.14.1", - "prettier-plugin-svelte": "^3.4.1", "release-plan": "^0.17.2", "replace": "^1.2.2", "shepherd.js": "workspace:*", - "svelte": "^5.49.1", "typescript": "^5.9.3" }, "packageManager": "pnpm@10.28.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c487b3bd..9c446b62a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,9 +35,6 @@ importers: eslint: specifier: ^9.39.2 version: 9.39.2(jiti@2.6.1) - eslint-plugin-svelte: - specifier: ^3.14.0 - version: 3.14.0(eslint@9.39.2(jiti@2.6.1))(svelte@5.49.1) postcss: specifier: ^8.5.6 version: 8.5.6 @@ -47,9 +44,6 @@ importers: prettier-plugin-astro: specifier: ^0.14.1 version: 0.14.1 - prettier-plugin-svelte: - specifier: ^3.4.1 - version: 3.4.1(prettier@3.3.3)(svelte@5.49.1) release-plan: specifier: ^0.17.2 version: 0.17.2 @@ -59,9 +53,6 @@ importers: shepherd.js: specifier: workspace:* version: link:shepherd.js - svelte: - specifier: ^5.49.1 - version: 5.49.1 typescript: specifier: ^5.9.3 version: 5.9.3 @@ -225,15 +216,9 @@ importers: '@rollup/plugin-terser': specifier: ^0.4.4 version: 0.4.4(rollup@4.57.1) - '@sveltejs/vite-plugin-svelte': - specifier: ^6.2.4 - version: 6.2.4(svelte@5.49.1)(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2)) '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 - '@testing-library/svelte': - specifier: ^5.3.1 - version: 5.3.1(svelte@5.49.1)(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2))(vitest@4.0.18) '@vitest/coverage-v8': specifier: ^4.0.18 version: 4.0.18(vitest@4.0.18) @@ -267,9 +252,6 @@ importers: eslint-plugin-cypress: specifier: ^5.2.1 version: 5.2.1(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-svelte: - specifier: ^2.46.1 - version: 2.46.1(eslint@9.39.2(jiti@2.6.1))(svelte@5.49.1) eslint-plugin-vitest: specifier: ^0.5.4 version: 0.5.4(@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18) @@ -294,12 +276,6 @@ importers: prettier: specifier: ^3.8.1 version: 3.8.1 - prettier-plugin-svelte: - specifier: ^3.4.1 - version: 3.4.1(prettier@3.8.1)(svelte@5.49.1) - renamer: - specifier: ^5.0.2 - version: 5.0.2 replace: specifier: ^1.2.2 version: 1.2.2 @@ -330,27 +306,12 @@ importers: rollup-plugin-serve: specifier: ^2.0.3 version: 2.0.3 - rollup-plugin-svelte: - specifier: ^7.2.3 - version: 7.2.3(rollup@4.57.1)(svelte@5.49.1) rollup-plugin-visualizer: specifier: ^5.14.0 version: 5.14.0(rollup@4.57.1) start-server-and-test: specifier: ^2.1.3 version: 2.1.3 - svelte: - specifier: ^5.49.1 - version: 5.49.1 - svelte-eslint-parser: - specifier: ^1.4.1 - version: 1.4.1(svelte@5.49.1) - svelte-preprocess: - specifier: ^6.0.3 - version: 6.0.3(@babel/core@7.28.6)(postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(yaml@2.8.2))(postcss@8.5.6)(svelte@5.49.1)(typescript@5.9.3) - svelte2tsx: - specifier: 0.7.13 - version: 0.7.13(svelte@5.49.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -2148,10 +2109,6 @@ packages: rollup: optional: true - '@rollup/pluginutils@4.2.1': - resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} - engines: {node: '>= 8.0.0'} - '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} @@ -2424,21 +2381,6 @@ packages: peerDependencies: acorn: ^8.9.0 - '@sveltejs/vite-plugin-svelte-inspector@5.0.2': - resolution: {integrity: sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==} - engines: {node: ^20.19 || ^22.12 || >=24} - peerDependencies: - '@sveltejs/vite-plugin-svelte': ^6.0.0-next.0 - svelte: ^5.0.0 - vite: ^6.3.0 || ^7.0.0 - - '@sveltejs/vite-plugin-svelte@6.2.4': - resolution: {integrity: sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==} - engines: {node: ^20.19 || ^22.12 || >=24} - peerDependencies: - svelte: ^5.0.0 - vite: ^6.3.0 || ^7.0.0 - '@swc/helpers@0.5.18': resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==} @@ -2564,25 +2506,6 @@ packages: '@types/react-dom': optional: true - '@testing-library/svelte-core@1.0.0': - resolution: {integrity: sha512-VkUePoLV6oOYwSUvX6ShA8KLnJqZiYMIbP2JW2t0GLWLkJxKGvuH5qrrZBV/X7cXFnLGuFQEC7RheYiZOW68KQ==} - engines: {node: '>=16'} - peerDependencies: - svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0 - - '@testing-library/svelte@5.3.1': - resolution: {integrity: sha512-8Ez7ZOqW5geRf9PF5rkuopODe5RGy3I9XR+kc7zHh26gBiktLaxTfKmhlGaSHYUOTQE7wFsLMN9xCJVCszw47w==} - engines: {node: '>= 10'} - peerDependencies: - svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0 - vite: '*' - vitest: '*' - peerDependenciesMeta: - vite: - optional: true - vitest: - optional: true - '@tootallnate/once@1.1.2': resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} engines: {node: '>= 6'} @@ -3079,10 +3002,6 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} - array-back@6.2.2: - resolution: {integrity: sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==} - engines: {node: '>=12.17'} - array-find-index@1.0.2: resolution: {integrity: sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==} engines: {node: '>=0.10.0'} @@ -3338,10 +3257,6 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} - chalk-template@0.4.0: - resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} - engines: {node: '>=12'} - chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -3504,19 +3419,6 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} - command-line-args@6.0.1: - resolution: {integrity: sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==} - engines: {node: '>=12.20'} - peerDependencies: - '@75lb/nature': latest - peerDependenciesMeta: - '@75lb/nature': - optional: true - - command-line-usage@7.0.3: - resolution: {integrity: sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==} - engines: {node: '>=12.20.0'} - commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} @@ -3699,10 +3601,6 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - current-module-paths@1.1.3: - resolution: {integrity: sha512-7AH+ZTRKikdK4s1RmY0l6067UD/NZc7p3zZVZxvmnH80G31kr0y0W0E6ibYM4IS01MEm8DiC5FnTcgcgkbFHoA==} - engines: {node: '>=12.17'} - cypress-plugin-tab@1.0.5: resolution: {integrity: sha512-QtTJcifOVwwbeMP3hsOzQOKf3EqKsLyjtg9ZAGlYDntrCRXrsQhe4ZQGIthRMRLKpnP6/tTk6G0gJ2sZUfRliQ==} @@ -3752,9 +3650,6 @@ packages: decode-named-character-reference@1.2.0: resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} - dedent-js@1.0.1: - resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} - deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -4009,37 +3904,11 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - eslint-compat-utils@0.5.1: - resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==} - engines: {node: '>=12'} - peerDependencies: - eslint: '>=6.0.0' - eslint-plugin-cypress@5.2.1: resolution: {integrity: sha512-HTJLbcd7fwJ4agbHinZ4FUIl38bUTJT3BmH8zdgS2V32LETmPqCtWHi3xlgZ2vpX0aW6kQoHCVVqHm8NxZJ9sA==} peerDependencies: eslint: '>=9' - eslint-plugin-svelte@2.46.1: - resolution: {integrity: sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==} - engines: {node: ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0-0 || ^9.0.0-0 - svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 - peerDependenciesMeta: - svelte: - optional: true - - eslint-plugin-svelte@3.14.0: - resolution: {integrity: sha512-Isw0GvaMm0yHxAj71edAdGFh28ufYs+6rk2KlbbZphnqZAzrH3Se3t12IFh2H9+1F/jlDhBBL4oiOJmLqmYX0g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.1 || ^9.0.0 - svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 - peerDependenciesMeta: - svelte: - optional: true - eslint-plugin-vitest@0.5.4: resolution: {integrity: sha512-um+odCkccAHU53WdKAw39MY61+1x990uXjSPguUCq3VcEHdqJrOb8OTMrbYlY6f9jAKx7x98kLVlIe3RJeJqoQ==} engines: {node: ^18.0.0 || >= 20.0.0} @@ -4053,10 +3922,6 @@ packages: vitest: optional: true - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4086,10 +3951,6 @@ packages: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - esquery@1.6.0: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} @@ -4195,9 +4056,6 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-diff@1.3.0: - resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -4248,15 +4106,6 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} - file-set@5.3.0: - resolution: {integrity: sha512-FKCxdjLX0J6zqTWdT0RXIxNF/n7MyXXnsSUp0syLEOCKdexvPZ02lNNv2a+gpK9E3hzUYF3+eFZe32ci7goNUg==} - engines: {node: '>=12.17'} - peerDependencies: - '@75lb/nature': latest - peerDependenciesMeta: - '@75lb/nature': - optional: true - file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -4271,15 +4120,6 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - find-replace@5.0.2: - resolution: {integrity: sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==} - engines: {node: '>=14'} - peerDependencies: - '@75lb/nature': latest - peerDependenciesMeta: - '@75lb/nature': - optional: true - find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -5016,12 +4856,6 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} - known-css-properties@0.35.0: - resolution: {integrity: sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==} - - known-css-properties@0.37.0: - resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} - kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} @@ -5143,10 +4977,6 @@ packages: engines: {node: '>=8.0.0'} hasBin: true - load-module@5.0.0: - resolution: {integrity: sha512-zZBnYIvAuP2TprnRisam+N/A3v+JX60pvdKoHQRKyl4xlHLQQLpp7JKNyEQ6D3Si0/QIQMgXko3PtV+cx6L7mA==} - engines: {node: '>=12.20'} - loader-utils@3.3.1: resolution: {integrity: sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==} engines: {node: '>= 12.13.0'} @@ -5201,9 +5031,6 @@ packages: longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} - lower-case@2.0.2: - resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} - lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -5614,9 +5441,6 @@ packages: nlcst-to-string@4.0.0: resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} - no-case@3.0.4: - resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} - node-emoji@2.2.0: resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} engines: {node: '>=18'} @@ -5895,9 +5719,6 @@ packages: parse5@8.0.0: resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} - pascal-case@3.1.2: - resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} - path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -6080,24 +5901,6 @@ packages: ts-node: optional: true - postcss-load-config@6.0.1: - resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} - engines: {node: '>= 18'} - peerDependencies: - jiti: '>=1.21.0' - postcss: '>=8.0.9' - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - jiti: - optional: true - postcss: - optional: true - tsx: - optional: true - yaml: - optional: true - postcss-merge-longhand@5.1.7: resolution: {integrity: sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==} engines: {node: ^10 || ^12 || >=14.0} @@ -6349,24 +6152,6 @@ packages: peerDependencies: postcss: ^8.4.32 - postcss-safe-parser@6.0.0: - resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.3.3 - - postcss-safe-parser@7.0.1: - resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} - engines: {node: '>=18.0'} - peerDependencies: - postcss: ^8.4.31 - - postcss-scss@4.0.9: - resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.4.29 - postcss-selector-parser@6.0.10: resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} engines: {node: '>=4'} @@ -6418,12 +6203,6 @@ packages: resolution: {integrity: sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==} engines: {node: ^14.15.0 || >=16.0.0} - prettier-plugin-svelte@3.4.1: - resolution: {integrity: sha512-xL49LCloMoZRvSwa6IEdN2GV6cq2IqpYGstYtMT+5wmml1/dClEoI0MZR78MiVPpu6BdQFfN0/y73yO6+br5Pg==} - peerDependencies: - prettier: ^3.0.0 - svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 - prettier@3.3.3: resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} engines: {node: '>=14'} @@ -6446,11 +6225,6 @@ packages: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} - printj@1.3.1: - resolution: {integrity: sha512-GA3TdL8szPK4AQ2YnOe/b+Y1jUFwmmGMMK/qbY7VcE3Z7FU8JstbKiKRzO6CIiAKPhTO8m01NoQ0V5f3jc4OGg==} - engines: {node: '>=0.8'} - hasBin: true - prismjs@1.30.0: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} @@ -6686,11 +6460,6 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} - renamer@5.0.2: - resolution: {integrity: sha512-RGscQTpahtyp6DeEOjp1it2f0kac32oNK4+l4zjzXU8BNGCkYPOl6P6KAgBEwaP++6Bwt+vg2MJ/aJwtljMsSg==} - engines: {node: '>=18'} - hasBin: true - replace@1.2.2: resolution: {integrity: sha512-C4EDifm22XZM2b2JOYe6Mhn+lBsLBAvLbK8drfUQLTfD1KYl/n3VaW/CDju0Ny4w3xTtegBpg8YNSpFJPUDSjA==} engines: {node: '>= 6'} @@ -6730,10 +6499,6 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - resolve.exports@2.0.3: - resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} - engines: {node: '>=10'} - resolve@1.22.11: resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} engines: {node: '>= 0.4'} @@ -6806,13 +6571,6 @@ packages: rollup-plugin-serve@2.0.3: resolution: {integrity: sha512-gQKmfQng17+jOsX5tmDanvJkm0f9XLqWVvXsD7NGd1SlneT+U1j/HjslDUXQz6cqwLnVDRc6xF2lj6rre+eeeQ==} - rollup-plugin-svelte@7.2.3: - resolution: {integrity: sha512-LlniP+h00DfM+E4eav/Kk8uGjgPUjGIBfrAS/IxQvsuFdqSM0Y2sXf31AdxuIGSW9GsmocDqOfaxR5QNno/Tgw==} - engines: {node: '>=10'} - peerDependencies: - rollup: '>=2.0.0' - svelte: '>=3.5.0' - rollup-plugin-visualizer@5.14.0: resolution: {integrity: sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==} engines: {node: '>=18'} @@ -7100,10 +6858,6 @@ packages: stream-combiner@0.0.4: resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==} - stream-read-all@4.0.0: - resolution: {integrity: sha512-4MdJwfor9RkFCH1GCDCrEsLVqei+FrtogHtgyf2OdTlOq/+6+pW6FG1xzkdeK8/fd8/rGA7l3oJ1WolxNLX85w==} - engines: {node: '>=12.20'} - stream-replace-string@2.0.0: resolution: {integrity: sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==} @@ -7207,67 +6961,6 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svelte-eslint-parser@0.43.0: - resolution: {integrity: sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 - peerDependenciesMeta: - svelte: - optional: true - - svelte-eslint-parser@1.4.1: - resolution: {integrity: sha512-1eqkfQ93goAhjAXxZiu1SaKI9+0/sxp4JIWQwUpsz7ybehRE5L8dNuz7Iry7K22R47p5/+s9EM+38nHV2OlgXA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.24.0} - peerDependencies: - svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 - peerDependenciesMeta: - svelte: - optional: true - - svelte-preprocess@6.0.3: - resolution: {integrity: sha512-PLG2k05qHdhmRG7zR/dyo5qKvakhm8IJ+hD2eFRQmMLHp7X3eJnjeupUtvuRpbNiF31RjVw45W+abDwHEmP5OA==} - engines: {node: '>= 18.0.0'} - peerDependencies: - '@babel/core': ^7.10.2 - coffeescript: ^2.5.1 - less: ^3.11.3 || ^4.0.0 - postcss: ^7 || ^8 - postcss-load-config: '>=3' - pug: ^3.0.0 - sass: ^1.26.8 - stylus: '>=0.55' - sugarss: ^2.0.0 || ^3.0.0 || ^4.0.0 - svelte: ^4.0.0 || ^5.0.0-next.100 || ^5.0.0 - typescript: ^5.0.0 - peerDependenciesMeta: - '@babel/core': - optional: true - coffeescript: - optional: true - less: - optional: true - postcss: - optional: true - postcss-load-config: - optional: true - pug: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - typescript: - optional: true - - svelte2tsx@0.7.13: - resolution: {integrity: sha512-aObZ93/kGAiLXA/I/kP+x9FriZM+GboB/ReOIGmLNbVGEd2xC+aTCppm3mk1cc9I/z60VQf7b2QDxC3jOXu3yw==} - peerDependencies: - svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 - typescript: ^4.9.4 || ^5.0.0 - svelte@5.49.1: resolution: {integrity: sha512-jj95WnbKbXsXXngYj28a4zx8jeZx50CN/J4r0CEeax2pbfdsETv/J1K8V9Hbu3DCXnpHz5qAikICuxEooi7eNQ==} engines: {node: '>=18'} @@ -7285,10 +6978,6 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - table-layout@4.1.1: - resolution: {integrity: sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==} - engines: {node: '>=12.17'} - tailwindcss@4.1.18: resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} @@ -7482,10 +7171,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - typical@7.3.0: - resolution: {integrity: sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==} - engines: {node: '>=12.17'} - uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} @@ -8132,10 +7817,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - wordwrapjs@5.1.1: - resolution: {integrity: sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==} - engines: {node: '>=12.17'} - wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -10875,11 +10556,6 @@ snapshots: optionalDependencies: rollup: 4.57.1 - '@rollup/pluginutils@4.2.1': - dependencies: - estree-walker: 2.0.2 - picomatch: 2.3.1 - '@rollup/pluginutils@5.3.0(rollup@4.57.1)': dependencies: '@types/estree': 1.0.8 @@ -11133,23 +10809,7 @@ snapshots: '@sveltejs/acorn-typescript@1.0.8(acorn@8.15.0)': dependencies: acorn: 8.15.0 - - '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.49.1)(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2)))(svelte@5.49.1)(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2))': - dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.49.1)(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2)) - obug: 2.1.1 - svelte: 5.49.1 - vite: 7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2) - - '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.49.1)(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2))': - dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.49.1)(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2)))(svelte@5.49.1)(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2)) - deepmerge: 4.3.1 - magic-string: 0.30.21 - obug: 2.1.1 - svelte: 5.49.1 - vite: 7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2) - vitefu: 1.1.1(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2)) + optional: true '@swc/helpers@0.5.18': dependencies: @@ -11230,7 +10890,7 @@ snapshots: '@testing-library/dom@10.4.1': dependencies: - '@babel/code-frame': 7.28.6 + '@babel/code-frame': 7.29.0 '@babel/runtime': 7.28.6 '@types/aria-query': 5.0.4 aria-query: 5.3.0 @@ -11257,19 +10917,6 @@ snapshots: optionalDependencies: '@types/react': 19.2.10 - '@testing-library/svelte-core@1.0.0(svelte@5.49.1)': - dependencies: - svelte: 5.49.1 - - '@testing-library/svelte@5.3.1(svelte@5.49.1)(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2))(vitest@4.0.18)': - dependencies: - '@testing-library/dom': 10.4.1 - '@testing-library/svelte-core': 1.0.0(svelte@5.49.1) - svelte: 5.49.1 - optionalDependencies: - vite: 7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2) - vitest: 4.0.18(@types/node@25.1.0)(@vitest/ui@4.0.18)(happy-dom@16.8.1)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2) - '@tootallnate/once@1.1.2': {} '@tootallnate/once@2.0.0': {} @@ -11861,8 +11508,6 @@ snapshots: aria-query@5.3.2: {} - array-back@6.2.2: {} - array-find-index@1.0.2: {} array-iterate@2.0.1: {} @@ -12389,10 +12034,6 @@ snapshots: chai@6.2.2: {} - chalk-template@0.4.0: - dependencies: - chalk: 4.1.2 - chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -12542,20 +12183,6 @@ snapshots: comma-separated-tokens@2.0.3: {} - command-line-args@6.0.1: - dependencies: - array-back: 6.2.2 - find-replace: 5.0.2 - lodash.camelcase: 4.3.0 - typical: 7.3.0 - - command-line-usage@7.0.3: - dependencies: - array-back: 6.2.2 - chalk-template: 0.4.0 - table-layout: 4.1.1 - typical: 7.3.0 - commander@10.0.1: {} commander@11.1.0: {} @@ -12779,8 +12406,6 @@ snapshots: csstype@3.2.3: {} - current-module-paths@1.1.3: {} - cypress-plugin-tab@1.0.5: dependencies: ally.js: 1.4.1 @@ -12867,8 +12492,6 @@ snapshots: dependencies: character-entities: 2.0.2 - dedent-js@1.0.1: {} - deep-extend@0.6.0: {} deep-is@0.1.4: {} @@ -13143,53 +12766,11 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-compat-utils@0.5.1(eslint@9.39.2(jiti@2.6.1)): - dependencies: - eslint: 9.39.2(jiti@2.6.1) - semver: 7.7.3 - eslint-plugin-cypress@5.2.1(eslint@9.39.2(jiti@2.6.1)): dependencies: eslint: 9.39.2(jiti@2.6.1) globals: 16.5.0 - eslint-plugin-svelte@2.46.1(eslint@9.39.2(jiti@2.6.1))(svelte@5.49.1): - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) - '@jridgewell/sourcemap-codec': 1.5.5 - eslint: 9.39.2(jiti@2.6.1) - eslint-compat-utils: 0.5.1(eslint@9.39.2(jiti@2.6.1)) - esutils: 2.0.3 - known-css-properties: 0.35.0 - postcss: 8.5.6 - postcss-load-config: 3.1.4(postcss@8.5.6) - postcss-safe-parser: 6.0.0(postcss@8.5.6) - postcss-selector-parser: 6.1.2 - semver: 7.7.3 - svelte-eslint-parser: 0.43.0(svelte@5.49.1) - optionalDependencies: - svelte: 5.49.1 - transitivePeerDependencies: - - ts-node - - eslint-plugin-svelte@3.14.0(eslint@9.39.2(jiti@2.6.1))(svelte@5.49.1): - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1)) - '@jridgewell/sourcemap-codec': 1.5.5 - eslint: 9.39.2(jiti@2.6.1) - esutils: 2.0.3 - globals: 16.5.0 - known-css-properties: 0.37.0 - postcss: 8.5.6 - postcss-load-config: 3.1.4(postcss@8.5.6) - postcss-safe-parser: 7.0.1(postcss@8.5.6) - semver: 7.7.3 - svelte-eslint-parser: 1.4.1(svelte@5.49.1) - optionalDependencies: - svelte: 5.49.1 - transitivePeerDependencies: - - ts-node - eslint-plugin-vitest@0.5.4(@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18): dependencies: '@typescript-eslint/utils': 7.18.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) @@ -13201,11 +12782,6 @@ snapshots: - supports-color - typescript - eslint-scope@7.2.2: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 @@ -13256,7 +12832,8 @@ snapshots: transitivePeerDependencies: - supports-color - esm-env@1.2.2: {} + esm-env@1.2.2: + optional: true espree@10.4.0: dependencies: @@ -13264,12 +12841,6 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.15.0) eslint-visitor-keys: 4.2.1 - espree@9.6.1: - dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) - eslint-visitor-keys: 3.4.3 - esquery@1.6.0: dependencies: estraverse: 5.3.0 @@ -13277,6 +12848,7 @@ snapshots: esrap@2.2.2: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + optional: true esrecurse@4.3.0: dependencies: @@ -13413,8 +12985,6 @@ snapshots: fast-deep-equal@3.1.3: {} - fast-diff@1.3.0: {} - fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -13461,11 +13031,6 @@ snapshots: dependencies: flat-cache: 4.0.1 - file-set@5.3.0: - dependencies: - array-back: 6.2.2 - fast-glob: 3.3.3 - file-uri-to-path@1.0.0: {} filelist@1.0.4: @@ -13478,8 +13043,6 @@ snapshots: dependencies: to-regex-range: 5.0.1 - find-replace@5.0.2: {} - find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -14240,6 +13803,7 @@ snapshots: is-reference@3.0.3: dependencies: '@types/estree': 1.0.8 + optional: true is-stream@2.0.1: {} @@ -14390,10 +13954,6 @@ snapshots: kleur@4.1.5: {} - known-css-properties@0.35.0: {} - - known-css-properties@0.37.0: {} - kolorist@1.8.0: {} ky@1.14.2: {} @@ -14491,10 +14051,6 @@ snapshots: - bufferutil - utf-8-validate - load-module@5.0.0: - dependencies: - array-back: 6.2.2 - loader-utils@3.3.1: {} local-pkg@1.1.2: @@ -14503,7 +14059,8 @@ snapshots: pkg-types: 2.3.0 quansync: 0.2.11 - locate-character@3.0.0: {} + locate-character@3.0.0: + optional: true locate-path@5.0.0: dependencies: @@ -14543,10 +14100,6 @@ snapshots: longest-streak@3.1.0: {} - lower-case@2.0.2: - dependencies: - tslib: 2.8.1 - lru-cache@10.4.3: {} lru-cache@11.2.5: {} @@ -15286,11 +14839,6 @@ snapshots: dependencies: '@types/nlcst': 2.0.3 - no-case@3.0.4: - dependencies: - lower-case: 2.0.2 - tslib: 2.8.1 - node-emoji@2.2.0: dependencies: '@sindresorhus/is': 4.6.0 @@ -15629,11 +15177,6 @@ snapshots: entities: 6.0.1 optional: true - pascal-case@3.1.2: - dependencies: - no-case: 3.0.4 - tslib: 2.8.1 - path-browserify@1.0.1: {} path-exists@4.0.0: {} @@ -15785,15 +15328,6 @@ snapshots: optionalDependencies: postcss: 8.5.6 - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(yaml@2.8.2): - dependencies: - lilconfig: 3.1.3 - optionalDependencies: - jiti: 2.6.1 - postcss: 8.5.6 - yaml: 2.8.2 - optional: true - postcss-merge-longhand@5.1.7(postcss@8.5.6): dependencies: postcss: 8.5.6 @@ -16034,18 +15568,6 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-safe-parser@6.0.0(postcss@8.5.6): - dependencies: - postcss: 8.5.6 - - postcss-safe-parser@7.0.1(postcss@8.5.6): - dependencies: - postcss: 8.5.6 - - postcss-scss@4.0.9(postcss@8.5.6): - dependencies: - postcss: 8.5.6 - postcss-selector-parser@6.0.10: dependencies: cssesc: 3.0.0 @@ -16099,16 +15621,6 @@ snapshots: prettier: 3.3.3 sass-formatter: 0.7.9 - prettier-plugin-svelte@3.4.1(prettier@3.3.3)(svelte@5.49.1): - dependencies: - prettier: 3.3.3 - svelte: 5.49.1 - - prettier-plugin-svelte@3.4.1(prettier@3.8.1)(svelte@5.49.1): - dependencies: - prettier: 3.8.1 - svelte: 5.49.1 - prettier@3.3.3: {} prettier@3.8.1: {} @@ -16125,8 +15637,6 @@ snapshots: dependencies: parse-ms: 4.0.0 - printj@1.3.1: {} - prismjs@1.30.0: {} proc-log@3.0.0: {} @@ -16431,23 +15941,6 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 - renamer@5.0.2: - dependencies: - array-back: 6.2.2 - chalk: 5.6.2 - command-line-args: 6.0.1 - command-line-usage: 7.0.3 - current-module-paths: 1.1.3 - fast-diff: 1.3.0 - file-set: 5.3.0 - global-dirs: 3.0.1 - load-module: 5.0.0 - printj: 1.3.1 - stream-read-all: 4.0.0 - typical: 7.3.0 - transitivePeerDependencies: - - '@75lb/nature' - replace@1.2.2: dependencies: chalk: 2.4.2 @@ -16476,8 +15969,6 @@ snapshots: resolve-from@5.0.0: {} - resolve.exports@2.0.3: {} - resolve@1.22.11: dependencies: is-core-module: 2.16.1 @@ -16592,13 +16083,6 @@ snapshots: mime: 3.0.0 opener: 1.5.2 - rollup-plugin-svelte@7.2.3(rollup@4.57.1)(svelte@5.49.1): - dependencies: - '@rollup/pluginutils': 4.2.1 - resolve.exports: 2.0.3 - rollup: 4.57.1 - svelte: 5.49.1 - rollup-plugin-visualizer@5.14.0(rollup@4.57.1): dependencies: open: 8.4.2 @@ -16980,8 +16464,6 @@ snapshots: dependencies: duplexer: 0.1.2 - stream-read-all@4.0.0: {} - stream-replace-string@2.0.0: {} string-argv@0.3.2: {} @@ -17082,43 +16564,6 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-eslint-parser@0.43.0(svelte@5.49.1): - dependencies: - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - postcss: 8.5.6 - postcss-scss: 4.0.9(postcss@8.5.6) - optionalDependencies: - svelte: 5.49.1 - - svelte-eslint-parser@1.4.1(svelte@5.49.1): - dependencies: - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - postcss: 8.5.6 - postcss-scss: 4.0.9(postcss@8.5.6) - postcss-selector-parser: 7.1.1 - optionalDependencies: - svelte: 5.49.1 - - svelte-preprocess@6.0.3(@babel/core@7.28.6)(postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(yaml@2.8.2))(postcss@8.5.6)(svelte@5.49.1)(typescript@5.9.3): - dependencies: - svelte: 5.49.1 - optionalDependencies: - '@babel/core': 7.28.6 - postcss: 8.5.6 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(yaml@2.8.2) - typescript: 5.9.3 - - svelte2tsx@0.7.13(svelte@5.49.1)(typescript@5.9.3): - dependencies: - dedent-js: 1.0.1 - pascal-case: 3.1.2 - svelte: 5.49.1 - typescript: 5.9.3 - svelte@5.49.1: dependencies: '@jridgewell/remapping': 2.3.5 @@ -17136,6 +16581,7 @@ snapshots: locate-character: 3.0.0 magic-string: 0.30.21 zimmerframe: 1.1.4 + optional: true svgo@2.8.0: dependencies: @@ -17160,11 +16606,6 @@ snapshots: symbol-tree@3.2.4: optional: true - table-layout@4.1.1: - dependencies: - array-back: 6.2.2 - wordwrapjs: 5.1.1 - tailwindcss@4.1.18: {} tapable@2.3.0: {} @@ -17327,8 +16768,6 @@ snapshots: typescript@5.9.3: {} - typical@7.3.0: {} - uc.micro@2.1.0: {} ufo@1.6.1: {} @@ -17617,10 +17056,6 @@ snapshots: optionalDependencies: vite: 6.4.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2) - vitefu@1.1.1(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2)): - optionalDependencies: - vite: 7.3.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2) - vitest@4.0.18(@types/node@25.1.0)(@vitest/ui@4.0.18)(happy-dom@16.8.1)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.18 @@ -17880,8 +17315,6 @@ snapshots: word-wrap@1.2.5: {} - wordwrapjs@5.1.1: {} - wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -18011,7 +17444,8 @@ snapshots: yoctocolors@2.1.2: {} - zimmerframe@1.1.4: {} + zimmerframe@1.1.4: + optional: true zod-to-json-schema@3.25.1(zod@3.25.76): dependencies: diff --git a/shepherd.js/.gitignore b/shepherd.js/.gitignore index 653ef766b..79b428836 100644 --- a/shepherd.js/.gitignore +++ b/shepherd.js/.gitignore @@ -5,6 +5,7 @@ /.log/ /.nyc_output/ /coverage/ +/cypress/ /test/coverage/ /node_modules/ /tmp/ diff --git a/shepherd.js/eslint.config.js b/shepherd.js/eslint.config.js index fe1f32365..dc9ee5ac6 100644 --- a/shepherd.js/eslint.config.js +++ b/shepherd.js/eslint.config.js @@ -1,10 +1,8 @@ import js from '@eslint/js'; -import svelte from 'eslint-plugin-svelte'; import cypress from 'eslint-plugin-cypress/flat'; import globals from 'globals'; import ts from '@typescript-eslint/eslint-plugin'; import tsParser from '@typescript-eslint/parser'; -import svelteParser from 'svelte-eslint-parser'; export default [ // Base config for all files @@ -28,19 +26,6 @@ export default [ } }, - // Svelte files - ...svelte.configs['flat/recommended'], - { - files: ['**/*.svelte'], - languageOptions: { - parser: svelteParser - }, - rules: { - 'svelte/no-at-html-tags': 'off', - 'svelte/valid-compile': 'off' - } - }, - // TypeScript files { files: ['**/*.ts'], @@ -115,7 +100,6 @@ export default [ '.eslintrc.js', '.prettierrc.js', 'rollup.config.mjs', - 'svelte.config.js', 'eslint.config.js', 'test/unit/babel.config.cjs' ], diff --git a/shepherd.js/package.json b/shepherd.js/package.json index c24f83aae..87c1015fe 100644 --- a/shepherd.js/package.json +++ b/shepherd.js/package.json @@ -18,16 +18,20 @@ "Robbie Wagner ", "Chuck Carpenter " ], - "main": "./dist/js/shepherd.mjs", + "main": "./dist/cjs/shepherd.cjs", "module": "./dist/js/shepherd.mjs", "exports": { ".": { "import": { "types": "./dist/js/shepherd.d.mts", "default": "./dist/js/shepherd.mjs" + }, + "require": { + "types": "./dist/cjs/shepherd.d.cts", + "default": "./dist/cjs/shepherd.cjs" } }, - "./dist/css/shepherd.css": "./dist/css/shepherd.css" + "./dist/*": "./dist/*" }, "type": "module", "scripts": { @@ -41,8 +45,8 @@ "lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\"", "lint:js": "eslint . --cache", "lint:js:fix": "eslint . --fix", - "lint:prettier": "prettier --check '**/*.{js,svelte,ts}'", - "lint:prettier:fix": "prettier --write '**/*.{js,svelte,ts}'", + "lint:prettier": "prettier --check '**/*.{js,ts}'", + "lint:prettier:fix": "prettier --write '**/*.{js,ts}'", "prepack": "pnpm build", "start-test-server": "http-server . -p 9002", "test": "vitest", @@ -73,9 +77,7 @@ "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-replace": "^6.0.3", "@rollup/plugin-terser": "^0.4.4", - "@sveltejs/vite-plugin-svelte": "^6.2.4", "@testing-library/jest-dom": "^6.9.1", - "@testing-library/svelte": "^5.3.1", "@vitest/coverage-v8": "^4.0.18", "@vitest/expect": "^4.0.18", "autoprefixer": "^10.4.24", @@ -87,7 +89,6 @@ "dts-bundle-generator": "^9.5.1", "eslint": "^9.39.2", "eslint-plugin-cypress": "^5.2.1", - "eslint-plugin-svelte": "^2.46.1", "eslint-plugin-vitest": "^0.5.4", "execa": "^9.6.1", "globals": "^17.2.0", @@ -96,8 +97,6 @@ "lodash": "^4.17.23", "postcss": "^8.5.6", "prettier": "^3.8.1", - "prettier-plugin-svelte": "^3.4.1", - "renamer": "^5.0.2", "replace": "^1.2.2", "resize-observer-polyfill": "^1.5.1", "rimraf": "^6.1.2", @@ -108,13 +107,8 @@ "rollup-plugin-livereload": "^2.0.5", "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-serve": "^2.0.3", - "rollup-plugin-svelte": "^7.2.3", "rollup-plugin-visualizer": "^5.14.0", "start-server-and-test": "^2.1.3", - "svelte": "^5.49.1", - "svelte-eslint-parser": "^1.4.1", - "svelte-preprocess": "^6.0.3", - "svelte2tsx": "0.7.13", "typescript": "^5.9.3", "vite": "^7.3.1", "vitest": "^4.0.18" diff --git a/shepherd.js/rollup.config.mjs b/shepherd.js/rollup.config.mjs index 3788bb9a7..5d420b51f 100644 --- a/shepherd.js/rollup.config.mjs +++ b/shepherd.js/rollup.config.mjs @@ -10,10 +10,7 @@ import license from 'rollup-plugin-license'; import postcss from 'rollup-plugin-postcss'; import replace from '@rollup/plugin-replace'; import { nodeResolve } from '@rollup/plugin-node-resolve'; -import { sveltePreprocess } from 'svelte-preprocess'; -import svelte from 'rollup-plugin-svelte'; import { visualizer } from 'rollup-plugin-visualizer'; -import { emitDts } from 'svelte2tsx'; import terser from '@rollup/plugin-terser'; const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); @@ -23,18 +20,9 @@ const isDev = process.env.DEVELOPMENT; const env = isDev ? 'development' : 'production'; const plugins = [ - svelte({ - preprocess: sveltePreprocess({ - globalStyle: true, - typescript: true - }), - emitCss: true - }), nodeResolve({ browser: true, - exportConditions: ['svelte'], - extensions: ['.js', '.json', '.mjs', '.svelte', '.ts'], - modulesOnly: true, + extensions: ['.js', '.json', '.mjs', '.ts'], preferBuiltins: false }), replace({ @@ -42,7 +30,10 @@ const plugins = [ preventAssignment: true }), babel({ - extensions: ['.cjs', '.js', '.ts', '.mjs', '.html', '.svelte'] + extensions: ['.cjs', '.js', '.ts', '.mjs'], + babelHelpers: 'bundled', + presets: ['@babel/preset-typescript'], + exclude: 'node_modules/**' }), postcss({ plugins: isDev ? [autoprefixer] : [autoprefixer, cssnanoPlugin], @@ -76,48 +67,30 @@ if (process.env.DEVELOPMENT) { ); } +const sharedConfig = { + input: 'src/shepherd.ts', + // More aggressive tree shaking + treeshake: { + moduleSideEffects: (id) => id.endsWith('.css'), + propertyReadSideEffects: false, + unknownGlobalSideEffects: false + } +}; + export default [ + // ESM build { - input: 'src/shepherd.ts', - + ...sharedConfig, output: { dir: 'tmp/js', entryFileNames: '[name].mjs', format: 'es', sourcemap: true }, - // More aggressive tree shaking - treeshake: { - moduleSideEffects: false, - propertyReadSideEffects: false, - unknownGlobalSideEffects: false - }, plugins: [ - { - name: 'Build Declarations', - buildStart: async () => { - console.log('Generating Svelte declarations for ESM'); - - await emitDts({ - svelteShimsPath: import.meta.resolve( - 'svelte2tsx/svelte-shims-v4.d.ts' - ), - declarationDir: 'tmp/js' - }); - - console.log('Rename .svelte.d.ts to .d.svelte.ts'); - - await execaCommand( - `renamer --find .svelte.d.ts --replace .d.svelte.ts tmp/js/**`, - { - stdio: 'inherit' - } - ); - } - }, ...plugins, { - name: 'After build tweaks', + name: 'After ESM build tweaks', closeBundle: async () => { console.log('Copying CSS to the root'); @@ -136,6 +109,15 @@ export default [ } ); + console.log('Generating TypeScript declarations'); + + await execaCommand( + `npx tsc --declaration --emitDeclarationOnly --declarationDir tmp/js --skipLibCheck`, + { + stdio: 'inherit' + } + ); + console.log('Rollup TS declarations to one file'); await execaCommand( @@ -145,7 +127,7 @@ export default [ } ); - console.log('Move shepherd.js from tmp to dist'); + console.log('Move shepherd.mjs from tmp to dist'); await execaCommand( `mv ./tmp/js/shepherd.mjs ./tmp/js/shepherd.mjs.map ./dist/js/`, @@ -166,5 +148,45 @@ export default [ } } ] + }, + // CJS build + { + ...sharedConfig, + output: { + dir: 'tmp/cjs', + entryFileNames: '[name].cjs', + format: 'cjs', + sourcemap: true, + exports: 'named' + }, + plugins: [ + ...plugins, + { + name: 'After CJS build tweaks', + closeBundle: async () => { + await execaCommand(`mkdir -p ./dist/cjs`, { + stdio: 'inherit' + }); + + console.log('Move shepherd.cjs from tmp to dist'); + + await execaCommand( + `mv ./tmp/cjs/shepherd.cjs ./tmp/cjs/shepherd.cjs.map ./dist/cjs/`, + { + stdio: 'inherit' + } + ); + + console.log('Copy CJS declarations'); + + await execaCommand( + `cp ./dist/js/shepherd.d.mts ./dist/cjs/shepherd.d.cts`, + { + stdio: 'inherit' + } + ); + } + } + ] } ]; diff --git a/shepherd.js/src/components/shepherd-button.css b/shepherd.js/src/components/shepherd-button.css new file mode 100644 index 000000000..908909a16 --- /dev/null +++ b/shepherd.js/src/components/shepherd-button.css @@ -0,0 +1,29 @@ +.shepherd-button { + background: rgb(50, 136, 230); + border: 0; + border-radius: 3px; + color: rgba(255, 255, 255, 0.75); + cursor: pointer; + margin-right: 0.5rem; + padding: 0.5rem 1.5rem; + transition: all 0.5s ease; +} + +.shepherd-button:not(:disabled):hover { + background: rgb(25, 111, 204); + color: rgba(255, 255, 255, 0.75); +} + +.shepherd-button.shepherd-button-secondary { + background: rgb(241, 242, 243); + color: rgba(0, 0, 0, 0.75); +} + +.shepherd-button.shepherd-button-secondary:not(:disabled):hover { + background: rgb(214, 217, 219); + color: rgba(0, 0, 0, 0.75); +} + +.shepherd-button:disabled { + cursor: not-allowed; +} diff --git a/shepherd.js/src/components/shepherd-button.svelte b/shepherd.js/src/components/shepherd-button.svelte deleted file mode 100644 index 4c2d52ee7..000000000 --- a/shepherd.js/src/components/shepherd-button.svelte +++ /dev/null @@ -1,66 +0,0 @@ - - - - - diff --git a/shepherd.js/src/components/shepherd-button.ts b/shepherd.js/src/components/shepherd-button.ts new file mode 100644 index 000000000..ab21a0a7e --- /dev/null +++ b/shepherd.js/src/components/shepherd-button.ts @@ -0,0 +1,40 @@ +import { h } from '../utils/dom.ts'; +import { isFunction } from '../utils/type-check.ts'; +import type { Step, StepOptionsButton } from '../step.ts'; +import './shepherd-button.css'; + +function getConfigOption(option: unknown, step: Step): unknown { + if (isFunction(option)) { + return option.call(step); + } + return option; +} + +export function createShepherdButton( + config: StepOptionsButton, + step: Step +): HTMLButtonElement { + const action = config.action ? config.action.bind(step.tour) : null; + const disabled = config.disabled + ? getConfigOption(config.disabled, step) + : false; + const label = config.label ? getConfigOption(config.label, step) : null; + const text = config.text ? getConfigOption(config.text, step) : null; + + const btn = h('button', { + 'aria-label': label || null, + class: `${config.classes || ''} shepherd-button ${ + config.secondary ? 'shepherd-button-secondary' : '' + }`, + disabled: disabled || null, + onclick: action, + tabindex: '0', + type: 'button' + }) as HTMLButtonElement; + + if (text) { + btn.innerHTML = text as string; + } + + return btn; +} diff --git a/shepherd.js/src/components/shepherd-cancel-icon.css b/shepherd.js/src/components/shepherd-cancel-icon.css new file mode 100644 index 000000000..191d8ed2d --- /dev/null +++ b/shepherd.js/src/components/shepherd-cancel-icon.css @@ -0,0 +1,23 @@ +.shepherd-cancel-icon { + background: transparent; + border: none; + color: rgba(128, 128, 128, 0.75); + font-size: 2em; + cursor: pointer; + font-weight: normal; + margin: 0; + padding: 0; + transition: color 0.5s ease; +} + +.shepherd-cancel-icon:hover { + color: rgba(0, 0, 0, 0.75); +} + +.shepherd-has-title .shepherd-content .shepherd-cancel-icon { + color: rgba(128, 128, 128, 0.75); +} + +.shepherd-has-title .shepherd-content .shepherd-cancel-icon:hover { + color: rgba(0, 0, 0, 0.75); +} diff --git a/shepherd.js/src/components/shepherd-cancel-icon.svelte b/shepherd.js/src/components/shepherd-cancel-icon.svelte deleted file mode 100644 index e2d387f97..000000000 --- a/shepherd.js/src/components/shepherd-cancel-icon.svelte +++ /dev/null @@ -1,46 +0,0 @@ - - - - - diff --git a/shepherd.js/src/components/shepherd-cancel-icon.ts b/shepherd.js/src/components/shepherd-cancel-icon.ts new file mode 100644 index 000000000..28e838dd2 --- /dev/null +++ b/shepherd.js/src/components/shepherd-cancel-icon.ts @@ -0,0 +1,26 @@ +import { h } from '../utils/dom.ts'; +import type { Step, StepOptionsCancelIcon } from '../step.ts'; +import './shepherd-cancel-icon.css'; + +export function createShepherdCancelIcon( + cancelIcon: StepOptionsCancelIcon, + step: Step +): HTMLButtonElement { + const handleCancelClick = (e: Event) => { + e.preventDefault(); + step.cancel(); + }; + + const btn = h( + 'button', + { + 'aria-label': cancelIcon.label ? cancelIcon.label : 'Close Tour', + class: 'shepherd-cancel-icon', + onclick: handleCancelClick, + type: 'button' + }, + h('span', { 'aria-hidden': 'true' }, '\u00D7') + ) as HTMLButtonElement; + + return btn; +} diff --git a/shepherd.js/src/components/shepherd-content.css b/shepherd.js/src/components/shepherd-content.css new file mode 100644 index 000000000..61adb9171 --- /dev/null +++ b/shepherd.js/src/components/shepherd-content.css @@ -0,0 +1,5 @@ +.shepherd-content { + border-radius: 5px; + outline: none; + padding: 0; +} diff --git a/shepherd.js/src/components/shepherd-content.svelte b/shepherd.js/src/components/shepherd-content.svelte deleted file mode 100644 index 4edb9ddeb..000000000 --- a/shepherd.js/src/components/shepherd-content.svelte +++ /dev/null @@ -1,30 +0,0 @@ - - -
- {#if !isUndefined(step.options.title) || (step.options.cancelIcon && step.options.cancelIcon.enabled)} - - {/if} - - {#if !isUndefined(step.options.text)} - - {/if} - - {#if Array.isArray(step.options.buttons) && step.options.buttons.length} - - {/if} -
- - diff --git a/shepherd.js/src/components/shepherd-content.ts b/shepherd.js/src/components/shepherd-content.ts new file mode 100644 index 000000000..2e315d252 --- /dev/null +++ b/shepherd.js/src/components/shepherd-content.ts @@ -0,0 +1,32 @@ +import { h } from '../utils/dom.ts'; +import { createShepherdFooter } from './shepherd-footer.ts'; +import { createShepherdHeader } from './shepherd-header.ts'; +import { createShepherdText } from './shepherd-text.ts'; +import { isUndefined } from '../utils/type-check.ts'; +import type { Step } from '../step.ts'; +import './shepherd-content.css'; + +export function createShepherdContent( + descriptionId: string, + labelId: string, + step: Step +): HTMLDivElement { + const content = h('div', { class: 'shepherd-content' }) as HTMLDivElement; + + if ( + !isUndefined(step.options.title) || + (step.options.cancelIcon && step.options.cancelIcon.enabled) + ) { + content.append(createShepherdHeader(labelId, step)); + } + + if (!isUndefined(step.options.text)) { + content.append(createShepherdText(descriptionId, step)); + } + + if (Array.isArray(step.options.buttons) && step.options.buttons.length) { + content.append(createShepherdFooter(step)); + } + + return content; +} diff --git a/shepherd.js/src/components/shepherd-element.css b/shepherd.js/src/components/shepherd-element.css new file mode 100644 index 000000000..6bc841d30 --- /dev/null +++ b/shepherd.js/src/components/shepherd-element.css @@ -0,0 +1,82 @@ +.shepherd-element { + background: #fff; + border: none; + border-radius: 5px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + margin: 0; + max-width: 400px; + opacity: 0; + outline: none; + padding: 0; + transition: + opacity 0.3s, + visibility 0.3s; + visibility: hidden; + width: 100%; + z-index: 9999; +} + +.shepherd-enabled.shepherd-element { + opacity: 1; + visibility: visible; +} + +.shepherd-element[data-popper-reference-hidden]:not(.shepherd-centered) { + opacity: 0; + pointer-events: none; + visibility: hidden; +} + +.shepherd-element, +.shepherd-element *, +.shepherd-element *:after, +.shepherd-element *:before { + box-sizing: border-box; +} + +.shepherd-arrow, +.shepherd-arrow::before { + position: absolute; + width: 16px; + height: 16px; + z-index: -1; +} + +.shepherd-arrow:before { + content: ''; + transform: rotate(45deg); + background: #fff; +} + +.shepherd-element[data-popper-placement^='top'] > .shepherd-arrow { + bottom: -8px; +} + +.shepherd-element[data-popper-placement^='bottom'] > .shepherd-arrow { + top: -8px; +} + +.shepherd-element[data-popper-placement^='left'] > .shepherd-arrow { + right: -8px; +} + +.shepherd-element[data-popper-placement^='right'] > .shepherd-arrow { + left: -8px; +} + +.shepherd-element.shepherd-centered > .shepherd-arrow { + opacity: 0; +} + +/** +* Arrow on top of tooltip centered horizontally, with title color +*/ +.shepherd-element.shepherd-has-title[data-popper-placement^='bottom'] + > .shepherd-arrow::before { + background-color: #e6e6e6; +} + +.shepherd-target-click-disabled.shepherd-enabled.shepherd-target, +.shepherd-target-click-disabled.shepherd-enabled.shepherd-target * { + pointer-events: none; +} diff --git a/shepherd.js/src/components/shepherd-element.svelte b/shepherd.js/src/components/shepherd-element.svelte deleted file mode 100644 index 78bbcaf25..000000000 --- a/shepherd.js/src/components/shepherd-element.svelte +++ /dev/null @@ -1,279 +0,0 @@ - - - - {#if step.options.arrow && step.options.attachTo && step.options.attachTo.element && step.options.attachTo.on} -
- {/if} - -
- - diff --git a/shepherd.js/src/components/shepherd-element.ts b/shepherd.js/src/components/shepherd-element.ts new file mode 100644 index 000000000..e773d13ec --- /dev/null +++ b/shepherd.js/src/components/shepherd-element.ts @@ -0,0 +1,184 @@ +import { h } from '../utils/dom.ts'; +import { createShepherdContent } from './shepherd-content.ts'; +import { isUndefined, isString } from '../utils/type-check.ts'; +import type { Step } from '../step.ts'; +import './shepherd-element.css'; + +const KEY_TAB = 9; +const KEY_ESC = 27; +const LEFT_ARROW = 37; +const RIGHT_ARROW = 39; + +export interface ShepherdElementOptions { + classPrefix?: string; + descriptionId: string; + labelId: string; + step: Step; +} + +export interface ShepherdElementResult { + element: HTMLDialogElement; + cleanup: () => void; +} + +export function createShepherdElement( + options: ShepherdElementOptions +): ShepherdElementResult { + const { classPrefix, descriptionId, labelId, step } = options; + + let attachToElement: HTMLElement | null | undefined; + + // Focusable attachTo elements + let focusableAttachToElements: Element[] | undefined; + let firstFocusableAttachToElement: Element | undefined; + let lastFocusableAttachToElement: Element | undefined; + + // Focusable dialog elements + let firstFocusableDialogElement: Element | undefined; + let focusableDialogElements: Element[] | undefined; + let lastFocusableDialogElement: Element | undefined; + + const hasCancelIcon = step.options?.cancelIcon?.enabled ?? false; + const hasTitle = step.options?.title ?? false; + + /** + * Setup keydown events to allow closing the modal with ESC + * + * Borrowed from this great post! https://bitsofco.de/accessible-modal-dialog/ + */ + const handleKeyDown = (e: KeyboardEvent) => { + const { tour } = step; + switch (e.keyCode) { + case KEY_TAB: + if ( + (!focusableAttachToElements || + focusableAttachToElements.length === 0) && + focusableDialogElements && + focusableDialogElements.length === 0 + ) { + e.preventDefault(); + break; + } + // Backward tab + if (e.shiftKey) { + if ( + document.activeElement === firstFocusableDialogElement || + document.activeElement?.classList.contains('shepherd-element') + ) { + e.preventDefault(); + ( + (lastFocusableAttachToElement ?? + lastFocusableDialogElement) as HTMLElement + )?.focus(); + } else if (document.activeElement === firstFocusableAttachToElement) { + e.preventDefault(); + (lastFocusableDialogElement as HTMLElement)?.focus(); + } + } else { + if (document.activeElement === lastFocusableDialogElement) { + e.preventDefault(); + ( + (firstFocusableAttachToElement ?? + firstFocusableDialogElement) as HTMLElement + )?.focus(); + } else if (document.activeElement === lastFocusableAttachToElement) { + e.preventDefault(); + (firstFocusableDialogElement as HTMLElement)?.focus(); + } + } + break; + case KEY_ESC: + if (tour.options.exitOnEsc) { + e.preventDefault(); + e.stopPropagation(); + step.cancel(); + } + break; + case LEFT_ARROW: + if (tour.options.keyboardNavigation) { + e.preventDefault(); + e.stopPropagation(); + tour.back(); + } + break; + case RIGHT_ARROW: + if (tour.options.keyboardNavigation) { + e.preventDefault(); + e.stopPropagation(); + tour.next(); + } + break; + default: + break; + } + }; + + // Build the dialog element + const element = h('dialog', { + 'aria-describedby': !isUndefined(step.options.text) ? descriptionId : null, + 'aria-labelledby': step.options.title ? labelId : null, + class: [ + 'shepherd-element', + hasCancelIcon ? 'shepherd-has-cancel-icon' : '', + hasTitle ? 'shepherd-has-title' : '' + ] + .filter(Boolean) + .join(' '), + [`data-${classPrefix}shepherd-step-id`]: step.id, + onkeydown: handleKeyDown, + open: 'true' + }) as HTMLDialogElement; + + // Add arrow if needed + if ( + step.options.arrow && + step.options.attachTo && + step.options.attachTo.element && + step.options.attachTo.on + ) { + element.append( + h('div', { class: 'shepherd-arrow', 'data-popper-arrow': '' }) + ); + } + + // Add content + element.append(createShepherdContent(descriptionId, labelId, step)); + + // Dynamic class management + if (isString(step.options.classes)) { + const classes = step.options.classes.split(' ').filter((c) => !!c.length); + if (classes.length) { + element.classList.add(...classes); + } + } + + // Setup focusable element tracking (equivalent of onMount) + const focusableSelector = + 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]'; + + focusableDialogElements = [...element.querySelectorAll(focusableSelector)]; + firstFocusableDialogElement = focusableDialogElements[0]; + lastFocusableDialogElement = + focusableDialogElements[focusableDialogElements.length - 1]; + + const attachTo = step._getResolvedAttachToOptions(); + if (attachTo?.element) { + attachToElement = attachTo.element as HTMLElement; + step._storeOriginalTabIndex(attachToElement); + attachToElement.tabIndex = 0; + focusableAttachToElements = [ + attachToElement, + ...attachToElement.querySelectorAll(focusableSelector) + ]; + firstFocusableAttachToElement = focusableAttachToElements[0]; + lastFocusableAttachToElement = + focusableAttachToElements[focusableAttachToElements.length - 1]; + attachToElement.addEventListener('keydown', handleKeyDown); + } + + const cleanup = () => { + attachToElement?.removeEventListener('keydown', handleKeyDown); + }; + + return { element, cleanup }; +} diff --git a/shepherd.js/src/components/shepherd-footer.css b/shepherd.js/src/components/shepherd-footer.css new file mode 100644 index 000000000..565fe5a2d --- /dev/null +++ b/shepherd.js/src/components/shepherd-footer.css @@ -0,0 +1,11 @@ +.shepherd-footer { + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + display: flex; + justify-content: flex-end; + padding: 0 0.75rem 0.75rem; +} + +.shepherd-footer .shepherd-button:last-child { + margin-right: 0; +} diff --git a/shepherd.js/src/components/shepherd-footer.svelte b/shepherd.js/src/components/shepherd-footer.svelte deleted file mode 100644 index 92b1bab4c..000000000 --- a/shepherd.js/src/components/shepherd-footer.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - -
- {#if buttons} - {#each buttons as config} - - {/each} - {/if} -
- - diff --git a/shepherd.js/src/components/shepherd-footer.ts b/shepherd.js/src/components/shepherd-footer.ts new file mode 100644 index 000000000..b730173b9 --- /dev/null +++ b/shepherd.js/src/components/shepherd-footer.ts @@ -0,0 +1,16 @@ +import { h } from '../utils/dom.ts'; +import { createShepherdButton } from './shepherd-button.ts'; +import type { Step } from '../step.ts'; +import './shepherd-footer.css'; + +export function createShepherdFooter(step: Step): HTMLElement { + const footer = h('footer', { class: 'shepherd-footer' }); + + if (step.options.buttons) { + for (const config of step.options.buttons) { + footer.append(createShepherdButton(config, step)); + } + } + + return footer; +} diff --git a/shepherd.js/src/components/shepherd-header.css b/shepherd.js/src/components/shepherd-header.css new file mode 100644 index 000000000..99b68f5ec --- /dev/null +++ b/shepherd.js/src/components/shepherd-header.css @@ -0,0 +1,14 @@ +.shepherd-header { + align-items: center; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + display: flex; + justify-content: flex-end; + line-height: 2em; + padding: 0.75rem 0.75rem 0; +} + +.shepherd-has-title .shepherd-content .shepherd-header { + background: #e6e6e6; + padding: 1em; +} diff --git a/shepherd.js/src/components/shepherd-header.svelte b/shepherd.js/src/components/shepherd-header.svelte deleted file mode 100644 index 003cc9f5b..000000000 --- a/shepherd.js/src/components/shepherd-header.svelte +++ /dev/null @@ -1,36 +0,0 @@ - - -
- {#if title} - - {/if} - - {#if cancelIcon && cancelIcon.enabled} - - {/if} -
- - diff --git a/shepherd.js/src/components/shepherd-header.ts b/shepherd.js/src/components/shepherd-header.ts new file mode 100644 index 000000000..d9e0cb69e --- /dev/null +++ b/shepherd.js/src/components/shepherd-header.ts @@ -0,0 +1,19 @@ +import { h } from '../utils/dom.ts'; +import { createShepherdCancelIcon } from './shepherd-cancel-icon.ts'; +import { createShepherdTitle } from './shepherd-title.ts'; +import type { Step } from '../step.ts'; +import './shepherd-header.css'; + +export function createShepherdHeader(labelId: string, step: Step): HTMLElement { + const header = h('header', { class: 'shepherd-header' }); + + if (step.options.title) { + header.append(createShepherdTitle(labelId, step.options.title)); + } + + if (step.options.cancelIcon && step.options.cancelIcon.enabled) { + header.append(createShepherdCancelIcon(step.options.cancelIcon, step)); + } + + return header; +} diff --git a/shepherd.js/src/components/shepherd-modal.css b/shepherd.js/src/components/shepherd-modal.css new file mode 100644 index 000000000..56f625da2 --- /dev/null +++ b/shepherd.js/src/components/shepherd-modal.css @@ -0,0 +1,29 @@ +.shepherd-modal-overlay-container { + height: 0; + left: 0; + opacity: 0; + overflow: hidden; + pointer-events: none; + position: fixed; + top: 0; + transition: + all 0.3s ease-out, + height 0ms 0.3s, + opacity 0.3s 0ms; + width: 100vw; + z-index: 9997; +} + +.shepherd-modal-overlay-container.shepherd-modal-is-visible { + height: 100vh; + opacity: 0.5; + transition: + all 0.3s ease-out, + height 0s 0s, + opacity 0.3s 0s; + transform: translateZ(0); +} + +.shepherd-modal-overlay-container.shepherd-modal-is-visible path { + pointer-events: all; +} diff --git a/shepherd.js/src/components/shepherd-modal.svelte b/shepherd.js/src/components/shepherd-modal.svelte deleted file mode 100644 index 72a8b797c..000000000 --- a/shepherd.js/src/components/shepherd-modal.svelte +++ /dev/null @@ -1,323 +0,0 @@ - - - - - - - diff --git a/shepherd.js/src/components/shepherd-modal.ts b/shepherd.js/src/components/shepherd-modal.ts new file mode 100644 index 000000000..295d88229 --- /dev/null +++ b/shepherd.js/src/components/shepherd-modal.ts @@ -0,0 +1,302 @@ +import { svgEl } from '../utils/dom.ts'; +import { makeOverlayPath } from '../utils/overlay-path.ts'; +import type { Step } from '../step.ts'; +import './shepherd-modal.css'; + +interface OpeningProperty { + width: number; + height: number; + x: number; + y: number; + r: + | number + | { + topLeft: number; + bottomLeft: number; + bottomRight: number; + topRight: number; + }; +} + +type ModalRadiusType = + | number + | { + topLeft?: number; + bottomLeft?: number; + bottomRight?: number; + topRight?: number; + }; + +export interface ShepherdModalAPI { + closeModalOpening: () => void; + destroy: () => void; + hide: () => void; + positionModal: ( + modalOverlayOpeningPadding?: number, + modalOverlayOpeningRadius?: ModalRadiusType, + modalOverlayOpeningXOffset?: number, + modalOverlayOpeningYOffset?: number, + scrollParent?: HTMLElement | null, + targetElement?: HTMLElement | null, + extraHighlights?: HTMLElement[] + ) => void; + setupForStep: (step: Step) => void; + show: () => void; + getElement: () => SVGElement; +} + +export function createShepherdModal(container: HTMLElement): ShepherdModalAPI { + let rafId: number | undefined; + let openingProperties: OpeningProperty[] = [ + { width: 0, height: 0, x: 0, y: 0, r: 0 } + ]; + + // Build SVG elements + const pathEl = svgEl('path'); + const element = svgEl( + 'svg', + { class: 'shepherd-modal-overlay-container' }, + pathEl + ); + + element.addEventListener('touchmove', _preventModalOverlayTouch); + + // Initial render + _updatePath(); + + container.append(element); + + function _updatePath() { + pathEl.setAttribute('d', makeOverlayPath(openingProperties)); + } + + function closeModalOpening() { + openingProperties = [{ width: 0, height: 0, x: 0, y: 0, r: 0 }]; + _updatePath(); + } + + function hide() { + element.classList.remove('shepherd-modal-is-visible'); + _cleanupStepEventListeners(); + } + + function show() { + element.classList.add('shepherd-modal-is-visible'); + } + + function positionModal( + modalOverlayOpeningPadding = 0, + modalOverlayOpeningRadius: ModalRadiusType = 0, + modalOverlayOpeningXOffset = 0, + modalOverlayOpeningYOffset = 0, + scrollParent?: HTMLElement | null, + targetElement?: HTMLElement | null, + extraHighlights?: HTMLElement[] + ) { + if (targetElement) { + const elementsToHighlight = [targetElement, ...(extraHighlights || [])]; + const newOpenings: OpeningProperty[] = []; + + for (const el of elementsToHighlight) { + if (!el) continue; + + // Skip duplicate elements + if ( + elementsToHighlight.indexOf(el) !== + elementsToHighlight.lastIndexOf(el) + ) { + continue; + } + + const { y, height } = _getVisibleHeight(el, scrollParent); + const { x, width, left } = el.getBoundingClientRect(); + + // Check if the element is contained by another element. + // Use _getVisibleHeight for otherElement too so both sides + // compare scroll-clipped geometry on the y-axis. + const isContained = elementsToHighlight.some((otherElement) => { + if (otherElement === el) return false; + const otherRect = otherElement.getBoundingClientRect(); + const { y: otherY, height: otherHeight } = _getVisibleHeight( + otherElement, + scrollParent + ); + return ( + x >= otherRect.left && + x + width <= otherRect.left + otherRect.width && + y >= otherY && + y + height <= otherY + otherHeight + ); + }); + + if (isContained) continue; + + newOpenings.push({ + width: width + modalOverlayOpeningPadding * 2, + height: height + modalOverlayOpeningPadding * 2, + x: + (x || left) + + modalOverlayOpeningXOffset - + modalOverlayOpeningPadding, + y: y + modalOverlayOpeningYOffset - modalOverlayOpeningPadding, + r: modalOverlayOpeningRadius as OpeningProperty['r'] + }); + } + + openingProperties = newOpenings; + } else { + closeModalOpening(); + return; + } + + _updatePath(); + } + + function setupForStep(step: Step) { + _cleanupStepEventListeners(); + + if (step.tour.options.useModalOverlay) { + _styleForStep(step); + show(); + } else { + hide(); + } + } + + function destroy() { + _cleanupStepEventListeners(); + element.removeEventListener('touchmove', _preventModalOverlayTouch); + element.remove(); + } + + function getElement() { + return element; + } + + // --- Private helpers --- + + function _preventModalOverlayTouch(e: Event) { + e.stopPropagation(); + } + + const _preventModalBodyTouch = (e: Event) => { + e.preventDefault(); + }; + + function _addStepEventListeners() { + window.addEventListener('touchmove', _preventModalBodyTouch, { + passive: false + }); + } + + function _cleanupStepEventListeners() { + if (rafId) { + cancelAnimationFrame(rafId); + rafId = undefined; + } + + window.removeEventListener('touchmove', _preventModalBodyTouch, { + passive: false + } as EventListenerOptions); + } + + function _styleForStep(step: Step) { + const { + modalOverlayOpeningPadding, + modalOverlayOpeningRadius, + modalOverlayOpeningXOffset = 0, + modalOverlayOpeningYOffset = 0 + } = step.options; + + const iframeOffset = _getIframeOffset(step.target); + const scrollParent = _getScrollParent(step.target); + + const rafLoop = () => { + rafId = undefined; + positionModal( + modalOverlayOpeningPadding, + modalOverlayOpeningRadius, + modalOverlayOpeningXOffset + iframeOffset.left, + modalOverlayOpeningYOffset + iframeOffset.top, + scrollParent, + step.target, + step._resolvedExtraHighlightElements + ); + rafId = requestAnimationFrame(rafLoop); + }; + + rafLoop(); + _addStepEventListeners(); + } + + function _getScrollParent(el?: HTMLElement | null): HTMLElement | null { + if (!el) return null; + + const isHtmlElement = el instanceof HTMLElement; + const overflowY = isHtmlElement && window.getComputedStyle(el).overflowY; + const isScrollable = overflowY !== 'hidden' && overflowY !== 'visible'; + + if (isScrollable && el.scrollHeight >= el.clientHeight) { + return el; + } + + return _getScrollParent(el.parentElement); + } + + function _getIframeOffset(el?: HTMLElement | null) { + const offset = { top: 0, left: 0 }; + + if (!el) return offset; + + let targetWindow: Window | null = el.ownerDocument.defaultView; + + try { + while (targetWindow && targetWindow !== window.top) { + const targetIframe = targetWindow?.frameElement; + + if (targetIframe) { + const rect = targetIframe.getBoundingClientRect(); + offset.top += rect.top + targetIframe.scrollTop; + offset.left += rect.left + targetIframe.scrollLeft; + } + + targetWindow = targetWindow.parent; + } + } catch { + // Cross-origin iframe — stop traversal and use the offset accumulated so far. + } + + return offset; + } + + function _getVisibleHeight( + el: HTMLElement, + scrollParent?: HTMLElement | null + ) { + const elementRect = el.getBoundingClientRect(); + let top = elementRect.y || elementRect.top; + let bottom = elementRect.bottom || top + elementRect.height; + + if (scrollParent) { + const scrollRect = scrollParent.getBoundingClientRect(); + const scrollTop = scrollRect.y || scrollRect.top; + const scrollBottom = scrollRect.bottom || scrollTop + scrollRect.height; + + top = Math.max(top, scrollTop); + bottom = Math.min(bottom, scrollBottom); + } + + const height = Math.max(bottom - top, 0); + return { y: top, height }; + } + + return { + closeModalOpening, + destroy, + hide, + positionModal, + setupForStep, + show, + getElement + }; +} diff --git a/shepherd.js/src/components/shepherd-text.css b/shepherd.js/src/components/shepherd-text.css new file mode 100644 index 000000000..470503acf --- /dev/null +++ b/shepherd.js/src/components/shepherd-text.css @@ -0,0 +1,14 @@ +.shepherd-text { + color: rgba(0, 0, 0, 0.75); + font-size: 1rem; + line-height: 1.3em; + padding: 0.75em; +} + +.shepherd-text p { + margin-top: 0; +} + +.shepherd-text p:last-child { + margin-bottom: 0; +} diff --git a/shepherd.js/src/components/shepherd-text.svelte b/shepherd.js/src/components/shepherd-text.svelte deleted file mode 100644 index d485d2732..000000000 --- a/shepherd.js/src/components/shepherd-text.svelte +++ /dev/null @@ -1,38 +0,0 @@ - - -
- - diff --git a/shepherd.js/src/components/shepherd-text.ts b/shepherd.js/src/components/shepherd-text.ts new file mode 100644 index 000000000..ecf275ba5 --- /dev/null +++ b/shepherd.js/src/components/shepherd-text.ts @@ -0,0 +1,28 @@ +import { h } from '../utils/dom.ts'; +import { isHTMLElement, isFunction } from '../utils/type-check.ts'; +import type { Step } from '../step.ts'; +import './shepherd-text.css'; + +export function createShepherdText( + descriptionId: string, + step: Step +): HTMLDivElement { + const el = h('div', { + class: 'shepherd-text', + id: descriptionId + }) as HTMLDivElement; + + let text = step.options.text; + + if (isFunction(text)) { + text = text.call(step); + } + + if (isHTMLElement(text)) { + el.appendChild(text); + } else { + el.innerHTML = text as string; + } + + return el; +} diff --git a/shepherd.js/src/components/shepherd-title.css b/shepherd.js/src/components/shepherd-title.css new file mode 100644 index 000000000..aca3fa769 --- /dev/null +++ b/shepherd.js/src/components/shepherd-title.css @@ -0,0 +1,9 @@ +.shepherd-title { + color: rgba(0, 0, 0, 0.75); + display: flex; + font-size: 1rem; + font-weight: normal; + flex: 1 0 auto; + margin: 0; + padding: 0; +} diff --git a/shepherd.js/src/components/shepherd-title.svelte b/shepherd.js/src/components/shepherd-title.svelte deleted file mode 100644 index 6a33e40e0..000000000 --- a/shepherd.js/src/components/shepherd-title.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - - -

- - diff --git a/shepherd.js/src/components/shepherd-title.ts b/shepherd.js/src/components/shepherd-title.ts new file mode 100644 index 000000000..54123df3c --- /dev/null +++ b/shepherd.js/src/components/shepherd-title.ts @@ -0,0 +1,19 @@ +import { h } from '../utils/dom.ts'; +import { isFunction } from '../utils/type-check.ts'; +import type { StringOrStringFunction } from '../step.ts'; +import './shepherd-title.css'; + +export function createShepherdTitle( + labelId: string, + title: StringOrStringFunction +): HTMLHeadingElement { + const el = h('h3', { + id: labelId, + class: 'shepherd-title' + }) as HTMLHeadingElement; + + const resolvedTitle = isFunction(title) ? title() : title; + el.innerHTML = resolvedTitle; + + return el; +} diff --git a/shepherd.js/src/step.ts b/shepherd.js/src/step.ts index 3ecc5ab2d..1ca5904d7 100644 --- a/shepherd.js/src/step.ts +++ b/shepherd.js/src/step.ts @@ -19,10 +19,12 @@ import { destroyTooltip, mergeTooltipConfig } from './utils/floating-ui.ts'; -import ShepherdElement from './components/shepherd-element.svelte'; +import { + createShepherdElement, + type ShepherdElementResult +} from './components/shepherd-element.ts'; import { type Tour } from './tour.ts'; import type { ComputePositionConfig } from '@floating-ui/dom'; -import { createClassComponent } from 'svelte/legacy'; export type StepText = | string @@ -311,6 +313,7 @@ export class Step extends Evented { el?: HTMLElement | null; declare id: string; declare options: StepOptions; + shepherdElementComponent?: ShepherdElementResult; target?: HTMLElement | null; tour: Tour; @@ -369,8 +372,23 @@ export class Step extends Evented { * Triggers `destroy` event */ destroy() { + this._teardownElements(); + this.trigger('destroy'); + } + + /** + * Internal cleanup that tears down the tooltip, component, and DOM element + * without emitting the public "destroy" event. + * @private + */ + _teardownElements() { destroyTooltip(this); + if (this.shepherdElementComponent) { + this.shepherdElementComponent.cleanup(); + this.shepherdElementComponent = undefined; + } + if (isHTMLElement(this.el)) { this.el.remove(); this.el = null; @@ -378,8 +396,6 @@ export class Step extends Evented { this._updateStepTargetOnHide(); this._originalTabIndexes.clear(); - - this.trigger('destroy'); } /** @@ -466,10 +482,12 @@ export class Step extends Evented { updateStepOptions(options: StepOptions) { Object.assign(this.options, options); - // @ts-expect-error TODO: get types for Svelte components if (this.shepherdElementComponent) { - // @ts-expect-error TODO: get types for Svelte components - this.shepherdElementComponent.$set({ step: this }); + // Recreate the element with updated options + if (this.el) { + this._teardownElements(); + this._setupElements(); + } } } @@ -530,22 +548,17 @@ export class Step extends Evented { const descriptionId = `${this.id}-description`; const labelId = `${this.id}-label`; - // @ts-expect-error TODO: get types for Svelte components - this.shepherdElementComponent = createClassComponent({ - component: ShepherdElement, - target: this.tour.options.stepsContainer || document.body, - props: { - classPrefix: this.classPrefix, - descriptionId, - labelId, - step: this, - // @ts-expect-error TODO: investigate where styles comes from - styles: this.styles - } + this.shepherdElementComponent = createShepherdElement({ + classPrefix: this.classPrefix, + descriptionId, + labelId, + step: this }); - // @ts-expect-error TODO: get types for Svelte components - return this.shepherdElementComponent.getElement(); + const target = this.tour.options.stepsContainer || document.body; + target.append(this.shepherdElementComponent.element); + + return this.shepherdElementComponent.element; } /** @@ -682,8 +695,7 @@ export class Step extends Evented { this.el.hidden = false; } - // @ts-expect-error TODO: get types for Svelte components - const content = this.shepherdElementComponent.getElement(); + const content = this.shepherdElementComponent!.element; const target = this.target || document.body; const extraHighlightElements = this._resolvedExtraHighlightElements; diff --git a/shepherd.js/src/tour.ts b/shepherd.js/src/tour.ts index 6d028ee73..199acbca5 100644 --- a/shepherd.js/src/tour.ts +++ b/shepherd.js/src/tour.ts @@ -9,8 +9,10 @@ import { } from './utils/type-check.ts'; import { cleanupSteps } from './utils/cleanup.ts'; import { normalizePrefix, uuid } from './utils/general.ts'; -import ShepherdModal from './components/shepherd-modal.svelte'; -import { createClassComponent } from 'svelte/legacy'; +import { + createShepherdModal, + type ShepherdModalAPI +} from './components/shepherd-modal.ts'; export interface EventOptions { previous?: Step | null; @@ -110,8 +112,7 @@ export class Tour extends Evented { currentStep?: Step | null; focusedElBeforeOpen?: HTMLElement | null; id?: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - modal?: any | null; + modal?: ShepherdModalAPI | null; options: TourOptions; steps: Array; @@ -384,14 +385,8 @@ export class Tour extends Evented { if (event === 'cancel' || event === 'complete') { if (this.modal) { - const modalContainer = document.querySelector( - '.shepherd-modal-overlay-container' - ); - - if (modalContainer) { - modalContainer.remove(); - this.modal = null; - } + this.modal.destroy(); + this.modal = null; } } @@ -414,14 +409,8 @@ export class Tour extends Evented { * setupModal create the modal container and instance */ setupModal() { - this.modal = createClassComponent({ - component: ShepherdModal, - target: this.options.modalContainer || document.body, - props: { - // @ts-expect-error TODO: investigate where styles comes from - styles: this.styles - } - }); + const container = this.options.modalContainer || document.body; + this.modal = createShepherdModal(container); } /** diff --git a/shepherd.js/src/utils/dom.ts b/shepherd.js/src/utils/dom.ts new file mode 100644 index 000000000..70132bba5 --- /dev/null +++ b/shepherd.js/src/utils/dom.ts @@ -0,0 +1,58 @@ +type Attrs = Record; +type Child = Node | string | null | undefined | false; + +/** + * Create an HTML element with attributes and children. + * Attributes starting with "on" are added as event listeners. + * Null/undefined/false attribute values are skipped. + */ +export function h( + tag: string, + attrs?: Attrs | null, + ...children: Child[] +): HTMLElement { + const el = document.createElement(tag); + applyAttrs(el, attrs); + appendChildren(el, children); + return el; +} + +/** + * Create an SVG element with attributes and children. + */ +export function svgEl( + tag: string, + attrs?: Attrs | null, + ...children: Child[] +): SVGElement { + const el = document.createElementNS('http://www.w3.org/2000/svg', tag); + applyAttrs(el, attrs); + appendChildren(el, children); + return el; +} + +function applyAttrs(el: Element, attrs?: Attrs | null) { + if (!attrs) return; + + for (const [key, value] of Object.entries(attrs)) { + if (value === null || value === undefined || value === false) continue; + + if (key.startsWith('on') && typeof value === 'function') { + el.addEventListener(key.slice(2).toLowerCase(), value as EventListener); + } else if (key === 'disabled' && value === true) { + (el as HTMLButtonElement).disabled = true; + } else { + el.setAttribute(key, String(value)); + } + } +} + +function appendChildren(el: Element, children: Child[]) { + for (const child of children) { + if (child != null && child !== false) { + el.append( + typeof child === 'string' ? document.createTextNode(child) : child + ); + } + } +} diff --git a/shepherd.js/src/utils/floating-ui.ts b/shepherd.js/src/utils/floating-ui.ts index edeaa0264..acec7b78f 100644 --- a/shepherd.js/src/utils/floating-ui.ts +++ b/shepherd.js/src/utils/floating-ui.ts @@ -34,9 +34,8 @@ export function setupTooltip(step: Step): ComputePositionConfig { if (shouldCenter) { target = document.body; - // @ts-expect-error TODO: fix this type error when we type Svelte - const content = step.shepherdElementComponent.getElement(); - content.classList.add('shepherd-centered'); + const content = step.shepherdElementComponent?.element; + content?.classList.add('shepherd-centered'); } step.cleanup = autoUpdate(target, step.el as HTMLElement, () => { diff --git a/shepherd.js/svelte.config.js b/shepherd.js/svelte.config.js deleted file mode 100644 index 995d646a1..000000000 --- a/shepherd.js/svelte.config.js +++ /dev/null @@ -1,12 +0,0 @@ -import preprocess from 'svelte-preprocess'; - -/** - * This will add autocompletion if you're working with SvelteKit - * - * @type {import('@sveltejs/kit').Config} - */ -const config = { - preprocess: preprocess({}) -}; - -export default config; diff --git a/shepherd.js/test/cypress/integration/modal.cy.js b/shepherd.js/test/cypress/integration/modal.cy.js index e2d6ed768..5c9899a3d 100644 --- a/shepherd.js/test/cypress/integration/modal.cy.js +++ b/shepherd.js/test/cypress/integration/modal.cy.js @@ -69,13 +69,13 @@ describe('Modal mode', () => { tour = setupTour(Shepherd, {}, null, { useModalOverlay: true }); }); - it('removes shepherd-modal-is-visible class from the overlay', async () => { + it('removes shepherd-modal-is-visible class from the overlay', () => { tour.start(); - await cy - .get('.shepherd-modal-overlay-container') - .should('have.class', 'shepherd-modal-is-visible'); - - tour.hide(); + cy.get('.shepherd-modal-overlay-container') + .should('have.class', 'shepherd-modal-is-visible') + .then(() => { + tour.hide(); + }); cy.get('.shepherd-modal-overlay-container').should( 'not.have.class', 'shepherd-modal-is-visible' diff --git a/shepherd.js/test/unit/components/shepherd-button.spec.js b/shepherd.js/test/unit/components/shepherd-button.spec.js index 6507a7e63..8669ff7dd 100644 --- a/shepherd.js/test/unit/components/shepherd-button.spec.js +++ b/shepherd.js/test/unit/components/shepherd-button.spec.js @@ -1,196 +1,127 @@ -import { cleanup, render } from '@testing-library/svelte'; -import { tick } from 'svelte'; -import { beforeEach, describe, expect, it } from 'vitest'; -import ShepherdButton from '../../../src/components/shepherd-button.svelte'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { createShepherdButton } from '../../../src/components/shepherd-button.ts'; describe('component/ShepherdButton', () => { - beforeEach(cleanup); + let container; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + container.remove(); + }); describe('disabled', () => { it('should be enabled by default', () => { - const config = {}; + const button = createShepherdButton({}, undefined); + container.appendChild(button); - const { container } = render(ShepherdButton, { - props: { - config - } - }); - - const button = container.querySelector('.shepherd-button'); expect(button.disabled).toBeFalsy(); }); it('is enabled when false', () => { - const config = { - disabled: false - }; - - const { container } = render(ShepherdButton, { - props: { - config - } - }); + const button = createShepherdButton({ disabled: false }, undefined); + container.appendChild(button); - const button = container.querySelector('.shepherd-button'); expect(button.disabled).toBeFalsy(); }); it('can be disabled with boolean', () => { - const config = { - disabled: true - }; - - const { container } = render(ShepherdButton, { - props: { - config - } - }); + const button = createShepherdButton({ disabled: true }, undefined); + container.appendChild(button); - const button = container.querySelector('.shepherd-button'); expect(button.disabled).toBeTruthy(); }); it('can be disabled with function', () => { - const config = { - disabled: () => true - }; + const button = createShepherdButton({ disabled: () => true }, undefined); + container.appendChild(button); - const { container } = render(ShepherdButton, { - props: { - config - } - }); - - const button = container.querySelector('.shepherd-button'); expect(button.disabled).toBeTruthy(); }); + }); - it('label - string', () => { - const config = { - label: 'Test' - }; - - const { container } = render(ShepherdButton, { - props: { - config - } - }); + describe('label', () => { + it('string', () => { + const button = createShepherdButton({ label: 'Test' }, undefined); + container.appendChild(button); - const button = container.querySelector('.shepherd-button'); expect(button).toHaveAttribute('aria-label', 'Test'); }); - it('label - number', () => { - const config = { - label: 5 - }; - - const { container } = render(ShepherdButton, { - props: { - config - } - }); + it('number', () => { + const button = createShepherdButton({ label: 5 }, undefined); + container.appendChild(button); - const button = container.querySelector('.shepherd-button'); expect(button).toHaveAttribute('aria-label', '5'); }); - it('label - funtion', async () => { + it('function', () => { + const button = createShepherdButton({ label: () => 'Test' }, undefined); + container.appendChild(button); + + expect(button).toHaveAttribute('aria-label', 'Test'); + }); + + it('function re-creation uses updated value', () => { let label = 'Test'; - const labelFunction = () => label; - const config = { - label: labelFunction - }; - - const { container, rerender } = render(ShepherdButton, { - props: { - config - } - }); - - const button = container.querySelector('.shepherd-button'); + const button = createShepherdButton({ label: () => label }, undefined); + container.appendChild(button); expect(button).toHaveAttribute('aria-label', 'Test'); label = 'Test 2'; - - rerender({ - config: { label: () => label } - }); - - await tick(); - - const buttonUpdated = container.querySelector('.shepherd-button'); + const buttonUpdated = createShepherdButton( + { label: () => label }, + undefined + ); + container.appendChild(buttonUpdated); expect(buttonUpdated).toHaveAttribute('aria-label', 'Test 2'); }); - it('label - null', () => { - const config = { - label: null - }; + it('null', () => { + const button = createShepherdButton({ label: null }, undefined); + container.appendChild(button); - const { container } = render(ShepherdButton, { - props: { - config - } - }); - - const button = container.querySelector('.shepherd-button'); expect(button).not.toHaveAttribute('aria-label'); }); - it('label - undefined', () => { - const config = {}; - - const { container } = render(ShepherdButton, { - props: { - config - } - }); + it('undefined', () => { + const button = createShepherdButton({}, undefined); + container.appendChild(button); - const button = container.querySelector('.shepherd-button'); expect(button).not.toHaveAttribute('aria-label'); }); + }); - it('text - string', () => { - const config = { - text: 'Test' - }; + describe('text', () => { + it('string', () => { + const button = createShepherdButton({ text: 'Test' }, undefined); + container.appendChild(button); - const { container } = render(ShepherdButton, { - props: { - config - } - }); + expect(button).toHaveTextContent('Test'); + }); + + it('function', () => { + const button = createShepherdButton({ text: () => 'Test' }, undefined); + container.appendChild(button); - const button = container.querySelector('.shepherd-button'); expect(button).toHaveTextContent('Test'); }); - it('text - function', async () => { + it('function re-creation uses updated value', () => { let text = 'Test'; - const textFunction = () => text; - const config = { - text: textFunction - }; - - const { container, rerender } = render(ShepherdButton, { - props: { - config - } - }); - - const button = container.querySelector('.shepherd-button'); + const button = createShepherdButton({ text: () => text }, undefined); + container.appendChild(button); expect(button).toHaveTextContent('Test'); text = 'Test 2'; - - rerender({ - config: { text: () => text } - }); - - await tick(); - - const buttonUpdated = container.querySelector('.shepherd-button'); + const buttonUpdated = createShepherdButton( + { text: () => text }, + undefined + ); + container.appendChild(buttonUpdated); expect(buttonUpdated).toHaveTextContent('Test 2'); }); }); diff --git a/shepherd.js/test/unit/components/shepherd-content.spec.js b/shepherd.js/test/unit/components/shepherd-content.spec.js index 231741ed6..716ae037b 100644 --- a/shepherd.js/test/unit/components/shepherd-content.spec.js +++ b/shepherd.js/test/unit/components/shepherd-content.spec.js @@ -1,9 +1,17 @@ -import { cleanup, render } from '@testing-library/svelte'; -import { beforeEach, describe, expect, it } from 'vitest'; -import ShepherdContent from '../../../src/components/shepherd-content.svelte'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { createShepherdContent } from '../../../src/components/shepherd-content.ts'; describe('components/ShepherdContent', () => { - beforeEach(cleanup); + let container; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + container.remove(); + }); describe('header', () => { it('is rendered when title is present and cancelIcon is enabled', () => { @@ -16,7 +24,8 @@ describe('components/ShepherdContent', () => { } }; - const { container } = render(ShepherdContent, { props: { step } }); + const el = createShepherdContent('test-desc', 'test-label', step); + container.appendChild(el); expect( container.querySelector('.shepherd-content .shepherd-header') @@ -30,7 +39,8 @@ describe('components/ShepherdContent', () => { } }; - const { container } = render(ShepherdContent, { props: { step } }); + const el = createShepherdContent('test-desc', 'test-label', step); + container.appendChild(el); expect( container.querySelector('.shepherd-content .shepherd-header') @@ -46,7 +56,8 @@ describe('components/ShepherdContent', () => { } }; - const { container } = render(ShepherdContent, { props: { step } }); + const el = createShepherdContent('test-desc', 'test-label', step); + container.appendChild(el); expect( container.querySelector('.shepherd-content .shepherd-header') @@ -60,7 +71,8 @@ describe('components/ShepherdContent', () => { } }; - const { container } = render(ShepherdContent, { props: { step } }); + const el = createShepherdContent('test-desc', 'test-label', step); + container.appendChild(el); expect( container.querySelector('.shepherd-header') diff --git a/shepherd.js/test/unit/components/shepherd-element.spec.js b/shepherd.js/test/unit/components/shepherd-element.spec.js index 3039033f4..5a00f6698 100644 --- a/shepherd.js/test/unit/components/shepherd-element.spec.js +++ b/shepherd.js/test/unit/components/shepherd-element.spec.js @@ -1,32 +1,49 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { cleanup, fireEvent, render } from '@testing-library/svelte'; -import ShepherdElement from '../../../src/components/shepherd-element.svelte'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { createShepherdElement } from '../../../src/components/shepherd-element.ts'; import { Step } from '../../../src/step'; import { Tour } from '../../../src/tour'; +function fireKeyDown(el, keyCode, opts = {}) { + el.dispatchEvent( + new KeyboardEvent('keydown', { keyCode, bubbles: true, ...opts }) + ); +} + describe('components/ShepherdElement', () => { - describe('arrow', () => { - beforeEach(cleanup); + let container; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + container.remove(); + }); - it('arrows shown by default', async () => { + describe('arrow', () => { + it('arrows shown by default', () => { const testElement = document.createElement('div'); const tour = new Tour(); const step = new Step(tour, { attachTo: { element: testElement, on: 'top' } }); - const { container } = render(ShepherdElement, { - props: { - step - } + const { element, cleanup } = createShepherdElement({ + descriptionId: 'test-desc', + labelId: 'test-label', + step }); + container.appendChild(element); expect( container.querySelectorAll('.shepherd-element .shepherd-arrow').length ).toBe(1); + + cleanup(); }); - it('arrow: false hides arrows', async () => { + it('arrow: false hides arrows', () => { const testElement = document.createElement('div'); const tour = new Tour(); const step = new Step(tour, { @@ -34,18 +51,21 @@ describe('components/ShepherdElement', () => { attachTo: { element: testElement, on: 'top' } }); - const { container } = render(ShepherdElement, { - props: { - step - } + const { element, cleanup } = createShepherdElement({ + descriptionId: 'test-desc', + labelId: 'test-label', + step }); + container.appendChild(element); expect( container.querySelectorAll('.shepherd-element .shepherd-arrow').length ).toBe(0); + + cleanup(); }); - it('arrow: object with padding shows arrow', async () => { + it('arrow: object with padding shows arrow', () => { const testElement = document.createElement('div'); const tour = new Tour(); const step = new Step(tour, { @@ -53,18 +73,21 @@ describe('components/ShepherdElement', () => { attachTo: { element: testElement, on: 'top' } }); - const { container } = render(ShepherdElement, { - props: { - step - } + const { element, cleanup } = createShepherdElement({ + descriptionId: 'test-desc', + labelId: 'test-label', + step }); + container.appendChild(element); expect( container.querySelectorAll('.shepherd-element .shepherd-arrow').length ).toBe(1); + + cleanup(); }); - it('arrow: empty object shows arrow', async () => { + it('arrow: empty object shows arrow', () => { const testElement = document.createElement('div'); const tour = new Tour(); const step = new Step(tour, { @@ -72,54 +95,59 @@ describe('components/ShepherdElement', () => { attachTo: { element: testElement, on: 'top' } }); - const { container } = render(ShepherdElement, { - props: { - step - } + const { element, cleanup } = createShepherdElement({ + descriptionId: 'test-desc', + labelId: 'test-label', + step }); + container.appendChild(element); expect( container.querySelectorAll('.shepherd-element .shepherd-arrow').length ).toBe(1); + + cleanup(); }); }); describe('handleKeyDown', () => { - beforeEach(cleanup); - - it('exitOnEsc: true - ESC cancels the tour', async () => { + it('exitOnEsc: true - ESC cancels the tour', () => { const tour = new Tour(); const step = new Step(tour, {}); const stepCancelSpy = vi.spyOn(step, 'cancel'); - const { container } = render(ShepherdElement, { - props: { - step - } - }); - fireEvent.keyDown(container.querySelector('.shepherd-element'), { - keyCode: 27 + const { element, cleanup } = createShepherdElement({ + descriptionId: 'test-desc', + labelId: 'test-label', + step }); + container.appendChild(element); + + fireKeyDown(element, 27); expect(stepCancelSpy).toHaveBeenCalled(); + + cleanup(); }); - it('exitOnEsc: false - ESC does not cancel the tour', async () => { + it('exitOnEsc: false - ESC does not cancel the tour', () => { const tour = new Tour({ exitOnEsc: false }); const step = new Step(tour, {}); const stepCancelSpy = vi.spyOn(step, 'cancel'); - const { container } = render(ShepherdElement, { - props: { - step - } - }); - fireEvent.keyDown(container.querySelector('.shepherd-element'), { - keyCode: 27 + const { element, cleanup } = createShepherdElement({ + descriptionId: 'test-desc', + labelId: 'test-label', + step }); + container.appendChild(element); + + fireKeyDown(element, 27); expect(stepCancelSpy).not.toHaveBeenCalled(); + + cleanup(); }); - it('keyboardNavigation: true - arrow keys move between steps', async () => { + it('keyboardNavigation: true - arrow keys move between steps', () => { const tour = new Tour(); const step = new Step(tour, {}); let propagateValue = 0; @@ -128,40 +156,40 @@ describe('components/ShepherdElement', () => { const tourNextStub = vi.spyOn(tour, 'next').mockImplementation(() => {}); // Add a keystroke listener to a parent to test event propagation - document.body.addEventListener('keydown', (event) => { - // listen to ESC, KEY_RIGHT, KEY_LEFT + const propagateHandler = (event) => { if ([27, 37, 39].includes(event.keyCode)) { propagateValue += 1; } - }); + }; + document.body.addEventListener('keydown', propagateHandler); expect(tourBackStub).not.toHaveBeenCalled(); expect(tourNextStub).not.toHaveBeenCalled(); - const { container } = render(ShepherdElement, { - props: { - step - } - }); - fireEvent.keyDown(container.querySelector('.shepherd-element'), { - keyCode: 39 + const { element, cleanup } = createShepherdElement({ + descriptionId: 'test-desc', + labelId: 'test-label', + step }); + container.appendChild(element); + + fireKeyDown(element, 39); expect(tourNextStub).toHaveBeenCalled(); // There should be no event propagation expect(propagateValue).toBe(0); - fireEvent.keyDown(container.querySelector('.shepherd-element'), { - keyCode: 37 - }); + fireKeyDown(element, 37); expect(tourBackStub).toHaveBeenCalled(); // There should be no event propagation expect(propagateValue).toBe(0); tourBackStub.mockRestore(); tourNextStub.mockRestore(); + document.body.removeEventListener('keydown', propagateHandler); + cleanup(); }); - it('keyboardNavigation: false - arrow keys do not move between steps', async () => { + it('keyboardNavigation: false - arrow keys do not move between steps', () => { const tour = new Tour({ keyboardNavigation: false }); const step = new Step(tour, {}); let propagateValue = 0; @@ -170,37 +198,210 @@ describe('components/ShepherdElement', () => { const tourNextStub = vi.spyOn(tour, 'next').mockImplementation(() => {}); // Add a keystroke listener to a parent to test event propagation - document.body.addEventListener('keydown', (event) => { - // listen to ESC, KEY_RIGHT, KEY_LEFT + const propagateHandler = (event) => { if ([27, 37, 39].includes(event.keyCode)) { propagateValue += 1; } - }); + }; + document.body.addEventListener('keydown', propagateHandler); expect(tourBackStub).not.toHaveBeenCalled(); expect(tourNextStub).not.toHaveBeenCalled(); - const { container } = render(ShepherdElement, { - props: { - step - } - }); - fireEvent.keyDown(container.querySelector('.shepherd-element'), { - keyCode: 39 + const { element, cleanup } = createShepherdElement({ + descriptionId: 'test-desc', + labelId: 'test-label', + step }); + container.appendChild(element); + + fireKeyDown(element, 39); expect(tourNextStub).not.toHaveBeenCalled(); // There should be event propagation expect(propagateValue).toBe(1); - fireEvent.keyDown(container.querySelector('.shepherd-element'), { - keyCode: 37 - }); + fireKeyDown(element, 37); expect(tourBackStub).not.toHaveBeenCalled(); // There should be another event propagation expect(propagateValue).toBe(2); tourBackStub.mockRestore(); tourNextStub.mockRestore(); + document.body.removeEventListener('keydown', propagateHandler); + cleanup(); + }); + + it('Tab key: prevents default when no focusable elements exist', () => { + const tour = new Tour(); + // Step with no buttons, no cancel icon — dialog has no focusable children + const step = new Step(tour, {}); + + const { element, cleanup } = createShepherdElement({ + descriptionId: 'test-desc', + labelId: 'test-label', + step + }); + container.appendChild(element); + + const prevented = []; + element.addEventListener( + 'keydown', + (e) => { + prevented.push(e.defaultPrevented); + }, + { capture: false } + ); + + fireKeyDown(element, 9, { cancelable: true }); + expect(prevented[0]).toBe(true); + + cleanup(); + }); + + it('Shift+Tab from first dialog element focuses last attachTo element', () => { + const tour = new Tour(); + const attachToEl = document.createElement('div'); + const attachToBtn = document.createElement('button'); + attachToBtn.textContent = 'Attach Button'; + attachToEl.appendChild(attachToBtn); + container.appendChild(attachToEl); + + const step = new Step(tour, { + attachTo: { element: attachToEl, on: 'bottom' }, + buttons: [{ text: 'Next', action: () => {} }] + }); + + const { element, cleanup } = createShepherdElement({ + descriptionId: 'test-desc', + labelId: 'test-label', + step + }); + container.appendChild(element); + + // First focusable dialog element is the "Next" button + const dialogBtn = element.querySelector('button.shepherd-button'); + expect(dialogBtn).toBeTruthy(); + dialogBtn.focus(); + + const focusSpy = vi.spyOn(attachToBtn, 'focus'); + fireKeyDown(element, 9, { shiftKey: true }); + // The last focusable attachTo element (attachToBtn) should be focused + expect(focusSpy).toHaveBeenCalled(); + + focusSpy.mockRestore(); + cleanup(); + }); + + it('Shift+Tab from first attachTo element focuses last dialog element', () => { + const tour = new Tour(); + const attachToEl = document.createElement('button'); + attachToEl.textContent = 'Target'; + container.appendChild(attachToEl); + + const step = new Step(tour, { + attachTo: { element: attachToEl, on: 'bottom' }, + buttons: [{ text: 'Next', action: () => {} }] + }); + + const { element, cleanup } = createShepherdElement({ + descriptionId: 'test-desc', + labelId: 'test-label', + step + }); + container.appendChild(element); + + // Focus the attachTo element (first focusable attach-to element) + attachToEl.focus(); + + const dialogBtn = element.querySelector('button.shepherd-button'); + const focusSpy = vi.spyOn(dialogBtn, 'focus'); + // Fire on the attachTo element since it also has the keydown listener + fireKeyDown(attachToEl, 9, { shiftKey: true }); + expect(focusSpy).toHaveBeenCalled(); + + focusSpy.mockRestore(); + cleanup(); + }); + + it('forward Tab from last dialog element focuses first attachTo element', () => { + const tour = new Tour(); + const attachToEl = document.createElement('button'); + attachToEl.textContent = 'Target'; + container.appendChild(attachToEl); + + const step = new Step(tour, { + attachTo: { element: attachToEl, on: 'bottom' }, + buttons: [{ text: 'Next', action: () => {} }] + }); + + const { element, cleanup } = createShepherdElement({ + descriptionId: 'test-desc', + labelId: 'test-label', + step + }); + container.appendChild(element); + + // Focus the last dialog element (the button) + const dialogBtn = element.querySelector('button.shepherd-button'); + dialogBtn.focus(); + + const focusSpy = vi.spyOn(attachToEl, 'focus'); + fireKeyDown(element, 9); + // First focusable attachTo element should receive focus + expect(focusSpy).toHaveBeenCalled(); + + focusSpy.mockRestore(); + cleanup(); + }); + + it('forward Tab from last attachTo element focuses first dialog element', () => { + const tour = new Tour(); + const attachToEl = document.createElement('button'); + attachToEl.textContent = 'Target'; + container.appendChild(attachToEl); + + const step = new Step(tour, { + attachTo: { element: attachToEl, on: 'bottom' }, + buttons: [{ text: 'Next', action: () => {} }] + }); + + const { element, cleanup } = createShepherdElement({ + descriptionId: 'test-desc', + labelId: 'test-label', + step + }); + container.appendChild(element); + + // Focus the last attachTo element (the button itself, since it's the only one) + attachToEl.focus(); + + const dialogBtn = element.querySelector('button.shepherd-button'); + const focusSpy = vi.spyOn(dialogBtn, 'focus'); + // Fire on the attachTo element + fireKeyDown(attachToEl, 9); + expect(focusSpy).toHaveBeenCalled(); + + focusSpy.mockRestore(); + cleanup(); + }); + + it('unhandled key falls through to default case', () => { + const tour = new Tour(); + const step = new Step(tour, {}); + + const { element, cleanup } = createShepherdElement({ + descriptionId: 'test-desc', + labelId: 'test-label', + step + }); + container.appendChild(element); + + // Press an unrelated key (e.g. 'A' = keyCode 65) and ensure nothing breaks + fireKeyDown(element, 65); + // No error thrown, element is still in DOM + expect(element.parentNode).toBe(container); + + cleanup(); }); }); }); diff --git a/shepherd.js/test/unit/components/shepherd-footer.spec.js b/shepherd.js/test/unit/components/shepherd-footer.spec.js index ccc2dc399..d20cf91fa 100644 --- a/shepherd.js/test/unit/components/shepherd-footer.spec.js +++ b/shepherd.js/test/unit/components/shepherd-footer.spec.js @@ -1,10 +1,18 @@ -import { cleanup, render } from '@testing-library/svelte'; -import { beforeEach, describe, expect, it } from 'vitest'; -import ShepherdFooter from '../../../src/components/shepherd-footer.svelte'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { createShepherdFooter } from '../../../src/components/shepherd-footer.ts'; import defaultButtons from '../../cypress/utils/default-buttons.js'; describe('components/ShepherdFooter', () => { - beforeEach(cleanup); + let container; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + container.remove(); + }); it('renders no buttons if an empty array is passed to `options.buttons`', () => { const step = { @@ -13,11 +21,8 @@ describe('components/ShepherdFooter', () => { } }; - const { container } = render(ShepherdFooter, { - props: { - step - } - }); + const el = createShepherdFooter(step); + container.appendChild(el); const buttons = container.querySelectorAll( '.shepherd-footer .shepherd-button' @@ -28,11 +33,8 @@ describe('components/ShepherdFooter', () => { it('renders no buttons if nothing is passed to `options.buttons`', () => { const step = { options: {} }; - const { container } = render(ShepherdFooter, { - props: { - step - } - }); + const el = createShepherdFooter(step); + container.appendChild(el); const buttons = container.querySelectorAll( '.shepherd-footer .shepherd-button' @@ -47,11 +49,8 @@ describe('components/ShepherdFooter', () => { } }; - const { container } = render(ShepherdFooter, { - props: { - step - } - }); + const el = createShepherdFooter(step); + container.appendChild(el); const buttons = container.querySelectorAll( '.shepherd-footer .shepherd-button' diff --git a/shepherd.js/test/unit/components/shepherd-header.spec.js b/shepherd.js/test/unit/components/shepherd-header.spec.js index ff619751f..e960b6c81 100644 --- a/shepherd.js/test/unit/components/shepherd-header.spec.js +++ b/shepherd.js/test/unit/components/shepherd-header.spec.js @@ -1,11 +1,19 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { cleanup, fireEvent, render } from '@testing-library/svelte'; -import ShepherdHeader from '../../../src/components/shepherd-header.svelte'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { createShepherdHeader } from '../../../src/components/shepherd-header.ts'; import { Tour } from '../../../src/tour'; import { Step } from '../../../src/step'; describe('components/ShepherdHeader', () => { - beforeEach(cleanup); + let container; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + container.remove(); + }); it('cancel icon is added when cancelIcon.enabled === true', () => { const step = { @@ -16,11 +24,8 @@ describe('components/ShepherdHeader', () => { } }; - const { container } = render(ShepherdHeader, { - props: { - step - } - }); + const el = createShepherdHeader('test-label', step); + container.appendChild(el); const cancelIcon = container.querySelector('.shepherd-cancel-icon'); expect(cancelIcon).toBeInTheDocument(); @@ -37,11 +42,8 @@ describe('components/ShepherdHeader', () => { } }; - const { container } = render(ShepherdHeader, { - props: { - step - } - }); + const el = createShepherdHeader('test-label', step); + container.appendChild(el); const cancelIcon = container.querySelector('.shepherd-cancel-icon'); @@ -58,11 +60,8 @@ describe('components/ShepherdHeader', () => { } }; - const { container } = render(ShepherdHeader, { - props: { - step - } - }); + const el = createShepherdHeader('test-label', step); + container.appendChild(el); expect(container.querySelector('.shepherd-cancel-icon')).toHaveAttribute( 'aria-label', @@ -79,13 +78,10 @@ describe('components/ShepherdHeader', () => { }); const stepCancelSpy = vi.spyOn(step, 'cancel'); - const { container } = render(ShepherdHeader, { - props: { - step - } - }); + const el = createShepherdHeader('test-label', step); + container.appendChild(el); - fireEvent.click(container.querySelector('.shepherd-cancel-icon')); + container.querySelector('.shepherd-cancel-icon').click(); expect(stepCancelSpy).toHaveBeenCalled(); }); }); diff --git a/shepherd.js/test/unit/components/shepherd-modal.spec.js b/shepherd.js/test/unit/components/shepherd-modal.spec.js index c71e1c5d3..576dc7255 100644 --- a/shepherd.js/test/unit/components/shepherd-modal.spec.js +++ b/shepherd.js/test/unit/components/shepherd-modal.spec.js @@ -1,17 +1,25 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; -import ShepherdModal from '../../../src/components/shepherd-modal.svelte'; -import { mount, unmount } from 'svelte'; - -const classPrefix = ''; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { createShepherdModal } from '../../../src/components/shepherd-modal.ts'; +import { Step } from '../../../src/step'; +import { Tour } from '../../../src/tour'; describe('components/ShepherdModal', () => { + let container; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + container.remove(); + }); + describe('closeModalOpening()', function () { - it('sets values back to 0', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body - }); + it('sets values back to 0', () => { + const modal = createShepherdModal(container); - await modalComponent.positionModal(0, 0, 0, 0, null, { + modal.positionModal(0, 0, 0, 0, null, { getBoundingClientRect() { return { height: 250, @@ -22,49 +30,41 @@ describe('components/ShepherdModal', () => { } }); - let modalPath = modalComponent.getElement().querySelector('path'); + let modalPath = modal.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', 'M1024,768H0V0H1024V768ZM20,20a0,0,0,0,0-0,0V270a0,0,0,0,0,0,0H520a0,0,0,0,0,0-0V20a0,0,0,0,0-0-0Z' ); - await modalComponent.closeModalOpening(); + modal.closeModalOpening(); - modalPath = modalComponent.getElement().querySelector('path'); + modalPath = modal.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' ); - - unmount(modalComponent); }); }); describe('positionModal()', function () { - it('sets the correct attributes when positioning modal opening', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } - }); + it('sets the correct attributes when positioning modal opening', () => { + const modal = createShepherdModal(container); - await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for mount - let modalPath = modalComponent.getElement().querySelector('path'); + let modalPath = modal.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' ); - await modalComponent.closeModalOpening(); + modal.closeModalOpening(); - modalPath = modalComponent.getElement().querySelector('path'); + modalPath = modal.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' ); - await modalComponent.positionModal(0, 0, 0, 0, null, { + modal.positionModal(0, 0, 0, 0, null, { getBoundingClientRect() { return { height: 250, @@ -75,31 +75,23 @@ describe('components/ShepherdModal', () => { } }); - modalPath = modalComponent.getElement().querySelector('path'); + modalPath = modal.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', 'M1024,768H0V0H1024V768ZM20,20a0,0,0,0,0-0,0V270a0,0,0,0,0,0,0H520a0,0,0,0,0,0-0V20a0,0,0,0,0-0-0Z' ); - - unmount(modalComponent); }); - it('sets the correct attributes with padding', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } - }); - await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for mount + it('sets the correct attributes with padding', () => { + const modal = createShepherdModal(container); - let modalPath = modalComponent.getElement().querySelector('path'); + let modalPath = modal.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' ); - await modalComponent.positionModal(10, 0, 0, 0, null, { + modal.positionModal(10, 0, 0, 0, null, { getBoundingClientRect() { return { height: 250, @@ -110,39 +102,31 @@ describe('components/ShepherdModal', () => { } }); - modalPath = modalComponent.getElement().querySelector('path'); + modalPath = modal.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', 'M1024,768H0V0H1024V768ZM10,10a0,0,0,0,0-0,0V280a0,0,0,0,0,0,0H530a0,0,0,0,0,0-0V10a0,0,0,0,0-0-0Z' ); - - unmount(modalComponent); }); - it('sets the correct attributes when positioning modal opening with border radius as number', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } - }); - await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for mount + it('sets the correct attributes when positioning modal opening with border radius as number', () => { + const modal = createShepherdModal(container); - let modalPath = modalComponent.getElement().querySelector('path'); + let modalPath = modal.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' ); - await modalComponent.closeModalOpening(); + modal.closeModalOpening(); - modalPath = modalComponent.getElement().querySelector('path'); + modalPath = modal.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' ); - await modalComponent.positionModal(0, 10, 0, 0, null, { + modal.positionModal(0, 10, 0, 0, null, { getBoundingClientRect() { return { height: 250, @@ -153,39 +137,31 @@ describe('components/ShepherdModal', () => { } }); - modalPath = modalComponent.getElement().querySelector('path'); + modalPath = modal.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', 'M1024,768H0V0H1024V768ZM30,20a10,10,0,0,0-10,10V260a10,10,0,0,0,10,10H510a10,10,0,0,0,10-10V30a10,10,0,0,0-10-10Z' ); - - unmount(modalComponent); }); - it('sets the correct attributes when positioning modal opening with border radius as object', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } - }); - await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for mount + it('sets the correct attributes when positioning modal opening with border radius as object', () => { + const modal = createShepherdModal(container); - let modalPath = modalComponent.getElement().querySelector('path'); + let modalPath = modal.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' ); - await modalComponent.closeModalOpening(); + modal.closeModalOpening(); - modalPath = modalComponent.getElement().querySelector('path'); + modalPath = modal.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' ); - await modalComponent.positionModal( + modal.positionModal( 0, { topLeft: 1, bottomLeft: 2, bottomRight: 3 }, 0, @@ -203,24 +179,17 @@ describe('components/ShepherdModal', () => { } ); - modalPath = modalComponent.getElement().querySelector('path'); + modalPath = modal.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', 'M1024,768H0V0H1024V768ZM21,20a1,1,0,0,0-1,1V268a2,2,0,0,0,2,2H517a3,3,0,0,0,3-3V20a0,0,0,0,0-0-0Z' ); - - unmount(modalComponent); }); - it('sets the correct attributes when target is overflowing from scroll parent', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } - }); + it('sets the correct attributes when target is overflowing from scroll parent', () => { + const modal = createShepherdModal(container); - await modalComponent.positionModal( + modal.positionModal( 0, 0, 0, @@ -247,24 +216,17 @@ describe('components/ShepherdModal', () => { } ); - const modalPath = modalComponent.getElement().querySelector('path'); + const modalPath = modal.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', 'M1024,768H0V0H1024V768ZM10,100a0,0,0,0,0-0,0V350a0,0,0,0,0,0,0H510a0,0,0,0,0,0-0V100a0,0,0,0,0-0-0Z' ); - - unmount(modalComponent); }); - it('sets the correct attributes when target fits inside scroll parent', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } - }); + it('sets the correct attributes when target fits inside scroll parent', () => { + const modal = createShepherdModal(container); - await modalComponent.positionModal( + modal.positionModal( 0, 0, 0, @@ -291,26 +253,17 @@ describe('components/ShepherdModal', () => { } ); - const modalPath = modalComponent.getElement().querySelector('path'); + const modalPath = modal.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', 'M1024,768H0V0H1024V768ZM10,100a0,0,0,0,0-0,0V350a0,0,0,0,0,0,0H510a0,0,0,0,0,0-0V100a0,0,0,0,0-0-0Z' ); - - unmount(modalComponent); }); - it('allows setting an x-axis offset', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } - }); - - await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for mount + it('allows setting an x-axis offset', () => { + const modal = createShepherdModal(container); - modalComponent.positionModal(0, 0, 50, 0, null, { + modal.positionModal(0, 0, 50, 0, null, { getBoundingClientRect() { return { height: 250, @@ -321,15 +274,14 @@ describe('components/ShepherdModal', () => { } }); - await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for DOM update - let modalPath = modalComponent.getElement().querySelector('path'); + let modalPath = modal.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', 'M1024,768H0V0H1024V768ZM60,10a0,0,0,0,0-0,0V260a0,0,0,0,0,0,0H560a0,0,0,0,0,0-0V10a0,0,0,0,0-0-0Z' ); - modalComponent.positionModal(0, 0, 100, 0, null, { + modal.positionModal(0, 0, 100, 0, null, { getBoundingClientRect() { return { height: 250, @@ -340,28 +292,18 @@ describe('components/ShepherdModal', () => { } }); - await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for DOM update - modalPath = modalComponent.getElement().querySelector('path'); + modalPath = modal.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', 'M1024,768H0V0H1024V768ZM110,10a0,0,0,0,0-0,0V260a0,0,0,0,0,0,0H610a0,0,0,0,0,0-0V10a0,0,0,0,0-0-0Z' ); - - unmount(modalComponent); }); - it('allows setting a y-axis offset', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } - }); + it('allows setting a y-axis offset', () => { + const modal = createShepherdModal(container); - await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for mount - - modalComponent.positionModal(0, 0, 0, 35, null, { + modal.positionModal(0, 0, 0, 35, null, { getBoundingClientRect() { return { height: 250, @@ -372,15 +314,14 @@ describe('components/ShepherdModal', () => { } }); - await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for DOM update - let modalPath = modalComponent.getElement().querySelector('path'); + let modalPath = modal.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', 'M1024,768H0V0H1024V768ZM10,45a0,0,0,0,0-0,0V295a0,0,0,0,0,0,0H510a0,0,0,0,0,0-0V45a0,0,0,0,0-0-0Z' ); - modalComponent.positionModal(0, 0, 0, 75, null, { + modal.positionModal(0, 0, 0, 75, null, { getBoundingClientRect() { return { height: 250, @@ -391,26 +332,18 @@ describe('components/ShepherdModal', () => { } }); - await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for DOM update - modalPath = modalComponent.getElement().querySelector('path'); + modalPath = modal.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', 'M1024,768H0V0H1024V768ZM10,85a0,0,0,0,0-0,0V335a0,0,0,0,0,0,0H510a0,0,0,0,0,0-0V85a0,0,0,0,0-0-0Z' ); - - unmount(modalComponent); }); - it('sets the correct attributes with extraHighlights', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } - }); + it('sets the correct attributes with extraHighlights', () => { + const modal = createShepherdModal(container); - await modalComponent.positionModal( + modal.positionModal( 0, 0, 0, @@ -440,24 +373,17 @@ describe('components/ShepherdModal', () => { ] ); - const modalPath = modalComponent.getElement().querySelector('path'); + const modalPath = modal.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', 'M1024,768H0V0H1024V768ZM20,20a0,0,0,0,0-0,0V270a0,0,0,0,0,0,0H520a0,0,0,0,0,0-0V20a0,0,0,0,0-0-0ZM50,50a0,0,0,0,0-0,0V150a0,0,0,0,0,0,0H150a0,0,0,0,0,0-0V50a0,0,0,0,0-0-0Z' ); - - unmount(modalComponent); }); - it('sets the correct attributes with multiple extraHighlights', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } - }); + it('sets the correct attributes with multiple extraHighlights', () => { + const modal = createShepherdModal(container); - await modalComponent.positionModal( + modal.positionModal( 0, 0, 0, @@ -497,101 +423,301 @@ describe('components/ShepherdModal', () => { ] ); - const modalPath = modalComponent.getElement().querySelector('path'); + const modalPath = modal.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', 'M1024,768H0V0H1024V768ZM20,20a0,0,0,0,0-0,0V270a0,0,0,0,0,0,0H520a0,0,0,0,0,0-0V20a0,0,0,0,0-0-0ZM50,50a0,0,0,0,0-0,0V150a0,0,0,0,0,0,0H150a0,0,0,0,0,0-0V50a0,0,0,0,0-0-0ZM200,200a0,0,0,0,0-0,0V250a0,0,0,0,0,0,0H250a0,0,0,0,0,0-0V200a0,0,0,0,0-0-0Z' ); + }); + + it('skips duplicate elements in extraHighlights', () => { + const modal = createShepherdModal(container); + + const sharedElement = { + getBoundingClientRect() { + return { + height: 100, + x: 50, + y: 50, + width: 100, + top: 50, + bottom: 150, + left: 50, + right: 150 + }; + } + }; + + modal.positionModal( + 0, + 0, + 0, + 0, + null, + { + getBoundingClientRect() { + return { + height: 250, + x: 20, + y: 20, + width: 500, + top: 20, + bottom: 270, + left: 20, + right: 520 + }; + } + }, + // Pass the same element twice — both duplicates are skipped + [sharedElement, sharedElement] + ); - unmount(modalComponent); + const modalPath = modal.getElement().querySelector('path'); + const d = modalPath.getAttribute('d'); + // Duplicate elements are both skipped, only the main target cutout remains + // Outer path close + target cutout close = 2 Z's + const cutouts = d.split('Z').length - 1; + expect(cutouts).toBe(2); }); }); describe('setupForStep()', function () { - let hideStub, showStub; + it('useModalOverlay: false hides the modal', () => { + const modal = createShepherdModal(container); + modal.show(); + expect(modal.getElement()).toHaveClass('shepherd-modal-is-visible'); - afterEach(() => { - hideStub.mockRestore(); - showStub.mockRestore(); + const tour = new Tour({ useModalOverlay: false }); + const step = new Step(tour, {}); + + modal.setupForStep(step); + expect(modal.getElement()).not.toHaveClass('shepherd-modal-is-visible'); }); - it.skip('useModalOverlay: false, hides modal', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } + it('useModalOverlay: true shows the modal and calls _styleForStep', () => { + const modal = createShepherdModal(container); + const rafSpy = vi + .spyOn(window, 'requestAnimationFrame') + .mockImplementation(() => 1); + + const targetEl = document.createElement('div'); + container.appendChild(targetEl); + + const tour = new Tour({ useModalOverlay: true }); + const step = new Step(tour, { + attachTo: { element: targetEl, on: 'bottom' } }); + // Resolve attachTo so step.target is set + step._resolveAttachToOptions(); + step.target = targetEl; - const step = { - options: {}, - tour: { - options: { - useModalOverlay: false - } - } - }; - hideStub = vi.spyOn(modalComponent, 'hide').mockImplementation(() => {}); - showStub = vi.spyOn(modalComponent, 'show').mockImplementation(() => {}); - await modalComponent.setupForStep(step); + modal.setupForStep(step); - expect(hideStub).toHaveBeenCalled(); - expect(showStub.called).not.toHaveBeenCalled(); + expect(modal.getElement()).toHaveClass('shepherd-modal-is-visible'); + // _styleForStep calls rafLoop which calls requestAnimationFrame + expect(rafSpy).toHaveBeenCalled(); - unmount(modalComponent); + rafSpy.mockRestore(); }); + }); + + describe('show/hide', function () { + it('show adds classes', () => { + const modal = createShepherdModal(container); + + modal.show(); + + expect(modal.getElement()).toHaveClass('shepherd-modal-is-visible'); + }); + + it('hide removes classes', () => { + const modal = createShepherdModal(container); + modal.show(); + + modal.hide(); + + expect(modal.getElement()).not.toHaveClass('shepherd-modal-is-visible'); + }); + }); + + describe('destroy()', function () { + it('removes the modal element from the DOM', () => { + const modal = createShepherdModal(container); + expect( + container.querySelector('.shepherd-modal-overlay-container') + ).toBeTruthy(); + + modal.destroy(); + expect( + container.querySelector('.shepherd-modal-overlay-container') + ).toBeNull(); + }); + }); - it.skip('useModalOverlay: true, shows modal', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix + describe('_getScrollParent (via setupForStep)', function () { + it('recurses to find a scrollable parent element', () => { + const modal = createShepherdModal(container); + const rafSpy = vi + .spyOn(window, 'requestAnimationFrame') + .mockImplementation(() => 1); + + // Create a scrollable parent + const scrollParent = document.createElement('div'); + Object.defineProperty(scrollParent, 'scrollHeight', { value: 500 }); + Object.defineProperty(scrollParent, 'clientHeight', { value: 200 }); + + container.appendChild(scrollParent); + + const targetEl = document.createElement('div'); + scrollParent.appendChild(targetEl); + + // Mock getComputedStyle so the target has 'visible' overflow (not scrollable) + // and the scroll parent has 'auto' overflow (scrollable), forcing recursion + const origGetComputedStyle = window.getComputedStyle; + vi.spyOn(window, 'getComputedStyle').mockImplementation((el) => { + if (el === targetEl) { + return { overflowY: 'visible' }; } + if (el === scrollParent) { + return { overflowY: 'auto' }; + } + return origGetComputedStyle(el); }); - const step = { - options: {}, - tour: { - options: { - useModalOverlay: true - } - } - }; - hideStub = vi.spyOn(modalComponent, 'hide').mockImplementation(() => {}); - showStub = vi.spyOn(modalComponent, 'show').mockImplementation(() => {}); - await modalComponent.setupForStep(step); + const tour = new Tour({ useModalOverlay: true }); + const step = new Step(tour, { + attachTo: { element: targetEl, on: 'bottom' } + }); + step._resolveAttachToOptions(); + step.target = targetEl; + + // setupForStep triggers _styleForStep -> _getScrollParent + modal.setupForStep(step); - expect(hideStub).not.toHaveBeenCalled(); - expect(showStub).toHaveBeenCalled(); + expect(modal.getElement()).toHaveClass('shepherd-modal-is-visible'); - unmount(modalComponent); + rafSpy.mockRestore(); + vi.mocked(window.getComputedStyle).mockRestore(); }); }); - describe('show/hide', function () { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } + describe('_preventModalBodyTouch (via _addStepEventListeners)', function () { + it('prevents default on window touchmove after setupForStep', () => { + const modal = createShepherdModal(container); + const rafSpy = vi + .spyOn(window, 'requestAnimationFrame') + .mockImplementation(() => 1); + + const targetEl = document.createElement('div'); + container.appendChild(targetEl); + + const tour = new Tour({ useModalOverlay: true }); + const step = new Step(tour, { + attachTo: { element: targetEl, on: 'bottom' } + }); + step._resolveAttachToOptions(); + step.target = targetEl; + + modal.setupForStep(step); + + // _addStepEventListeners was called, so window has a touchmove listener + const touchEvent = new Event('touchmove', { + bubbles: true, + cancelable: true + }); + const preventSpy = vi.spyOn(touchEvent, 'preventDefault'); + window.dispatchEvent(touchEvent); + expect(preventSpy).toHaveBeenCalled(); + + // Clean up: hide triggers _cleanupStepEventListeners which removes the listener + modal.hide(); + rafSpy.mockRestore(); }); + }); - it('show adds classes', async () => { - await modalComponent.show(); + describe('_preventModalOverlayTouch', function () { + it('stops propagation on touchmove events', () => { + const modal = createShepherdModal(container); + const svgEl = modal.getElement(); - expect(modalComponent.getElement()).toHaveClass( - 'shepherd-modal-is-visible' - ); + const touchEvent = new Event('touchmove', { + bubbles: true, + cancelable: true + }); + const stopSpy = vi.spyOn(touchEvent, 'stopPropagation'); + + svgEl.dispatchEvent(touchEvent); + expect(stopSpy).toHaveBeenCalled(); }); + }); + + describe('_getIframeOffset (via setupForStep)', function () { + it('accumulates offset when element is inside an iframe', () => { + const modal = createShepherdModal(container); + const rafSpy = vi + .spyOn(window, 'requestAnimationFrame') + .mockImplementation(() => 1); + + const targetEl = document.createElement('div'); + container.appendChild(targetEl); + + // Simulate the element being inside an iframe by mocking ownerDocument.defaultView + const fakeIframe = document.createElement('iframe'); + Object.defineProperty(fakeIframe, 'getBoundingClientRect', { + value: () => ({ + top: 10, + left: 20, + width: 100, + height: 100, + x: 20, + y: 10 + }) + }); + Object.defineProperty(fakeIframe, 'scrollTop', { value: 5 }); + Object.defineProperty(fakeIframe, 'scrollLeft', { value: 3 }); - it('hide removes classes', async () => { - await modalComponent.hide(); + const fakeChildWindow = { + frameElement: fakeIframe, + parent: window + }; - expect(modalComponent.getElement()).not.toHaveClass( - 'shepherd-modal-is-visible' + const origDescriptor = Object.getOwnPropertyDescriptor( + targetEl.ownerDocument, + 'defaultView' ); + Object.defineProperty(targetEl.ownerDocument, 'defaultView', { + value: fakeChildWindow, + configurable: true + }); + + const tour = new Tour({ useModalOverlay: true }); + const step = new Step(tour, { + attachTo: { element: targetEl, on: 'bottom' } + }); + step._resolveAttachToOptions(); + step.target = targetEl; + + // This triggers _styleForStep -> _getIframeOffset, which should + // walk up through fakeChildWindow and accumulate the iframe offset + modal.setupForStep(step); + + // Restore defaultView before any assertions (jsdom needs it for instanceof checks) + if (origDescriptor) { + Object.defineProperty( + targetEl.ownerDocument, + 'defaultView', + origDescriptor + ); + } else { + Object.defineProperty(targetEl.ownerDocument, 'defaultView', { + value: window, + configurable: true + }); + } + + expect(modal.getElement()).toHaveClass('shepherd-modal-is-visible'); - unmount(modalComponent); + rafSpy.mockRestore(); }); }); }); diff --git a/shepherd.js/test/unit/components/shepherd-text.spec.js b/shepherd.js/test/unit/components/shepherd-text.spec.js index 63092c458..1a4552303 100644 --- a/shepherd.js/test/unit/components/shepherd-text.spec.js +++ b/shepherd.js/test/unit/components/shepherd-text.spec.js @@ -1,9 +1,17 @@ -import { cleanup, render } from '@testing-library/svelte'; -import { beforeEach, describe, expect, it } from 'vitest'; -import ShepherdText from '../../../src/components/shepherd-text.svelte'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { createShepherdText } from '../../../src/components/shepherd-text.ts'; describe('components/ShepherdText', () => { - beforeEach(cleanup); + let container; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + container.remove(); + }); it('adds plain text to the content', () => { const step = { @@ -12,11 +20,8 @@ describe('components/ShepherdText', () => { } }; - const { container } = render(ShepherdText, { - props: { - step - } - }); + const el = createShepherdText('test-desc', step); + container.appendChild(el); expect(container.querySelector('.shepherd-text')).toHaveTextContent( 'I am some test text.' @@ -30,11 +35,8 @@ describe('components/ShepherdText', () => { } }; - const { container } = render(ShepherdText, { - props: { - step - } - }); + const el = createShepherdText('test-desc', step); + container.appendChild(el); expect(container.querySelector('.shepherd-text')).toContainHTML( '

I am some test text.

' @@ -48,14 +50,29 @@ describe('components/ShepherdText', () => { } }; - const { container } = render(ShepherdText, { - props: { - step - } - }); + const el = createShepherdText('test-desc', step); + container.appendChild(el); expect(container.querySelector('.shepherd-text')).toHaveTextContent( 'I am some test text.' ); }); + + it('appends an HTMLElement when text is a DOM node', () => { + const paragraph = document.createElement('p'); + paragraph.textContent = 'I am a DOM node.'; + + const step = { + options: { + text: paragraph + } + }; + + const el = createShepherdText('test-desc', step); + container.appendChild(el); + + const textEl = container.querySelector('.shepherd-text'); + expect(textEl.contains(paragraph)).toBe(true); + expect(textEl).toHaveTextContent('I am a DOM node.'); + }); }); diff --git a/shepherd.js/test/unit/components/shepherd-title.spec.js b/shepherd.js/test/unit/components/shepherd-title.spec.js index 62b117df3..fb6fe8e3f 100644 --- a/shepherd.js/test/unit/components/shepherd-title.spec.js +++ b/shepherd.js/test/unit/components/shepherd-title.spec.js @@ -1,16 +1,21 @@ -import { cleanup, render } from '@testing-library/svelte'; -import { beforeEach, describe, expect, it } from 'vitest'; -import ShepherdTitle from '../../../src/components/shepherd-title.svelte'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { createShepherdTitle } from '../../../src/components/shepherd-title.ts'; describe('components/ShepherdTitle', () => { - beforeEach(cleanup); + let container; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + container.remove(); + }); it('adds plain title to the content', () => { - const { container } = render(ShepherdTitle, { - props: { - title: 'I am some test title.' - } - }); + const el = createShepherdTitle('test-label', 'I am some test title.'); + container.appendChild(el); expect(container.querySelector('.shepherd-title')).toHaveTextContent( 'I am some test title.' @@ -18,11 +23,8 @@ describe('components/ShepherdTitle', () => { }); it('applies the title from a function', () => { - const { container } = render(ShepherdTitle, { - props: { - title: () => 'I am some test title.' - } - }); + const el = createShepherdTitle('test-label', () => 'I am some test title.'); + container.appendChild(el); expect(container.querySelector('.shepherd-title')).toHaveTextContent( 'I am some test title.' diff --git a/shepherd.js/test/unit/setupTests.js b/shepherd.js/test/unit/setupTests.js index 9d434a854..de19733c4 100644 --- a/shepherd.js/test/unit/setupTests.js +++ b/shepherd.js/test/unit/setupTests.js @@ -4,19 +4,6 @@ import { expect } from 'vitest'; expect.extend(matchers); -// Configure Svelte to force client-side rendering -vi.doMock('svelte', async () => { - const svelteModule = await vi.importActual('svelte'); - return { - ...svelteModule, - mount: - svelteModule.mount || - function (component, options) { - return svelteModule.createRoot(component, options); - } - }; -}); - // Set browser environment Object.defineProperty(globalThis, 'IS_BROWSER', { value: true, diff --git a/shepherd.js/test/unit/test-helpers.js b/shepherd.js/test/unit/test-helpers.js deleted file mode 100644 index 78b719cb8..000000000 --- a/shepherd.js/test/unit/test-helpers.js +++ /dev/null @@ -1,35 +0,0 @@ -import { mount, unmount } from 'svelte'; - -export function renderComponent(Component, options = {}) { - const { props = {}, target } = options; - - const container = target || document.createElement('div'); - document.body.appendChild(container); - - let component; - try { - component = mount(Component, { - target: container, - props - }); - } catch (error) { - console.error('Error mounting component:', error); - throw error; - } - - return { - container, - component, - unmount: () => { - if (component) { - unmount(component); - } - if (container.parentNode) { - container.parentNode.removeChild(container); - } - }, - rerender: (newProps) => { - Object.assign(component, newProps); - } - }; -} diff --git a/shepherd.js/vitest.config.ts b/shepherd.js/vitest.config.ts index 7dfd2ad4c..2111b8d9b 100644 --- a/shepherd.js/vitest.config.ts +++ b/shepherd.js/vitest.config.ts @@ -1,18 +1,6 @@ import { defineConfig } from 'vitest/config'; -import { svelte } from '@sveltejs/vite-plugin-svelte'; -import preprocess from 'svelte-preprocess'; export default defineConfig({ - plugins: [ - svelte({ - preprocess: preprocess({}), - compilerOptions: { - dev: false - }, - hot: false, - emitCss: false - }) - ], test: { globals: true, environment: 'happy-dom', @@ -21,7 +9,7 @@ export default defineConfig({ provider: 'v8', reporter: ['text', 'lcov', 'html'], reportsDirectory: './test/coverage', - include: ['src/**/*.{ts,svelte}'], + include: ['src/**/*.ts'], exclude: [ '**/*.spec.{js,ts}', '**/*.test.{js,ts}', @@ -41,8 +29,5 @@ export default defineConfig({ define: { 'import.meta.vitest': undefined, 'import.meta.env.SSR': false - }, - optimizeDeps: { - include: ['svelte'] } });