All files / src/components/Skeleton Skeleton.vue

100% Statements 34/34
97.14% Branches 34/35
100% Functions 9/9
100% Lines 24/24

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228                2597x                                           2597x 6832x 2597x 2597x 2597x         2597x 2597x 2597x 5x       5x     2597x         2597x 2597x 2597x       2597x   2597x 2597x 2597x 157x 74x                 2529x                                               2597x 68x                                                                                                                                                                                                                                                              
<!-- src/components/skeleton/VSkeleton.vue -->
<script setup lang="ts">
import { computed, warn } from "vue";
import { skeletonVariants } from "./variants";
import type { SkeletonBaseProps, SkeletonVariantName } from "./types";
import { shimmerPath } from "./utils/shimmer";
import { useLoadingConfig } from "../../config";
import { resolveSkeletonAppearance } from "../../skeletonize/appearance";
const props = withDefaults(
  defineProps<
    SkeletonBaseProps & {
      variant?: SkeletonVariantName;
      /** Set this on exactly ONE Skeleton instance per loading region
       *  (e.g. the outer Table/Card, not every nested cell) to announce
       *  it to screen readers as "loading". Every Skeleton is
       *  aria-hidden by default since it's a decorative placeholder —
       *  announcing every single one would be noisy and unhelpful. */
      label?: string;
    }
  >(),
  {
    variant: "block",
    // explicit undefined opts out of Vue's Boolean casting, so an unset
    // `outlined` can fall through to the global config
    outlined: undefined,
  }
);
 
/* Appearance props resolve as: explicit prop > global config > built-in
   default. They carry no withDefaults, so "not set" stays detectable. */
const config = useLoadingConfig();
const conf = () => config.skeleton ?? {};
const appearance = computed(() => resolveSkeletonAppearance(props, conf()));
const angle = computed(() => props.angle ?? conf().angle ?? 90);
const outlined = computed(() => props.outlined ?? conf().outlined);
 
/* An unknown variant (a typo, or a name from a newer version) would
   otherwise render an empty, zero-height box with no hint why. Fall back
   to the plain block and say so; Vue's `warn` is a no-op in production. */
const resolvedVariant = computed<SkeletonVariantName>(() => {
  const name = props.variant ?? "block";
  if (Object.prototype.hasOwnProperty.call(skeletonVariants, name)) return name;
  warn(
    `[vue-smart-loading-kit] Unknown Skeleton variant "${name}", rendering "block" instead. ` +
      `Valid variants: ${Object.keys(skeletonVariants).join(", ")}.`
  );
  return "block";
});
 
const Comp = computed(() => skeletonVariants[resolvedVariant.value]);
 
/* `variant` and `label` belong to this wrapper only. Variants don't
   declare them, so passing them down would render them as stray HTML
   attributes (e.g. <div variant="avatar" label="...">). */
const variantProps = computed(() => {
  const { variant, label, ...rest } = props;
  return rest;
});
/* Where the sheen's background-position slides from/to for this angle.
   See utils/shimmer.ts for why the endpoints are opposite corners. */
const shimmer = computed(() => shimmerPath(angle.value));
 
const outlinedCfg = computed(() => {
  const v = outlined.value;
  if (!v) return { enabled: false, width: "1px", style: "solid" };
  if (v === true) return { enabled: true, width: "1px", style: "solid" };
  return {
    enabled: v.enabled ?? true,
    width: typeof v.width === "number" ? `${v.width}px` : v.width ?? "1px",
    style: v.style ?? "solid",
  };
});
</script>
 
<template>
  <div
    class="vslk-skeleton-container"
    :class="[
      `vslk-sk--anim-${appearance.animation}`,
      `vslk-sk--v-${resolvedVariant}`,
      { 'vslk-sk--outlined': outlinedCfg.enabled },
    ]"
    :style="{
      '--vslk-sk-base': appearance.base,
      '--vslk-sk-hi': appearance.highlight,
      '--vslk-sk-duration': appearance.duration,
      '--vslk-sk-outline-w': outlinedCfg.width,
      '--vslk-sk-outline-style': outlinedCfg.style,
      '--vslk-sk-w': typeof props.width === 'number' ? `${props.width}px` : props.width,
      '--vslk-sk-h': typeof props.height === 'number' ? `${props.height}px` : props.height,
      '--vslk-sk-r': typeof props.radius === 'number' ? `${props.radius}px` : props.radius,
      '--vslk-sk-shimmer-angle': typeof angle === 'number' ? `${angle}deg` : angle,
      '--vslk-sk-shimmer-from': shimmer.from,
      '--vslk-sk-shimmer-to': shimmer.to,
    }"
    v-bind="props.label
      ? { role: 'status', 'aria-live': 'polite', 'aria-busy': 'true' }
      : { 'aria-hidden': 'true' }"
  >
    <component :is="Comp" v-bind="variantProps" />
    <span v-if="props.label" class="vslk-sr-only">{{ props.label }}</span>
  </div>
</template>
 
<style>
/* Base shape */
.vslk-skeleton-container .vslk-sk-shape {
  background-color: var(--vslk-sk-base);
  position: relative;
  overflow: hidden;
  width: var(--vslk-sk-w, 100%);
  height: var(--vslk-sk-h, 20px);
  border-radius: var(--vslk-sk-r, 4px);
  box-sizing: border-box; /* برای فیکس ماندن ابعاد در حالت outlined */
}
 
/* -----------------------------
   Normal animations (Filled)
------------------------------ */
 
/* Shimmer — a translucent sheen sweeping over the solid base color.
   background-size is generous in BOTH axes (300% 300%) so there's
   enough pattern to slide through regardless of angle or the shape's
   own aspect ratio. The keyframes slide between the JS-computed
   --vslk-sk-shimmer-from/-to corners (see utils/shimmer.ts), which stay
   inside the valid 0%-100% range and move the band along the
   gradient's own direction, fully across the box, at any angle. Both ends of the sheen
   fade to fully transparent, so there's never a hard seam regardless
   of color contrast either. */
.vslk-sk--anim-shimmer .vslk-sk-shape {
  /* The bright band is intentionally narrow (45%-50%-55%, not a full
     0%-50%-100% spread) — combined with the large background-size
     needed for non-90deg angles, a full-width spread meant a large
     fraction of the box showed near-opaque highlight at any given
     moment, looking like a solid color panel sliding over rather than
     a thin streak of light. */
 
  /* Fallback for browsers without relative color syntax support —
     fades directly to `transparent`, which can show a faint dark/muddy
     dip near the fade-out since transparent's RGB is (0,0,0) and plain
     gradients interpolate RGB and alpha together. The next declaration
     overrides this wherever it's understood: rgb(from ...) rebuilds
     the SAME hue at alpha 0, so the fade is alpha-only, no hue shift.
 
     With background-size 300%, a stop spread of `w` percent of the
     pattern is `3w` percent of the BOX's own extent once rendered, so
     this 10% spread is a band ~30% of the box wide. utils/shimmer.ts
     relies on that width when proving the band always clears the box
     (tests/Skeleton/shimmer.test.ts models it) — change the stops and
     the test's `halfBand` together. */
  background-image: linear-gradient(
    var(--vslk-sk-shimmer-angle, 90deg),
    transparent 45%,
    var(--vslk-sk-hi) 50%,
    transparent 55%
  );
  background-image: linear-gradient(
    var(--vslk-sk-shimmer-angle, 90deg),
    rgb(from var(--vslk-sk-hi) r g b / 0) 45%,
    var(--vslk-sk-hi) 50%,
    rgb(from var(--vslk-sk-hi) r g b / 0) 55%
  );
  background-size: 300% 300%;
  background-repeat: no-repeat;
  animation: vslk-shimmer var(--vslk-sk-duration) infinite linear;
}
 
@keyframes vslk-shimmer {
  0% { background-position: var(--vslk-sk-shimmer-from, 100% 50%); }
  100% { background-position: var(--vslk-sk-shimmer-to, 0% 50%); }
}
.vslk-sk--anim-pulse .vslk-sk-shape {
  animation: vslk-pulse var(--vslk-sk-duration) infinite ease-in-out;
}
 
@keyframes vslk-pulse {
  0%, 100% { background-color: var(--vslk-sk-base); }
  50% { background-color: var(--vslk-sk-hi); }
}
 
/* Outlined mode (Animated border)
   A literal "sliding gradient" along a thin border requires
   mask-composite or dual background-clip tricks, both of which are
   unreliable across browsers in practice. Instead, both shimmer and
   pulse animate the border COLOR between base and highlight — visually
   a smooth sweep rather than a moving gradient, but guaranteed to
   animate consistently everywhere with zero exotic CSS features. */
 
.vslk-sk--outlined .vslk-sk-shape {
  background-color: transparent !important;
  background-image: none !important;
  border: var(--vslk-sk-outline-w, 1px) var(--vslk-sk-outline-style, solid) var(--vslk-sk-base);
  box-sizing: border-box;
}
 
.vslk-sk--outlined.vslk-sk--anim-shimmer .vslk-sk-shape,
.vslk-sk--outlined.vslk-sk--anim-pulse .vslk-sk-shape {
  animation: vslk-pulse-border var(--vslk-sk-duration) infinite ease-in-out !important;
}
 
@keyframes vslk-pulse-border {
  0%, 100% { border-color: var(--vslk-sk-base); }
  50% { border-color: var(--vslk-sk-hi); }
}
 
/* -----------------------------
   Utility & Accessibility
------------------------------ */
.vslk-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
 
@media (prefers-reduced-motion: reduce) {
  .vslk-sk--anim-shimmer .vslk-sk-shape,
  .vslk-sk--anim-pulse .vslk-sk-shape,
  .vslk-sk--outlined .vslk-sk-shape {
    animation: none !important;
  }
}
</style>