feat(ui): consolidate shared UI patterns into reusable components
- Token cleanup: replace raw hex colors (#c084fc, #60a5fa, #6ee7b7, #fdba74) in BoardCard.vue with nexus-tokens.css CSS variables (--clr-iris, --clr-bao, --clr-agent, --clr-review) - New composable: useFormatDate (formatDate, relativeTime, toDateInputValue, minutesSince, hoursSince) — extracts duplicated date helpers from TaskBoardView and BoardCard - New composable: useConfirm — reusable confirmation dialog logic (open/close, error/success state, Escape binding, body scroll lock) - New component: StatusPill — unified state pill for backlog/progress/ review/blocked/done, replacing inline .detail-state-pill classes - New component: SkeletonLoader — loading placeholder with shimmer animation (card/text/circle variants) - TaskBoardView: imports StatusPill & useFormatDate, removes 35+ lines of duplicated helpers - ui/index.ts: exports new StatusPill & SkeletonLoader - Build verified: pnpm build green (vue-tsc --noEmit + vite build pass)
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* SkeletonLoader — Platzhalter-Animation für Ladezustände
|
||||
*
|
||||
* Extrahiert aus dem Muster in TaskStrip (skeleton-pulse) und
|
||||
* board-loading spinner. Zentrale Komponente mit zwei Varianten:
|
||||
*
|
||||
* - 'card' — Rechteckiger Karten-Skeleton
|
||||
* - 'text' — Zeilen-Skeleton für Text
|
||||
* - 'circle' — Runder Skeleton (Avatar, Dot)
|
||||
*
|
||||
* Alle Farben aus nexus-tokens.css Token.
|
||||
*/
|
||||
|
||||
withDefaults(defineProps<{
|
||||
/** Darstellungsform */
|
||||
variant?: 'card' | 'text' | 'circle'
|
||||
/** Höhe in px (nur für card/text) */
|
||||
height?: number
|
||||
/** Breite in px (optional, sonst 100%) */
|
||||
width?: number
|
||||
}>(), {
|
||||
variant: 'card',
|
||||
height: 78,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="skeleton"
|
||||
:class="variant"
|
||||
:style="{
|
||||
height: variant === 'circle' ? `${width || height || 32}px` : `${height}px`,
|
||||
width: width ? `${width}px` : '100%',
|
||||
}"
|
||||
>
|
||||
<div v-if="variant === 'text'" class="skeleton-line" style="width: 60%"></div>
|
||||
<div v-if="variant === 'text'" class="skeleton-line" style="width: 85%; margin-top: 8px"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.skeleton {
|
||||
border-radius: var(--r-sm, 10px);
|
||||
background: var(--glass);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.skeleton::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
rgba(124, 108, 255, .06) 40%,
|
||||
rgba(124, 108, 255, .12) 50%,
|
||||
rgba(124, 108, 255, .06) 60%,
|
||||
transparent 100%
|
||||
);
|
||||
animation: skeleton-shimmer 1.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.skeleton.text {
|
||||
background: transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.skeleton-line {
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background: var(--glass-2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.skeleton-line::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
rgba(124, 108, 255, .08) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
animation: skeleton-shimmer 1.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.skeleton.circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
@keyframes skeleton-shimmer {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(180%); }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user