mirror of
https://github.com/temetro/temetro.git
synced 2026-07-26 11:58:14 +00:00
e8f3ed9ffe
`npm run lint` reported 99 errors and 26 warnings across 65 files and had presumably been failing for a while — next.config.ts sets eslint.ignoreDuringBuilds, so the build never surfaced it. 68 were in vendored code: components/charts and components/ai-elements, pulled from upstream registries. Re-linting those reports upstream's style back at us, and "fixing" them means diverging and eating conflicts on every update — ai-elements already has exactly this carve-out on the TypeScript side via ignoreBuildErrors. Ignore both, plus the two registry files in components/ui (carousel publishes its api from an effect; the sidebar skeleton picks a random width). The other 31 were ours, nearly all react-hooks/set-state-in-effect on the same shape: an effect that re-seeds form state when a dialog opens or a selection changes. Moved to render-phase adjustment, which is both what React recommends and a real fix — the effect version paints one frame of the *previous* record's values before correcting itself. Two carried sharper bugs: the employee dialog could keep a typed password across a switch to another member, and use-wallet-sync could carry `linked` over to a newly-selected patient, briefly offering to push a record to someone else's wallet. The rest: useIsMobile and speech-support detection become useSyncExternalStore (correct on first paint, no mount flash); refs mirroring state are written in effects rather than during render; the care-team fetch moves into its effect behind a reload key, dropping an exhaustive-deps suppression. Two effects in chat-panel keep the rule disabled with a reason. Both are what effects are for: draining the queued-message buffer when the transport goes idle, and resolving ?thread from the URL — the latter mints an id with nanoid(), so moving it into render would just trade this error for a purity one. Also removes a dead /explore-era import and unused directives found on the way. lint now exits 0, with the ai-elements tsc carve-out unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
214 lines
5.7 KiB
TypeScript
214 lines
5.7 KiB
TypeScript
"use client";
|
|
|
|
import { Slot } from "radix-ui";
|
|
import * as React from "react";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
// Types
|
|
type TimelineContextValue = {
|
|
activeStep: number;
|
|
setActiveStep: (step: number) => void;
|
|
};
|
|
|
|
// Context
|
|
const TimelineContext = React.createContext<TimelineContextValue | undefined>(
|
|
undefined,
|
|
);
|
|
|
|
const useTimeline = () => {
|
|
const context = React.useContext(TimelineContext);
|
|
if (!context) {
|
|
throw new Error("useTimeline must be used within a Timeline");
|
|
}
|
|
return context;
|
|
};
|
|
|
|
// Components
|
|
interface TimelineProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
defaultValue?: number;
|
|
value?: number;
|
|
onValueChange?: (value: number) => void;
|
|
orientation?: "horizontal" | "vertical";
|
|
}
|
|
|
|
function Timeline({
|
|
defaultValue = 1,
|
|
value,
|
|
onValueChange,
|
|
orientation = "vertical",
|
|
className,
|
|
...props
|
|
}: TimelineProps) {
|
|
const [activeStep, setInternalStep] = React.useState(defaultValue);
|
|
|
|
const setActiveStep = React.useCallback(
|
|
(step: number) => {
|
|
if (value === undefined) {
|
|
setInternalStep(step);
|
|
}
|
|
onValueChange?.(step);
|
|
},
|
|
[value, onValueChange],
|
|
);
|
|
|
|
const currentStep = value ?? activeStep;
|
|
|
|
return (
|
|
<TimelineContext.Provider
|
|
value={{ activeStep: currentStep, setActiveStep }}
|
|
>
|
|
<div
|
|
className={cn(
|
|
"group/timeline flex data-[orientation=horizontal]:w-full data-[orientation=horizontal]:flex-row data-[orientation=vertical]:flex-col",
|
|
className,
|
|
)}
|
|
data-orientation={orientation}
|
|
data-slot="timeline"
|
|
{...props}
|
|
/>
|
|
</TimelineContext.Provider>
|
|
);
|
|
}
|
|
|
|
// TimelineContent
|
|
function TimelineContent({
|
|
className,
|
|
...props
|
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
return (
|
|
<div
|
|
className={cn("text-muted-foreground text-sm", className)}
|
|
data-slot="timeline-content"
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// TimelineDate
|
|
interface TimelineDateProps extends React.HTMLAttributes<HTMLTimeElement> {
|
|
asChild?: boolean;
|
|
}
|
|
|
|
function TimelineDate({
|
|
asChild = false,
|
|
className,
|
|
...props
|
|
}: TimelineDateProps) {
|
|
const Comp = asChild ? Slot.Root : "time";
|
|
|
|
return (
|
|
<Comp
|
|
className={cn(
|
|
"mb-1 block font-medium text-muted-foreground text-xs group-data-[orientation=vertical]/timeline:max-sm:h-4",
|
|
className,
|
|
)}
|
|
data-slot="timeline-date"
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// TimelineHeader
|
|
function TimelineHeader({
|
|
className,
|
|
...props
|
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
return (
|
|
<div className={cn(className)} data-slot="timeline-header" {...props} />
|
|
);
|
|
}
|
|
|
|
// TimelineIndicator
|
|
interface TimelineIndicatorProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
asChild?: boolean;
|
|
}
|
|
|
|
function TimelineIndicator({
|
|
// Accepted for API parity but unused: COSS/Base UI composes with `render`,
|
|
// not `asChild`. Destructured so it never lands on the DOM node.
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
asChild: _asChild = false,
|
|
className,
|
|
children,
|
|
...props
|
|
}: TimelineIndicatorProps) {
|
|
return (
|
|
<div
|
|
aria-hidden="true"
|
|
className={cn(
|
|
"group-data-[orientation=horizontal]/timeline:-top-6 group-data-[orientation=horizontal]/timeline:-translate-y-1/2 group-data-[orientation=vertical]/timeline:-left-6 group-data-[orientation=vertical]/timeline:-translate-x-1/2 absolute size-4 rounded-full border-2 border-primary/20 group-data-[orientation=vertical]/timeline:top-0 group-data-[orientation=horizontal]/timeline:left-0 group-data-completed/timeline-item:border-primary",
|
|
className,
|
|
)}
|
|
data-slot="timeline-indicator"
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// TimelineItem
|
|
interface TimelineItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
step: number;
|
|
}
|
|
|
|
function TimelineItem({ step, className, ...props }: TimelineItemProps) {
|
|
const { activeStep } = useTimeline();
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"group/timeline-item relative flex flex-1 flex-col gap-0.5 group-data-[orientation=vertical]/timeline:ms-8 group-data-[orientation=horizontal]/timeline:mt-8 group-data-[orientation=horizontal]/timeline:not-last:pe-8 group-data-[orientation=vertical]/timeline:not-last:pb-12 has-[+[data-completed]]:[&_[data-slot=timeline-separator]]:bg-primary",
|
|
className,
|
|
)}
|
|
data-completed={step <= activeStep || undefined}
|
|
data-slot="timeline-item"
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// TimelineSeparator
|
|
function TimelineSeparator({
|
|
className,
|
|
...props
|
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
return (
|
|
<div
|
|
aria-hidden="true"
|
|
className={cn(
|
|
"group-data-[orientation=horizontal]/timeline:-top-6 group-data-[orientation=horizontal]/timeline:-translate-y-1/2 group-data-[orientation=vertical]/timeline:-left-6 group-data-[orientation=vertical]/timeline:-translate-x-1/2 absolute self-start bg-primary/10 group-last/timeline-item:hidden group-data-[orientation=horizontal]/timeline:h-0.5 group-data-[orientation=vertical]/timeline:h-[calc(100%-1rem-0.25rem)] group-data-[orientation=horizontal]/timeline:w-[calc(100%-1rem-0.25rem)] group-data-[orientation=vertical]/timeline:w-0.5 group-data-[orientation=horizontal]/timeline:translate-x-4.5 group-data-[orientation=vertical]/timeline:translate-y-4.5",
|
|
className,
|
|
)}
|
|
data-slot="timeline-separator"
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// TimelineTitle
|
|
function TimelineTitle({
|
|
className,
|
|
...props
|
|
}: React.HTMLAttributes<HTMLHeadingElement>) {
|
|
return (
|
|
<h3
|
|
className={cn("font-medium text-sm", className)}
|
|
data-slot="timeline-title"
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export {
|
|
Timeline,
|
|
TimelineContent,
|
|
TimelineDate,
|
|
TimelineHeader,
|
|
TimelineIndicator,
|
|
TimelineItem,
|
|
TimelineSeparator,
|
|
TimelineTitle,
|
|
};
|