CProgressBar

main
fandi.susanto.bts 1 month ago
parent f10a4c4de8
commit 5835a078bb

@ -0,0 +1,156 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { CProgressBarSize, CProgressBarVariant } from './types'
const props = withDefaults(
defineProps<{
value?: number
max?: number
indeterminate?: boolean
showValue?: boolean
size?: CProgressBarSize
variant?: CProgressBarVariant
ariaLabel?: string
}>(),
{
value: 0,
max: 100,
indeterminate: false,
showValue: false,
size: 'medium',
variant: 'default',
ariaLabel: undefined,
},
)
defineSlots<{
value(props: { value: number; max: number; percentage: number }): unknown
}>()
const normalizedMax = computed(() =>
Number.isFinite(props.max) && props.max > 0 ? props.max : 100,
)
const normalizedValue = computed(() => {
if (!Number.isFinite(props.value)) return 0
return Math.min(Math.max(props.value, 0), normalizedMax.value)
})
const percentage = computed(() => (normalizedValue.value / normalizedMax.value) * 100)
const percentageLabel = computed(() => `${Math.round(percentage.value)}%`)
</script>
<template>
<div
class="c-progress-bar"
:class="[`is-${size}`, `is-${variant}`, { 'is-indeterminate': indeterminate }]"
role="progressbar"
:aria-label="ariaLabel"
:aria-valuemin="indeterminate ? undefined : 0"
:aria-valuemax="indeterminate ? undefined : normalizedMax"
:aria-valuenow="indeterminate ? undefined : normalizedValue"
>
<span class="track" aria-hidden="true">
<span
class="fill"
:style="indeterminate ? undefined : { width: `${percentage}%` }"
/>
</span>
<span v-if="showValue && !indeterminate" class="value">
<slot
name="value"
:value="normalizedValue"
:max="normalizedMax"
:percentage="percentage"
>
{{ percentageLabel }}
</slot>
</span>
</div>
</template>
<style scoped lang="scss">
.c-progress-bar {
display: flex;
align-items: center;
width: 100%;
min-width: 0;
gap: 7px;
.track {
position: relative;
box-sizing: border-box;
flex: 1 1 auto;
height: 10px;
overflow: hidden;
background: var(--c-progress-track-color, #e5e8ec);
border: 1px solid var(--c-border-color, #cfd4da);
border-radius: var(--c-border-radius, 3px);
}
.fill {
position: absolute;
inset-block: 0;
inset-inline-start: 0;
background: var(--c-primary-color, #286aa6);
transition: width 160ms ease-out;
}
.value {
flex: none;
min-width: 3.5em;
color: var(--c-muted-text-color, #626a75);
font-size: 12px;
line-height: 1;
text-align: end;
font-variant-numeric: tabular-nums;
}
&.is-small .track {
height: 6px;
}
&.is-large .track {
height: 14px;
}
&.is-success .fill {
background: var(--c-success-color, #2f7d32);
}
&.is-warning .fill {
background: var(--c-warning-color, #f0ad32);
}
&.is-danger .fill {
background: var(--c-danger-color, #b42318);
}
&.is-indeterminate .fill {
width: 35%;
animation: c-progress-bar-indeterminate 1.2s ease-in-out infinite;
}
}
@keyframes c-progress-bar-indeterminate {
from {
transform: translateX(-110%);
}
to {
transform: translateX(300%);
}
}
@media (prefers-reduced-motion: reduce) {
.c-progress-bar {
.fill {
transition: none;
}
&.is-indeterminate .fill {
animation-duration: 2.4s;
}
}
}
</style>

@ -0,0 +1,2 @@
export type CProgressBarSize = 'small' | 'medium' | 'large'
export type CProgressBarVariant = 'default' | 'success' | 'warning' | 'danger'

@ -11,6 +11,7 @@ export { default as CMenu } from './components/menu/CMenu.vue'
export { default as CMultiSelect } from './components/multi-select/CMultiSelect.vue'
export { default as CNumberInput } from './components/number-input/CNumberInput.vue'
export { default as CPassword } from './components/password/CPassword.vue'
export { default as CProgressBar } from './components/progress-bar/CProgressBar.vue'
export { default as CRadio } from './components/radio/CRadio.vue'
export { default as CSelect } from './components/select/CSelect.vue'
export { default as CSeparator } from './components/separator/CSeparator.vue'
@ -34,6 +35,10 @@ export type {
CMultiSelectValue,
} from './components/multi-select/types'
export type { CRadioValue } from './components/radio/types'
export type {
CProgressBarSize,
CProgressBarVariant,
} from './components/progress-bar/types'
export type {
CSelectKeyAccessor,
CSelectLabelAccessor,

@ -120,6 +120,13 @@ export function useTopNavigation() {
active: route.name === 'auto-complete',
command: () => void router.push({ name: 'auto-complete' }),
},
{
id: 'progress-bar',
label: 'Progress Bar',
icon: '▰',
active: route.name === 'progress-bar',
command: () => void router.push({ name: 'progress-bar' }),
},
{ type: 'separator' },
{
id: 'icon',
@ -244,6 +251,13 @@ export function useComponentNavigation() {
active: route.name === 'auto-complete',
command: () => void router.push({ name: 'auto-complete' }),
},
{
id: 'progress-bar',
label: 'Progress Bar',
icon: '▰',
active: route.name === 'progress-bar',
command: () => void router.push({ name: 'progress-bar' }),
},
{ type: 'separator' },
{
id: 'icon',

@ -0,0 +1,136 @@
<script setup lang="ts">
import { ref } from 'vue'
import { CButton, CProgressBar, CSeparator } from '@/index'
import CCodeBlock from '@/documentation/CCodeBlock.vue'
const progress = ref(42)
function adjustProgress(amount: number) {
progress.value = Math.min(100, Math.max(0, progress.value + amount))
}
const basicUsage = `<CProgressBar :value="42" />
<CProgressBar :value="42" show-value />`
const indeterminateUsage = `<CProgressBar indeterminate aria-label="Loading report" />`
const customValueUsage = `<CProgressBar :value="3" :max="8" show-value>
<template #value="{ value, max }">
{{ value }} / {{ max }} files
</template>
</CProgressBar>`
</script>
<template>
<article class="form-page">
<header class="page-header">
<div><p class="category">Feedback</p><h1>Progress Bar</h1></div>
<p>
<code>CProgressBar</code> communicates completion for bounded work or ongoing activity
whose duration is not yet known.
</p>
</header>
<CSeparator />
<section class="section">
<h2>Determinate progress</h2>
<p>
Set <code>value</code> when progress can be measured. Values are clamped between zero and
<code>max</code>, which defaults to <code>100</code>. Add <code>show-value</code> to display a
compact percentage beside the track.
</p>
<div class="preview">
<CProgressBar :value="progress" />
<CProgressBar :value="progress" show-value aria-label="Interactive progress" />
<div class="actions">
<CButton size="small" @click="adjustProgress(-10)">10</CButton>
<CButton size="small" @click="adjustProgress(10)">+10</CButton>
<span>Current value: {{ progress }}</span>
</div>
</div>
<CCodeBlock class="code-sample" :code="basicUsage" />
</section>
<section class="section">
<h2>Indeterminate progress</h2>
<p>
Use <code>indeterminate</code> while work is active but no meaningful percentage is
available. In this mode value-related ARIA attributes and the optional value text are
omitted. Supply an accessible label describing the operation.
</p>
<div class="preview">
<CProgressBar indeterminate aria-label="Loading report" />
</div>
<CCodeBlock class="code-sample" :code="indeterminateUsage" />
</section>
<section class="section">
<h2>Semantic variants</h2>
<p>
Variants communicate state without changing behavior. Keep the default for ordinary
progress, and reserve semantic colors for meaningful outcomes or thresholds.
</p>
<div class="preview variants">
<div><span>Default</span><CProgressBar :value="65" /></div>
<div><span>Success</span><CProgressBar :value="100" variant="success" /></div>
<div><span>Warning</span><CProgressBar :value="72" variant="warning" /></div>
<div><span>Danger</span><CProgressBar :value="88" variant="danger" /></div>
</div>
</section>
<section class="section">
<h2>Sizes</h2>
<div class="preview variants">
<div><span>Small (6px)</span><CProgressBar :value="55" size="small" /></div>
<div><span>Medium (10px)</span><CProgressBar :value="55" /></div>
<div><span>Large (14px)</span><CProgressBar :value="55" size="large" /></div>
</div>
</section>
<section class="section">
<h2>Custom maximum and value text</h2>
<p>
Use the <code>value</code> slot when a percentage is less useful than application-specific
units. The slot receives the normalized <code>value</code>, <code>max</code>, and calculated
<code>percentage</code>.
</p>
<div class="preview">
<CProgressBar :value="3" :max="8" show-value>
<template #value="{ value, max }">{{ value }} / {{ max }} files</template>
</CProgressBar>
</div>
<CCodeBlock class="code-sample" :code="customValueUsage" />
</section>
<section class="section">
<h2>Properties</h2>
<dl class="property-list">
<div><dt><code>value</code></dt><dd>Current numeric value. Defaults to <code>0</code> and is clamped to the valid range.</dd></div>
<div><dt><code>max</code></dt><dd>Positive upper bound used to calculate the percentage. Defaults to <code>100</code>.</dd></div>
<div><dt><code>indeterminate</code></dt><dd>Displays ongoing activity without claiming a measurable value.</dd></div>
<div><dt><code>show-value</code></dt><dd>Displays the rounded percentage beside determinate progress.</dd></div>
<div><dt><code>size</code></dt><dd><code>small</code>, <code>medium</code>, or <code>large</code>.</dd></div>
<div><dt><code>variant</code></dt><dd><code>default</code>, <code>success</code>, <code>warning</code>, or <code>danger</code>.</dd></div>
<div><dt><code>aria-label</code></dt><dd>Accessible name for the operation represented by the progress bar.</dd></div>
</dl>
</section>
</article>
</template>
<style scoped lang="scss">
@use './form-demo.scss';
.actions {
display: flex;
align-items: center;
gap: 7px;
}
.variants > div {
display: grid;
grid-template-columns: 100px minmax(0, 1fr);
align-items: center;
gap: 10px;
}
</style>

@ -96,6 +96,11 @@ const router = createRouter({
name: 'auto-complete',
component: () => import('@/pages/components/AutoComplete.vue'),
},
{
path: 'progress-bar',
name: 'progress-bar',
component: () => import('@/pages/components/ProgressBar.vue'),
},
{
path: 'icon',
name: 'icon',

Loading…
Cancel
Save