diff --git a/playground/app.vue b/playground/app.vue
index c2b16a52..548772be 100644
--- a/playground/app.vue
+++ b/playground/app.vue
@@ -36,6 +36,7 @@ const components = [
'navigation-menu',
'pagination',
'popover',
+ 'progress',
'radio-group',
'select',
'select-menu',
diff --git a/playground/pages/progress.vue b/playground/pages/progress.vue
new file mode 100644
index 00000000..088ede32
--- /dev/null
+++ b/playground/pages/progress.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/runtime/components/Progress.vue b/src/runtime/components/Progress.vue
new file mode 100644
index 00000000..87f7deb7
--- /dev/null
+++ b/src/runtime/components/Progress.vue
@@ -0,0 +1,152 @@
+
+
+
+
+
+
+
+
+ {{ percent }}%
+
+
+
+
+
+
+
+
+
+
diff --git a/src/runtime/types/index.d.ts b/src/runtime/types/index.d.ts
index 53a7f21f..ce0c28b7 100644
--- a/src/runtime/types/index.d.ts
+++ b/src/runtime/types/index.d.ts
@@ -26,6 +26,7 @@ export * from '../components/Modal.vue'
export * from '../components/NavigationMenu.vue'
export * from '../components/Pagination.vue'
export * from '../components/Popover.vue'
+export * from '../components/Progress.vue'
export * from '../components/RadioGroup.vue'
export * from '../components/Select.vue'
export * from '../components/SelectMenu.vue'
diff --git a/src/templates.ts b/src/templates.ts
index 0edef1c1..5511f4a1 100644
--- a/src/templates.ts
+++ b/src/templates.ts
@@ -152,6 +152,106 @@ export function addTemplates(options: ModuleOptions, nuxt: Nuxt) {
from { opacity: 1; transform: translateX(0); }
to { opacity: 0; transform: translateX(-200px); }
}
+ @keyframes carousel {
+ 0%,
+ 100% {
+ width: 50%
+ }
+ 0% {
+ transform: translateX(-100%)
+ }
+ 100% {
+ transform: translateX(200%)
+ }
+ }
+ @keyframes carousel-vertical {
+ 0%,
+ 100% {
+ height: 50%
+ }
+ 0% {
+ transform: translateY(-100%)
+ }
+ 100% {
+ transform: translateY(200%)
+ }
+ }
+
+ @keyframes carousel-inverse {
+ 0%,
+ 100% {
+ width: 50%
+ }
+ 0% {
+ transform: translateX(200%)
+ }
+ 100% {
+ transform: translateX(-100%)
+ }
+ }
+ @keyframes carousel-inverse-vertical {
+ 0%
+ 100% {
+ height: 50%
+ }
+ 0% {
+ transform: translateY(200%)
+ }
+ 100% {
+ transform: translateY(-100%)
+ }
+ }
+
+ @keyframes swing {
+ 0%,
+ 100% {
+ width: 50%
+ }
+ 0%,
+ 100% {
+ transform: translateX(-25%)
+ }
+ 50% {
+ transform: translateX(125%)
+ }
+ }
+ @keyframes swing-vertical {
+ 0%,
+ 100% {
+ height: 50%
+ }
+ 0%,
+ 100% {
+ transform: translateY(-25%)
+ }
+ 50% {
+ transform: translateY(125%)
+ }
+ }
+
+ @keyframes elastic {
+ /* Firefox doesn't do "margin: 0 auto", we have to play with margin-left */
+ 0%,
+ 100% {
+ width: 50%;
+ margin-left: 25%;
+ }
+ 50% {
+ width: 90%;
+ margin-left: 5%;
+ }
+ }
+ @keyframes elastic-vertical {
+ 0%,
+ 100% {
+ height: 50%;
+ margin-top: 25%;
+ }
+ 50% {
+ height: 90%;
+ margin-top: 5%;
+ }
+ }
}
@theme {
diff --git a/src/theme/index.ts b/src/theme/index.ts
index 6c389bad..2354152c 100644
--- a/src/theme/index.ts
+++ b/src/theme/index.ts
@@ -25,6 +25,7 @@ export { default as modal } from './modal'
export { default as navigationMenu } from './navigation-menu'
export { default as pagination } from './pagination'
export { default as popover } from './popover'
+export { default as progress } from './progress'
export { default as radioGroup } from './radio-group'
export { default as select } from './select'
export { default as selectMenu } from './select-menu'
diff --git a/src/theme/progress.ts b/src/theme/progress.ts
new file mode 100644
index 00000000..3597ed1b
--- /dev/null
+++ b/src/theme/progress.ts
@@ -0,0 +1,204 @@
+export default (config: { colors: string[] }) => ({
+ slots: {
+ root: 'gap-2',
+ base: 'relative overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700',
+ indicator: 'rounded-full size-full transition-transform duration-200 ease-out',
+ status: 'flex justify-end text-gray-400 dark:text-gray-500 transition-transform',
+ steps: 'grid items-end',
+ step: 'transition-opacity truncate text-end row-start-1 col-start-1'
+ },
+ variants: {
+ animation: {
+ 'carousel': '',
+ 'carousel-inverse': '',
+ 'swing': '',
+ 'elastic': ''
+ },
+ color: Object.fromEntries(config.colors.map((color: string) => [color, {
+ indicator: `bg-${color}-500 dark:bg-${color}-400`,
+ steps: `text-${color}-500 dark:text-${color}-400`
+ }])),
+ size: {
+ '2xs': {
+ status: 'text-xs',
+ steps: 'text-xs'
+ },
+ 'xs': {
+ status: 'text-xs',
+ steps: 'text-xs'
+ },
+ 'sm': {
+ status: 'text-sm',
+ steps: 'text-sm'
+ },
+ 'md': {
+ status: 'text-sm',
+ steps: 'text-sm'
+ },
+ 'lg': {
+ status: 'text-sm',
+ steps: 'text-sm'
+ },
+ 'xl': {
+ status: 'text-base',
+ steps: 'text-base'
+ },
+ '2xl': {
+ status: 'text-base',
+ steps: 'text-base'
+ }
+ },
+ step: {
+ active: {
+ step: 'opacity-100'
+ },
+ first: {
+ step: 'opacity-100 text-gray-500 dark:text-gray-400'
+ },
+ other: {
+ step: 'opacity-0'
+ }
+ },
+ orientation: {
+ horizontal: {
+ root: 'w-full flex flex-col',
+ base: 'w-full',
+ status: 'flex-row'
+ },
+ vertical: {
+ root: 'h-full flex flex-row-reverse',
+ base: 'h-full',
+ status: 'flex-col'
+ }
+ },
+ inverted: {
+ true: {
+ status: 'self-end'
+ }
+ }
+ },
+ compoundVariants: [{
+ inverted: true,
+ orientation: 'horizontal',
+ class: {
+ step: 'text-start',
+ status: 'flex-row-reverse'
+ }
+ }, {
+ inverted: true,
+ orientation: 'vertical',
+ class: {
+ steps: 'items-start',
+ status: 'flex-col-reverse'
+ }
+ }, {
+ orientation: 'horizontal',
+ size: '2xs',
+ class: 'h-px'
+ }, {
+ orientation: 'horizontal',
+ size: 'xs',
+ class: 'h-0.5'
+ }, {
+ orientation: 'horizontal',
+ size: 'sm',
+ class: 'h-1'
+ }, {
+ orientation: 'horizontal',
+ size: 'md',
+ class: 'h-2'
+ }, {
+ orientation: 'horizontal',
+ size: 'lg',
+ class: 'h-3'
+ }, {
+ orientation: 'horizontal',
+ size: 'xl',
+ class: 'h-4'
+ }, {
+ orientation: 'horizontal',
+ size: '2xl',
+ class: 'h-5'
+ }, {
+ orientation: 'vertical',
+ size: '2xs',
+ class: 'w-px'
+ }, {
+ orientation: 'vertical',
+ size: 'xs',
+ class: 'w-0.5'
+ }, {
+ orientation: 'vertical',
+ size: 'sm',
+ class: 'w-1'
+ }, {
+ orientation: 'vertical',
+ size: 'md',
+ class: 'w-2'
+ }, {
+ orientation: 'vertical',
+ size: 'lg',
+ class: 'w-3'
+ }, {
+ orientation: 'vertical',
+ size: 'xl',
+ class: 'w-4'
+ }, {
+ orientation: 'vertical',
+ size: '2xl',
+ class: 'w-5'
+ }, {
+ orientation: 'horizontal',
+ animation: 'carousel',
+ class: {
+ indicator: 'data-[state=indeterminate]:animate-[carousel_2s_ease-in-out_infinite]'
+ }
+ }, {
+ orientation: 'vertical',
+ animation: 'carousel',
+ class: {
+ indicator: 'data-[state=indeterminate]:animate-[carousel-vertical_2s_ease-in-out_infinite]'
+ }
+ }, {
+ orientation: 'horizontal',
+ animation: 'carousel-inverse',
+ class: {
+ indicator: 'data-[state=indeterminate]:animate-[carousel-inverse_2s_ease-in-out_infinite]'
+ }
+ }, {
+ orientation: 'vertical',
+ animation: 'carousel-inverse',
+ class: {
+ indicator: 'data-[state=indeterminate]:animate-[carousel-inverse-vertical_2s_ease-in-out_infinite]'
+ }
+ }, {
+ orientation: 'horizontal',
+ animation: 'swing',
+ class: {
+ indicator: 'data-[state=indeterminate]:animate-[swing_2s_ease-in-out_infinite]'
+ }
+ }, {
+ orientation: 'vertical',
+ animation: 'swing',
+ class: {
+ indicator: 'data-[state=indeterminate]:animate-[swing-vertical_2s_ease-in-out_infinite]'
+ }
+ }, {
+ orientation: 'horizontal',
+ animation: 'elastic',
+ class: {
+ indicator: 'data-[state=indeterminate]:animate-[elastic_2s_ease-in-out_infinite]'
+ }
+ }, {
+ orientation: 'vertical',
+ animation: 'elastic',
+ class: {
+ indicator: 'data-[state=indeterminate]:animate-[elastic-vertical_2s_ease-in-out_infinite]'
+ }
+ }],
+ defaultVariants: {
+ animation: 'carousel',
+ color: 'primary',
+ size: 'md'
+ }
+})
diff --git a/test/components/Progress.spec.ts b/test/components/Progress.spec.ts
new file mode 100644
index 00000000..a9f740cf
--- /dev/null
+++ b/test/components/Progress.spec.ts
@@ -0,0 +1,33 @@
+import { describe, it, expect } from 'vitest'
+import Progress, { type ProgressProps, type ProgressSlots } from '../../src/runtime/components/Progress.vue'
+import ComponentRender from '../component-render'
+import theme from '#build/ui/progress'
+
+describe('Progress', () => {
+ const sizes = Object.keys(theme.variants.size) as any
+ const colors = Object.keys(theme.variants.color) as any
+ const orientations = Object.keys(theme.variants.orientation) as any
+ const animations = Object.keys(theme.variants.animation) as any
+ const max = ['Waiting...', 'Cloning...', 'Migrating...', 'Deploying...', 'Done!']
+
+ it.each([
+ // Props
+ ['with modelValue', { props: { modelValue: 50 } }],
+ ['with status', { props: { modelValue: 50, status: true } }],
+ ['with status inverted', { props: { modelValue: 50, status: true, inverted: true } }],
+ ['with max', { props: { modelValue: 2, status: true, max } }],
+ ['with max inverted', { props: { modelValue: 2, status: true, inverted: true, max } }],
+ ['with as', { props: { as: 'span' } }],
+ ...sizes.map((size: string) => [`with size ${size}`, { props: { size } }]),
+ ...colors.map((color: string) => [`with color ${color}`, { props: { color } }]),
+ ...orientations.map((orientation: string) => [`with orientation ${orientation}`, { props: { orientation } }]),
+ ...animations.map((animation: string) => [`with animation ${animation}`, { props: { animation } }]),
+ ['with class', { props: { class: 'w-48' } }],
+ ['with ui', { props: { ui: { base: 'bg-white dark:bg-gray-900' } } }],
+ // Slots
+ ['with status slot', { slots: { status: () => 'Status slot' } }]
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props?: ProgressProps, slots?: Partial }) => {
+ const html = await ComponentRender(nameOrHtml, options, Progress)
+ expect(html).toMatchSnapshot()
+ })
+})
diff --git a/test/components/__snapshots__/Progress.spec.ts.snap b/test/components/__snapshots__/Progress.spec.ts.snap
new file mode 100644
index 00000000..699fc054
--- /dev/null
+++ b/test/components/__snapshots__/Progress.spec.ts.snap
@@ -0,0 +1,260 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Progress > renders with animation carousel correctly 1`] = `
+""
+`;
+
+exports[`Progress > renders with animation carousel-inverse correctly 1`] = `
+""
+`;
+
+exports[`Progress > renders with animation elastic correctly 1`] = `
+""
+`;
+
+exports[`Progress > renders with animation swing correctly 1`] = `
+""
+`;
+
+exports[`Progress > renders with as correctly 1`] = `
+""
+`;
+
+exports[`Progress > renders with class correctly 1`] = `
+""
+`;
+
+exports[`Progress > renders with color green correctly 1`] = `
+""
+`;
+
+exports[`Progress > renders with color primary correctly 1`] = `
+""
+`;
+
+exports[`Progress > renders with color red correctly 1`] = `
+""
+`;
+
+exports[`Progress > renders with max correctly 1`] = `
+"
+
50%
+
+
+
Waiting...
+
Cloning...
+
Migrating...
+
Deploying...
+
Done!
+
+
"
+`;
+
+exports[`Progress > renders with max inverted correctly 1`] = `
+"
+
50%
+
+
+
Waiting...
+
Cloning...
+
Migrating...
+
Deploying...
+
Done!
+
+
"
+`;
+
+exports[`Progress > renders with modelValue correctly 1`] = `
+""
+`;
+
+exports[`Progress > renders with orientation horizontal correctly 1`] = `
+""
+`;
+
+exports[`Progress > renders with orientation vertical correctly 1`] = `
+""
+`;
+
+exports[`Progress > renders with size 2xl correctly 1`] = `
+""
+`;
+
+exports[`Progress > renders with size 2xs correctly 1`] = `
+""
+`;
+
+exports[`Progress > renders with size lg correctly 1`] = `
+""
+`;
+
+exports[`Progress > renders with size md correctly 1`] = `
+""
+`;
+
+exports[`Progress > renders with size sm correctly 1`] = `
+""
+`;
+
+exports[`Progress > renders with size xl correctly 1`] = `
+""
+`;
+
+exports[`Progress > renders with size xs correctly 1`] = `
+""
+`;
+
+exports[`Progress > renders with status correctly 1`] = `
+""
+`;
+
+exports[`Progress > renders with status inverted correctly 1`] = `
+""
+`;
+
+exports[`Progress > renders with status slot correctly 1`] = `
+""
+`;
+
+exports[`Progress > renders with ui correctly 1`] = `
+""
+`;