Files
temetro/frontend/components/shimmering-text.tsx
T
Khalid Abdi 321a6298a4 frontend: pharmacy inventory, lab add-result & analysis charts
- Pharmacy: the sidebar entry now expands into Pharmacy + a new Inventory page
  (searchable medication stock with derived in-stock/low/out availability,
  backed by /api/inventory). Fix the dispensing-queue "Expiring" badge so it
  only flags courses ending within the next 7 days, not ones already elapsed.
- Lab "Add analysis result": the patient picker no longer lists patients until
  you type and supports arrow-key + Enter selection; the test field offers a
  catalog of common analyses with an Advanced option (custom analysis + ref
  range); submitted results now show immediately in a Recent results feed.
- Analysis: add a Live panel (real-time line chart) and replace the flat trend
  sparklines with bklit-ui area + bar charts (installed from the @bklit shadcn
  registry; chart CSS variables wired into the theme for light and dark).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 19:22:07 +03:00

85 lines
2.1 KiB
TypeScript

"use client";
import { motion, useReducedMotion, type Variants } from "motion/react";
import { type ComponentProps, useCallback } from "react";
import { cn } from "@/lib/utils";
export type ShimmeringTextProps = Omit<
ComponentProps<typeof motion.span>,
"children"
> & {
/** The text to render with the shimmering effect. */
text: string;
/**
* Duration in seconds for one shimmer cycle.
* @defaultValue 1
*/
duration?: number;
/**
* Whether the shimmer animation is paused.
* @defaultValue false
*/
isStopped?: boolean;
};
export function ShimmeringText({
text,
duration = 1,
isStopped = false,
className,
...props
}: ShimmeringTextProps) {
const reducedMotion = useReducedMotion();
const stopped = isStopped || reducedMotion === true;
const createCharVariants = useCallback(
(charIndex: number): Variants => ({
running: {
color: ["var(--color)", "var(--shimmering-color)", "var(--color)"],
transition: {
duration,
repeat: Number.POSITIVE_INFINITY,
repeatType: "loop",
repeatDelay: text.length * 0.05,
delay: (charIndex * duration) / text.length,
ease: "easeInOut",
},
},
stopped: {
color: "var(--color)",
transition: {
duration: duration * 0.5,
ease: "easeOut",
},
},
}),
[duration, text.length]
);
return (
<motion.span
className={cn(
"inline-flex select-none items-center leading-none",
"[--color:var(--muted-foreground)] [--shimmering-color:var(--foreground)]",
className
)}
{...props}
>
{text.split("").map((char, index) => (
<motion.span
animate={stopped ? "stopped" : "running"}
aria-hidden
className="inline-block whitespace-pre leading-none"
initial="stopped"
// biome-ignore lint/suspicious/noArrayIndexKey: static label text, order never changes
key={index}
variants={createCharVariants(index)}
>
{char}
</motion.span>
))}
<span className="sr-only">{text}</span>
</motion.span>
);
}