@umituz/web-design-system 5.0.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Stepper Component (Organism)
3
+ * @description Horizontal or vertical step indicator with derived status,
4
+ * optional click navigation and per-step override/lock control
5
+ */
6
+
7
+ import type { ReactNode } from 'react';
8
+ import { Check } from 'lucide-react';
9
+ import { cn } from '../../infrastructure/utils';
10
+
11
+ export type StepperStepStatus = 'completed' | 'current' | 'upcoming' | 'error';
12
+
13
+ export interface StepperStep {
14
+ id: string;
15
+ label: ReactNode;
16
+ description?: ReactNode;
17
+ icon?: ReactNode;
18
+ status?: StepperStepStatus;
19
+ locked?: boolean;
20
+ optional?: boolean;
21
+ }
22
+
23
+ export interface StepperProps {
24
+ steps: readonly StepperStep[];
25
+ currentStep: number;
26
+ orientation?: 'horizontal' | 'vertical';
27
+ onStepSelect?: (index: number) => void;
28
+ canSelect?: (index: number) => boolean;
29
+ ariaLabel?: string;
30
+ className?: string;
31
+ }
32
+
33
+ function deriveStatus(step: StepperStep, index: number, currentStep: number): StepperStepStatus {
34
+ if (step.status !== undefined) return step.status;
35
+ if (index < currentStep) return 'completed';
36
+ if (index === currentStep) return 'current';
37
+ return 'upcoming';
38
+ }
39
+
40
+ const dotClasses: Record<StepperStepStatus, string> = {
41
+ completed: 'flex size-7 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground',
42
+ current: 'flex size-7 shrink-0 items-center justify-center rounded-full border-2 border-primary bg-background text-primary ring-4 ring-primary/15',
43
+ error: 'flex size-7 shrink-0 items-center justify-center rounded-full border-2 border-destructive bg-background text-destructive ring-4 ring-destructive/15',
44
+ upcoming: 'flex size-7 shrink-0 items-center justify-center rounded-full border border-border bg-muted text-muted-foreground',
45
+ };
46
+
47
+ const labelClasses: Record<StepperStepStatus, string> = {
48
+ completed: 'text-xs font-semibold text-foreground',
49
+ current: 'text-xs font-bold text-foreground',
50
+ error: 'text-xs font-bold text-destructive',
51
+ upcoming: 'text-xs font-medium text-muted-foreground',
52
+ };
53
+
54
+ function StepperNode({ step, index, status }: { step: StepperStep; index: number; status: StepperStepStatus }) {
55
+ return (
56
+ <>
57
+ <span className={dotClasses[status]}>
58
+ {status === 'completed' && step.icon === undefined ? (
59
+ <Check className="size-4" aria-hidden="true" />
60
+ ) : (
61
+ step.icon ?? <span className="text-[11px] font-bold">{index + 1}</span>
62
+ )}
63
+ </span>
64
+ <span className="flex min-w-0 flex-col gap-0.5 text-left">
65
+ <span className={cn(labelClasses[status], 'flex items-center gap-1.5')}>
66
+ {step.label}
67
+ {step.optional && <span className="font-normal text-muted-foreground">(optional)</span>}
68
+ </span>
69
+ {step.description !== undefined && (
70
+ <span className="text-[11px] leading-snug text-muted-foreground">{step.description}</span>
71
+ )}
72
+ </span>
73
+ </>
74
+ );
75
+ }
76
+
77
+ export function Stepper({
78
+ steps,
79
+ currentStep,
80
+ orientation = 'horizontal',
81
+ onStepSelect,
82
+ canSelect,
83
+ ariaLabel,
84
+ className,
85
+ }: StepperProps) {
86
+ const interactive = onStepSelect !== undefined;
87
+ return (
88
+ <ol
89
+ aria-label={ariaLabel}
90
+ className={cn(
91
+ orientation === 'horizontal' ? 'flex w-full items-start gap-2' : 'flex w-full flex-col',
92
+ className,
93
+ )}
94
+ >
95
+ {steps.map((step, index) => {
96
+ const status = deriveStatus(step, index, currentStep);
97
+ const clickable =
98
+ interactive && !step.locked && (canSelect ? canSelect(index) : index < currentStep);
99
+ const last = index === steps.length - 1;
100
+ return (
101
+ <li
102
+ key={step.id}
103
+ aria-current={status === 'current' ? 'step' : undefined}
104
+ className={cn(
105
+ orientation === 'horizontal'
106
+ ? 'flex min-w-0 flex-1 items-center gap-2'
107
+ : cn('relative flex items-start gap-3', !last && 'pb-5'),
108
+ )}
109
+ >
110
+ {orientation === 'vertical' && !last && (
111
+ <span
112
+ aria-hidden="true"
113
+ className={cn(
114
+ 'absolute bottom-1 left-3.5 top-9 w-px',
115
+ status === 'completed' ? 'bg-primary/60' : 'bg-border',
116
+ )}
117
+ />
118
+ )}
119
+ {clickable ? (
120
+ <button
121
+ type="button"
122
+ onClick={() => onStepSelect?.(index)}
123
+ className="flex min-w-0 cursor-pointer items-center gap-2 rounded-md p-0.5 text-left transition-colors hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
124
+ >
125
+ <StepperNode step={step} index={index} status={status} />
126
+ </button>
127
+ ) : (
128
+ <div className="flex min-w-0 items-center gap-2">
129
+ <StepperNode step={step} index={index} status={status} />
130
+ </div>
131
+ )}
132
+ {orientation === 'horizontal' && !last && (
133
+ <span
134
+ aria-hidden="true"
135
+ className={cn(
136
+ 'hidden h-px min-w-4 flex-1 sm:block',
137
+ status === 'completed' ? 'bg-primary/60' : 'bg-border',
138
+ )}
139
+ />
140
+ )}
141
+ </li>
142
+ );
143
+ })}
144
+ </ol>
145
+ );
146
+ }
@@ -178,5 +178,11 @@ export type {
178
178
  export { ConfirmDialog } from './ConfirmDialog';
179
179
  export type { ConfirmDialogProps } from './ConfirmDialog';
180
180
 
181
+ export { Stepper } from './Stepper';
182
+ export type { StepperProps, StepperStep, StepperStepStatus } from './Stepper';
183
+
184
+ export { PlanPicker } from './PlanPicker';
185
+ export type { PlanPickerProps, PlanPickerPlan, PlanPickerCycle } from './PlanPicker';
186
+
181
187
  export { CoverArt } from './CoverArt';
182
188
  export type { CoverArtProps } from './CoverArt';