Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
92 changes: 86 additions & 6 deletions docs/content/docs/2.components/progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,46 @@ props:
---
::

### Variant

Use the `variant` prop to change the style of the Progress. Defaults to `linear`.

::component-code
---
ignore:
- class
external:
- modelValue
props:
modelValue: 50
variant: circular
class: 'justify-center'
---
::

::note
The `circular` variant is rendered as an SVG, so the `track` slot is used for the background circle instead of the `base` background.
::

### Thickness

Use the `thickness` prop to change the stroke width of the `circular` variant, in pixels. Defaults to `auto`, which derives the thickness from the [`size`](#size) prop.

::component-code
---
ignore:
- class
- variant
external:
- modelValue
props:
modelValue: 50
variant: circular
thickness: 4
class: 'justify-center'
---
::

### Max

Use the `max` prop to set the maximum value of the Progress.
Expand Down Expand Up @@ -60,43 +100,70 @@ props:
### Status

Use the `status` prop to display the current Progress value above the bar.
With the `circular` variant, the status is displayed in the center of the circle instead.

::component-code
---
ignore:
- class
external:
- modelValue
props:
modelValue: 50
variant: linear
status: true
class: 'justify-center'
---
::

Use the `#status` slot to customize the content, it receives the current `percent` value.

```vue
<template>
<UProgress v-model="value" variant="circular">
<template #status="{ percent }">
<span class="text-lg font-semibold text-highlighted">{{ percent }}%</span>
</template>
</UProgress>
</template>
```

### Indeterminate

When no `v-model` is set or the value is `null`, the Progress becomes _indeterminate_. The progress bar is animated as a `carousel`, but you can change it using the [`animation`](#animation) prop.
When no `v-model` is set or the value is `null`, the Progress becomes _indeterminate_. The progress bar is animated as a `carousel`, but you can change it using the [`animation`](#animation) prop. \
The `circular` variant is indeterminate under the same conditions, which makes it suitable as a loading spinner.

::component-code
---
ignore:
- class
external:
- modelValue
props:
modelValue: null
variant: linear
class: 'justify-center'
---
::

### Animation

Use the `animation` prop to change the animation of the Progress to an inverse carousel, a swinging bar or an elastic bar. Defaults to `carousel`.
Use the `animation` prop to change the animation of the Progress to an inverse carousel, a swinging bar or an elastic bar. Defaults to `carousel`. \
Each animation has a `circular` equivalent.

::component-code
---
ignore:
- class
props:
animation: swing
variant: linear
class: 'justify-center'
---
::

::tip
The animation is automatically disabled when the user prefers reduced motion, the indeterminate bar is displayed as a full width pulse instead.
With the `linear` variant, the animation is automatically disabled when the user prefers reduced motion, the indeterminate bar is displayed as a full width pulse instead.
::

### Orientation
Expand All @@ -113,37 +180,50 @@ props:
---
::

::note
The `circular` variant is always a circle, so the `orientation` prop only controls the axis along which the [steps](#max) are laid out next to it.
::

### Color

Use the `color` prop to change the color of the Slider.
Use the `color` prop to change the color of the Progress.

::component-code
---
props:
color: neutral
variant: linear
---
::

### Size

Use the `size` prop to change the size of the Slider.
Use the `size` prop to change the size of the Progress. With the `circular` variant, the `size` prop controls the diameter of the circle and the default stroke width, which you can override with the [`thickness`](#thickness) prop.

::component-code
---
ignore:
- class
props:
size: xl
variant: linear
class: 'justify-center'
---
::

### Inverted

Use the `inverted` prop to visually invert the Progress.
Use the `inverted` prop to visually invert the Progress. With the `circular` variant, this reverses the direction in which the circle is filled.

::component-code
---
ignore:
- class
props:
inverted: true
variant: linear
modelValue: 25
class: 'justify-center'
---
::

Expand Down
27 changes: 23 additions & 4 deletions playgrounds/nuxt/app/pages/components/progress.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const colors = Object.keys(theme.variants.color)
const sizes = Object.keys(theme.variants.size)
const animations = Object.keys(theme.variants.animation)
const orientations = Object.keys(theme.variants.orientation)
const variants = Object.keys(theme.variants.variant)

const attrs = reactive({
color: [theme.defaultVariants.color],
Expand All @@ -14,6 +15,7 @@ const attrs = reactive({
})

const orientation = ref('horizontal' as keyof typeof theme.variants.orientation)
const variant = ref('linear' as keyof typeof theme.variants.variant)

const value1 = ref(0)
const value2 = ref(0)
Expand Down Expand Up @@ -46,22 +48,39 @@ onMounted(() => {
<USelect v-model="attrs.size" :items="sizes" multiple />
<USelect v-model="attrs.animation" :items="animations" multiple />
<USelect v-model="orientation" :items="orientations" />
<USelect v-model="variant" :items="variants" />
</Navbar>

<Matrix
v-slot="props"
:attrs="attrs"
:container-props="{ 'data-orientation': orientation }"
container-class="gap-4 data-[orientation=horizontal]:w-48 data-[orientation=vertical]:h-48 data-[orientation=vertical]:flex-row"
:container-props="{ 'data-orientation': orientation, 'data-variant': variant }"
container-class="gap-4 data-[orientation=horizontal]:data-[variant=linear]:w-48 data-[orientation=vertical]:data-[variant=linear]:h-48 data-[orientation=vertical]:flex-row"
>
<UProgress :orientation="orientation" v-bind="props" />
<UProgress v-model="value2" :max="max" status :orientation="orientation" v-bind="props" />
<UProgress :orientation="orientation" :variant="variant" v-bind="props" inverted />
<UProgress
v-model="value2"
:max="max"
status
:orientation="orientation"
:variant="variant"
v-bind="props"
/>
<UProgress
v-model="value2"
:max="max"
status
inverted
:orientation="orientation"
:variant="variant"
v-bind="props"
/>
<UProgress
v-model="value2"
:max="max"
:thickness="5"
:orientation="orientation"
:variant="variant"
v-bind="props"
/>
</Matrix>
Expand Down
86 changes: 71 additions & 15 deletions src/runtime/components/Progress.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export interface ProgressProps extends Pick<ProgressRootProps, 'getValueLabel' |
*/
color?: Progress['variants']['color']
/**
* The orientation of the progress bar.
* The orientation of the progress bar. When `circular` variant is enabled, controls the axis along which the steps are lait out next to it
* @defaultValue 'horizontal'
*/
orientation?: Progress['variants']['orientation']
Expand All @@ -38,6 +38,17 @@ export interface ProgressProps extends Pick<ProgressRootProps, 'getValueLabel' |
* @defaultValue 'carousel'
*/
animation?: Progress['variants']['animation']
/**
* The progress bar variant
* @defaultValue 'linear'
*/
variant?: Progress['variants']['variant']
/**
* The thickness of the circular progress stroke in pixels, `auto` derives it from the `size` prop.
* Only applies to the `circular` variant.
* @defaultValue 'auto'
*/
thickness?: 'auto' | number
class?: any
ui?: Progress['slots']
}
Expand Down Expand Up @@ -65,7 +76,9 @@ import { tv } from '../utils/tv'
const _props = withDefaults(defineProps<ProgressProps>(), {
inverted: false,
modelValue: null,
orientation: 'horizontal'
orientation: 'horizontal',
variant: 'linear',
thickness: 'auto'
})
const emits = defineEmits<ProgressEmits>()
const slots = defineSlots<ProgressSlots>()
Expand Down Expand Up @@ -109,24 +122,44 @@ const indicatorStyle = computed(() => {
return
}

if (props.orientation === 'vertical') {
return {
transform: `translateY(${props.inverted ? '' : '-'}${100 - percent.value}%)`
}
} else {
if (dir.value === 'rtl') {
if (props.variant === 'linear') {
if (props.orientation === 'vertical') {
return {
transform: `translateX(${props.inverted ? '-' : ''}${100 - percent.value}%)`
transform: `translateY(${props.inverted ? '' : '-'}${100 - percent.value}%)`
}
} else {
return {
transform: `translateX(${props.inverted ? '' : '-'}${100 - percent.value}%)`
if (dir.value === 'rtl') {
return {
transform: `translateX(${props.inverted ? '-' : ''}${100 - percent.value}%)`
}
} else {
return {
transform: `translateX(${props.inverted ? '' : '-'}${100 - percent.value}%)`
}
}
}
} else {
const strokeDasharray = `${percent.value ?? 25}, 100`

return {
'stroke-dasharray': strokeDasharray
}
}
})

const thicknessStyle = computed(() => {
if (props.variant === 'linear') return

if (props.thickness === 'auto') return

return {
'--ui-progress-thickness': `${props.thickness}px`
}
})

const statusStyle = computed(() => {
if (props.variant === 'circular') return undefined

const value = `${Math.max(percent.value ?? 0, 0)}%`
return props.orientation === 'vertical' ? { height: value } : { width: value }
})
Expand Down Expand Up @@ -167,20 +200,43 @@ const ui = computed(() => tv({ extend: theme, ...(appConfig.ui?.progress || {})
size: props.size,
color: props.color,
orientation: props.orientation,
inverted: props.inverted
inverted: props.inverted,
variant: props.variant,
thickness: props.thickness === 'auto' ? 'auto' : undefined
}))
</script>

<template>
<Primitive :as="props.as" :data-orientation="props.orientation" data-slot="root" :class="ui.root({ class: [props.ui?.root, props.class] })">
<div v-if="!isIndeterminate && (props.status || !!slots.status)" data-slot="status" :class="ui.status({ class: props.ui?.status })" :style="statusStyle">
<div v-if="!isIndeterminate && (props.status || !!slots.status) && props.variant === 'linear'" data-slot="status" :class="ui.status({ class: props.ui?.status })" :style="statusStyle">
<slot name="status" :percent="percent">
{{ percent }}%
</slot>
</div>

<ProgressRoot v-bind="rootProps" :max="realMax" data-slot="base" :class="ui.base({ class: props.ui?.base })" style="transform: translateZ(0)">
<ProgressIndicator data-slot="indicator" :class="ui.indicator({ class: props.ui?.indicator })" :style="indicatorStyle" />
<ProgressRoot v-bind="rootProps" :max="realMax" data-slot="base" :class="ui.base({ class: props.ui?.base })" :style="['transform: translateZ(0)', thicknessStyle]">
<template v-if="props.variant === 'circular'">
<svg viewBox="0 0 100 100" data-slot="circle" :class="ui.circle({ class: props.ui?.circle })">
<circle cx="50" cy="50" data-slot="track" :class="ui.track({ class: props.ui?.track })" />
<ProgressIndicator as-child>
<circle
cx="50"
cy="50"
pathLength="100"
data-slot="indicator"
:class="ui.indicator({ class: props.ui?.indicator })"
:style="indicatorStyle"
/>
</ProgressIndicator>
</svg>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<div v-if="!isIndeterminate && (props.status || !!slots.status)" data-slot="status" :class="ui.status({ class: props.ui?.status })" :style="statusStyle">
<slot name="status" :percent="percent">
{{ percent }}%
</slot>
</div>
</template>

<ProgressIndicator v-else-if="props.variant === 'linear'" data-slot="indicator" :class="ui.indicator({ class: props.ui?.indicator })" :style="indicatorStyle" />
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</ProgressRoot>

<div v-if="hasSteps" data-slot="steps" :class="ui.steps({ class: props.ui?.steps })">
Expand Down
Loading
Loading