@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.
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/__tests__/components.smoke.test.tsx +34 -0
- package/src/__tests__/markdown-text.test.tsx +85 -0
- package/src/__tests__/modal.test.tsx +46 -0
- package/src/__tests__/plan-picker.test.tsx +81 -0
- package/src/__tests__/stepper.test.tsx +100 -0
- package/src/presentation/molecules/MarkdownText.tsx +171 -0
- package/src/presentation/molecules/index.ts +3 -0
- package/src/presentation/organisms/Modal.tsx +15 -3
- package/src/presentation/organisms/PlanPicker.tsx +202 -0
- package/src/presentation/organisms/Stepper.tsx +146 -0
- package/src/presentation/organisms/index.ts +6 -0
|
@@ -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';
|