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 @@ + + + + + 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`] = ` +"
+
50%
+
+
+
+ +
" +`; + +exports[`Progress > renders with status inverted correctly 1`] = ` +"
+
50%
+
+
+
+ +
" +`; + +exports[`Progress > renders with status slot correctly 1`] = ` +"
+ +
+
+
+ +
" +`; + +exports[`Progress > renders with ui correctly 1`] = ` +"
+ +
+
+
+ +
" +`;