Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e780769
feat(Carousel): add headless carousel component with CSS scroll-snap
claude Apr 12, 2026
71baee5
fix(Carousel): add missing defineEmits for v-model consistency
claude Apr 12, 2026
78f67ea
fix(Carousel): drag support, autoplay, rename Slide to Item
johnleider Apr 13, 2026
778b101
fix(Carousel): review cleanup — imports, aria, locale, drag lifecycle
johnleider Apr 14, 2026
d535539
fix(Carousel): remove gap and peek props, measure layout from DOM
johnleider Apr 14, 2026
e9b098d
docs(Carousel): add gap between slides in basic example
johnleider Apr 14, 2026
66d54a9
fix(Carousel): add id prop, simplify unregister, remove redundant def…
johnleider Apr 14, 2026
0f95f0c
docs(Carousel): move autoplay to multi-slide example, use chevron ico…
johnleider Apr 14, 2026
05afcf0
fix(Carousel): expose pause, resume, remaining, isPaused in slot props
johnleider Apr 14, 2026
34c4d49
docs(Carousel): update autoplay FAQ to use built-in prop
johnleider Apr 14, 2026
4f1eaac
docs(Carousel): tweak import code style
johnleider Apr 14, 2026
e43fbf6
fix(Carousel): remove enroll and mandatory props, hardcode mandatory …
johnleider Apr 14, 2026
927a78e
fix(Carousel): use parts registry for sub-component registration
johnleider Apr 14, 2026
08b8cc1
feat(Carousel): add Indicator, Progress, and LiveRegion sub-components
johnleider Apr 14, 2026
9b5d4ac
fix(Carousel): add CarouselTicket with el, use registry for DOM measu…
johnleider Apr 14, 2026
3bd130e
fix(Carousel): rename rootRef to rootEl, fix LiveRegion timeout cleanup
johnleider Apr 14, 2026
c0a3ce6
docs(Carousel): remove baked-in cursor, add cursor classes to examples
johnleider Apr 14, 2026
e0ff07d
docs(Carousel): add JSDoc examples to all sub-component exports
johnleider Apr 14, 2026
0635992
fix(Carousel): add behavior prop, remove baked-in styles, clean up In…
johnleider Apr 14, 2026
9f51e20
docs(Carousel): add scrollbar-hide and select-none to viewport examples
johnleider Apr 14, 2026
38bda0a
docs(Carousel): update anatomy, accessibility table, and recipes for …
johnleider Apr 14, 2026
cf2fb2b
fix(Carousel): inspection round 1 — a11y, patterns, docs
johnleider Apr 14, 2026
ac57984
fix(Carousel): add aria-controls to Indicator tabs, id to Item slides
johnleider Apr 14, 2026
1a52144
docs(Carousel): add indicator example with overlaid navigation buttons
johnleider Apr 15, 2026
6ea7749
fix(Carousel): restore gap and peek props for structural layout
johnleider Apr 15, 2026
3119b39
Revert "fix(Carousel): restore gap and peek props for structural layout"
johnleider Apr 15, 2026
7616319
fix(Carousel): bake scrollbar-hide and user-select into Viewport styles
johnleider Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions .claude/rules/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,10 @@ ComponentName/

### Rules
- `defineOptions({ name: '...' })` — **always required** (100%)
- All imports go in `<script lang="ts">`, not `<script setup>`
- **ALL imports go in `<script lang="ts">`, NEVER in `<script setup>`** — this includes Vue imports (`ref`, `toRef`, `watch`, etc.), composable imports, and utility imports. `<script setup>` must contain zero import statements.
- Props interface and slot props interface exported from regular script
- Context `[useX, provideX]` exported from regular script
- Cleanup: use `onBeforeUnmount` for unregistration, not `onUnmounted`

## Context Provision Pattern

Expand Down Expand Up @@ -163,8 +164,9 @@ const slotProps = toRef((): ComponentRootSlotProps => ({
Every interactive component must have:
1. Correct `role` attribute
2. Relevant `aria-*` state attributes
3. `aria-disabled` when disabled
3. `aria-disabled` when disabled — always include as `boolean`, not `true | undefined`
4. Keyboard event handlers
5. All user-facing strings (`aria-label`, etc.) must use `useLocale()` and `locale.t()` — never hardcode English. Tests assert `toBeDefined()` for locale strings, not exact values.

## Disabled Pattern (100% enforced)

Expand Down Expand Up @@ -210,7 +212,7 @@ const [, , context] = createFooContext(options)
useProxyModel(context, model, { multiple })
```

**Do not** declare `defineEmits('update:model-value')` alongside `defineModel` — it's redundant.
`defineEmits('update:model-value')` is redundant alongside `defineModel`, but **include it anyway** — vue-devtools requires the explicit emit declaration for event tracking.

## Barrel Export Pattern (100% enforced)

Expand All @@ -234,3 +236,7 @@ export const Component = { Root: ComponentRoot, Item: ComponentItem }
- Slot props via `<slot v-bind="slotProps" />`
- Hidden inputs conditionally rendered: `<ComponentHiddenInput v-if="name" />`
- `v-if` for structural conditionals, never `v-show` (except Combobox filtered items)

## Slot Attrs Double-Fire Hazard

Slot `attrs` objects include `onClick` and other event handlers. These are already bound to the outer `<Atom>` wrapper via `mergeProps`. Consumers must **only** spread slot `attrs` onto their own element when using `renderless` mode. In non-renderless mode (default), spreading `attrs` onto a child element causes handlers to fire twice — once on the child, then again on the Atom wrapper via event bubbling. When writing examples for new components, never `v-bind="attrs"` on children inside a non-renderless component.
1 change: 1 addition & 0 deletions .claude/rules/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ Description paragraph.
- Import from `@vuetify/v0`
- Keep examples minimal and focused
- One concept per example file
- **Never `v-bind="attrs"` on children of non-renderless components** — slot `attrs` include `onClick` and other handlers already bound to the outer Atom wrapper. Spreading them onto an inner element causes double-firing (click on child fires handler, event bubbles to parent which fires it again). Only use slot `attrs` in `renderless` mode where there is no wrapper element.

## Markdown Directives

Expand Down
31 changes: 31 additions & 0 deletions apps/docs/src/examples/components/carousel/basic.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script setup lang="ts">
import { Carousel } from '@vuetify/v0'
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js'
</script>

<template>
<div class="flex flex-col gap-4">
<Carousel.Root>
<Carousel.Viewport class="rounded-lg overflow-hidden gap-4 cursor-grab data-[dragging]:cursor-grabbing">
<Carousel.Item
v-for="i in 5"
:key="i"
class="flex items-center justify-center h-48 rounded-lg text-lg font-medium bg-surface-variant text-on-surface-variant w-full shrink-0"
:value="i"
>
Slide {{ i }}
</Carousel.Item>
</Carousel.Viewport>

<div class="flex items-center justify-center gap-2 mt-3">
<Carousel.Previous class="p-2 rounded-full border border-divider hover:bg-surface-variant disabled:opacity-40">
<svg class="size-5" viewBox="0 0 24 24"><path :d="mdiChevronLeft" fill="currentColor" /></svg>
</Carousel.Previous>

<Carousel.Next class="p-2 rounded-full border border-divider hover:bg-surface-variant disabled:opacity-40">
<svg class="size-5" viewBox="0 0 24 24"><path :d="mdiChevronRight" fill="currentColor" /></svg>
</Carousel.Next>
</div>
</Carousel.Root>
</div>
</template>
38 changes: 38 additions & 0 deletions apps/docs/src/examples/components/carousel/indicator.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script setup lang="ts">
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js'
import { Carousel } from '@vuetify/v0'
</script>

<template>
<Carousel.Root>
<div class="relative">
<Carousel.Viewport class="rounded-lg overflow-hidden gap-4 cursor-grab data-[dragging]:cursor-grabbing">
<Carousel.Item
v-for="i in 5"
:key="i"
class="flex items-center justify-center h-48 rounded-lg text-lg font-medium bg-surface-variant text-on-surface-variant w-full shrink-0"
:value="i"
>
Slide {{ i }}
</Carousel.Item>
</Carousel.Viewport>

<Carousel.Previous class="absolute left-2 top-1/2 -translate-y-1/2 p-2 rounded-full bg-surface/80 hover:bg-surface disabled:opacity-0 transition-opacity">
<svg class="size-5" viewBox="0 0 24 24"><path :d="mdiChevronLeft" fill="currentColor" /></svg>
</Carousel.Previous>

<Carousel.Next class="absolute right-2 top-1/2 -translate-y-1/2 p-2 rounded-full bg-surface/80 hover:bg-surface disabled:opacity-0 transition-opacity">
<svg class="size-5" viewBox="0 0 24 24"><path :d="mdiChevronRight" fill="currentColor" /></svg>
</Carousel.Next>
</div>

<Carousel.Indicator v-slot="{ items }" class="flex gap-1.5 justify-center mt-3">
<button
v-for="item in items"
:key="item.index"
v-bind="item.attrs"
class="size-2 rounded-full bg-surface-variant transition-colors data-[selected]:bg-primary"
/>
</Carousel.Indicator>
</Carousel.Root>
</template>
55 changes: 55 additions & 0 deletions apps/docs/src/examples/components/carousel/multi-slide.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<script setup lang="ts">
import { Carousel, Switch } from '@vuetify/v0'

const items = [
{ id: 1, label: 'Design' },
{ id: 2, label: 'Develop' },
{ id: 3, label: 'Test' },
{ id: 4, label: 'Deploy' },
{ id: 5, label: 'Monitor' },
{ id: 6, label: 'Iterate' },
]
</script>

<template>
<Carousel.Root v-slot="{ isAutoplay, play, stop }" :autoplay="3000" circular :per-view="3">
<Carousel.Viewport class="rounded-lg gap-3 cursor-grab data-[dragging]:cursor-grabbing">
<Carousel.Item
v-for="item in items"
:key="item.id"
class="flex items-center justify-center h-32 rounded-lg text-sm font-medium bg-surface-variant text-on-surface-variant flex-[0_0_calc((100%-1.5rem)/3)]"
:value="item.id"
>
{{ item.label }}
</Carousel.Item>
</Carousel.Viewport>

<div class="flex items-center justify-center gap-2 mt-3">
<Carousel.Previous class="px-3 py-1.5 rounded-lg border border-divider text-sm hover:bg-surface-variant disabled:opacity-40">
Previous
</Carousel.Previous>

<Carousel.Next class="px-3 py-1.5 rounded-lg border border-divider text-sm hover:bg-surface-variant disabled:opacity-40">
Next
</Carousel.Next>
</div>

<label class="flex items-center justify-center gap-2 mt-3 cursor-pointer">
<Switch.Root
class="inline-flex items-center border-none bg-transparent p-0 outline-none"
:model-value="isAutoplay"
@update:model-value="$event ? play() : stop()"
>
<Switch.Track
class="relative inline-flex items-center w-11 h-6 rounded-full bg-surface-variant transition-colors data-[state=checked]:bg-primary"
>
<Switch.Thumb
class="![visibility:visible] block size-4 rounded-full bg-white shadow-sm transition-transform translate-x-1 data-[state=checked]:translate-x-6"
/>
</Switch.Track>
</Switch.Root>

<span class="text-sm">Autoplay</span>
</label>
</Carousel.Root>
</template>
26 changes: 26 additions & 0 deletions apps/docs/src/examples/components/carousel/peek.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script setup lang="ts">
import { Carousel } from '@vuetify/v0'

const slides = [
{ id: 1, label: 'Mountains', color: 'bg-sky-800 text-white' },
{ id: 2, label: 'Desert', color: 'bg-amber-700 text-white' },
{ id: 3, label: 'Forest', color: 'bg-emerald-800 text-white' },
{ id: 4, label: 'Ocean', color: 'bg-blue-900 text-white' },
]
</script>

<template>
<Carousel.Root :per-view="1">
<Carousel.Viewport class="rounded-lg gap-4 px-12 cursor-grab data-[dragging]:cursor-grabbing">
<Carousel.Item
v-for="slide in slides"
:key="slide.id"
class="flex items-center justify-center h-40 rounded-lg text-lg font-medium flex-[0_0_100%]"
:class="slide.color"
:value="slide.id"
>
{{ slide.label }}
</Carousel.Item>
</Carousel.Viewport>
</Carousel.Root>
</template>
1 change: 1 addition & 0 deletions apps/docs/src/pages/components/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ Components with meaningful HTML defaults. Render semantic elements by default bu
| - | - |
| [Avatar](/components/semantic/avatar) | Image/fallback avatar with priority loading |
| [Breadcrumbs](/components/semantic/breadcrumbs) | Navigation breadcrumbs with overflow detection and truncation |
| [Carousel](/components/semantic/carousel) | Scroll-snap slide navigation with multi-slide display and drag/swipe |
| [Pagination](/components/semantic/pagination) | Page navigation with semantic `<nav>` wrapper |
| [Progress](/components/semantic/progress/) | Headless progress bar with multi-segment and buffer support |
| [Snackbar](/components/semantic/snackbar) | Toast notification with queue, positioning, and auto-dismiss |
Expand Down
Loading
Loading