From 996642b04ea1e9a128c557410ffadc75e611a0ec Mon Sep 17 00:00:00 2001 From: Sam Piper Date: Sat, 13 Apr 2024 18:30:41 +0100 Subject: [PATCH] feat: proper timeline component (not my jank one) --- .../actions/SignInFlowProgress/index.tsx | 105 ++++----- packages/ui/components/ui/timeline.tsx | 215 ++++++++++++------ 2 files changed, 204 insertions(+), 116 deletions(-) diff --git a/apps/forge/src/components/signin/actions/SignInFlowProgress/index.tsx b/apps/forge/src/components/signin/actions/SignInFlowProgress/index.tsx index 99c2506..c2d6453 100644 --- a/apps/forge/src/components/signin/actions/SignInFlowProgress/index.tsx +++ b/apps/forge/src/components/signin/actions/SignInFlowProgress/index.tsx @@ -1,65 +1,68 @@ import React from "react"; -import {Timeline, TimelineItem} from "@ui/components/ui/timeline.tsx"; -import {Card, CardContent, CardHeader} from "@ui/components/ui/card.tsx"; +import { Timeline, TimelineDot, TimelineHeading, TimelineItem, TimelineLine } from "@ui/components/ui/timeline.tsx"; +import { Card, CardContent, CardHeader } from "@ui/components/ui/card.tsx"; import { - AnyStep, - EnqueueSteps, - FlowType, RegisterSteps, - SignInSteps, - SignOutSteps, + AnyStep, + EnqueueSteps, + FlowType, + RegisterSteps, + SignInSteps, + SignOutSteps, } from "@/components/signin/actions/SignInManager/types.ts"; interface SignInProgressProps { - currentStep: AnyStep; - flowType: FlowType; - totalSteps: number; - children: React.ReactNode; + currentStep: AnyStep; + flowType: FlowType; + totalSteps: number; + children: React.ReactNode; } -const SignInFlowProgress: React.FC = ({currentStep, flowType}) => { - let stepTitles: string[] = []; +const SignInFlowProgress: React.FC = ({ currentStep, flowType }) => { + let stepTitles: string[] = []; - switch (flowType) { - case FlowType.SignIn: - stepTitles = Object.values(SignInSteps); - break; - case FlowType.SignOut: - stepTitles = Object.values(SignOutSteps); - break; - case FlowType.Register: - stepTitles = Object.values(RegisterSteps); - break; - case FlowType.Enqueue: - stepTitles = Object.values(EnqueueSteps); - break; - default: - console.warn(`Unsupported flowType: ${flowType}`); - break; - } + switch (flowType) { + case FlowType.SignIn: + stepTitles = Object.values(SignInSteps); + break; + case FlowType.SignOut: + stepTitles = Object.values(SignOutSteps); + break; + case FlowType.Register: + stepTitles = Object.values(RegisterSteps); + break; + case FlowType.Enqueue: + stepTitles = Object.values(EnqueueSteps); + break; + default: + console.warn(`Unsupported flowType: ${flowType}`); + break; + } - const currentStepIndex = stepTitles.indexOf(currentStep as string); + const currentStepIndex = stepTitles.indexOf(currentStep as string); - return ( - - -

{'Progress'}

-
- - - {stepTitles.map((stepTitle, index) => { - const isCompleted = index < currentStepIndex; - const isCurrent = index === currentStepIndex; + return ( + + +

{"Progress"}

+
+ + + {stepTitles.map((stepTitle, index) => { + const isCompleted = index < currentStepIndex; + const status = isCompleted ? "done" : "default"; - return ( - - {stepTitle} - - ); - })} - - -
- ); + return ( + + {stepTitle} + + {index < stepTitles.length - 1 && } + + ); + })} +
+
+
+ ); }; export default SignInFlowProgress; diff --git a/packages/ui/components/ui/timeline.tsx b/packages/ui/components/ui/timeline.tsx index e857b98..3dcb818 100644 --- a/packages/ui/components/ui/timeline.tsx +++ b/packages/ui/components/ui/timeline.tsx @@ -1,78 +1,163 @@ -import React from 'react'; -import {cn} from "@ui/lib/utils"; +import React from "react"; +import { VariantProps, cva } from "class-variance-authority"; +import { Check, Circle, X } from "lucide-react"; -const TimelineContent: React.FC<{ children: React.ReactNode }> = ({children}) => ( -
{children}
+import { cn } from "@ui/lib/utils"; + +const timelineVariants = cva("flex flex-col items-stretch", { + variants: { + positions: { + left: "[&>li]:grid-cols-[0_min-content_1fr]", + right: "[&>li]:grid-cols-[1fr_min-content]", + center: "[&>li]:grid-cols-[1fr_min-content_1fr]", + }, + }, + defaultVariants: { + positions: "left", + }, +}); + +interface TimelineProps extends React.HTMLAttributes, VariantProps {} + +const Timeline = React.forwardRef( + ({ children, className, positions, ...props }, ref) => { + return ( +
    + {children} +
+ ); + }, ); +Timeline.displayName = "Timeline"; -const TimelineDot: React.FC<{ isCompleted: boolean }> = ({isCompleted}) => ( -
+const timelineItemVariants = cva("grid items-center gap-x-2", { + variants: { + status: { + done: "text-primary", + default: "text-muted-foreground", + }, + }, + defaultVariants: { + status: "default", + }, +}); + +interface TimelineItemProps extends React.HTMLAttributes, VariantProps {} + +const TimelineItem = React.forwardRef(({ className, status, ...props }, ref) => ( +
  • +)); +TimelineItem.displayName = "TimelineItem"; + +const timelineDotVariants = cva( + "col-start-2 col-end-3 row-start-1 row-end-1 flex size-4 items-center justify-center rounded-full border border-current", + { + variants: { + status: { + default: "[&>svg]:hidden", + current: "[&>.lucide-circle]:fill-current [&>.lucide-circle]:text-current [&>svg:not(.lucide-circle)]:hidden", + done: "bg-primary [&>.lucide-check]:text-background [&>svg:not(.lucide-check)]:hidden", + error: "border-destructive bg-destructive [&>.lucide-x]:text-background [&>svg:not(.lucide-x)]:hidden", + }, + }, + defaultVariants: { + status: "default", + }, + }, ); -type TimelineItemProps = { - children: React.ReactNode; - className?: string; - isCompleted?: boolean; - isCurrent?: boolean; - orientation?: 'horizontal' | 'vertical'; -}; - -const TimelineItem: React.FC = ({ - children, - className, - isCompleted, - isCurrent, - orientation = 'vertical' - }) => ( -
    - - {orientation === 'vertical' ? {children} : -
    {children}
    } +interface TimelineDotProps extends React.HTMLAttributes, VariantProps { + customIcon?: React.ReactNode; +} + +const TimelineDot = React.forwardRef( + ({ className, status, customIcon, ...props }, ref) => ( +
    + + + + {customIcon}
    + ), ); +TimelineDot.displayName = "TimelineDot"; -TimelineItem.displayName = "TimelineItem"; +const timelineContentVariants = cva("row-start-2 row-end-2 pb-8 text-muted-foreground", { + variants: { + side: { + right: "col-start-3 col-end-4 mr-auto text-left", + left: "col-start-1 col-end-2 ml-auto text-right", + }, + }, + defaultVariants: { + side: "right", + }, +}); + +interface TimelineConent + extends React.HTMLAttributes, + VariantProps {} + +const TimelineContent = React.forwardRef(({ className, side, ...props }, ref) => ( +

    +)); +TimelineContent.displayName = "TimelineContent"; + +const timelineHeadingVariants = cva("row-start-1 row-end-1 line-clamp-1 max-w-full truncate", { + variants: { + side: { + right: "col-start-3 col-end-4 mr-auto text-left", + left: "col-start-1 col-end-2 ml-auto text-right", + }, + variant: { + primary: "text-base font-medium text-primary", + secondary: "text-sm font-light text-muted-foreground", + }, + }, + defaultVariants: { + side: "right", + variant: "primary", + }, +}); + +interface TimelineConent + extends React.HTMLAttributes, + VariantProps {} -type TimelineProps = { - children: React.ReactNode; - orientation?: 'horizontal' | 'vertical'; - currentStepIndex: number; -}; -const Timeline: React.FC = ({children, orientation = 'vertical', currentStepIndex}) => { - const timelineItems = React.Children.toArray(children); +const TimelineHeading = React.forwardRef( + ({ className, side, variant, ...props }, ref) => ( +

    + ), +); +TimelineHeading.displayName = "TimelineHeading"; + +interface TimelineLineProps extends React.HTMLAttributes { + done?: boolean; +} +const TimelineLine = React.forwardRef( + ({ className, done = false, ...props }, ref) => { return ( -

    - {timelineItems.map((child, index) => ( - - {index > 0 && ( - orientation === 'vertical' ? -
    : -
    - )} - {React.cloneElement(child as React.ReactElement, { - orientation, - isCompleted: index < currentStepIndex - })} -
    - ))} -
    +
    ); -}; - -Timeline.displayName = "Timeline"; + }, +); +TimelineLine.displayName = "TimelineLine"; -export {Timeline, TimelineItem}; +export { Timeline, TimelineDot, TimelineItem, TimelineContent, TimelineHeading, TimelineLine };