Skip to content

Commit

Permalink
Merge pull request #171 from RitvikSardana/develop-ritvik-circular-pr…
Browse files Browse the repository at this point in the history
…ogressbar

feat: circular progress bar
  • Loading branch information
RitvikSardana authored Aug 22, 2024
2 parents 490ee7a + 83a6b51 commit c59a3df
Show file tree
Hide file tree
Showing 4 changed files with 286 additions and 0 deletions.
33 changes: 33 additions & 0 deletions src/components/CircularProgressBar.story.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
## Props


### step
The current step of the progress bar.
It is required.

### totalSteps
The total number of steps in the progress bar. Default value is 100.
It is required.

### showPercentage
If true, the percentage of the progress will be shown in the center of the circle. Else, the absolute value of the current step will be shown. Default value is false.

### size
The size of the progress bar. Default value is 'md'.
Available options are 'xs', 'sm', 'md', 'lg', 'xl'.

### theme
The theme of the progress bar. Default value is 'black'.
Available options are 'black', 'red', 'green', 'blue', 'orange'.

If a string is passed, the predefined theme will be used, and if the color does not match any predefined theme, the default theme will be used.
If a custom theme is needed, an object with primary and secondary colors can be passed.

### themeComplete
The color of the completed progress. Default value is #76f7be (light green).

### variant
The variant of the progress bar. Default value is 'solid'.
Available options are 'solid', 'outline'.

When the variant is 'solid', the progress bar on complete will be filled with the progress color. When the variant is 'outline', the progress bar on complete will be an outline with the progress color.
62 changes: 62 additions & 0 deletions src/components/CircularProgressBar.story.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<template>
<Story :layout="{ type: 'grid', width: 500, heigt: 500 }">
<Variant title="Default">
<div class="p-2 w-full h-full">
<CircularProgressBar :step="1" :totalSteps="4" />
</div>
</Variant>
<Variant title="Size">
<div class="p-2 w-full h-full">
<CircularProgressBar
:step="1"
:totalSteps="4"
size="lg"
:showPercentage="true"
/>
</div>
</Variant>
<Variant title="Theme">
<div class="p-2 w-full h-full">
<CircularProgressBar :step="3" :totalSteps="4" theme="orange" />
</div>
</Variant>
<Variant title="Custom Theme">
<div class="p-2 w-full h-full">
<CircularProgressBar
:step="2"
:totalSteps="6"
:theme="{
primary: '#2376f5',
secondary: '#ddd5d5',
}"
/>
</div>
</Variant>
<Variant title="Solid Variant">
<div class="p-2 w-full h-full">
<CircularProgressBar
:step="9"
:totalSteps="9"
variant="solid"
themeComplete="lightgreen"
/>
</div>
</Variant>
<Variant title="Outline Variant">
<div class="p-2 w-full h-full">
<CircularProgressBar
:step="9"
:totalSteps="9"
variant="outline"
themeComplete="lightgreen"
/>
</div>
</Variant>
</Story>
</template>

<script setup>
import CircularProgressBar from './CircularProgressBar.vue'
</script>

<style></style>
190 changes: 190 additions & 0 deletions src/components/CircularProgressBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
<template>
<div
class="progressbar"
role="progressbar"
:class="{
completed: isCompleted,
fillOuter: variant === 'outline',
}"
>
<div v-if="!isCompleted">
<p v-if="!showPercentage">{{ step }}</p>
<p v-else>{{ progress.toFixed(0) }}%</p>
</div>
<div v-else class="check-icon" />
</div>
</template>

<script setup lang="ts">
import { computed } from 'vue'
interface Props {
step: number
totalSteps: number
showPercentage?: boolean
variant?: Variant
theme?: string | ThemeProps
size?: Size
themeComplete?: string
}
const props = withDefaults(defineProps<Props>(), {
step: 1,
totalSteps: 4,
showPercentage: false,
theme: 'black',
size: 'md',
themeComplete: 'lightgreen',
variant: 'solid',
})
type Variant = 'solid' | 'outline'
type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
interface SizeProps {
ringSize: string
ringBarWidth: string
innerTextFontSize: string
}
// predefined sizes for the circular progress bar
const sizeMap: Record<Size, SizeProps> = {
xs: {
ringSize: '30px',
ringBarWidth: '6px',
innerTextFontSize: props.showPercentage ? '8px' : '12px',
},
sm: {
ringSize: '42px',
ringBarWidth: '10px',
innerTextFontSize: props.showPercentage ? '12px' : '16px',
},
md: {
ringSize: '60px',
ringBarWidth: '14px',
innerTextFontSize: props.showPercentage ? '16px' : '20px',
},
lg: {
ringSize: '84px',
ringBarWidth: '18px',
innerTextFontSize: props.showPercentage ? '20px' : '24px',
},
xl: {
ringSize: '108px',
ringBarWidth: '22px',
innerTextFontSize: props.showPercentage ? '24px' : '28px',
},
}
const size = computed(() => sizeMap[props.size] || sizeMap['md'])
type Theme = 'black' | 'red' | 'green' | 'blue' | 'orange'
interface ThemeProps {
primary: string
secondary: string
}
// predefined themes for the circular progress bar
const themeMap: Record<Theme, ThemeProps> = {
black: {
primary: '#333',
secondary: '#888',
},
red: {
primary: '#FF0000',
secondary: '#FFD7D7',
},
green: {
primary: '#22C55E',
secondary: '#b1ffda',
},
blue: {
primary: '#2376f5',
secondary: '#D7D7FF',
},
orange: {
primary: '#FFA500',
secondary: '#FFE5CC',
},
}
const theme = computed(() => {
if (typeof props.theme === 'string') {
return themeMap[props.theme as Theme] || themeMap['black']
}
return props.theme
})
const progress = computed(() => (props.step / props.totalSteps) * 100)
const isCompleted = computed(() => props.step === props.totalSteps)
</script>

<style scoped>
.progressbar {
--size: v-bind(size.ringSize);
--bar-width: v-bind(size.ringBarWidth);
--font-size: v-bind(size.innerTextFontSize);
--color-progress: v-bind(theme.primary);
--color-remaining-circle: v-bind(theme.secondary);
--color-complete: v-bind($props.themeComplete);
--progress: v-bind(progress + '%');
width: var(--size);
height: var(--size);
border-radius: 50%;
display: grid;
place-items: center;
position: relative;
font-size: var(--font-size);
}
@property --progress {
syntax: '<length-percentage>';
inherits: true;
initial-value: 0%;
}
.progressbar::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
background: conic-gradient(
var(--color-progress) var(--progress),
var(--color-remaining-circle) 0%
);
transition: --progress 500ms linear;
aspect-ratio: 1 / 1;
align-self: center;
}
.progressbar::after {
content: '';
position: absolute;
background: white;
border-radius: inherit;
z-index: 1;
width: calc(100% - var(--bar-width));
aspect-ratio: 1 / 1;
}
.progressbar > div {
z-index: 2;
position: relative;
}
.progressbar.completed:not(.fillOuter)::after {
background: var(--color-complete);
}
.progressbar.completed.fillOuter::before {
background: var(--color-complete);
}
.check-icon {
width: 15px;
height: 15px;
background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODUiIGhlaWdodD0iODUiIHZpZXdCb3g9IjUgMzAgNzUgMTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0zNS40MjM3IDUzLjczMjdMNjcuOTc4NyAyMS4xNzc3TDcyLjk4OTUgMjYuMTg0MkwzNS40MTk1IDYzLjc1TDEyLjg4NiA0MS4yMTIyTDE3Ljg5MjUgMzYuMjAxNUwzNS40MjM3IDUzLjczMjdaIiBmaWxsPSIjMWYxYTM4Ii8+Cjwvc3ZnPgo=');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
</style>
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export { default as CommandPaletteItem } from './components/CommandPalette/Comma
export { default as ListFilter } from './components/ListFilter/ListFilter.vue'
export { default as Calendar } from './components/Calendar/Calendar.vue'
export { default as NestedPopover } from './components/ListFilter/NestedPopover.vue'
export { default as CircularProgressBar } from './components/CircularProgressBar.vue'

// directives
export { default as onOutsideClickDirective } from './directives/onOutsideClick.js'
Expand Down

0 comments on commit c59a3df

Please sign in to comment.