mirror of
https://github.com/temetro/temetro.git
synced 2026-07-26 11:58:14 +00:00
321a6298a4
- 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>
85 lines
2.1 KiB
TypeScript
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>
|
|
);
|
|
}
|