@@ -26,6 +26,7 @@ don't have to earn them the hard way.
2626- 🧱 Layouts with range — stacked or magazine-style aside (left * or* right), arrows wherever you want them
2727- 🎰 Slots for everything — custom headings, custom arrow icons, your markup wins
2828- 🎨 ~ 40 CSS variables to theme with, zero ` !important ` battles
29+ - 📐 SSR-safe responsive slide counts — ` --weburz-carousel-slides ` in a media query, no hydration snap
2930- 🧩 Auto-imported composables: ` useCarousel ` , ` useYouTubePlayer ` , ` useInstagramEmbed ` , ` useTikTokEmbed ` , ` useEmbedMetadata `
3031- 🟦 First-class TypeScript types
3132
@@ -132,9 +133,9 @@ Shared by all carousels:
132133| ---- | ---- | ------- | ----------- |
133134| ` options ` | ` EmblaOptionsType ` | ` {} ` | Embla carousel options |
134135| ` plugins ` | ` EmblaPluginType[] ` | ` [] ` | Embla plugins (autoplay, etc.) |
135- | ` slidesPerView ` | ` number ` | ` 1 ` | Slides visible at once |
136- | ` showArrows ` | ` boolean ` | ` true ` | Render prev/next arrows |
137- | ` showDots ` | ` boolean ` | ` true ` | Render dot pagination |
136+ | ` slidesPerView ` | ` number ` | ` 1 ` | Slides visible at once. For viewport-dependent counts prefer the ` --weburz-carousel-slides ` CSS variable — see [ Responsive slide counts ] ( #responsive-slide-counts-ssr-safe ) |
137+ | ` showArrows ` | ` boolean ` | ` true ` | Render prev/next arrows — auto-hidden when there's only one scroll position |
138+ | ` showDots ` | ` boolean ` | ` true ` | Render dot pagination — hidden when there's only one scroll position |
138139| ` arrowPosition ` | ` 'sides' \| 'below' ` | ` 'below' ` | Arrows flanking the dots (default) or beside the stage |
139140| ` layout ` | ` 'stacked' \| 'aside' ` | ` 'stacked' ` | ` 'aside' ` puts title, description, and nav in a side column next to the carousel (≥768px; stacks below that) |
140141| ` asidePosition ` | ` 'left' \| 'right' ` | ` 'left' ` | Which side the aside column sits on (only applies with ` layout="aside" ` ) |
@@ -248,6 +249,8 @@ styles:
248249 --weburz-carousel-accent : #00dc82 ;
249250 --weburz-carousel-gap : 1rem ; /* stack gap: heading / stage / nav */
250251 --weburz-carousel-slide-gap : 1rem ; /* gap between slides */
252+ /* --weburz-carousel-slides: 3; slides per view — wins over the slidesPerView
253+ prop; see Responsive slide counts below */
251254 /* Arrows are plain chevrons by default — add bg/border to get buttons back */
252255 --weburz-carousel-arrow-size : 2rem ;
253256 --weburz-carousel-arrow-font-size : 1.5rem ;
@@ -325,6 +328,41 @@ html.dark {
325328watcher. Arrows and dots default to ` currentColor ` , so even with zero mapping
326329they already match the surrounding text color.
327330
331+ ## Responsive slide counts (SSR-safe)
332+
333+ ` slidesPerView ` is a plain number, so driving it from JavaScript viewport
334+ detection (` useMediaQuery ` and friends) has an SSR problem: media queries
335+ can't run on the server, so the server guesses one count, the browser
336+ hydrates to another, and the first slide visibly snaps to its real width.
337+
338+ Set the count in CSS instead. The ` --weburz-carousel-slides ` variable takes
339+ precedence over the prop and lives in plain CSS, media queries included — the
340+ server-rendered HTML is correct for every viewport, no JavaScript involved:
341+
342+ ``` vue
343+ <BaseCarousel class="cards" :options="{ align: 'start' }">
344+ <BaseSlide v-for="card in cards" :key="card.id">…</BaseSlide>
345+ </BaseCarousel>
346+ ```
347+
348+ ``` css
349+ .cards {
350+ --weburz-carousel-slides : 1 ;
351+ }
352+
353+ @media (min-width : 48rem ) {
354+ .cards {
355+ --weburz-carousel-slides : 3 ;
356+ }
357+ }
358+ ```
359+
360+ This works on the platform carousels too — their slides live in the same
361+ cascade, so a class + media query on ` <TikTokCarousel> ` behaves identically.
362+
363+ Keep the prop for counts that are genuinely static (` :slides-per-view="2" ` )
364+ or genuinely dynamic at runtime — the variable, when set, always wins.
365+
328366## Module options
329367
330368| Option | Type | Default | Description |
@@ -351,59 +389,82 @@ export default defineNuxtConfig({
351389- ` useTikTokEmbed() ` — ` load ` / ` reload ` for TikTok's ` embed.js ` with the cache-bust trick needed after SPA navigation.
352390- ` useEmbedMetadata() ` — ` forYouTube(videoId) ` / ` forTikTok(url) ` fetch ` { title, authorName, thumbnailUrl } ` from the platforms' public oEmbed endpoints, cached per URL.
353391
354- ## Contribution
392+ ## Contributing
355393
356- <details >
357- <summary >Local development</summary >
394+ Contributions are welcome — bug reports, fixes, new platform quirk
395+ workarounds, docs. Day-to-day commands are wrapped in a
396+ [ Taskfile] ( https://taskfile.dev ) , so you don't have to memorize the pnpm
397+ script names.
358398
359- ``` bash
360- pnpm install
361- pnpm run dev:prepare # generate Nuxt type stubs
362- pnpm run dev # run the playground
363- pnpm run test
364- pnpm run lint
365- ```
399+ ### Prerequisites
366400
367- </details >
401+ - [ Node.js] ( https://nodejs.org/ ) ≥ 20
402+ - [ pnpm] ( https://pnpm.io/ ) 11 (` corepack enable ` or ` npm i -g pnpm ` )
403+ - [ Task] ( https://taskfile.dev/installation/ ) (` brew install go-task ` /
404+ ` scoop install task ` / see the install docs)
405+ - [ pre-commit] ( https://pre-commit.com/ ) — optional but recommended
406+ (` brew install pre-commit ` or ` pipx install pre-commit ` )
368407
369- <details >
370- <summary >Pre-commit hooks</summary >
408+ ### Getting started
371409
372- This repo uses [ pre-commit] ( https://pre-commit.com/ ) to enforce formatting,
373- lint staged files, and validate commit messages (conventional-commits style,
374- required by ` changelogen ` for the auto-generated [ CHANGELOG] ( ./CHANGELOG.md ) ).
410+ ``` bash
411+ git clone https://github.com/Weburz/carousel.git
412+ cd carousel
413+ task setup # pnpm install + generate Nuxt type stubs
414+ task setup:hooks # activate the pre-commit hooks (optional)
415+ task dev # run the playground at http://localhost:3000
416+ ```
375417
376- Install once per clone:
418+ The playground (` playground/ ` ) is a real Nuxt app with the module stubbed in —
419+ edit anything under ` src/ ` and it hot-reloads.
377420
378- ``` bash
379- # If you don't have pre-commit yet:
380- brew install pre-commit # macOS / Linuxbrew
381- # or: pipx install pre-commit
382- # or: pip install --user pre-commit
421+ ### Everyday tasks
383422
384- # Activate the hooks in this repo:
385- pnpm setup:hooks
386- ```
423+ Run ` task ` (no arguments) to see the full list. The ones you'll use most:
387424
388- Skip hooks for a single commit with ` git commit --no-verify ` (use sparingly).
425+ | Command | What it does |
426+ | ------- | ------------ |
427+ | ` task setup ` | Install dependencies and generate Nuxt type stubs (run once per clone) |
428+ | ` task dev ` | Run the playground with hot reload |
429+ | ` task test ` | Run the test suite once (` task test:watch ` for watch mode) |
430+ | ` task test:types ` | Type-check the module and the playground |
431+ | ` task lint ` | Lint everything (` task lint:fix ` to auto-fix) |
432+ | ` task check ` | Everything CI runs: lint, type-check, tests, build |
433+ | ` task build ` | Build the distributable module into ` dist/ ` |
434+ | ` task clean ` | Remove build artifacts (` task clean:all ` also nukes ` node_modules ` ) |
389435
390- </details >
436+ Before opening a PR, run ` task check ` — it mirrors the ` Code QA Checks `
437+ GitHub workflow, so if it passes locally, CI should too.
391438
392- <details >
393- <summary >Release flow</summary >
439+ ### Project layout
394440
395- Local one-liner that lints, tests, builds the module, bumps the version +
396- CHANGELOG via ` changelogen ` , publishes to npm, and pushes the tag:
441+ ```
442+ src/module.ts # Nuxt module entry — registers components & composables
443+ src/runtime/components/ # BaseCarousel, YouTubeCarousel, InstagramCarousel, TikTokCarousel, …
444+ src/runtime/composables/ # useCarousel, useYouTubePlayer, useEmbedMetadata, …
445+ src/runtime/types.ts # public TypeScript types
446+ playground/ # demo Nuxt app for local dev (deployed to GitHub Pages)
447+ test/ # Vitest + @nuxt/test-utils suite
448+ ```
449+
450+ ### Commit messages & hooks
451+
452+ Commits must follow
453+ [ Conventional Commits] ( https://www.conventionalcommits.org/ ) (` feat: … ` ,
454+ ` fix: … ` , ` chore: … ` ) — the CHANGELOG is auto-generated from them by
455+ ` changelogen ` . The pre-commit hooks (installed via ` task setup:hooks ` ) lint
456+ staged files and validate the commit message for you. Skip them for a single
457+ commit with ` git commit --no-verify ` (use sparingly).
397458
398- ``` bash
399- pnpm release
400- ```
459+ ### Releases (maintainers)
401460
402- The ` Deploy Playground ` workflow publishes ` playground/ ` to GitHub Pages on
403- every push to ` main ` . Enable Pages in the repository settings and set
404- ** Source ** to "GitHub Actions" to activate it.
461+ ``` bash
462+ task release
463+ ```
405464
406- </details >
465+ Lints, tests, builds the module, bumps the version + CHANGELOG via
466+ ` changelogen ` , publishes to npm, and pushes the tag. The ` Deploy Playground `
467+ workflow publishes ` playground/ ` to GitHub Pages on every push to ` main ` .
407468
408469## License
409470
0 commit comments