diff --git a/.changeset/visible-carousel-slides.md b/.changeset/visible-carousel-slides.md
new file mode 100644
index 000000000..62bccb5d2
--- /dev/null
+++ b/.changeset/visible-carousel-slides.md
@@ -0,0 +1,9 @@
+---
+"fulldev-ui": minor
+---
+
+Restore the Embla-powered Carousel and shadcn-style composition, responsive slide widths and `opts` API. Use the vanilla `embla-carousel` 8.6.0 package without React. Other interactive components continue to use Data Slot directly.
+
+This replaces the single-slide Carousel introduced in 0.13.0. Reinstall the registry component. Replace `defaultIndex`, `drag` and `loop` with `opts={{ startIndex, watchDrag, loop }}`; dragging is enabled by default. Restore `CarouselApi`, `CarouselOptions` and `CarouselPlugin` types and `carousel:init` / `carousel:select` events. Listen to their `detail.api` for programmatic navigation instead of `carousel:set`. `CarouselContent` classes apply to the inner track again; use matching negative track margins and slide padding for spacing.
+
+Visible slide links stay interactive, and Embla handles focus scrolling, resizing and slide positions. Instances and event handlers are cleaned up before Astro page swaps. No observers override another library's accessibility state.
diff --git a/package.json b/package.json
index 57aceaf64..f52f997c3 100644
--- a/package.json
+++ b/package.json
@@ -75,7 +75,6 @@
"@astrojs/sitemap": "^3.7.3",
"@data-slot/accordion": "^1.0.2",
"@data-slot/alert-dialog": "^1.0.2",
- "@data-slot/carousel": "^1.0.2",
"@data-slot/collapsible": "^1.0.2",
"@data-slot/combobox": "^1.0.2",
"@data-slot/command": "^1.0.2",
@@ -114,7 +113,8 @@
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.2",
"tw-animate-css": "^1.4.0",
- "typescript": "^6.0.3"
+ "typescript": "^6.0.3",
+ "embla-carousel": "8.6.0"
},
"devDependencies": {
"@argos-ci/playwright": "^7.6.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7f988a8e8..2e614ba2d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -20,9 +20,6 @@ importers:
'@data-slot/alert-dialog':
specifier: ^1.0.2
version: 1.0.2
- '@data-slot/carousel':
- specifier: ^1.0.2
- version: 1.0.2
'@data-slot/collapsible':
specifier: ^1.0.2
version: 1.0.2
@@ -116,6 +113,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
+ embla-carousel:
+ specifier: 8.6.0
+ version: 8.6.0
lucide-static:
specifier: ^1.22.0
version: 1.34.0
@@ -462,9 +462,6 @@ packages:
'@data-slot/alert-dialog@1.0.2':
resolution: {integrity: sha512-muBiDNPYF3EXvNTC0nMK3qff95vaPdndGARaHsYFKx0tUI++6wkncIOPD0/PbSFng4wpIssVf4Ila7+VeXCxzg==}
- '@data-slot/carousel@1.0.2':
- resolution: {integrity: sha512-6ElknTlUM8rKp0sk5TN7gamQgXtmQo/9DIpAChOUXsmwa9Rp27sNO2N9OxMV+FSa3qfL7mImt7idJbYNZHe82g==}
-
'@data-slot/collapsible@1.0.2':
resolution: {integrity: sha512-aqBQpbbZBfA+1dQJ5MMMpd17pTCdiphqG77KbAuyctVCIUcT0QCtJ8XT/aw2mOrlAzFPuuTYuvQNhqauRyM+9Q==}
@@ -834,243 +831,287 @@ packages:
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-arm64@1.2.4':
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-arm64@1.3.2':
resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-arm@1.0.5':
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-arm@1.3.2':
resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.3.2':
resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.3.2':
resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.0.4':
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.3.2':
resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-x64@1.0.4':
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-x64@1.3.2':
resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-libvips-linuxmusl-arm64@1.3.2':
resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.3.2':
resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-linux-arm64@0.33.5':
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-arm64@0.35.3':
resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-arm@0.33.5':
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-arm@0.35.3':
resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==}
engines: {node: '>=20.9.0'}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-ppc64@0.35.3':
resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==}
engines: {node: '>=20.9.0'}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-riscv64@0.35.3':
resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==}
engines: {node: '>=20.9.0'}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-s390x@0.33.5':
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-s390x@0.35.3':
resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==}
engines: {node: '>=20.9.0'}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-x64@0.33.5':
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-x64@0.35.3':
resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.33.5':
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-linuxmusl-arm64@0.35.3':
resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-linuxmusl-x64@0.33.5':
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-linuxmusl-x64@0.35.3':
resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-wasm32@0.33.5':
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
@@ -1344,66 +1385,79 @@ packages:
resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.60.3':
resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==}
cpu: [arm]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.60.3':
resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.60.3':
resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.60.3':
resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==}
cpu: [loong64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.60.3':
resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==}
cpu: [loong64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.60.3':
resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.60.3':
resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==}
cpu: [ppc64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.60.3':
resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.60.3':
resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==}
cpu: [riscv64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.60.3':
resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.60.3':
resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.60.3':
resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-openbsd-x64@4.60.3':
resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==}
@@ -1511,24 +1565,28 @@ packages:
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.3.3':
resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.3.3':
resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.3.3':
resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.3.3':
resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==}
@@ -2141,6 +2199,9 @@ packages:
electron-to-chromium@1.5.349:
resolution: {integrity: sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==}
+ embla-carousel@8.6.0:
+ resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==}
+
emmet@2.4.11:
resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==}
@@ -2779,24 +2840,28 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [musl]
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
@@ -4863,10 +4928,6 @@ snapshots:
dependencies:
'@data-slot/core': 1.0.2
- '@data-slot/carousel@1.0.2':
- dependencies:
- '@data-slot/core': 1.0.2
-
'@data-slot/collapsible@1.0.2':
dependencies:
'@data-slot/core': 1.0.2
@@ -6480,6 +6541,8 @@ snapshots:
electron-to-chromium@1.5.349: {}
+ embla-carousel@8.6.0: {}
+
emmet@2.4.11:
dependencies:
'@emmetio/abbreviation': 2.3.3
diff --git a/public/r/carousel.json b/public/r/carousel.json
index 8586df5fd..6bb6b84aa 100644
--- a/public/r/carousel.json
+++ b/public/r/carousel.json
@@ -2,9 +2,9 @@
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "carousel",
"title": "Carousel",
- "description": "An accessible carousel built on native scrolling with Data Slot.",
+ "description": "A carousel with motion, swipe and responsive slides built using Embla.",
"dependencies": [
- "@data-slot/carousel@^1.0.2",
+ "embla-carousel@8.6.0",
"@lucide/astro"
],
"registryDependencies": [
@@ -13,17 +13,17 @@
"files": [
{
"path": "src/components/ui/carousel/carousel.astro",
- "content": "---\nimport type { HTMLAttributes } from \"astro/types\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype Props = HTMLAttributes<\"div\"> & {\n defaultIndex?: number\n orientation?: \"horizontal\" | \"vertical\"\n drag?: boolean\n loop?: boolean\n}\n\nconst {\n class: className,\n defaultIndex,\n orientation = \"horizontal\",\n drag,\n loop,\n ...props\n} = Astro.props\n---\n\n
\n \n
\n\n\n",
+ "content": "---\nimport type { HTMLAttributes } from \"astro/types\"\nimport type { EmblaOptionsType } from \"embla-carousel\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype Props = HTMLAttributes<\"div\"> & {\n opts?: EmblaOptionsType\n orientation?: \"horizontal\" | \"vertical\"\n}\n\nconst {\n class: className,\n opts,\n orientation = opts?.axis === \"y\" ? \"vertical\" : \"horizontal\",\n ...props\n} = Astro.props\n\nconst resolvedOpts: EmblaOptionsType = {\n ...opts,\n axis: orientation === \"horizontal\" ? \"x\" : \"y\",\n}\n---\n\n\n \n
\n\n\n",
"type": "registry:ui"
},
{
"path": "src/components/ui/carousel/carousel-content.astro",
- "content": "---\nimport type { HTMLAttributes } from \"astro/types\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype Props = HTMLAttributes<\"div\">\n\nconst { class: className, ...props } = Astro.props\n---\n\n\n \n
\n",
+ "content": "---\nimport type { HTMLAttributes } from \"astro/types\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype Props = HTMLAttributes<\"div\">\n\nconst { class: className, ...props } = Astro.props\n---\n\n\n",
"type": "registry:ui"
},
{
"path": "src/components/ui/carousel/carousel-item.astro",
- "content": "---\nimport type { HTMLAttributes } from \"astro/types\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype Props = HTMLAttributes<\"div\">\n\nconst { class: className, ...props } = Astro.props\n---\n\n\n \n
\n",
+ "content": "---\nimport type { HTMLAttributes } from \"astro/types\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype Props = HTMLAttributes<\"div\">\n\nconst { class: className, ...props } = Astro.props\n---\n\n\n \n
\n",
"type": "registry:ui"
},
{
@@ -38,7 +38,7 @@
},
{
"path": "src/components/ui/carousel/index.ts",
- "content": "export type { CarouselController, CarouselOptions } from \"@data-slot/carousel\"\n\nexport { default as Carousel } from \"./carousel.astro\"\nexport { default as CarouselContent } from \"./carousel-content.astro\"\nexport { default as CarouselItem } from \"./carousel-item.astro\"\nexport { default as CarouselPrevious } from \"./carousel-previous.astro\"\nexport { default as CarouselNext } from \"./carousel-next.astro\"\n",
+ "content": "export type {\n EmblaCarouselType as CarouselApi,\n EmblaOptionsType as CarouselOptions,\n EmblaPluginType as CarouselPlugin,\n} from \"embla-carousel\"\n\nexport { default as Carousel } from \"./carousel.astro\"\nexport { default as CarouselContent } from \"./carousel-content.astro\"\nexport { default as CarouselItem } from \"./carousel-item.astro\"\nexport { default as CarouselPrevious } from \"./carousel-previous.astro\"\nexport { default as CarouselNext } from \"./carousel-next.astro\"\n",
"type": "registry:ui"
}
],
diff --git a/public/r/registry.json b/public/r/registry.json
index 6ef55485b..2f97e82c8 100644
--- a/public/r/registry.json
+++ b/public/r/registry.json
@@ -1079,13 +1079,13 @@
"name": "carousel",
"type": "registry:ui",
"title": "Carousel",
- "description": "An accessible carousel built on native scrolling with Data Slot.",
+ "description": "A carousel with motion, swipe and responsive slides built using Embla.",
"categories": [
"ui"
],
"docs": "https://ui.full.dev/components/carousel/",
"dependencies": [
- "@data-slot/carousel@^1.0.2",
+ "embla-carousel@8.6.0",
"@lucide/astro"
],
"registryDependencies": [
diff --git a/public/r/reviews-3.json b/public/r/reviews-3.json
index cc38797dc..fd4c8ad71 100644
--- a/public/r/reviews-3.json
+++ b/public/r/reviews-3.json
@@ -15,7 +15,7 @@
"files": [
{
"path": "src/components/blocks/reviews-3.astro",
- "content": "---\nimport type { ImageMetadata } from \"astro\"\nimport type { HTMLAttributes } from \"astro/types\"\nimport QuoteIcon from \"@lucide/astro/icons/quote\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\"\nimport {\n Carousel,\n CarouselContent,\n CarouselItem,\n CarouselNext,\n CarouselPrevious,\n} from \"@/components/ui/carousel\"\nimport { Rating } from \"@/components/ui/rating\"\nimport {\n Section,\n SectionContainer,\n SectionDescription,\n SectionTitle,\n} from \"@/components/ui/section\"\n\ntype Props = HTMLAttributes<\"section\"> & {\n title?: string\n description?: string\n reviews: {\n rating: number\n quote: string\n image?: ImageMetadata\n initials: string\n name: string\n role: string\n }[]\n}\n\nconst { class: className, title, description, reviews, ...props } = Astro.props\n---\n\n\n {\n (title || description) && (\n \n {title && {title}}\n {description && (\n \n {description}\n \n )}\n \n )\n }\n \n \n \n {\n reviews.map((item) => (\n \n \n
\n
\n
\n {item.quote}\n
\n
\n
\n \n {item.initials}\n \n
\n
\n {item.name}\n
\n
{item.role}
\n
\n
\n
\n \n ))\n }\n \n \n \n \n \n\n",
+ "content": "---\nimport type { ImageMetadata } from \"astro\"\nimport type { HTMLAttributes } from \"astro/types\"\nimport QuoteIcon from \"@lucide/astro/icons/quote\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\"\nimport {\n Carousel,\n CarouselContent,\n CarouselItem,\n CarouselNext,\n CarouselPrevious,\n} from \"@/components/ui/carousel\"\nimport { Rating } from \"@/components/ui/rating\"\nimport {\n Section,\n SectionContainer,\n SectionDescription,\n SectionTitle,\n} from \"@/components/ui/section\"\n\ntype Props = HTMLAttributes<\"section\"> & {\n title?: string\n description?: string\n reviews: {\n rating: number\n quote: string\n image?: ImageMetadata\n initials: string\n name: string\n role: string\n }[]\n}\n\nconst { class: className, title, description, reviews, ...props } = Astro.props\n---\n\n\n {\n (title || description) && (\n \n {title && {title}}\n {description && (\n \n {description}\n \n )}\n \n )\n }\n \n \n \n {\n reviews.map((item) => (\n \n \n
\n
\n
\n {item.quote}\n
\n
\n
\n \n {item.initials}\n \n
\n
\n {item.name}\n
\n
{item.role}
\n
\n
\n
\n \n ))\n }\n \n \n \n \n \n\n",
"type": "registry:block"
}
],
diff --git a/registry.json b/registry.json
index 673508033..276e60e8a 100644
--- a/registry.json
+++ b/registry.json
@@ -981,10 +981,10 @@
"name": "carousel",
"type": "registry:ui",
"title": "Carousel",
- "description": "An accessible carousel built on native scrolling with Data Slot.",
+ "description": "A carousel with motion, swipe and responsive slides built using Embla.",
"categories": ["ui"],
"docs": "https://ui.full.dev/components/carousel/",
- "dependencies": ["@data-slot/carousel@^1.0.2", "@lucide/astro"],
+ "dependencies": ["embla-carousel@8.6.0", "@lucide/astro"],
"registryDependencies": ["@fulldev/button"],
"files": [
{
diff --git a/src/components/blocks/reviews-3.astro b/src/components/blocks/reviews-3.astro
index cdb54608e..65599dc3d 100644
--- a/src/components/blocks/reviews-3.astro
+++ b/src/components/blocks/reviews-3.astro
@@ -50,7 +50,10 @@ const { class: className, title, description, reviews, ...props } = Astro.props
)
}
-
+
{
reviews.map((item) => (
diff --git a/src/components/ui/carousel/carousel-content.astro b/src/components/ui/carousel/carousel-content.astro
index 4bf30500a..a48c82f17 100644
--- a/src/components/ui/carousel/carousel-content.astro
+++ b/src/components/ui/carousel/carousel-content.astro
@@ -8,15 +8,17 @@ type Props = HTMLAttributes<"div">
const { class: className, ...props } = Astro.props
---
-
-
+
diff --git a/src/components/ui/carousel/carousel-item.astro b/src/components/ui/carousel/carousel-item.astro
index 2145af160..5fca8ab12 100644
--- a/src/components/ui/carousel/carousel-item.astro
+++ b/src/components/ui/carousel/carousel-item.astro
@@ -10,9 +10,12 @@ const { class: className, ...props } = Astro.props
& {
- defaultIndex?: number
+ opts?: EmblaOptionsType
orientation?: "horizontal" | "vertical"
- drag?: boolean
- loop?: boolean
}
const {
class: className,
- defaultIndex,
- orientation = "horizontal",
- drag,
- loop,
+ opts,
+ orientation = opts?.axis === "y" ? "vertical" : "horizontal",
...props
} = Astro.props
+
+const resolvedOpts: EmblaOptionsType = {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+}
---
diff --git a/src/components/ui/carousel/index.ts b/src/components/ui/carousel/index.ts
index 7e06994a2..0019f3ccf 100644
--- a/src/components/ui/carousel/index.ts
+++ b/src/components/ui/carousel/index.ts
@@ -1,4 +1,8 @@
-export type { CarouselController, CarouselOptions } from "@data-slot/carousel"
+export type {
+ EmblaCarouselType as CarouselApi,
+ EmblaOptionsType as CarouselOptions,
+ EmblaPluginType as CarouselPlugin,
+} from "embla-carousel"
export { default as Carousel } from "./carousel.astro"
export { default as CarouselContent } from "./carousel-content.astro"
diff --git a/src/content/pages/components/carousel.mdx b/src/content/pages/components/carousel.mdx
index 2f8ad463c..b979a2f54 100644
--- a/src/content/pages/components/carousel.mdx
+++ b/src/content/pages/components/carousel.mdx
@@ -1,7 +1,7 @@
---
type: doc
title: Carousel
-description: An accessible carousel built on native scrolling with Data Slot.
+description: A carousel with motion, swipe and responsive slides built using Embla.
---
```astro live props={{ name: 'carousel' }}
@@ -15,7 +15,7 @@ import {
} from "@/components/ui/carousel"
---
-
+
+
+
```
-## Events
+## Events and API
-Listen for `carousel:change` on the root to react to a new active slide.
+Listen for `carousel:init` to receive the Embla API. `carousel:select` fires after
+selection or reinitialization. Both events expose `detail.api`, `canScrollPrev`,
+`canScrollNext`, `selectedScrollSnap` and `scrollSnapList`.
```ts
-const carousel = document.querySelector('[data-slot="carousel"]')
+import type { CarouselApi } from "@/components/ui/carousel"
-carousel?.addEventListener("carousel:change", (event) => {
- const { index } = (event as CustomEvent<{ index: number }>).detail
- console.log(index)
+const carousel = document.querySelector('[data-slot="carousel"]')
+carousel?.addEventListener("carousel:init", (event) => {
+ const { api } = (event as CustomEvent<{ api: CarouselApi }>).detail
+ api.scrollTo(2)
})
```
-Dispatch `carousel:set` on the root to select an index or move by one slide.
+Register the listener before initialization. For code running later, the root's
+`__fulldevCarouselApi` property holds the current API. `CarouselOptions` and
+`CarouselPlugin` re-export Embla's corresponding types. Plugin instances are
+configured through Embla's API, not serialized Astro props.
-```ts
-carousel?.dispatchEvent(
- new CustomEvent("carousel:set", { detail: { index: 2 } })
-)
+Instances are destroyed before Astro page swaps and recreated on page load.
+Visible links retain normal focus behavior; focusing an offscreen slide lets
+Embla bring it into view. No custom `inert` or `aria-hidden` overrides are added.
+
+## Multiple visible slides
+
+Use CSS widths just as in shadcn/ui. This example shows three clickable slides
+at once, with extra slides reachable through dragging, buttons or keyboard.
-carousel?.dispatchEvent(
- new CustomEvent("carousel:set", { detail: { action: "next" } })
-)
+```astro live props={{ name: 'carousel' }}
+---
+import {
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselNext,
+ CarouselPrevious,
+} from "@/components/ui/carousel"
+---
+
+
+
+ {
+ [1, 2, 3, 4, 5].map((number) => (
+
+
+ Slide {number}
+
+
+ ))
+ }
+
+
+
+
```
-The root exposes its current index as `data-index`. Each item exposes
-`data-state="active"` or `data-state="inactive"`. Data Slot also keeps the
-previous and next controls' `disabled` and `aria-disabled` states in sync.
+## Migration from 0.13
+
+Reinstall the registry component so markup and runtime stay together.
-## API Reference
+| 0.13 | Embla carousel |
+| ---------------------------------- | -------------------------------------------------------------- |
+| `defaultIndex={1}` | `opts={{ startIndex: 1 }}` |
+| `loop` | `opts={{ loop: true }}` |
+| `drag` | Enabled by default; disable with `opts={{ watchDrag: false }}` |
+| `CarouselController` | `CarouselApi` |
+| `carousel:change` | `carousel:select` |
+| `carousel:set` | Call methods on `detail.api` from `carousel:init` |
+| Content classes style the viewport | Content classes style the inner track |
-See the [GitHub source code](https://github.com/fulldotdev/ui/tree/main/src/components/ui/carousel)
-for component props and the
-[@data-slot/carousel README](https://github.com/bejamas/data-slot/tree/main/packages/carousel)
-for controller methods, events, keyboard behavior, and accessibility details.
+The former one-full-width-slide restriction no longer applies. Use matching
+negative margins on content and padding on items for spacing, like shadcn/ui.
diff --git a/tests/data-slot.spec.ts b/tests/data-slot.spec.ts
index 842a25833..91d586829 100644
--- a/tests/data-slot.spec.ts
+++ b/tests/data-slot.spec.ts
@@ -515,7 +515,7 @@ test("resizable: pointer, keyboard bounds and collapsible panel", async ({
await expect(outerHandle).toHaveAttribute("aria-valuenow", "35")
})
-test("carousel: controls, full-slide state and keyboard navigation", async ({
+test("carousel: controls, drag, loop, vertical and keyboard navigation", async ({
page,
}) => {
await open(page, "carousel")
@@ -525,15 +525,12 @@ test("carousel: controls, full-slide state and keyboard navigation", async ({
const previous = root.getByRole("button", { name: "Previous slide" })
const next = root.getByRole("button", { name: "Next slide" })
- await expect(root).toHaveAttribute("data-index", "0")
+ await expect(root).toHaveAttribute("data-selected-scroll-snap", "0")
await expect(previous).toBeDisabled()
- await expect(items.nth(0)).toHaveAttribute("data-state", "active")
- await expect(items.nth(1)).toHaveAttribute("aria-hidden", "true")
- await expect(items.nth(1)).toHaveAttribute("inert", "")
const contentBox = await content.boundingBox()
const itemBox = await items.nth(0).boundingBox()
if (!contentBox || !itemBox) throw new Error("Carousel has no layout box")
- expect(Math.abs(contentBox.width - itemBox.width)).toBeLessThanOrEqual(1)
+ expect(Math.abs(contentBox.width + 16 - itemBox.width)).toBeLessThanOrEqual(1)
await page.mouse.move(
contentBox.x + contentBox.width * 0.75,
@@ -546,40 +543,37 @@ test("carousel: controls, full-slide state and keyboard navigation", async ({
{ steps: 6 }
)
await page.mouse.up()
- await expect(root).toHaveAttribute("data-index", "1")
+ await expect(root).toHaveAttribute("data-selected-scroll-snap", "1")
await previous.click()
- await expect(root).toHaveAttribute("data-index", "0")
+ await expect(root).toHaveAttribute("data-selected-scroll-snap", "0")
await next.click()
- await expect(root).toHaveAttribute("data-index", "1")
- await expect(items.nth(1)).toHaveAttribute("data-state", "active")
+ await expect(root).toHaveAttribute("data-selected-scroll-snap", "1")
await previous.click()
- await expect(root).toHaveAttribute("data-index", "0")
+ await expect(root).toHaveAttribute("data-selected-scroll-snap", "0")
await next.focus()
await next.press("ArrowRight")
- await expect(root).toHaveAttribute("data-index", "1")
+ await expect(root).toHaveAttribute("data-selected-scroll-snap", "1")
await next.press("End")
- await expect(root).toHaveAttribute("data-index", "2")
+ await expect(root).toHaveAttribute("data-selected-scroll-snap", "2")
await expect(next).toBeDisabled()
await previous.press("Home")
- await expect(root).toHaveAttribute("data-index", "0")
+ await expect(root).toHaveAttribute("data-selected-scroll-snap", "0")
const loopRoot = page
.locator(".live-code-layout")
.nth(1)
.locator('[data-slot="carousel"]')
- const loopItems = loopRoot.locator('[data-slot="carousel-item"]')
const loopPrevious = loopRoot.getByRole("button", { name: "Previous slide" })
const loopNext = loopRoot.getByRole("button", { name: "Next slide" })
- await expect(loopRoot).toHaveAttribute("data-index", "1")
- await expect(loopItems.nth(1)).toHaveAttribute("data-state", "active")
+ await expect(loopRoot).toHaveAttribute("data-selected-scroll-snap", "1")
await loopPrevious.click()
- await expect(loopRoot).toHaveAttribute("data-index", "0")
+ await expect(loopRoot).toHaveAttribute("data-selected-scroll-snap", "0")
await loopPrevious.click()
- await expect(loopRoot).toHaveAttribute("data-index", "2")
+ await expect(loopRoot).toHaveAttribute("data-selected-scroll-snap", "2")
await loopNext.click()
- await expect(loopRoot).toHaveAttribute("data-index", "0")
+ await expect(loopRoot).toHaveAttribute("data-selected-scroll-snap", "0")
const verticalRoot = page
.locator(".live-code-layout")
@@ -594,7 +588,9 @@ test("carousel: controls, full-slide state and keyboard navigation", async ({
name: "Previous slide",
})
await verticalContent.scrollIntoViewIfNeeded()
- const verticalContentBox = await verticalContent.boundingBox()
+ const verticalContentBox = await verticalRoot
+ .locator('[data-slot="carousel-container"]')
+ .boundingBox()
const verticalItemBox = await verticalItems.nth(0).boundingBox()
if (!verticalContentBox || !verticalItemBox)
throw new Error("Vertical carousel has no layout box")
@@ -602,17 +598,37 @@ test("carousel: controls, full-slide state and keyboard navigation", async ({
Math.abs(verticalContentBox.height - verticalItemBox.height)
).toBeLessThanOrEqual(1)
await verticalNext.click()
- await expect(verticalRoot).toHaveAttribute("data-index", "1")
+ await expect(verticalRoot).toHaveAttribute("data-selected-scroll-snap", "1")
await verticalPrevious.click()
- await expect(verticalRoot).toHaveAttribute("data-index", "0")
+ await expect(verticalRoot).toHaveAttribute("data-selected-scroll-snap", "0")
await expect
- .poll(() => verticalContent.evaluate((element) => element.scrollTop))
+ .poll(() =>
+ verticalItems
+ .nth(0)
+ .evaluate(
+ (element) =>
+ -element.getBoundingClientRect().top +
+ element
+ .closest('[data-slot="carousel-content"]')!
+ .getBoundingClientRect().top
+ )
+ )
.toBeLessThan(verticalContentBox.height * 0.2)
await verticalNext.focus()
await verticalNext.press("ArrowDown")
- await expect(verticalRoot).toHaveAttribute("data-index", "1")
+ await expect(verticalRoot).toHaveAttribute("data-selected-scroll-snap", "1")
await expect
- .poll(() => verticalContent.evaluate((element) => element.scrollTop))
+ .poll(() =>
+ verticalItems
+ .nth(0)
+ .evaluate(
+ (element) =>
+ -element.getBoundingClientRect().top +
+ element
+ .closest('[data-slot="carousel-content"]')!
+ .getBoundingClientRect().top
+ )
+ )
.toBeGreaterThan(verticalContentBox.height * 0.8)
const verticalDragBox = await verticalContent.boundingBox()
if (!verticalDragBox) throw new Error("Vertical carousel moved out of view")
@@ -627,7 +643,7 @@ test("carousel: controls, full-slide state and keyboard navigation", async ({
{ steps: 6 }
)
await page.mouse.up()
- await expect(verticalRoot).toHaveAttribute("data-index", "2")
+ await expect(verticalRoot).toHaveAttribute("data-selected-scroll-snap", "2")
})
test("command: new trigger slot, nested command filtering and dismissal", async ({
@@ -694,3 +710,65 @@ test("sidebar: mobile nested search, focus restoration and navigation reinitiali
).toBeVisible()
}
})
+
+test("carousel: visible links, focus, end controls and responsive sizing", async ({
+ page,
+ isMobile,
+ browserName,
+}) => {
+ await open(page, "carousel")
+ const root = page.locator('[data-slot="carousel"][data-demo="multiple"]')
+ await root.scrollIntoViewIfNeeded()
+ const links = root.getByRole("link")
+ const second = links.nth(1)
+ if (isMobile) {
+ const box = await second.boundingBox()
+ if (!box) throw new Error("Visible slide has no layout box")
+ await page.touchscreen.tap(box.x + box.width / 2, box.y + box.height / 2)
+ } else await second.click()
+ await expect(page).toHaveURL(/#slide-2$/)
+ await second.focus()
+ await expect(second).toBeFocused()
+ await page.waitForTimeout(200)
+ await expect(second).toBeFocused()
+ await second.press(browserName === "webkit" ? "Alt+Tab" : "Tab")
+ await expect(links.nth(2)).toBeFocused()
+ await links.nth(2).press(browserName === "webkit" ? "Alt+Tab" : "Tab")
+ await expect(links.nth(3)).toBeFocused()
+ await expect(root).toHaveAttribute("data-can-scroll-prev", "true")
+ const next = root.getByRole("button", { name: "Next slide" })
+ await links.nth(3).press("End")
+ await expect(next).toBeDisabled()
+ await expect(links.nth(3)).toBeFocused()
+ await links.nth(3).press("Home")
+ await expect(
+ root.getByRole("button", { name: "Previous slide" })
+ ).toBeDisabled()
+ await page.setViewportSize({ width: 768, height: 1000 })
+ await expect(next).toBeEnabled()
+ await links.nth(1).focus()
+ await expect(links.nth(1)).toBeFocused()
+ await links.nth(1).press("Enter")
+ await expect(page).toHaveURL(/#slide-2$/)
+})
+
+test("carousel: Astro cleanup and reinitialization do not duplicate controls", async ({
+ page,
+}) => {
+ await open(page, "carousel")
+ const root = demo(page).locator('[data-slot="carousel"]')
+ for (let pass = 0; pass < 2; pass++) {
+ const cleaned = await root.evaluate((element) => {
+ document.dispatchEvent(new Event("astro:before-swap"))
+ return !("__fulldevCarouselApi" in element)
+ })
+ expect(cleaned).toBe(true)
+ await page.evaluate(() => {
+ document.dispatchEvent(new Event("astro:page-load"))
+ document.dispatchEvent(new Event("astro:page-load"))
+ })
+ await expect(root).toHaveAttribute("data-selected-scroll-snap", "0")
+ await root.getByRole("button", { name: "Next slide" }).click()
+ await expect(root).toHaveAttribute("data-selected-scroll-snap", "1")
+ }
+})