feat(stepper): implement multi-step wizard for appointment confirmation process
This commit is contained in:
@@ -34,11 +34,28 @@ beforeEach(() => {
|
||||
post.mockResolvedValue({ success: true, data: {} });
|
||||
});
|
||||
|
||||
/** همهٔ فیلدهای مبلغ (تومان) در ردیفهای پرداخت. */
|
||||
/**
|
||||
* فرم ویزارد شد: «بیمه و هزینه» → «پرداخت» → «تأیید».
|
||||
* ردیفهای پرداخت در مرحلهٔ دوماند و وضعیت پرداخت در مرحلهٔ سوم.
|
||||
*/
|
||||
const nextStep = () => fireEvent.click(screen.getByRole('button', { name: 'مرحلهٔ بعد' }));
|
||||
|
||||
/** همهٔ فیلدهای مبلغ (تومان) در ردیفهای پرداخت — مرحلهٔ «پرداخت». */
|
||||
function amountInputs() {
|
||||
return screen.getAllByPlaceholderText('0') as HTMLInputElement[];
|
||||
}
|
||||
|
||||
/** از مرحلهٔ «بیمه و هزینه» به «پرداخت» میرود و ردیفهای مبلغ را برمیگرداند. */
|
||||
function goToPayment() {
|
||||
nextStep();
|
||||
return amountInputs();
|
||||
}
|
||||
|
||||
/** تا مرحلهٔ آخر جلو میرود؛ وضعیت پرداخت و دکمهٔ قطعی آنجا هستند. */
|
||||
function goToReview() {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'مرحلهٔ بعد' }));
|
||||
}
|
||||
|
||||
describe('ConfirmAppointmentModal', () => {
|
||||
it('بیمار، اقلام هزینه و جمع کل را نشان میدهد', () => {
|
||||
render();
|
||||
@@ -51,29 +68,36 @@ describe('ConfirmAppointmentModal', () => {
|
||||
it('ردیفِ اول پیشفرض برابر کل هزینه است و وضعیت «تسویه کامل» میشود', () => {
|
||||
render();
|
||||
// ۳٬۰۰۰٬۰۰۰ ریال = ۳۰۰٬۰۰۰ تومان
|
||||
expect(amountInputs()[0]).toHaveValue('۳۰۰٬۰۰۰');
|
||||
expect(goToPayment()[0]).toHaveValue('۳۰۰٬۰۰۰');
|
||||
goToReview();
|
||||
expect(screen.getByText('تسویه کامل')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('با تغییر دستی مبلغ به کمتر از کل، وضعیت «پرداخت جزئی» میشود', () => {
|
||||
render();
|
||||
fireEvent.change(amountInputs()[0], { target: { value: '100000' } });
|
||||
fireEvent.change(goToPayment()[0], { target: { value: '100000' } });
|
||||
goToReview();
|
||||
expect(screen.getByText('پرداخت جزئی')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('دکمهٔ تأیید فقط با مجموعِ بیشتر از جمع کل غیرفعال میشود', () => {
|
||||
render();
|
||||
const submit = screen.getByRole('button', { name: 'تأیید و قطعی کردن' });
|
||||
expect(submit).not.toBeDisabled();
|
||||
const rows = goToPayment();
|
||||
|
||||
fireEvent.change(amountInputs()[0], { target: { value: '9000000' } });
|
||||
fireEvent.change(rows[0], { target: { value: '9000000' } });
|
||||
expect(screen.getByText('مجموع پرداختها از مبلغ قابل پرداخت بیشتر است.')).toBeInTheDocument();
|
||||
expect(submit).toBeDisabled();
|
||||
// پرداختِ بیشتر از مبلغ، حتی جلوی رفتن به مرحلهٔ تأیید را میگیرد.
|
||||
expect(screen.getByRole('button', { name: 'مرحلهٔ بعد' })).toBeDisabled();
|
||||
|
||||
fireEvent.change(rows[0], { target: { value: '100000' } });
|
||||
goToReview();
|
||||
expect(screen.getByRole('button', { name: 'تأیید و قطعی کردن' })).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('پرداخت جزئی مجاز است و همان یک روش را ثبت میکند', async () => {
|
||||
render();
|
||||
fireEvent.change(amountInputs()[0], { target: { value: '100000' } });
|
||||
fireEvent.change(goToPayment()[0], { target: { value: '100000' } });
|
||||
goToReview();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'تأیید و قطعی کردن' }));
|
||||
|
||||
await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/appointment/a1/confirm', {
|
||||
@@ -85,7 +109,7 @@ describe('ConfirmAppointmentModal', () => {
|
||||
it('تقسیم پرداخت بین دو روش: مجموع ردیفها بهصورت آرایه ثبت میشود', async () => {
|
||||
render();
|
||||
// ردیف اول را به ۲۰۰٬۰۰۰ تومان کم میکنیم
|
||||
fireEvent.change(amountInputs()[0], { target: { value: '200000' } });
|
||||
fireEvent.change(goToPayment()[0], { target: { value: '200000' } });
|
||||
// افزودن روش دوم — پیشفرض با باقیماندهٔ ۱۰۰٬۰۰۰ تومان پر میشود
|
||||
fireEvent.click(screen.getByRole('button', { name: /افزودن روش/ }));
|
||||
|
||||
@@ -94,6 +118,7 @@ describe('ConfirmAppointmentModal', () => {
|
||||
expect(inputs[1]).toHaveValue('۱۰۰٬۰۰۰');
|
||||
|
||||
// مجموع = ۳۰۰٬۰۰۰ تومان = کل ⇒ تسویه کامل
|
||||
goToReview();
|
||||
expect(screen.getByText('تسویه کامل')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'تأیید و قطعی کردن' }));
|
||||
@@ -108,6 +133,7 @@ describe('ConfirmAppointmentModal', () => {
|
||||
|
||||
it('حذف ردیف اضافهشده مجموع را دوباره محاسبه میکند', () => {
|
||||
render();
|
||||
goToPayment();
|
||||
fireEvent.click(screen.getByRole('button', { name: /افزودن روش/ }));
|
||||
expect(amountInputs()).toHaveLength(2);
|
||||
|
||||
@@ -220,8 +246,9 @@ describe('ConfirmAppointmentModal — انتخاب بیمه', () => {
|
||||
|
||||
// ۵٬۹۵۲٬۰۰۰ × ۷۰٪ = ۴٬۱۶۶٬۴۰۰ سهم بیمه · ۱٬۷۸۵٬۶۰۰ سهم بیمار
|
||||
expect(await screen.findByText('سهم بیمار (قابل پرداخت)')).toBeInTheDocument();
|
||||
expect(amountInputs()[0]).toHaveValue('۱۷۸٬۵۶۰');
|
||||
expect(goToPayment()[0]).toHaveValue('۱۷۸٬۵۶۰');
|
||||
|
||||
goToReview();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'تأیید و قطعی کردن' }));
|
||||
await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/appointment/a1/confirm', {
|
||||
version: 1,
|
||||
@@ -252,8 +279,10 @@ describe('ConfirmAppointmentModal — انتخاب بیمه', () => {
|
||||
// تکمیلی ۹۰٪ منهای فرانشیز ۱۰٪ → ۱٬۴۲۸٬۴۸۰؛ سهم بیمار ۳۵۷٬۱۲۰ ریال = ۳۵٬۷۱۲ تومان.
|
||||
expect(await screen.findByText(/سهم بیمه پایه/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/سهم بیمه تکمیلی/)).toBeInTheDocument();
|
||||
nextStep();
|
||||
await waitFor(() => expect(amountInputs()[0]).toHaveValue('۳۵٬۷۱۲'));
|
||||
|
||||
goToReview();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'تأیید و قطعی کردن' }));
|
||||
await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/appointment/a1/confirm', {
|
||||
version: 1,
|
||||
@@ -273,8 +302,9 @@ describe('ConfirmAppointmentModal — انتخاب بیمه', () => {
|
||||
await pick('بدون بیمه تکمیلی', 'بیمه آسیا');
|
||||
|
||||
// بدون پایه: ۹۰٪ منهای فرانشیز ۱۰٪ از ۵٬۹۵۲٬۰۰۰ → ۴٬۷۶۱٬۶۰۰؛ بیمار ۱٬۱۹۰٬۴۰۰.
|
||||
await waitFor(() => expect(amountInputs()[0]).toHaveValue('۱۱۹٬۰۴۰'));
|
||||
expect(screen.queryByText(/سهم بیمه پایه/)).not.toBeInTheDocument();
|
||||
nextStep();
|
||||
await waitFor(() => expect(amountInputs()[0]).toHaveValue('۱۱۹٬۰۴۰'));
|
||||
});
|
||||
|
||||
it('نوبتِ بدون هزینهٔ ویزیت، «قیمت ویزیت آزاد» تنظیمات را نشان میدهد (نه صفر)', async () => {
|
||||
@@ -288,15 +318,18 @@ describe('ConfirmAppointmentModal — انتخاب بیمه', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText('مبلغ قابل پرداخت')).toBeInTheDocument();
|
||||
// ۵٬۹۵۲٬۰۰۰ ریال = ۵۹۵٬۲۰۰ تومان — همان مبلغی که سرور روی مراجعه میگذارد.
|
||||
nextStep();
|
||||
await waitFor(() => expect(amountInputs()[0]).toHaveValue('۵۹۵٬۲۰۰'));
|
||||
expect(screen.getByText('مبلغ قابل پرداخت')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('هزینهٔ ویزیتِ خودِ نوبت بر «قیمت ویزیت آزاد» اولویت دارد', async () => {
|
||||
mockInsurance(BOTH, 9_000_000);
|
||||
renderReference(); // نوبت خودش ۵٬۹۵۲٬۰۰۰ دارد
|
||||
|
||||
await screen.findByText('مبلغ قابل پرداخت');
|
||||
nextStep();
|
||||
await waitFor(() => expect(amountInputs()[0]).toHaveValue('۵۹۵٬۲۰۰'));
|
||||
});
|
||||
|
||||
@@ -309,6 +342,53 @@ describe('ConfirmAppointmentModal — انتخاب بیمه', () => {
|
||||
await pick('بدون بیمه', 'بیمه ایران');
|
||||
|
||||
// ۵٬۹۵۲٬۰۰۰ × ۳۰٪ = ۱٬۷۸۵٬۶۰۰ سهم بیمه · ۴٬۱۶۶٬۴۰۰ سهم بیمار
|
||||
await screen.findByText('سهم بیمار (قابل پرداخت)');
|
||||
nextStep();
|
||||
await waitFor(() => expect(amountInputs()[0]).toHaveValue('۴۱۶٬۶۴۰'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConfirmAppointmentModal — ویزارد', () => {
|
||||
const stepLabels = () =>
|
||||
Array.from(document.querySelectorAll('ol[aria-label="مراحل قطعی کردن نوبت"] li'))
|
||||
.map(li => li.textContent?.replace(/^\d+/, '').trim());
|
||||
|
||||
it('سه مرحله دارد و از «بیمه و هزینه» شروع میشود', () => {
|
||||
render();
|
||||
|
||||
expect(stepLabels()).toEqual(['بیمه و هزینه', 'پرداخت', 'تأیید']);
|
||||
// جدول هزینه در مرحلهٔ اول است، ردیف پرداخت هنوز نه.
|
||||
expect(screen.getByText('جمع کل')).toBeInTheDocument();
|
||||
expect(screen.queryByPlaceholderText('0')).toBeNull();
|
||||
});
|
||||
|
||||
it('در مرحلهٔ اول دکمهٔ قطعی وجود ندارد و «مرحلهٔ قبل» هم نیست', () => {
|
||||
render();
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'تأیید و قطعی کردن' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'مرحلهٔ قبل' })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'انصراف' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('مرحلهٔ قبل مقادیر واردشده را نگه میدارد', () => {
|
||||
render();
|
||||
|
||||
fireEvent.change(goToPayment()[0], { target: { value: '100000' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'مرحلهٔ قبل' }));
|
||||
expect(screen.getByText('جمع کل')).toBeInTheDocument();
|
||||
|
||||
// برگشت به پرداخت: مبلغ همان است، نه پیشفرضِ کل.
|
||||
nextStep();
|
||||
expect(amountInputs()[0]).toHaveValue('۱۰۰٬۰۰۰');
|
||||
});
|
||||
|
||||
it('مرحلهٔ آخر روشهای پرداخت را قبل از ثبت خلاصه میکند', () => {
|
||||
render();
|
||||
|
||||
fireEvent.change(goToPayment()[0], { target: { value: '100000' } });
|
||||
goToReview();
|
||||
|
||||
expect(screen.getByText('پرداخت نقدی')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'تأیید و قطعی کردن' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useAppointmentInsurance } from '../../hooks/useAppointmentInsurance';
|
||||
import Modal from '../ui/Modal';
|
||||
import PriceInput from '../ui/PriceInput';
|
||||
import SearchableSelect from '../ui/SearchableSelect';
|
||||
import Stepper from '../ui/Stepper';
|
||||
|
||||
/** همان چهار روشِ SessionPayment::METHODS در بکاند. */
|
||||
const METHOD_OPTIONS = [
|
||||
@@ -49,6 +50,20 @@ interface Props {
|
||||
queryKey?: unknown[];
|
||||
}
|
||||
|
||||
/**
|
||||
* مراحلِ قطعی کردن: اول «چقدر»، بعد «چطور»، آخر «تأیید».
|
||||
*
|
||||
* فرم قبلاً یک صفحهٔ بلند بود — بیمه، جدول هزینه، ردیفهای پرداخت و خلاصه با هم — و
|
||||
* با دو روش پرداخت از ارتفاع صفحه بلندتر میشد.
|
||||
*/
|
||||
const CONFIRM_STEPS = [
|
||||
{ key: 'cost', title: 'بیمه و هزینه' },
|
||||
{ key: 'payment', title: 'پرداخت' },
|
||||
{ key: 'review', title: 'تأیید' },
|
||||
] as const;
|
||||
|
||||
type ConfirmStepKey = (typeof CONFIRM_STEPS)[number]['key'];
|
||||
|
||||
/** یک ردیفِ پرداخت در تسویهٔ چندروشی. */
|
||||
interface PaymentRow {
|
||||
id: number;
|
||||
@@ -99,6 +114,7 @@ export default function ConfirmAppointmentModal({
|
||||
const [rows, setRows] = useState<PaymentRow[]>([makeRow()]);
|
||||
/** تا وقتی کاربر مبلغ را دست نزده، ردیفِ اول با کل مبلغ پر میماند. */
|
||||
const [touched, setTouched] = useState(false);
|
||||
const [stepIdx, setStepIdx] = useState(0);
|
||||
|
||||
// وقتی صفحهی میزبان نوبت را ندارد (مثل ردیف لیست) خودمان جزئیات را میگیریم:
|
||||
// مبلغ ویزیت و قیمت سرویسها فقط در detail هستند.
|
||||
@@ -225,6 +241,7 @@ export default function ConfirmAppointmentModal({
|
||||
nextId.current = 1;
|
||||
setRows([makeRow()]);
|
||||
setTouched(false);
|
||||
setStepIdx(0);
|
||||
}
|
||||
|
||||
function patchRow(id: number, patch: Partial<PaymentRow>) {
|
||||
@@ -254,6 +271,8 @@ export default function ConfirmAppointmentModal({
|
||||
}
|
||||
|
||||
const loading = detailQuery.isLoading && !appointment;
|
||||
const currentStep: ConfirmStepKey = CONFIRM_STEPS[Math.min(stepIdx, CONFIRM_STEPS.length - 1)].key;
|
||||
const isLastStep = currentStep === 'review';
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -263,17 +282,34 @@ export default function ConfirmAppointmentModal({
|
||||
onClose={handleClose}
|
||||
footer={
|
||||
<>
|
||||
{/* بستنِ فرم همیشه یک کلیک است؛ «مرحلهٔ قبل» جایش را نمیگیرد. */}
|
||||
<button type="button" className="btn ghost" onClick={handleClose}>
|
||||
انصراف
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn primary"
|
||||
disabled={loading || overpaid || confirmMut.isPending}
|
||||
onClick={() => confirmMut.mutate()}
|
||||
>
|
||||
{confirmMut.isPending ? 'در حال ثبت…' : 'تأیید و قطعی کردن'}
|
||||
</button>
|
||||
{stepIdx > 0 && (
|
||||
<button type="button" className="btn secondary" onClick={() => setStepIdx(i => i - 1)}>
|
||||
مرحلهٔ قبل
|
||||
</button>
|
||||
)}
|
||||
{isLastStep ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn primary"
|
||||
disabled={loading || overpaid || confirmMut.isPending}
|
||||
onClick={() => confirmMut.mutate()}
|
||||
>
|
||||
{confirmMut.isPending ? 'در حال ثبت…' : 'تأیید و قطعی کردن'}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="btn primary"
|
||||
disabled={loading || overpaid}
|
||||
onClick={() => setStepIdx(i => i + 1)}
|
||||
>
|
||||
مرحلهٔ بعد
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
@@ -295,6 +331,14 @@ export default function ConfirmAppointmentModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Stepper
|
||||
steps={CONFIRM_STEPS.map(st => ({ key: st.key, title: st.title }))}
|
||||
current={currentStep}
|
||||
ariaLabel="مراحل قطعی کردن نوبت"
|
||||
/>
|
||||
|
||||
{currentStep === 'cost' && (
|
||||
<>
|
||||
{/* بیمه — نوع خدمت فقط وقتی چند نوع فعال است پرسیده میشود. */}
|
||||
<div style={{ display: 'flex', gap: 10, marginBottom: 16, flexWrap: 'wrap' }}>
|
||||
{insurance.needsCategoryChoice && (
|
||||
@@ -390,6 +434,11 @@ export default function ConfirmAppointmentModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</>
|
||||
)}
|
||||
|
||||
{currentStep === 'payment' && (
|
||||
<>
|
||||
{/* پرداختها — تقسیم بین چند روش */}
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||
@@ -508,8 +557,31 @@ export default function ConfirmAppointmentModal({
|
||||
مجموع پرداختها از مبلغ قابل پرداخت بیشتر است.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
{currentStep === 'review' && (
|
||||
<>
|
||||
{/* خلاصهٔ همان چیزی که ثبت میشود */}
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid var(--border)', borderRadius: 'var(--r)',
|
||||
padding: '4px 14px 10px', marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<div style={rowStyle}>
|
||||
<span>مبلغ قابل پرداخت</span>
|
||||
<strong style={{ color: 'var(--text)' }}>{formatRial(payable)}</strong>
|
||||
</div>
|
||||
{rows.filter(r => tomanToRial(r.amountToman) > 0).map(r => (
|
||||
<div key={r.id} style={{ ...rowStyle, borderTop: '1px solid var(--border)' }}>
|
||||
<span>{METHOD_OPTIONS.find(m => m.value === r.method)?.label ?? r.method}</span>
|
||||
<strong style={{ color: 'var(--text)' }}>{formatRial(tomanToRial(r.amountToman))}</strong>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* خلاصه */}
|
||||
<div
|
||||
style={{
|
||||
background: 'var(--surface-2)', border: '1px solid var(--border)',
|
||||
@@ -540,6 +612,8 @@ export default function ConfirmAppointmentModal({
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
@@ -11,6 +11,7 @@ import Modal from '../ui/Modal';
|
||||
import PersianDateInput from '../ui/PersianDateInput';
|
||||
import PriceInput from '../ui/PriceInput';
|
||||
import SearchableSelect from '../ui/SearchableSelect';
|
||||
import Stepper from '../ui/Stepper';
|
||||
import ServiceSlotPicker from './ServiceSlotPicker';
|
||||
import type { ServicePick } from './ServiceSlotPicker';
|
||||
import type { BookingService } from '../../hooks/useDoctorBookingServices';
|
||||
@@ -379,7 +380,11 @@ export default function NewAppointmentModal({
|
||||
</div>
|
||||
|
||||
{stepKeys.length > 1 && (
|
||||
<Stepper keys={stepKeys} current={currentStep} />
|
||||
<Stepper
|
||||
steps={stepKeys.map(key => ({ key, title: STEP_TITLES[key] }))}
|
||||
current={currentStep}
|
||||
ariaLabel="مراحل ثبت نوبت"
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStep === 'service' && (
|
||||
@@ -685,57 +690,3 @@ function Step({ title, children }: { title: string; children: ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* نوار مراحل — «کجای کارم و چقدر مانده».
|
||||
*
|
||||
* فرم قبلاً همهٔ مراحل را با هم نشان میداد و مودال از ارتفاع صفحه بلندتر میشد؛
|
||||
* حالا هر بار یک مرحله دیده میشود و این نوار تنها چیزی است که کلِ مسیر را میگوید.
|
||||
*/
|
||||
function Stepper({ keys, current }: { keys: StepKey[]; current: StepKey }) {
|
||||
const activeIdx = keys.indexOf(current);
|
||||
|
||||
return (
|
||||
<ol
|
||||
aria-label="مراحل ثبت نوبت"
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8, listStyle: 'none',
|
||||
margin: '0 0 18px', padding: 0,
|
||||
}}
|
||||
>
|
||||
{keys.map((key, idx) => {
|
||||
const done = idx < activeIdx;
|
||||
const active = idx === activeIdx;
|
||||
|
||||
return (
|
||||
<li
|
||||
key={key}
|
||||
aria-current={active ? 'step' : undefined}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 8, flex: idx === keys.length - 1 ? '0 0 auto' : 1 }}
|
||||
>
|
||||
<span style={{
|
||||
display: 'grid', placeItems: 'center', width: 22, height: 22, borderRadius: 999,
|
||||
flexShrink: 0, fontSize: 11.5, fontWeight: 700,
|
||||
background: active || done ? 'var(--primary)' : 'var(--surface-3)',
|
||||
color: active || done ? 'var(--on-primary)' : 'var(--text-3)',
|
||||
}}>
|
||||
{idx + 1}
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: 12.5, whiteSpace: 'nowrap',
|
||||
fontWeight: active ? 700 : 500,
|
||||
color: active ? 'var(--text)' : 'var(--text-3)',
|
||||
}}>
|
||||
{STEP_TITLES[key]}
|
||||
</span>
|
||||
{idx < keys.length - 1 && (
|
||||
<span style={{
|
||||
flex: 1, height: 1, minWidth: 12,
|
||||
background: done ? 'var(--primary)' : 'var(--border)',
|
||||
}} />
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user