b82d88563a
- 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)
102 lines
2.2 KiB
Vue
102 lines
2.2 KiB
Vue
<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>
|