frontend phase 0: app shells, design system & data/contract patterns

Turn the starter into the Balinyaar foundation for the three actor
experiences and lock in the patterns later phases copy.

- Cleanup: remove toastDemo namespace, placeholder home page, and the two
  dead icons; fix BottomBar to use usePathname (locale-aware active tab).
- Three actor shells under (private-routes), no layout above [locale]:
  customer (customer) group with the 5-tab bottom nav; nurse (/nurse) and
  admin (/admin) on the shared sidebar engine. Role model via constants/roles
  + useActorRole (defaults to customer until roles land in f1-b2).
- services/{domain} reference (patients) with a mock behind a config seam,
  hierarchical query keys, deliberate staleTime, and mutation invalidation;
  shared ApiEnvelope/Paginated wire types + unwrap() in lib/api/types.
- Money (integer-safe IRR/Toman) + Shamsi-date utils; toEnglishDigits helper.
- Shared composites, each tested: OtpInput, PhoneNumberField, StepperHeader,
  StatusChip, PlaceholderScreen.
- i18n: seed nav/common/shell/patients in both locales; document namespace
  conventions. Update client/CLAUDE.md Project Structure + fix ColorSchemeScript
  doc drift. Add phase report, STATUS, and REQ-001 (envelope/casing/pagination).

Gate: npm run check + test:ci green (72 tests); build green with NEXT_PUBLIC_API_URL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamid
2026-07-02 01:19:21 +03:30
parent 2f2aec61a2
commit 94fdcbe0d1
65 changed files with 1632 additions and 227 deletions
@@ -0,0 +1,49 @@
import { useState } from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ThemeProvider } from '../../theme';
import OtpInput from './OtpInput';
function Harness({ length = 4, onComplete }: { length?: number; onComplete?: (v: string) => void }) {
const [value, setValue] = useState('');
return (
<ThemeProvider>
<OtpInput length={length} value={value} onChange={setValue} onComplete={onComplete} aria-label="code" />
</ThemeProvider>
);
}
describe('<OtpInput/> component', () => {
it('renders one box per length', () => {
render(<Harness length={5} />);
expect(screen.getAllByRole('textbox')).toHaveLength(5);
});
it('accepts a digit into the first box', async () => {
const user = userEvent.setup();
render(<Harness length={4} />);
const boxes = screen.getAllByRole('textbox') as HTMLInputElement[];
await user.type(boxes[0], '7');
expect(boxes[0].value).toBe('7');
});
it('normalizes Persian digits', async () => {
const user = userEvent.setup();
render(<Harness length={4} />);
const boxes = screen.getAllByRole('textbox') as HTMLInputElement[];
await user.type(boxes[0], '۹');
expect(boxes[0].value).toBe('9');
});
it('calls onComplete with the full code once every box is filled', async () => {
const user = userEvent.setup();
const onComplete = jest.fn();
render(<Harness length={4} onComplete={onComplete} />);
const boxes = screen.getAllByRole('textbox') as HTMLInputElement[];
await user.type(boxes[0], '1');
await user.type(boxes[1], '2');
await user.type(boxes[2], '3');
await user.type(boxes[3], '4');
expect(onComplete).toHaveBeenCalledWith('1234');
});
});
+135
View File
@@ -0,0 +1,135 @@
'use client';
import { ChangeEvent, ClipboardEvent, FunctionComponent, KeyboardEvent, useEffect, useMemo, useRef } from 'react';
import { Stack, TextField } from '@mui/material';
import { digitsOnly } from '@/utils';
export interface OtpInputProps {
/** Number of digit boxes. */
length?: number;
/** Controlled value — the digits entered so far. */
value: string;
/** Emits the full concatenated value on every change. */
onChange: (value: string) => void;
/** Fired once the value reaches `length` digits. */
onComplete?: (value: string) => void;
disabled?: boolean;
autoFocus?: boolean;
error?: boolean;
/** Accessible label for the group. */
'aria-label'?: string;
}
const DEFAULT_LENGTH = 5;
const BOX_SIZE = 48;
/**
* One-time-code input: `length` single-digit boxes with auto-advance, backspace-to-previous,
* and paste distribution. Digits are normalized (Persian/Arabic → ASCII) and the group is
* forced LTR so codes read left-to-right even inside the RTL (`fa`) layout.
* @component OtpInput
*/
const OtpInput: FunctionComponent<OtpInputProps> = ({
length = DEFAULT_LENGTH,
value,
onChange,
onComplete,
disabled,
autoFocus,
error,
'aria-label': ariaLabel,
}) => {
const inputRefs = useRef<Array<HTMLInputElement | null>>([]);
const chars = useMemo(
() => Array.from({ length }, (_, index) => value[index] ?? ''),
[value, length]
);
useEffect(() => {
if (autoFocus) inputRefs.current[0]?.focus();
}, [autoFocus]);
const focusBox = (index: number) => {
const target = inputRefs.current[index];
if (target) {
target.focus();
target.select();
}
};
const emit = (next: string[]) => {
const joined = next.join('');
onChange(joined);
if (next.every((char) => char !== '')) onComplete?.(joined);
};
const handleChange = (index: number, event: ChangeEvent<HTMLInputElement>) => {
const incoming = digitsOnly(event.target.value);
const next = [...chars];
if (!incoming) {
next[index] = '';
emit(next);
return;
}
if (incoming.length === 1) {
next[index] = incoming;
focusBox(index + 1);
} else {
// Paste / multi-char: fill sequentially from the current box.
for (let offset = 0; offset < incoming.length && index + offset < length; offset += 1) {
next[index + offset] = incoming[offset];
}
focusBox(Math.min(index + incoming.length, length - 1));
}
emit(next);
};
const handleKeyDown = (index: number, event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Backspace' && !chars[index]) {
focusBox(index - 1);
}
};
const handlePaste = (index: number, event: ClipboardEvent<HTMLInputElement>) => {
event.preventDefault();
const pasted = digitsOnly(event.clipboardData.getData('text'));
if (!pasted) return;
const next = [...chars];
for (let offset = 0; offset < pasted.length && index + offset < length; offset += 1) {
next[index + offset] = pasted[offset];
}
focusBox(Math.min(index + pasted.length, length - 1));
emit(next);
};
return (
<Stack direction="row" spacing={1} dir="ltr" role="group" aria-label={ariaLabel} sx={{ justifyContent: 'center' }}>
{chars.map((char, index) => (
<TextField
key={index}
value={char}
disabled={disabled}
error={error}
onChange={(event) => handleChange(index, event as ChangeEvent<HTMLInputElement>)}
onKeyDown={(event) => handleKeyDown(index, event as KeyboardEvent<HTMLInputElement>)}
onPaste={(event) => handlePaste(index, event as ClipboardEvent<HTMLInputElement>)}
inputRef={(el: HTMLInputElement | null) => {
inputRefs.current[index] = el;
}}
slotProps={{
htmlInput: {
inputMode: 'numeric',
maxLength: 1,
'aria-label': `${ariaLabel ?? 'digit'} ${index + 1}`,
style: { textAlign: 'center', fontSize: '1.25rem', width: BOX_SIZE, padding: 8 },
},
}}
/>
))}
</Stack>
);
};
export default OtpInput;
+4
View File
@@ -0,0 +1,4 @@
import OtpInput from './OtpInput';
export type { OtpInputProps } from './OtpInput';
export { OtpInput as default, OtpInput };
@@ -0,0 +1,46 @@
import { useState } from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ThemeProvider } from '../../theme';
import PhoneNumberField, { isIranianMobile } from './PhoneNumberField';
function Harness({ initial = '' }: { initial?: string }) {
const [value, setValue] = useState(initial);
return (
<ThemeProvider>
<PhoneNumberField value={value} onChange={setValue} label="Phone" />
</ThemeProvider>
);
}
describe('<PhoneNumberField/> component', () => {
it('normalizes Persian digits to ASCII', async () => {
const user = userEvent.setup();
render(<Harness />);
const input = screen.getByRole('textbox') as HTMLInputElement;
await user.type(input, '۰۹۱۲');
expect(input.value).toBe('0912');
});
it('strips non-digit characters', async () => {
const user = userEvent.setup();
render(<Harness />);
const input = screen.getByRole('textbox') as HTMLInputElement;
await user.type(input, 'a0b9c1');
expect(input.value).toBe('091');
});
it('caps the value at 11 digits', async () => {
const user = userEvent.setup();
render(<Harness />);
const input = screen.getByRole('textbox') as HTMLInputElement;
await user.type(input, '0912345678999');
expect(input.value).toBe('09123456789');
});
it('validates Iranian mobile numbers', () => {
expect(isIranianMobile('09123456789')).toBe(true);
expect(isIranianMobile('0912345678')).toBe(false);
expect(isIranianMobile('19123456789')).toBe(false);
});
});
@@ -0,0 +1,50 @@
'use client';
import { ChangeEvent, FunctionComponent } from 'react';
import TextField, { TextFieldProps } from '@mui/material/TextField';
import { digitsOnly } from '@/utils';
/** Iranian mobile numbers are 11 digits, e.g. 09123456789. */
export const IRAN_MOBILE_LENGTH = 11;
/** True when `value` is a well-formed Iranian mobile number (11 digits starting 09). */
export function isIranianMobile(value: string): boolean {
return /^09\d{9}$/.test(value);
}
export interface PhoneNumberFieldProps extends Omit<TextFieldProps, 'onChange' | 'value' | 'type'> {
/** Controlled value — normalized ASCII digits, no formatting. */
value: string;
/** Emits the normalized digit string (Persian/Arabic digits converted, capped at 11). */
onChange: (value: string) => void;
}
/**
* Iranian mobile-number field. Normalizes Persian/Arabic digits to ASCII, strips
* non-digits, and caps length. Forces LTR digit entry so it stays correct inside the
* RTL (`fa`) layout. Text (label/placeholder/helperText) is passed in by the caller.
* @component PhoneNumberField
*/
const PhoneNumberField: FunctionComponent<PhoneNumberFieldProps> = ({ value, onChange, slotProps, ...rest }) => {
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
onChange(digitsOnly(event.target.value).slice(0, IRAN_MOBILE_LENGTH));
};
return (
<TextField
value={value}
onChange={handleChange}
slotProps={{
htmlInput: {
dir: 'ltr',
inputMode: 'numeric',
maxLength: IRAN_MOBILE_LENGTH,
style: { textAlign: 'start' },
},
...slotProps,
}}
{...rest}
/>
);
};
export default PhoneNumberField;
@@ -0,0 +1,5 @@
import PhoneNumberField from './PhoneNumberField';
export type { PhoneNumberFieldProps } from './PhoneNumberField';
export { IRAN_MOBILE_LENGTH, isIranianMobile } from './PhoneNumberField';
export { PhoneNumberField as default, PhoneNumberField };
@@ -0,0 +1,32 @@
import { FunctionComponent } from 'react';
import { render, screen } from '@testing-library/react';
import { ThemeProvider } from '../../theme';
import PlaceholderScreen, { PlaceholderScreenProps } from './PlaceholderScreen';
const ComponentToTest: FunctionComponent<PlaceholderScreenProps> = (props) => (
<ThemeProvider>
<PlaceholderScreen {...props} />
</ThemeProvider>
);
describe('<PlaceholderScreen/> component', () => {
it('renders the title as a heading', () => {
render(<ComponentToTest title="Bookings" />);
expect(screen.getByRole('heading', { name: 'Bookings' })).toBeInTheDocument();
});
it('renders the description when provided', () => {
render(<ComponentToTest title="Bookings" description="Coming soon in a later phase." />);
expect(screen.getByText('Coming soon in a later phase.')).toBeInTheDocument();
});
it('omits the description when not provided', () => {
render(<ComponentToTest title="Wallet" />);
expect(screen.queryByText(/phase/i)).not.toBeInTheDocument();
});
it('renders the named icon', () => {
const { container } = render(<ComponentToTest title="Patients" icon="patients" />);
expect(container.querySelector('[data-icon="patients"]')).toBeInTheDocument();
});
});
@@ -0,0 +1,36 @@
import { FunctionComponent } from 'react';
import { Stack, Typography } from '@mui/material';
import AppIcon from '../common/AppIcon';
export interface PlaceholderScreenProps {
/** Screen title (already translated by the caller). */
title: string;
/** Optional supporting line (already translated). */
description?: string;
/** Registered AppIcon name to show above the title. */
icon?: string;
}
/**
* Empty-state scaffold for screens whose real content lands in a later phase.
* Presentational only — the caller passes already-translated copy so the component
* stays i18n-agnostic and reusable across all three actor shells.
* @component PlaceholderScreen
*/
const PlaceholderScreen: FunctionComponent<PlaceholderScreenProps> = ({ title, description, icon }) => (
<Stack
sx={{ alignItems: 'center', justifyContent: 'center', textAlign: 'center', gap: 1.5, py: 8, px: 2 }}
>
{icon && <AppIcon icon={icon} size={48} color="var(--bal-secondary)" />}
<Typography variant="h5" component="h1">
{title}
</Typography>
{description && (
<Typography variant="body1" sx={{ color: 'text.secondary', maxWidth: 420 }}>
{description}
</Typography>
)}
</Stack>
);
export default PlaceholderScreen;
@@ -0,0 +1,4 @@
import PlaceholderScreen from './PlaceholderScreen';
export type { PlaceholderScreenProps } from './PlaceholderScreen';
export { PlaceholderScreen as default, PlaceholderScreen };
@@ -0,0 +1,27 @@
import { FunctionComponent } from 'react';
import { render, screen } from '@testing-library/react';
import { ThemeProvider } from '../../theme';
import StatusChip, { StatusChipProps } from './StatusChip';
const ComponentToTest: FunctionComponent<StatusChipProps> = (props) => (
<ThemeProvider>
<StatusChip {...props} />
</ThemeProvider>
);
describe('<StatusChip/> component', () => {
it('renders the label', () => {
render(<ComponentToTest status="verified" label="Verified" />);
expect(screen.getByText('Verified')).toBeInTheDocument();
});
it('exposes the status via a data attribute', () => {
const { container } = render(<ComponentToTest status="pending" label="Pending" />);
expect(container.querySelector('[data-status="pending"]')).toBeInTheDocument();
});
it('renders the status icon for rejected', () => {
const { container } = render(<ComponentToTest status="rejected" label="Rejected" />);
expect(container.querySelector('[data-icon="rejected"]')).toBeInTheDocument();
});
});
@@ -0,0 +1,50 @@
import { FunctionComponent } from 'react';
import Chip, { ChipProps } from '@mui/material/Chip';
import AppIcon from '../common/AppIcon';
export type StatusKind = 'verified' | 'active' | 'pending' | 'rejected' | 'info' | 'neutral';
interface StatusStyle {
bg: string;
fg: string;
icon: string;
}
// Colors come from the semantic --bal-* tokens (both schemes defined in tokens.css),
// so the chip switches with the color scheme automatically. Never hard-code a hex here.
const STATUS_STYLE: Record<StatusKind, StatusStyle> = {
verified: { bg: 'var(--bal-success)', fg: 'var(--bal-success-contrast)', icon: 'verified' },
active: { bg: 'var(--bal-success)', fg: 'var(--bal-success-contrast)', icon: 'verified' },
pending: { bg: 'var(--bal-warning)', fg: 'var(--bal-warning-contrast)', icon: 'pending' },
rejected: { bg: 'var(--bal-error)', fg: 'var(--bal-error-contrast)', icon: 'rejected' },
info: { bg: 'var(--bal-info)', fg: 'var(--bal-info-contrast)', icon: 'info' },
neutral: { bg: 'var(--bal-divider)', fg: 'var(--bal-text-secondary)', icon: 'info' },
};
export interface StatusChipProps extends Omit<ChipProps, 'color' | 'icon' | 'label'> {
/** Semantic status that drives the color and icon. */
status: StatusKind;
/** Display text — already translated by the caller (labels are i18n keys off the code). */
label: string;
}
/**
* Brand-harmonized status chip (verified / pending / rejected / …). Composed from MUI
* Chip + the AppIcon registry; colors resolve from the --bal-* semantic tokens.
* @component StatusChip
*/
const StatusChip: FunctionComponent<StatusChipProps> = ({ status, label, size = 'small', sx, ...rest }) => {
const style = STATUS_STYLE[status];
return (
<Chip
data-status={status}
size={size}
label={label}
icon={<AppIcon icon={style.icon} size={16} color={style.fg} />}
sx={{ backgroundColor: style.bg, color: style.fg, fontWeight: 600, ...sx }}
{...rest}
/>
);
};
export default StatusChip;
@@ -0,0 +1,4 @@
import StatusChip from './StatusChip';
export type { StatusChipProps, StatusKind } from './StatusChip';
export { StatusChip as default, StatusChip };
@@ -0,0 +1,25 @@
import { FunctionComponent } from 'react';
import { render, screen } from '@testing-library/react';
import { ThemeProvider } from '../../theme';
import StepperHeader, { StepperHeaderProps } from './StepperHeader';
const STEPS = ['Phone', 'Verify', 'Profile'];
const ComponentToTest: FunctionComponent<Partial<StepperHeaderProps>> = ({ steps = STEPS, activeStep = 0 }) => (
<ThemeProvider>
<StepperHeader steps={steps} activeStep={activeStep} />
</ThemeProvider>
);
describe('<StepperHeader/> component', () => {
it('renders every step label', () => {
render(<ComponentToTest />);
STEPS.forEach((label) => expect(screen.getByText(label)).toBeInTheDocument());
});
it('marks the active step', () => {
const { container } = render(<ComponentToTest activeStep={1} />);
// MUI flags the active step's icon with the Mui-active class.
expect(container.querySelector('.Mui-active')).toBeInTheDocument();
});
});
@@ -0,0 +1,30 @@
import { FunctionComponent } from 'react';
import Stepper from '@mui/material/Stepper';
import Step from '@mui/material/Step';
import StepLabel from '@mui/material/StepLabel';
export interface StepperHeaderProps {
/** Ordered step labels — already translated by the caller. */
steps: string[];
/** Zero-based index of the active step. */
activeStep: number;
/** Optional alternative-label layout (labels under the dots). Defaults to true. */
alternativeLabel?: boolean;
}
/**
* Progress header for multi-step flows (onboarding + verification). Wraps MUI Stepper;
* direction is handled by the RTL-aware theme, so it flips correctly at `/fa`.
* @component StepperHeader
*/
const StepperHeader: FunctionComponent<StepperHeaderProps> = ({ steps, activeStep, alternativeLabel = true }) => (
<Stepper activeStep={activeStep} alternativeLabel={alternativeLabel} sx={{ py: 2 }}>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
);
export default StepperHeader;
@@ -0,0 +1,4 @@
import StepperHeader from './StepperHeader';
export type { StepperHeaderProps } from './StepperHeader';
export { StepperHeader as default, StepperHeader };
@@ -19,6 +19,19 @@ import PersonIcon from '@mui/icons-material/Person';
import ExitToAppIcon from '@mui/icons-material/ExitToApp';
import NotificationsIcon from '@mui/icons-material/NotificationsOutlined';
import DangerousIcon from '@mui/icons-material/Dangerous';
import EventNoteIcon from '@mui/icons-material/EventNote';
import GroupsIcon from '@mui/icons-material/Groups';
import PeopleAltIcon from '@mui/icons-material/PeopleAlt';
import WalletIcon from '@mui/icons-material/AccountBalanceWallet';
import PersonOutlineIcon from '@mui/icons-material/AccountCircleOutlined';
import DashboardIcon from '@mui/icons-material/Dashboard';
import VerifiedUserIcon from '@mui/icons-material/VerifiedUser';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
import CancelIcon from '@mui/icons-material/Cancel';
import MedicalServicesIcon from '@mui/icons-material/MedicalServices';
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings';
import AddIcon from '@mui/icons-material/Add';
/**
* List of all available Icon names
@@ -53,4 +66,17 @@ export const ICONS /* Note: Setting type disables property autocomplete :( was -
logout: ExitToAppIcon,
notifications: NotificationsIcon,
error: DangerousIcon,
bookings: EventNoteIcon,
patients: GroupsIcon,
users: PeopleAltIcon,
wallet: WalletIcon,
profile: PersonOutlineIcon,
dashboard: DashboardIcon,
verification: VerifiedUserIcon,
verified: CheckCircleIcon,
pending: HourglassEmptyIcon,
rejected: CancelIcon,
visits: MedicalServicesIcon,
admin: AdminPanelSettingsIcon,
add: AddIcon,
};
@@ -1,29 +0,0 @@
import { FunctionComponent } from 'react';
import { IconProps } from '../utils';
const CurrencyIcon: FunctionComponent<IconProps> = (props) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 36 36" {...props}>
<path
fill="#D99E82"
d="M35.222 33.598c-.647-2.101-1.705-6.059-2.325-7.566-.501-1.216-.969-2.438-1.544-3.014-.575-.575-1.553-.53-2.143.058 0 0-2.469 1.675-3.354 2.783-1.108.882-2.785 3.357-2.785 3.357-.59.59-.635 1.567-.06 2.143.576.575 1.798 1.043 3.015 1.544 1.506.62 5.465 1.676 7.566 2.325.359.11 1.74-1.271 1.63-1.63z"
/>
<path
fill="#EA596E"
d="M13.643 5.308c1.151 1.151 1.151 3.016 0 4.167l-4.167 4.168c-1.151 1.15-3.018 1.15-4.167 0L1.141 9.475c-1.15-1.151-1.15-3.016 0-4.167l4.167-4.167c1.15-1.151 3.016-1.151 4.167 0l4.168 4.167z"
/>
<path fill="#FFCC4D" d="M31.353 23.018l-4.17 4.17-4.163 4.165L7.392 15.726l8.335-8.334 15.626 15.626z" />
<path
fill="#292F33"
d="M32.078 34.763s2.709 1.489 3.441.757c.732-.732-.765-3.435-.765-3.435s-2.566.048-2.676 2.678z"
/>
<path fill="#CCD6DD" d="M2.183 10.517l8.335-8.335 5.208 5.209-8.334 8.335z" />
<path
fill="#99AAB5"
d="M3.225 11.558l8.334-8.334 1.042 1.042L4.267 12.6zm2.083 2.086l8.335-8.335 1.042 1.042-8.335 8.334z"
/>
</svg>
);
};
export default CurrencyIcon;
@@ -1,125 +0,0 @@
import { FunctionComponent } from 'react';
import { IconProps } from '../utils';
const YellowPlaneIcon: FunctionComponent<IconProps> = (props) => {
const styleOpacityAndEnableBackground = {
opacity: 0.2,
// enableBackground: 'new'
};
return (
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" xmlSpace="preserve" {...props}>
<path
style={{ fill: '#FFCE00' }}
d="M142.21,493.991c12.991,12.991,34.057,12.991,47.05,0c12.991-12.991,12.991-34.057,0-47.048
L65.057,322.742c-12.993-12.992-34.059-12.992-47.05,0s-12.989,34.055,0,47.048L142.21,493.991z"
/>
<circle style={{ fill: '#7D868C' }} cx="386.857" cy="125.141" r="35.932" />
<path
style={styleOpacityAndEnableBackground}
d="M391.846,120.154c-9.556-9.556-18.727-17.187-27.59-22.948
c-0.969,0.785-1.908,1.627-2.807,2.527c-14.033,14.034-14.033,36.786,0,50.816c14.031,14.034,36.786,14.034,50.816,0
c0.908-0.907,1.754-1.853,2.546-2.829C409.07,138.902,401.453,129.759,391.846,120.154z"
/>
<path
style={styleOpacityAndEnableBackground}
d="M75.949,333.636c-15.914,18.935-28.002,33.038-32.3,37.337
c-4.221,4.221-7.777,8.86-10.672,13.785l94.264,94.266c4.925-2.894,9.565-6.449,13.789-10.672
c4.298-4.301,18.399-16.384,37.334-32.303L75.949,333.636z"
/>
<path
style={{ fill: '#333E48' }}
d="M384.037,127.964c-30.663-30.663-63.439-47.604-94.099-16.941
C254.182,146.782,79.164,363.201,57.52,384.842c-19.227,19.231-19.23,50.409,0,69.636c19.23,19.233,50.409,19.228,69.636,0
c21.644-21.641,238.063-196.657,273.82-232.416C431.639,191.4,414.698,158.626,384.037,127.964z"
/>
<circle style={{ fill: '#7D868C' }} cx="218.905" cy="293.104" r="35.93" />
<path
style={styleOpacityAndEnableBackground}
d="M278.708,123.019c-25.32,28.083-74.009,86.548-119.486,141.296
l88.463,88.463c54.748-45.477,113.215-94.162,141.296-119.487L278.708,123.019z"
/>
<g>
<path
style={{ fill: '#FFCE00' }}
d="M384.46,458.665c27.283,27.281,71.513,27.279,98.795-0.003c27.28-27.279,27.283-71.511,0-98.793
L152.13,28.746c-27.283-27.283-71.513-27.283-98.793,0c-27.283,27.281-27.285,71.514-0.002,98.793L384.46,458.665z"
/>
<path
style={{ fill: '#FFCE00' }}
d="M84.341,435.945c-2.121,0-4.241-0.809-5.857-2.426c-3.236-3.235-3.236-8.48-0.002-11.716
l50.812-50.814c3.236-3.236,8.483-3.235,11.716-0.001c3.236,3.235,3.236,8.48,0,11.714l-50.812,50.814
C88.58,435.136,86.459,435.945,84.341,435.945z"
/>
</g>
<rect
x="174.379"
y="88.222"
transform="matrix(-0.7071 -0.7071 0.7071 -0.7071 200.0443 399.0237)"
style={styleOpacityAndEnableBackground}
width="16.568"
height="139.719"
/>
<rect
x="117.601"
y="31.432"
transform="matrix(-0.7071 -0.7071 0.7071 -0.7071 143.2742 261.929)"
style={styleOpacityAndEnableBackground}
width="16.568"
height="139.719"
/>
<rect
x="345.637"
y="259.464"
transform="matrix(-0.7071 -0.7071 0.7071 -0.7071 371.313 812.4501)"
style={styleOpacityAndEnableBackground}
width="16.568"
height="139.719"
/>
<rect
x="402.427"
y="316.259"
transform="matrix(-0.7071 -0.7071 0.7071 -0.7071 428.1006 949.5629)"
style={styleOpacityAndEnableBackground}
width="16.568"
height="139.719"
/>
<path
style={{ fill: '#1E252B' }}
d="M489.114,354.011L383.944,248.841c10.747-9.425,18.276-16.308,22.89-20.921
c16.43-16.43,22.038-34.95,16.673-55.044c-1.363-5.108-3.454-10.308-6.267-15.628c0.296-0.281,0.596-0.553,0.885-0.842
c2.167-2.167,4.076-4.523,5.724-7.024l41.209,41.209c1.618,1.617,3.739,2.426,5.858,2.426c2.12,0,4.24-0.808,5.858-2.426
c3.235-3.236,3.235-8.48,0-11.716l-46.323-46.323c0.406-2.427,0.626-4.902,0.626-7.411c0-11.811-4.599-22.914-12.95-31.265
c-8.351-8.352-19.454-12.953-31.265-12.953c-2.511,0-4.987,0.22-7.415,0.627L333.126,35.23c-3.235-3.235-8.48-3.236-11.716-0.001
c-3.236,3.235-3.236,8.48-0.001,11.714l41.209,41.21c-2.503,1.647-4.858,3.555-7.025,5.722c-0.288,0.288-0.562,0.59-0.843,0.886
c-5.319-2.812-10.518-4.902-15.627-6.267c-20.099-5.369-38.615,0.243-55.044,16.673c-4.61,4.611-11.492,12.142-20.921,22.893
L157.989,22.89C143.229,8.129,123.605,0,102.733,0S62.237,8.129,47.48,22.888c-14.76,14.76-22.89,34.383-22.89,55.254
c-0.001,20.873,8.127,40.497,22.887,55.254l114.564,114.564c-6.337,7.624-12.648,15.223-18.86,22.703
c-19.346,23.293-38.189,45.983-53.839,64.649l-18.428-18.429c-7.848-7.848-18.284-12.17-29.383-12.17s-21.534,4.322-29.382,12.172
c-16.198,16.199-16.198,42.559,0,58.761l25.683,25.684c-6.704,20.045-2.103,43.073,13.83,59.004
c10.865,10.867,25.311,16.85,40.677,16.85c6.339,0,12.515-1.034,18.354-2.994l25.658,25.658c8.102,8.101,18.74,12.15,29.381,12.15
c10.64,0,21.284-4.051,29.384-12.15c16.201-16.201,16.201-42.562-0.001-58.763l-18.43-18.43
c18.659-15.643,41.335-34.476,64.615-53.811c7.49-6.221,15.101-12.541,22.736-18.887l114.566,114.564
c14.757,14.756,34.379,22.885,55.251,22.887c0.002,0,0.002,0,0.004,0c20.87,0,40.495-8.129,55.254-22.89
c14.758-14.759,22.887-34.381,22.888-55.253C512.002,388.394,503.872,368.77,489.114,354.011z M386.86,97.491
c7.385,0,14.328,2.876,19.55,8.099c5.222,5.222,8.097,12.165,8.097,19.55c0,6.56-2.273,12.766-6.438,17.731
c-4.96-6.686-10.995-13.587-18.174-20.765c-7.179-7.179-14.08-13.215-20.767-18.176C374.093,99.765,380.301,97.491,386.86,97.491z
M295.795,116.881c12.28-12.282,24.689-16.218,39.053-12.38c12.86,3.434,27.034,13.026,43.329,29.323
c16.296,16.295,25.886,30.468,29.32,43.329c3.836,14.362-0.098,26.772-12.382,39.054c-4.413,4.414-12.113,11.434-22.915,20.895
l-97.303-97.303C284.365,128.993,291.385,121.29,295.795,116.881z M23.866,363.932c-9.74-9.742-9.74-25.593,0-35.333
c4.717-4.718,10.992-7.317,17.666-7.317c6.675,0,12.949,2.599,17.668,7.317l19.438,19.441C65.414,363.702,55.648,375,51.662,378.986
c-2.168,2.168-4.12,4.47-5.868,6.876L23.866,363.932z M183.401,452.801c9.742,9.741,9.742,25.59,0.001,35.331
c-9.742,9.741-25.594,9.742-35.336,0l-21.927-21.926c2.417-1.763,4.717-3.717,6.873-5.872c3.985-3.985,15.285-13.751,30.947-26.976
L183.401,452.801z M230.718,356.101c-53.485,44.418-99.676,82.779-109.419,92.521c-7.735,7.735-18.02,11.995-28.96,11.996
c-10.939,0-21.225-4.26-28.961-11.997c-15.968-15.966-15.968-41.949,0-57.92c9.744-9.743,48.118-55.949,92.55-109.452
c5.891-7.094,11.87-14.293,17.879-21.523l78.468,78.468C245.033,344.21,237.822,350.2,230.718,356.101z M477.397,452.804
c-11.631,11.632-27.093,18.038-43.539,18.038h-0.003c-16.447-0.001-31.909-6.406-43.538-18.034L59.192,121.681
c-11.631-11.628-18.036-27.09-18.034-43.538c0-16.447,6.406-31.91,18.037-43.541c11.631-11.629,27.093-18.034,43.539-18.034
c16.447,0,31.91,6.405,43.54,18.036l331.124,331.123c11.63,11.629,18.036,27.093,18.036,43.54
C495.434,425.713,489.029,441.175,477.397,452.804z"
/>
</svg>
);
};
export default YellowPlaneIcon;
+11 -1
View File
@@ -1,5 +1,15 @@
export * from './common';
import UserInfo from './UserInfo';
import PlaceholderScreen from './PlaceholderScreen';
import OtpInput from './OtpInput';
import PhoneNumberField from './PhoneNumberField';
import StepperHeader from './StepperHeader';
import StatusChip from './StatusChip';
export { UserInfo };
export { UserInfo, PlaceholderScreen, OtpInput, PhoneNumberField, StepperHeader, StatusChip };
export type { PlaceholderScreenProps } from './PlaceholderScreen';
export type { OtpInputProps } from './OtpInput';
export type { PhoneNumberFieldProps } from './PhoneNumberField';
export type { StepperHeaderProps } from './StepperHeader';
export type { StatusChipProps, StatusKind } from './StatusChip';