mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-14 12:14:41 +01:00
feat(Stepper): new component (#2733)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
{
|
||||
title: 'Address',
|
||||
description: 'Add your address here',
|
||||
icon: 'i-lucide-house'
|
||||
}, {
|
||||
title: 'Shipping',
|
||||
description: 'Set your preferred shipping method',
|
||||
icon: 'i-lucide-truck'
|
||||
}, {
|
||||
title: 'Checkout',
|
||||
description: 'Confirm your order'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UStepper ref="stepper" :items="items" class="w-full">
|
||||
<template #content="{ item }">
|
||||
<Placeholder class="aspect-video">
|
||||
This is the {{ item?.title }} step.
|
||||
</Placeholder>
|
||||
</template>
|
||||
</UStepper>
|
||||
</template>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
{
|
||||
slot: 'address',
|
||||
title: 'Address',
|
||||
description: 'Add your address here',
|
||||
icon: 'i-lucide-house'
|
||||
}, {
|
||||
slot: 'shipping',
|
||||
title: 'Shipping',
|
||||
description: 'Set your preferred shipping method',
|
||||
icon: 'i-lucide-truck'
|
||||
}, {
|
||||
slot: 'checkout',
|
||||
title: 'Checkout',
|
||||
description: 'Confirm your order'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UStepper :items="items" class="w-full">
|
||||
<template #address>
|
||||
<Placeholder class="aspect-video">
|
||||
Address
|
||||
</Placeholder>
|
||||
</template>
|
||||
|
||||
<template #shipping>
|
||||
<Placeholder class="aspect-video">
|
||||
Shipping
|
||||
</Placeholder>
|
||||
</template>
|
||||
|
||||
<template #checkout>
|
||||
<Placeholder class="aspect-video">
|
||||
Checkout
|
||||
</Placeholder>
|
||||
</template>
|
||||
</UStepper>
|
||||
</template>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const items = [
|
||||
{
|
||||
title: 'Address',
|
||||
description: 'Add your address here',
|
||||
icon: 'i-lucide-house'
|
||||
}, {
|
||||
title: 'Shipping',
|
||||
description: 'Set your preferred shipping method',
|
||||
icon: 'i-lucide-truck'
|
||||
}, {
|
||||
title: 'Checkout',
|
||||
description: 'Confirm your order'
|
||||
}
|
||||
]
|
||||
|
||||
const active = ref(0)
|
||||
|
||||
// Note: This is for demonstration purposes only. Don't do this at home.
|
||||
onMounted(() => {
|
||||
setInterval(() => {
|
||||
active.value = (active.value + 1) % items.length
|
||||
}, 2000)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UStepper v-model="active" :items="items" class="w-full">
|
||||
<template #content="{ item }">
|
||||
<Placeholder class="aspect-video">
|
||||
This is the {{ item?.title }} step.
|
||||
</Placeholder>
|
||||
</template>
|
||||
</UStepper>
|
||||
</template>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
{
|
||||
slot: 'address',
|
||||
title: 'Address',
|
||||
description: 'Add your address here',
|
||||
icon: 'i-lucide-house'
|
||||
}, {
|
||||
slot: 'shipping',
|
||||
title: 'Shipping',
|
||||
description: 'Set your preferred shipping method',
|
||||
icon: 'i-lucide-truck'
|
||||
}, {
|
||||
slot: 'checkout',
|
||||
title: 'Checkout',
|
||||
description: 'Confirm your order'
|
||||
}
|
||||
]
|
||||
|
||||
const stepper = useTemplateRef('stepper')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<UStepper ref="stepper" :items="items">
|
||||
<template #content="{ item }">
|
||||
<Placeholder class="aspect-video">
|
||||
{{ item.title }}
|
||||
</Placeholder>
|
||||
</template>
|
||||
</UStepper>
|
||||
|
||||
<div class="flex gap-2 justify-between mt-4">
|
||||
<UButton
|
||||
leading-icon="i-lucide-arrow-left"
|
||||
:disabled="!stepper?.hasPrev"
|
||||
@click="stepper?.prev()"
|
||||
>
|
||||
Prev
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
trailing-icon="i-lucide-arrow-right"
|
||||
:disabled="!stepper?.hasNext"
|
||||
@click="stepper?.next()"
|
||||
>
|
||||
Next
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
232
docs/content/3.components/stepper.md
Normal file
232
docs/content/3.components/stepper.md
Normal file
@@ -0,0 +1,232 @@
|
||||
---
|
||||
description: A set of steps that are used to indicate progress through a multi-step process.
|
||||
links:
|
||||
- label: Stepper
|
||||
icon: i-custom-reka-ui
|
||||
to: https://reka-ui.com/docs/components/stepper
|
||||
- label: GitHub
|
||||
icon: i-simple-icons-github
|
||||
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/Stepper.vue
|
||||
navigation.badge: New
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Items
|
||||
|
||||
Use the `items` prop as an array of objects with the following properties:
|
||||
|
||||
- `title?: string`{lang="ts-type"}
|
||||
- `description?: AvatarProps`{lang="ts-type"}
|
||||
- `content?: string`{lang="ts-type"}
|
||||
- `icon?: string`{lang="ts-type"}
|
||||
- `value?: string | number`{lang="ts-type"}
|
||||
- `disabled?: boolean`{lang="ts-type"}
|
||||
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
|
||||
|
||||
::component-code
|
||||
---
|
||||
ignore:
|
||||
- items
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
props:
|
||||
items:
|
||||
- title: 'Address'
|
||||
description: 'Add your address here'
|
||||
icon: 'i-lucide-house'
|
||||
- title: 'Shipping'
|
||||
description: 'Set your preferred shipping method'
|
||||
icon: 'i-lucide-truck'
|
||||
- title: 'Checkout'
|
||||
description: 'Confirm your order'
|
||||
class: 'w-full'
|
||||
---
|
||||
::
|
||||
|
||||
::note
|
||||
Click on the items to navigate through the steps.
|
||||
::
|
||||
|
||||
### Color
|
||||
|
||||
Use the `color` prop to change the color of the Stepper.
|
||||
|
||||
::component-code
|
||||
---
|
||||
ignore:
|
||||
- content
|
||||
- items
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
props:
|
||||
color: neutral
|
||||
items:
|
||||
- title: 'Address'
|
||||
description: 'Add your address here'
|
||||
icon: 'i-lucide-house'
|
||||
- title: 'Shipping'
|
||||
description: 'Set your preferred shipping method'
|
||||
icon: 'i-lucide-truck'
|
||||
- title: 'Checkout'
|
||||
description: 'Confirm your order'
|
||||
class: 'w-full'
|
||||
---
|
||||
::
|
||||
|
||||
### Size
|
||||
|
||||
Use the `size` prop to change the size of the Stepper.
|
||||
|
||||
::component-code
|
||||
---
|
||||
ignore:
|
||||
- content
|
||||
- items
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
props:
|
||||
size: xl
|
||||
items:
|
||||
- title: 'Address'
|
||||
description: 'Add your address here'
|
||||
icon: 'i-lucide-house'
|
||||
- title: 'Shipping'
|
||||
description: 'Set your preferred shipping method'
|
||||
icon: 'i-lucide-truck'
|
||||
- title: 'Checkout'
|
||||
description: 'Confirm your order'
|
||||
class: 'w-full'
|
||||
---
|
||||
::
|
||||
|
||||
### Orientation
|
||||
|
||||
Use the `orientation` prop to change the orientation of the Stepper. Defaults to `horizontal`.
|
||||
|
||||
::component-code
|
||||
---
|
||||
ignore:
|
||||
- content
|
||||
- items
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
props:
|
||||
orientation: vertical
|
||||
items:
|
||||
- title: 'Address'
|
||||
description: 'Add your address here'
|
||||
icon: 'i-lucide-house'
|
||||
- title: 'Shipping'
|
||||
description: 'Set your preferred shipping method'
|
||||
icon: 'i-lucide-truck'
|
||||
- title: 'Checkout'
|
||||
description: 'Confirm your order'
|
||||
class: 'w-full'
|
||||
---
|
||||
::
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` prop to disable navigation through the steps.
|
||||
|
||||
::component-code
|
||||
---
|
||||
ignore:
|
||||
- content
|
||||
- items
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
props:
|
||||
disabled: true
|
||||
items:
|
||||
- title: 'Address'
|
||||
description: 'Add your address here'
|
||||
icon: 'i-lucide-house'
|
||||
- title: 'Shipping'
|
||||
description: 'Set your preferred shipping method'
|
||||
icon: 'i-lucide-truck'
|
||||
- title: 'Checkout'
|
||||
description: 'Confirm your order'
|
||||
---
|
||||
::
|
||||
|
||||
::note{to="#with-controls"}
|
||||
This can be useful when you want to force navigation with controls.
|
||||
::
|
||||
|
||||
## Examples
|
||||
|
||||
### With controls
|
||||
|
||||
You can add additional controls for the stepper using buttons.
|
||||
|
||||
:component-example{name="stepper-with-controls-example"}
|
||||
|
||||
### Control active item
|
||||
|
||||
You can control the active item by using the `default-value` prop or the `v-model` directive with the index of the item.
|
||||
|
||||
:component-example{name="stepper-model-value-example"}
|
||||
|
||||
::tip
|
||||
You can also pass the `value` of one of the items if provided.
|
||||
::
|
||||
|
||||
### With content slot
|
||||
|
||||
Use the `#content` slot to customize the content of each item.
|
||||
|
||||
:component-example{name="stepper-content-slot-example"}
|
||||
|
||||
### With custom slot
|
||||
|
||||
Use the `slot` property to customize a specific item.
|
||||
|
||||
:component-example{name="stepper-custom-slot-example"}
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
:component-props
|
||||
|
||||
### Slots
|
||||
|
||||
:component-slots
|
||||
|
||||
### Emits
|
||||
|
||||
:component-emits
|
||||
|
||||
### Expose
|
||||
|
||||
You can access the typed component instance using [`useTemplateRef`](https://vuejs.org/api/composition-api-helpers.html#usetemplateref).
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const stepper = useTemplateRef('stepper')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UStepper ref="stepper" />
|
||||
</template>
|
||||
```
|
||||
|
||||
This will give you access to the following:
|
||||
|
||||
| Name | Type |
|
||||
| ---- | ---- |
|
||||
| `next`{lang="ts-type"} | `() => void`{lang="ts-type"} |
|
||||
| `prev`{lang="ts-type"} | `() => void`{lang="ts-type"} |
|
||||
| `hasNext`{lang="ts-type"} | `Ref<boolean>`{lang="ts-type"} |
|
||||
| `hasPrev`{lang="ts-type"} | `Ref<boolean>`{lang="ts-type"} |
|
||||
|
||||
## Theme
|
||||
|
||||
:component-theme
|
||||
@@ -54,6 +54,7 @@ const components = [
|
||||
'skeleton',
|
||||
'slideover',
|
||||
'slider',
|
||||
'stepper',
|
||||
'switch',
|
||||
'tabs',
|
||||
'table',
|
||||
|
||||
96
playground/app/pages/components/stepper.vue
Normal file
96
playground/app/pages/components/stepper.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
import theme from '#build/ui/stepper'
|
||||
|
||||
const sizes = Object.keys(theme.variants.size)
|
||||
const colors = Object.keys(theme.variants.color)
|
||||
const orientations = Object.keys(theme.variants.orientation)
|
||||
|
||||
const orientation = ref('horizontal' as const)
|
||||
const color = ref('primary' as const)
|
||||
const size = ref('md' as const)
|
||||
|
||||
const items = [
|
||||
{
|
||||
slot: 'address',
|
||||
title: 'Address',
|
||||
description: 'Add your address here',
|
||||
icon: 'i-lucide-house'
|
||||
}, {
|
||||
slot: 'shipping',
|
||||
title: 'Shipping',
|
||||
description: 'Set your preferred shipping method',
|
||||
icon: 'i-lucide-truck'
|
||||
}, {
|
||||
slot: 'payment',
|
||||
title: 'Payment',
|
||||
description: 'Select your payment method',
|
||||
icon: 'i-lucide-credit-card'
|
||||
}, {
|
||||
slot: 'checkout',
|
||||
title: 'Checkout',
|
||||
description: 'Confirm your order'
|
||||
}
|
||||
]
|
||||
|
||||
const stepper = useTemplateRef('stepper')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-10">
|
||||
<div class="flex items-center justify-center gap-2 ">
|
||||
<USelect v-model="color" :items="colors" placeholder="Color" />
|
||||
<USelect v-model="orientation" :items="orientations" placeholder="Orientation" />
|
||||
<USelect v-model="size" :items="sizes" placeholder="Size" />
|
||||
</div>
|
||||
|
||||
<UStepper
|
||||
ref="stepper"
|
||||
:items="items"
|
||||
:color="color"
|
||||
:orientation="orientation"
|
||||
:size="size"
|
||||
>
|
||||
<template #address>
|
||||
<Placeholder class="size-full min-h-60 min-w-60">
|
||||
Address
|
||||
</Placeholder>
|
||||
</template>
|
||||
|
||||
<template #shipping>
|
||||
<Placeholder class="size-full min-h-60 min-w-60">
|
||||
Shipping
|
||||
</Placeholder>
|
||||
</template>
|
||||
|
||||
<template #payment>
|
||||
<Placeholder class="size-full min-h-60 min-w-60">
|
||||
Payment
|
||||
</Placeholder>
|
||||
</template>
|
||||
|
||||
<template #checkout>
|
||||
<Placeholder class="size-full min-h-60 min-w-60">
|
||||
Checkout
|
||||
</Placeholder>
|
||||
</template>
|
||||
</UStepper>
|
||||
|
||||
<div class="flex gap-2 justify-between">
|
||||
<UButton
|
||||
leading-icon="i-lucide-arrow-left"
|
||||
:disabled="!stepper?.hasPrev"
|
||||
@click="stepper?.prev()"
|
||||
>
|
||||
Prev
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
trailing-icon="i-lucide-arrow-right"
|
||||
:disabled="!stepper?.hasNext"
|
||||
@click="stepper?.next()"
|
||||
>
|
||||
Next
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
44
src/devtools/runtime/examples/StepperExample.vue
Normal file
44
src/devtools/runtime/examples/StepperExample.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const items = [
|
||||
{
|
||||
slot: 'address',
|
||||
title: 'Address',
|
||||
description: 'Add your address here',
|
||||
icon: 'i-lucide-house'
|
||||
}, {
|
||||
slot: 'shipping',
|
||||
title: 'Shipping',
|
||||
description: 'Set your preferred shipping method',
|
||||
icon: 'i-lucide-truck'
|
||||
}, {
|
||||
slot: 'checkout',
|
||||
title: 'Checkout',
|
||||
description: 'Confirm your order'
|
||||
}
|
||||
]
|
||||
|
||||
const stepper = ref()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UStepper
|
||||
ref="stepper"
|
||||
:items="items"
|
||||
>
|
||||
<template #content="{ item }">
|
||||
<Placeholder class="size-full min-h-60 min-w-60">
|
||||
{{ item.title }}
|
||||
</Placeholder>
|
||||
<div class="flex gap-2 justify-between mt-2">
|
||||
<UButton variant="outline" :disabled="!stepper?.hasPrevious" leading-icon="i-lucide-arrow-left" @click="stepper.previous()">
|
||||
Back
|
||||
</UButton>
|
||||
<UButton :disabled="!stepper?.hasNext" trailing-icon="i-lucide-arrow-right" @click="stepper.next()">
|
||||
Next
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UStepper>
|
||||
</template>
|
||||
170
src/runtime/components/Stepper.vue
Normal file
170
src/runtime/components/Stepper.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<script lang="ts">
|
||||
import { tv, type VariantProps } from 'tailwind-variants'
|
||||
import type { StepperRootProps, StepperRootEmits } from 'reka-ui'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/stepper'
|
||||
import type { DynamicSlots } from '../types/utils'
|
||||
|
||||
const appConfig = _appConfig as AppConfig & { ui: { stepper: Partial<typeof theme> } }
|
||||
|
||||
const stepper = tv({ extend: tv(theme), ...(appConfig.ui?.stepper || {}) })
|
||||
|
||||
type StepperVariants = VariantProps<typeof stepper>
|
||||
|
||||
export interface StepperItem {
|
||||
slot?: string
|
||||
value?: string
|
||||
title?: string
|
||||
description?: string
|
||||
icon?: string
|
||||
content?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export interface StepperProps<T extends StepperItem> extends Pick<StepperRootProps, 'linear'> {
|
||||
/**
|
||||
* The element or component this component should render as.
|
||||
* @defaultValue 'div'
|
||||
*/
|
||||
as?: any
|
||||
items: T[]
|
||||
size?: StepperVariants['size']
|
||||
color?: StepperVariants['color']
|
||||
orientation?: StepperVariants['orientation']
|
||||
/**
|
||||
* The value of the step that should be active when initially rendered. Use when you do not need to control the state of the steps.
|
||||
*/
|
||||
defaultValue?: string | number
|
||||
disabled?: boolean
|
||||
ui?: Partial<typeof stepper.slots>
|
||||
class?: any
|
||||
}
|
||||
|
||||
export type StepperEmits<T> = Omit<StepperRootEmits, 'update:modelValue'> & {
|
||||
next: [payload: T]
|
||||
prev: [payload: T]
|
||||
}
|
||||
|
||||
type SlotProps<T extends StepperItem> = (props: { item: T }) => any
|
||||
|
||||
export type StepperSlots<T extends StepperItem> = {
|
||||
indicator: SlotProps<T>
|
||||
title: SlotProps<T>
|
||||
description: SlotProps<T>
|
||||
content: SlotProps<T>
|
||||
} & DynamicSlots<T, SlotProps<T>>
|
||||
|
||||
extendDevtoolsMeta({ example: 'StepperExample' })
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends StepperItem">
|
||||
import { computed } from 'vue'
|
||||
import { StepperRoot, StepperItem, StepperTrigger, StepperIndicator, StepperSeparator, StepperTitle, StepperDescription, useForwardProps } from 'reka-ui'
|
||||
import { reactivePick } from '@vueuse/core'
|
||||
import UIcon from './Icon.vue'
|
||||
|
||||
const props = withDefaults(defineProps<StepperProps<T>>(), {
|
||||
linear: true
|
||||
})
|
||||
const emits = defineEmits<StepperEmits<T>>()
|
||||
const slots = defineSlots<StepperSlots<T>>()
|
||||
|
||||
const modelValue = defineModel<string | number>()
|
||||
|
||||
const rootProps = useForwardProps(reactivePick(props, 'as', 'orientation', 'linear'))
|
||||
|
||||
const ui = computed(() => stepper({
|
||||
orientation: props.orientation,
|
||||
size: props.size,
|
||||
color: props.color
|
||||
}))
|
||||
|
||||
const currentStepIndex = computed({
|
||||
get() {
|
||||
const value = modelValue.value ?? props.defaultValue
|
||||
|
||||
return ((typeof value === 'string')
|
||||
? props.items.findIndex(item => item.value === value)
|
||||
: value) ?? 0
|
||||
},
|
||||
set(value: number) {
|
||||
modelValue.value = props.items?.[value]?.value ?? value
|
||||
}
|
||||
})
|
||||
|
||||
const currentStep = computed(() => props.items?.[currentStepIndex.value] as T)
|
||||
const hasNext = computed(() => currentStepIndex.value < props.items?.length - 1)
|
||||
const hasPrev = computed(() => currentStepIndex.value > 0)
|
||||
|
||||
defineExpose({
|
||||
next() {
|
||||
if (hasNext.value) {
|
||||
currentStepIndex.value += 1
|
||||
emits('next', currentStep.value)
|
||||
}
|
||||
},
|
||||
prev() {
|
||||
if (hasPrev.value) {
|
||||
currentStepIndex.value -= 1
|
||||
emits('prev', currentStep.value)
|
||||
}
|
||||
},
|
||||
hasNext,
|
||||
hasPrev
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<StepperRoot v-bind="rootProps" v-model="currentStepIndex" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<div :class="ui.header({ class: props.ui?.header })">
|
||||
<StepperItem
|
||||
v-for="(item, count) in items"
|
||||
:key="item.value ?? count"
|
||||
:step="count"
|
||||
:disabled="item.disabled || props.disabled"
|
||||
:class="ui.item({ class: props.ui?.item })"
|
||||
>
|
||||
<div :class="ui.container({ class: props.ui?.container })">
|
||||
<StepperTrigger :class="ui.trigger({ class: props.ui?.trigger })">
|
||||
<StepperIndicator :class="ui.indicator({ class: props.ui?.indicator })">
|
||||
<slot name="indicator" :item="item">
|
||||
<UIcon v-if="item.icon" :name="item.icon" :class="ui.icon({ class: props.ui?.indicator })" />
|
||||
<template v-else>
|
||||
{{ count + 1 }}
|
||||
</template>
|
||||
</slot>
|
||||
</StepperIndicator>
|
||||
</StepperTrigger>
|
||||
|
||||
<StepperSeparator
|
||||
v-if="count < items.length - 1"
|
||||
:class="ui.separator({ class: props.ui?.separator })"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div :class="ui.wrapper({ class: props.ui?.wrapper })">
|
||||
<StepperTitle :class="ui.title({ class: props.ui?.title })">
|
||||
<slot name="title" :item="currentStep">
|
||||
{{ item.title }}
|
||||
</slot>
|
||||
</StepperTitle>
|
||||
<StepperDescription :class="ui.description({ class: props.ui?.description })">
|
||||
<slot name="description" :item="currentStep">
|
||||
{{ item.description }}
|
||||
</slot>
|
||||
</StepperDescription>
|
||||
</div>
|
||||
</StepperItem>
|
||||
</div>
|
||||
|
||||
<div v-if="currentStep?.content || !!slots.content || (currentStep?.slot && !!slots[currentStep.slot]) || (currentStep?.value && !!slots[currentStep.value])" :class="ui.content({ class: props.ui?.description })">
|
||||
<slot
|
||||
:name="!!slots[currentStep?.slot ?? currentStep.value] ? currentStep.slot ?? currentStep.value : 'content'"
|
||||
:item="currentStep"
|
||||
>
|
||||
{{ currentStep?.content }}
|
||||
</slot>
|
||||
</div>
|
||||
</StepperRoot>
|
||||
</template>
|
||||
@@ -37,6 +37,7 @@ export * from '../components/Separator.vue'
|
||||
export * from '../components/Skeleton.vue'
|
||||
export * from '../components/Slideover.vue'
|
||||
export * from '../components/Slider.vue'
|
||||
export * from '../components/Stepper.vue'
|
||||
export * from '../components/Switch.vue'
|
||||
export * from '../components/Table.vue'
|
||||
export * from '../components/Tabs.vue'
|
||||
|
||||
@@ -37,6 +37,7 @@ export { default as separator } from './separator'
|
||||
export { default as skeleton } from './skeleton'
|
||||
export { default as slideover } from './slideover'
|
||||
export { default as slider } from './slider'
|
||||
export { default as stepper } from './stepper'
|
||||
export { default as switch } from './switch'
|
||||
export { default as table } from './table'
|
||||
export { default as tabs } from './tabs'
|
||||
|
||||
131
src/theme/stepper.ts
Normal file
131
src/theme/stepper.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import type { ModuleOptions } from '../module'
|
||||
|
||||
export default (options: Required<ModuleOptions>) => ({
|
||||
slots: {
|
||||
root: 'flex gap-4',
|
||||
header: 'flex',
|
||||
item: 'group text-center relative w-full',
|
||||
container: 'relative',
|
||||
trigger: 'rounded-full font-medium text-center align-middle flex items-center justify-center font-semibold group-data-[state=completed]:text-[var(--ui-bg)] group-data-[state=active]:text-[var(--ui-bg)] text-[var(--ui-text-muted)] bg-[var(--ui-bg-elevated)] focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
indicator: 'flex items-center justify-center size-full',
|
||||
icon: 'shrink-0',
|
||||
separator: 'absolute rounded-full group-data-[disabled]:opacity-75 bg-[var(--ui-border-accented)]',
|
||||
wrapper: '',
|
||||
title: 'font-medium text-[var(--ui-text)]',
|
||||
description: 'text-[var(--ui-text-muted)] text-wrap',
|
||||
content: 'size-full'
|
||||
},
|
||||
|
||||
variants: {
|
||||
orientation: {
|
||||
horizontal: {
|
||||
root: 'flex-col',
|
||||
container: 'flex justify-center',
|
||||
separator: 'top-[calc(50%-2px)] h-0.5',
|
||||
wrapper: 'mt-1'
|
||||
},
|
||||
vertical: {
|
||||
header: 'flex-col gap-4',
|
||||
item: 'flex text-left',
|
||||
separator: 'left-[calc(50%-1px)] -bottom-[10px] w-0.5'
|
||||
}
|
||||
},
|
||||
|
||||
size: {
|
||||
xs: {
|
||||
trigger: 'size-6 text-xs',
|
||||
icon: 'size-3',
|
||||
title: 'text-xs',
|
||||
description: 'text-xs',
|
||||
wrapper: 'mt-1.5'
|
||||
},
|
||||
sm: {
|
||||
trigger: 'size-8 text-sm',
|
||||
icon: 'size-4',
|
||||
title: 'text-xs',
|
||||
description: 'text-xs',
|
||||
wrapper: 'mt-2'
|
||||
},
|
||||
md: {
|
||||
trigger: 'size-10 text-base',
|
||||
icon: 'size-5',
|
||||
title: 'text-sm',
|
||||
description: 'text-sm',
|
||||
wrapper: 'mt-2.5'
|
||||
},
|
||||
lg: {
|
||||
trigger: 'size-12 text-lg',
|
||||
icon: 'size-6',
|
||||
title: 'text-base',
|
||||
description: 'text-base',
|
||||
wrapper: 'mt-3'
|
||||
},
|
||||
xl: {
|
||||
trigger: 'size-14 text-xl',
|
||||
icon: 'size-7',
|
||||
title: 'text-lg',
|
||||
description: 'text-lg',
|
||||
wrapper: 'mt-3.5'
|
||||
}
|
||||
},
|
||||
|
||||
color: {
|
||||
...Object.fromEntries((options.theme.colors || []).map((color: string) => [color, {
|
||||
trigger: `group-data-[state=completed]:bg-[var(--ui-${color})] group-data-[state=active]:bg-[var(--ui-${color})] focus-visible:outline-[var(--ui-${color})]`,
|
||||
separator: `group-data-[state=completed]:bg-[var(--ui-${color})]`
|
||||
}])),
|
||||
neutral: {
|
||||
trigger: `group-data-[state=completed]:bg-[var(--ui-bg-inverted)] group-data-[state=active]:bg-[var(--ui-bg-inverted)] focus-visible:outline-[var(--ui-border-inverted)]`,
|
||||
separator: `group-data-[state=completed]:bg-[var(--ui-bg-inverted)]`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
compoundVariants: [{
|
||||
orientation: 'horizontal',
|
||||
size: 'xs',
|
||||
class: { separator: 'left-[calc(50%+16px)] right-[calc(-50%+16px)]' }
|
||||
}, {
|
||||
orientation: 'horizontal',
|
||||
size: 'sm',
|
||||
class: { separator: 'left-[calc(50%+20px)] right-[calc(-50%+20px)]' }
|
||||
}, {
|
||||
orientation: 'horizontal',
|
||||
size: 'md',
|
||||
class: { separator: 'left-[calc(50%+28px)] right-[calc(-50%+28px)]' }
|
||||
}, {
|
||||
orientation: 'horizontal',
|
||||
size: 'lg',
|
||||
class: { separator: 'left-[calc(50%+32px)] right-[calc(-50%+32px)]' }
|
||||
}, {
|
||||
orientation: 'horizontal',
|
||||
size: 'xl',
|
||||
class: { separator: 'left-[calc(50%+36px)] right-[calc(-50%+36px)]' }
|
||||
}, {
|
||||
orientation: 'vertical',
|
||||
size: 'xs',
|
||||
class: { separator: 'top-[30px]', item: 'gap-1.5' }
|
||||
}, {
|
||||
orientation: 'vertical',
|
||||
size: 'sm',
|
||||
class: { separator: 'top-[38px]', item: 'gap-2' }
|
||||
}, {
|
||||
orientation: 'vertical',
|
||||
size: 'md',
|
||||
class: { separator: 'top-[46px]', item: 'gap-2.5' }
|
||||
}, {
|
||||
orientation: 'vertical',
|
||||
size: 'lg',
|
||||
class: { separator: 'top-[54px]', item: 'gap-3' }
|
||||
}, {
|
||||
orientation: 'vertical',
|
||||
size: 'xl',
|
||||
class: { separator: 'top-[62px]', item: 'gap-3.5' }
|
||||
}],
|
||||
|
||||
defaultVariants: {
|
||||
orientation: 'horizontal',
|
||||
size: 'md',
|
||||
color: 'primary'
|
||||
}
|
||||
})
|
||||
50
test/components/Stepper.spec.ts
Normal file
50
test/components/Stepper.spec.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import Stepper, { type StepperProps, type StepperSlots } from '../../src/runtime/components/Stepper.vue'
|
||||
import ComponentRender from '../component-render'
|
||||
import theme from '#build/ui/stepper'
|
||||
|
||||
describe('Stepper', () => {
|
||||
const sizes = Object.keys(theme.variants.size) as any
|
||||
|
||||
const items = [
|
||||
{
|
||||
title: 'Address',
|
||||
description: 'Add your address here',
|
||||
icon: 'i-lucide-house'
|
||||
}, {
|
||||
title: 'Shipping',
|
||||
description: 'Set your preferred shipping method',
|
||||
icon: 'i-lucide-truck'
|
||||
}, {
|
||||
slot: 'custom',
|
||||
title: 'Checkout',
|
||||
description: 'Confirm your order'
|
||||
}
|
||||
]
|
||||
|
||||
const props = { items }
|
||||
|
||||
it.each([
|
||||
// Props
|
||||
['with items', { props }],
|
||||
['with defaultValue', { props: { ...props, defaultValue: 1 } }],
|
||||
['with modelValue', { props: { ...props, modelValue: 1 } }],
|
||||
['with neutral color', { props: { ...props, color: 'neutral' } }],
|
||||
...sizes.map((size: string) => [`with size ${size} horizontal`, { props: { ...props, size } }]),
|
||||
...sizes.map((size: string) => [`with size ${size} vertical`, { props: { ...props, size, orientation: 'vertical' } }]),
|
||||
['without linear', { props: { ...props, linear: false } }],
|
||||
['with as', { props: { ...props, as: 'section' } }],
|
||||
['with class', { props: { ...props, class: 'gap-8' } }],
|
||||
['with ui', { props: { ...props, ui: { title: 'font-bold' } } }],
|
||||
// Slots
|
||||
['with default slot', { props, slots: { default: () => 'Default slot' } }],
|
||||
['with indicator slot', { props, slots: { indicator: () => 'Indicator slot' } }],
|
||||
['with title slot', { props, slots: { title: () => 'Title slot' } }],
|
||||
['with description slot', { props, slots: { description: () => 'Description slot' } }],
|
||||
['with content slot', { props, slots: { content: () => 'Content slot' } }],
|
||||
['with custom slot', { props, slots: { custom: () => 'Custom slot' } }]
|
||||
])('renders %s correctly', async (nameOrHtml: string, options: { props?: StepperProps<any>, slots?: Partial<StepperSlots<any>> }) => {
|
||||
const html = await ComponentRender(nameOrHtml, options, Stepper)
|
||||
expect(html).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
1009
test/components/__snapshots__/Stepper-vue.spec.ts.snap
Normal file
1009
test/components/__snapshots__/Stepper-vue.spec.ts.snap
Normal file
File diff suppressed because it is too large
Load Diff
1009
test/components/__snapshots__/Stepper.spec.ts.snap
Normal file
1009
test/components/__snapshots__/Stepper.spec.ts.snap
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user