ui phase 0
This commit is contained in:
@@ -93,7 +93,7 @@ const BankStatusPanel: FunctionComponent<BankStatusPanelProps> = ({
|
||||
{ibanLabel}
|
||||
</Typography>
|
||||
) : null}
|
||||
<Typography sx={{ fontWeight: 600, letterSpacing: 1 }} dir="ltr">
|
||||
<Typography sx={{ fontWeight: 500, letterSpacing: 1 }} dir="ltr">
|
||||
{ibanMasked}
|
||||
</Typography>
|
||||
{bankName ? (
|
||||
@@ -105,7 +105,7 @@ const BankStatusPanel: FunctionComponent<BankStatusPanelProps> = ({
|
||||
) : null}
|
||||
|
||||
{status === 'mismatch' && onReenter && reenterLabel ? (
|
||||
<AppButton color="primary" variant="outlined" startIcon="bank" onClick={onReenter} sx={{ m: 0, alignSelf: 'flex-start' }}>
|
||||
<AppButton color="primary" variant="outlined" startIcon="bank" onClick={onReenter} sx={{ alignSelf: 'flex-start' }}>
|
||||
{reenterLabel}
|
||||
</AppButton>
|
||||
) : null}
|
||||
|
||||
@@ -96,14 +96,14 @@ const BookingRequestSummaryCard: FunctionComponent<BookingRequestSummaryCardProp
|
||||
|
||||
<Stack sx={{ gap: 1.5 }}>
|
||||
<SummaryRow caption={t('summary_patient')}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
||||
{patientName}
|
||||
</Typography>
|
||||
</SummaryRow>
|
||||
|
||||
<SummaryRow caption={t('summary_service')}>
|
||||
<Stack sx={{ gap: 0.25, alignItems: 'flex-end' }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
||||
{variantLabel}
|
||||
</Typography>
|
||||
{variantPrice ? (
|
||||
@@ -113,13 +113,13 @@ const BookingRequestSummaryCard: FunctionComponent<BookingRequestSummaryCardProp
|
||||
</SummaryRow>
|
||||
|
||||
<SummaryRow caption={t('summary_address')}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, textAlign: 'end' }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500, textAlign: 'end' }}>
|
||||
{addressLabel}
|
||||
</Typography>
|
||||
</SummaryRow>
|
||||
|
||||
<SummaryRow caption={t('summary_when')}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, textAlign: 'end' }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500, textAlign: 'end' }}>
|
||||
{whenLabel}
|
||||
</Typography>
|
||||
</SummaryRow>
|
||||
|
||||
@@ -52,7 +52,7 @@ const CancellationPolicyDisclosure: FunctionComponent<CancellationPolicyDisclosu
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
{t(`lead_${preview.leadTimeLabel}`)}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'var(--bal-secondary)', fontWeight: 600 }}>
|
||||
<Typography variant="body2" sx={{ color: 'var(--bal-secondary)', fontWeight: 500 }}>
|
||||
{t('fee_percent', { percent: feePercent })}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
@@ -69,7 +69,7 @@ const CountdownTimer: FunctionComponent<CountdownTimerProps> = ({
|
||||
return (
|
||||
<Stack direction="row" sx={{ gap: 0.75, alignItems: 'center', color: 'text.secondary' }}>
|
||||
<AppIcon icon="pending" size={18} color="var(--bal-text-secondary)" />
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
||||
{elapsedText}
|
||||
</Typography>
|
||||
</Stack>
|
||||
@@ -92,7 +92,7 @@ const CountdownTimer: FunctionComponent<CountdownTimerProps> = ({
|
||||
return (
|
||||
<Stack sx={{ gap: 0.25, alignItems: 'center' }}>
|
||||
{label ? (
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 600 }}>
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 500 }}>
|
||||
{label}
|
||||
</Typography>
|
||||
) : null}
|
||||
|
||||
@@ -185,7 +185,7 @@ const DocumentUpload: FunctionComponent<DocumentUploadProps> = ({
|
||||
startIcon="upload"
|
||||
onClick={openPicker}
|
||||
disabled={disabled}
|
||||
sx={{ m: 0, alignSelf: 'flex-start' }}
|
||||
sx={{ alignSelf: 'flex-start' }}
|
||||
>
|
||||
{t('upload_reupload')}
|
||||
</AppButton>
|
||||
@@ -194,7 +194,7 @@ const DocumentUpload: FunctionComponent<DocumentUploadProps> = ({
|
||||
<Paper elevation={0} data-upload-state="uploading" sx={uploadedSx}>
|
||||
<Stack direction="row" sx={{ gap: 1, alignItems: 'center' }}>
|
||||
<AppIcon icon="upload" size={20} color="var(--bal-primary)" />
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, flexGrow: 1, wordBreak: 'break-all' }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500, flexGrow: 1, wordBreak: 'break-all' }}>
|
||||
{fileName}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||||
@@ -220,7 +220,7 @@ const DocumentUpload: FunctionComponent<DocumentUploadProps> = ({
|
||||
<AppIcon icon="document" size={28} color="var(--bal-primary)" />
|
||||
)}
|
||||
<Stack sx={{ gap: 0.25, flexGrow: 1, minWidth: 0 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, wordBreak: 'break-all' }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500, wordBreak: 'break-all' }}>
|
||||
{uploadedName}
|
||||
</Typography>
|
||||
<Stack direction="row" sx={{ gap: 0.5, alignItems: 'center' }}>
|
||||
@@ -230,7 +230,7 @@ const DocumentUpload: FunctionComponent<DocumentUploadProps> = ({
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<AppButton variant="text" color="primary" onClick={openPicker} disabled={disabled} sx={{ m: 0 }}>
|
||||
<AppButton variant="text" color="primary" onClick={openPicker} disabled={disabled}>
|
||||
{t('upload_change')}
|
||||
</AppButton>
|
||||
</Stack>
|
||||
@@ -253,7 +253,7 @@ const DocumentUpload: FunctionComponent<DocumentUploadProps> = ({
|
||||
startIcon="refresh"
|
||||
onClick={openPicker}
|
||||
disabled={disabled}
|
||||
sx={{ m: 0, alignSelf: 'flex-start' }}
|
||||
sx={{ alignSelf: 'flex-start' }}
|
||||
>
|
||||
{t('upload_retry')}
|
||||
</AppButton>
|
||||
@@ -287,7 +287,7 @@ const DocumentUpload: FunctionComponent<DocumentUploadProps> = ({
|
||||
}}
|
||||
>
|
||||
<AppIcon icon={capture ? 'camera' : 'upload'} size={28} color="var(--bal-primary)" />
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700 }}>
|
||||
{capture ? t('upload_capture') : t('upload_choose')}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||||
|
||||
@@ -59,7 +59,7 @@ const EarningsBalanceHeader: FunctionComponent<EarningsBalanceHeaderProps> = ({
|
||||
<Stack sx={{ gap: 0.75 }}>
|
||||
<Stack direction="row" sx={{ gap: 1, alignItems: 'center' }}>
|
||||
<AppIcon icon={isOwed ? 'warning' : 'wallet'} size={20} color={accent} />
|
||||
<Typography variant="subtitle2" sx={{ color: 'text.secondary', fontWeight: 600 }}>
|
||||
<Typography variant="subtitle2" sx={{ color: 'text.secondary', fontWeight: 500 }}>
|
||||
{isOwed ? t('balance_owed_label') : t('balance_net_label')}
|
||||
</Typography>
|
||||
</Stack>
|
||||
@@ -67,7 +67,7 @@ const EarningsBalanceHeader: FunctionComponent<EarningsBalanceHeaderProps> = ({
|
||||
<Typography component="span" sx={{ fontWeight: 800, fontSize: '2rem', color: accent }}>
|
||||
{formatIrrToToman(magnitude, locale)}
|
||||
</Typography>
|
||||
<Typography component="span" variant="subtitle1" sx={{ color: 'text.secondary', fontWeight: 600 }}>
|
||||
<Typography component="span" variant="subtitle1" sx={{ color: 'text.secondary', fontWeight: 500 }}>
|
||||
{tc('currency_toman')}
|
||||
</Typography>
|
||||
</Stack>
|
||||
@@ -101,7 +101,7 @@ const EarningsBalanceHeader: FunctionComponent<EarningsBalanceHeaderProps> = ({
|
||||
<Stack sx={{ gap: 0.75 }}>
|
||||
<Stack direction="row" sx={{ gap: 0.75, alignItems: 'center' }}>
|
||||
<AppIcon icon={bucket.icon} size={16} color={bucket.token} />
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 600 }}>
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 500 }}>
|
||||
{t(`bucket_${bucket.key}`)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
@@ -105,7 +105,6 @@ const EarningsRow: FunctionComponent<EarningsRowProps> = ({ item, onViewBooking,
|
||||
color="primary"
|
||||
endIcon="visits"
|
||||
onClick={() => onViewBooking(item.bookingId)}
|
||||
sx={{ m: 0 }}
|
||||
>
|
||||
{t('view_booking')}
|
||||
</AppButton>
|
||||
@@ -178,7 +177,6 @@ const StateAffordance: FunctionComponent<{
|
||||
color="primary"
|
||||
endIcon="wallet"
|
||||
onClick={() => onViewPayout(item.nursePayoutId as number)}
|
||||
sx={{ m: 0 }}
|
||||
>
|
||||
{t('view_payout')}
|
||||
</AppButton>
|
||||
|
||||
@@ -44,7 +44,7 @@ const GenderToggle: FunctionComponent<GenderToggleProps> = ({
|
||||
'& .MuiToggleButton-root': {
|
||||
flex: 1,
|
||||
py: 1.25,
|
||||
fontWeight: 600,
|
||||
fontWeight: 700,
|
||||
borderColor: error ? 'var(--bal-error)' : undefined,
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -161,7 +161,7 @@ const PatientForm: FunctionComponent<PatientFormProps> = ({
|
||||
|
||||
<Stack direction="row" sx={{ gap: 1, justifyContent: 'flex-end' }}>
|
||||
{onCancel ? (
|
||||
<AppButton variant="text" onClick={onCancel} disabled={submitting} sx={{ m: 0 }}>
|
||||
<AppButton variant="text" onClick={onCancel} disabled={submitting}>
|
||||
{cancelLabel}
|
||||
</AppButton>
|
||||
) : null}
|
||||
@@ -170,7 +170,6 @@ const PatientForm: FunctionComponent<PatientFormProps> = ({
|
||||
variant="contained"
|
||||
onClick={handleSubmit}
|
||||
disabled={submitting}
|
||||
sx={{ m: 0 }}
|
||||
>
|
||||
{submitLabel}
|
||||
</AppButton>
|
||||
|
||||
@@ -43,7 +43,7 @@ const PatientHeader: FunctionComponent<PatientHeaderProps> = ({
|
||||
{displayName}
|
||||
</Typography>
|
||||
{relationLabel ? (
|
||||
<Chip size="small" label={relationLabel} sx={{ bgcolor: 'var(--bal-primary-soft)', fontWeight: 600 }} />
|
||||
<Chip size="small" label={relationLabel} sx={{ bgcolor: 'var(--bal-primary-soft)', fontWeight: 500 }} />
|
||||
) : null}
|
||||
</Stack>
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ const PayoutHistoryRow: FunctionComponent<PayoutHistoryRowProps> = ({ item, onOp
|
||||
sx={{ justifyContent: 'space-between', alignItems: 'flex-start', gap: 1.5, flexWrap: 'wrap' }}
|
||||
>
|
||||
<Stack sx={{ gap: 0.25, minWidth: 0 }}>
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 600 }}>
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 500 }}>
|
||||
{t('payout_net_amount')}
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>
|
||||
@@ -110,7 +110,7 @@ const PayoutHistoryRow: FunctionComponent<PayoutHistoryRowProps> = ({ item, onOp
|
||||
) : null}
|
||||
|
||||
<Stack direction="row" sx={{ justifyContent: 'flex-end' }}>
|
||||
<AppButton variant="text" color="primary" endIcon="wallet" onClick={() => onOpen(item.id)} sx={{ m: 0 }}>
|
||||
<AppButton variant="text" color="primary" endIcon="wallet" onClick={() => onOpen(item.id)}>
|
||||
{t('view_payout_detail')}
|
||||
</AppButton>
|
||||
</Stack>
|
||||
@@ -124,7 +124,7 @@ const MetaLine: FunctionComponent<{ label: string; children: ReactNode }> = ({ l
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
||||
{children}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
@@ -49,7 +49,7 @@ const PriceBreakdown: FunctionComponent<PriceBreakdownProps> = ({ rows, totalLab
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
{row.label}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
||||
{formatIrrToToman(row.amountIrr, locale)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
@@ -47,12 +47,12 @@ const PriceDisplay: FunctionComponent<PriceDisplayProps> = ({
|
||||
<Stack sx={{ gap: 0.25, alignItems: align === 'center' ? 'center' : 'flex-start' }}>
|
||||
<Typography component="p" sx={{ fontWeight: 700 }}>
|
||||
{amount} {tc('currency_toman')}{' '}
|
||||
<Typography component="span" variant="body2" sx={{ color: 'text.secondary', fontWeight: 600 }}>
|
||||
<Typography component="span" variant="body2" sx={{ color: 'text.secondary', fontWeight: 500 }}>
|
||||
{unitLabel}
|
||||
</Typography>
|
||||
</Typography>
|
||||
{hasEstimate ? (
|
||||
<Typography variant="caption" sx={{ color: 'var(--bal-primary)', fontWeight: 600 }}>
|
||||
<Typography variant="caption" sx={{ color: 'var(--bal-primary)', fontWeight: 500 }}>
|
||||
{t('estimate_label')}: {total} {tc('currency_toman')} ·{' '}
|
||||
{t('estimate_for', { count: countLabel, unit: t(`count_${priceUnit}`) })}
|
||||
</Typography>
|
||||
|
||||
@@ -52,7 +52,7 @@ const RefundEtaBanner: FunctionComponent<RefundEtaBannerProps> = ({ channel, eta
|
||||
</Typography>
|
||||
<Typography variant="body2">{t(copy.bodyKey)}</Typography>
|
||||
{channel === 'bnpl_revert' && (
|
||||
<Typography variant="caption" sx={{ fontWeight: 600 }} data-testid="refund-eta-window">
|
||||
<Typography variant="caption" sx={{ fontWeight: 500 }} data-testid="refund-eta-window">
|
||||
{t('eta_business_days')}
|
||||
{eta ? ` · ${t('eta_expected_label', { date: formatShamsiDate(eta, locale) })}` : ''}
|
||||
</Typography>
|
||||
|
||||
@@ -62,7 +62,7 @@ const RefundStatusCard: FunctionComponent<RefundStatusCardProps> = ({ refund })
|
||||
</Typography>
|
||||
<Typography variant="body2">{t('failed_body')}</Typography>
|
||||
{refund.externalRevertReference && (
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, direction: 'ltr' }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 500, direction: 'ltr' }}>
|
||||
{t('reference_label')}: {refund.externalRevertReference}
|
||||
</Typography>
|
||||
)}
|
||||
@@ -97,7 +97,7 @@ const RefundStatusCard: FunctionComponent<RefundStatusCardProps> = ({ refund })
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
{t('reference_label')}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, direction: 'ltr' }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500, direction: 'ltr' }}>
|
||||
{refund.externalRevertReference}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
@@ -56,7 +56,7 @@ const RelationSelect: FunctionComponent<RelationSelectProps> = ({ options, value
|
||||
}}
|
||||
>
|
||||
{option.icon ? <AppIcon icon={option.icon} size={26} color="var(--bal-primary)" /> : null}
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
|
||||
{option.label}
|
||||
</Typography>
|
||||
</Paper>
|
||||
|
||||
@@ -38,7 +38,7 @@ const ServicePriceRow: FunctionComponent<ServicePriceRowProps> = ({
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1" sx={{ fontWeight: 600, flexGrow: 1 }}>
|
||||
<Typography variant="body1" sx={{ fontWeight: 500, flexGrow: 1 }}>
|
||||
{displayName}
|
||||
</Typography>
|
||||
<PriceDisplay price={priceIrr} priceUnit={priceUnit} sessionCount={sessionCount} align="start" />
|
||||
|
||||
@@ -41,7 +41,7 @@ const StatusChip: FunctionComponent<StatusChipProps> = ({ status, label, size =
|
||||
size={size}
|
||||
label={label}
|
||||
icon={<AppIcon icon={style.icon} size={16} color={style.fg} />}
|
||||
sx={{ backgroundColor: style.bg, color: style.fg, fontWeight: 600, ...sx }}
|
||||
sx={{ backgroundColor: style.bg, color: style.fg, fontWeight: 500, ...sx }}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -76,7 +76,7 @@ const VariantCard: FunctionComponent<VariantCardProps> = ({ variant, onEdit, onT
|
||||
) : null}
|
||||
|
||||
<Stack direction="row" sx={{ gap: 1, flexWrap: 'wrap' }}>
|
||||
<AppButton variant="outlined" color="primary" startIcon="edit" onClick={onEdit} sx={{ m: 0 }}>
|
||||
<AppButton variant="outlined" color="primary" startIcon="edit" onClick={onEdit}>
|
||||
{t('edit')}
|
||||
</AppButton>
|
||||
<AppButton
|
||||
@@ -84,7 +84,6 @@ const VariantCard: FunctionComponent<VariantCardProps> = ({ variant, onEdit, onT
|
||||
color={active ? 'inherit' : 'primary'}
|
||||
startIcon={active ? 'visibilityoff' : 'visibilityon'}
|
||||
onClick={onToggleActive}
|
||||
sx={{ m: 0 }}
|
||||
>
|
||||
{active ? t('deactivate') : t('activate')}
|
||||
</AppButton>
|
||||
|
||||
@@ -23,7 +23,7 @@ const AdminErrorState: FunctionComponent<AdminErrorStateProps> = ({ message, ret
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary', mb: 1.5 }}>
|
||||
{message}
|
||||
</Typography>
|
||||
<AppButton variant="outlined" color="primary" startIcon="refresh" onClick={onRetry} sx={{ m: 0 }}>
|
||||
<AppButton variant="outlined" color="primary" startIcon="refresh" onClick={onRetry}>
|
||||
{retryLabel}
|
||||
</AppButton>
|
||||
</Paper>
|
||||
|
||||
@@ -32,13 +32,13 @@ const AdminPager: FunctionComponent<AdminPagerProps> = ({
|
||||
if (pageCount <= 1) return null;
|
||||
return (
|
||||
<Stack direction="row" sx={{ gap: 1, alignItems: 'center', justifyContent: 'center', mt: 1 }}>
|
||||
<AppButton variant="text" color="primary" onClick={onPrev} disabled={page <= 1} sx={{ m: 0 }}>
|
||||
<AppButton variant="text" color="primary" onClick={onPrev} disabled={page <= 1}>
|
||||
{prevLabel}
|
||||
</AppButton>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
{indicator}
|
||||
</Typography>
|
||||
<AppButton variant="text" color="primary" onClick={onNext} disabled={page >= pageCount} sx={{ m: 0 }}>
|
||||
<AppButton variant="text" color="primary" onClick={onNext} disabled={page >= pageCount}>
|
||||
{nextLabel}
|
||||
</AppButton>
|
||||
</Stack>
|
||||
|
||||
@@ -69,7 +69,7 @@ const AuditLogRow: FunctionComponent<AuditLogRowProps> = ({ entry }) => {
|
||||
<TableBody>
|
||||
{fields.map(([field, delta]) => (
|
||||
<TableRow key={field}>
|
||||
<TableCell sx={{ fontWeight: 600 }}>{field}</TableCell>
|
||||
<TableCell sx={{ fontWeight: 500 }}>{field}</TableCell>
|
||||
<TableCell sx={{ color: 'text.secondary' }}>{displayValue(delta.old)}</TableCell>
|
||||
<TableCell>{displayValue(delta.new)}</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -47,11 +47,11 @@ const ConfigRow: FunctionComponent<ConfigRowProps> = ({ config, canEdit = false,
|
||||
</Box>
|
||||
|
||||
<Stack direction="row" sx={{ gap: 1, flexWrap: 'wrap' }}>
|
||||
<AppButton variant="text" color="primary" startIcon="history" onClick={() => onHistory?.(config)} sx={{ m: 0 }}>
|
||||
<AppButton variant="text" color="primary" startIcon="history" onClick={() => onHistory?.(config)}>
|
||||
{t('cfg_history')}
|
||||
</AppButton>
|
||||
{canEdit ? (
|
||||
<AppButton variant="outlined" color="primary" startIcon="edit" onClick={() => onEdit?.(config)} sx={{ m: 0 }}>
|
||||
<AppButton variant="outlined" color="primary" startIcon="edit" onClick={() => onEdit?.(config)}>
|
||||
{t('cfg_edit')}
|
||||
</AppButton>
|
||||
) : null}
|
||||
|
||||
@@ -93,7 +93,7 @@ const ConfirmDialog: FunctionComponent<ConfirmDialogProps> = ({
|
||||
) : null}
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<AppButton variant="text" color="inherit" onClick={close} disabled={loading} sx={{ m: 0 }}>
|
||||
<AppButton variant="text" color="inherit" onClick={close} disabled={loading}>
|
||||
{cancelLabel}
|
||||
</AppButton>
|
||||
<AppButton
|
||||
@@ -102,7 +102,6 @@ const ConfirmDialog: FunctionComponent<ConfirmDialogProps> = ({
|
||||
onClick={confirm}
|
||||
disabled={confirmDisabled}
|
||||
startIcon={loading ? <CircularProgress size={16} color="inherit" /> : undefined}
|
||||
sx={{ m: 0 }}
|
||||
>
|
||||
{confirmLabel}
|
||||
</AppButton>
|
||||
|
||||
@@ -38,7 +38,7 @@ const DocumentViewer: FunctionComponent<DocumentViewerProps> = ({ document }) =>
|
||||
>
|
||||
<Stack direction="row" sx={{ gap: 1, alignItems: 'center', mb: 1 }}>
|
||||
<AppIcon icon="document" size={18} color="var(--bal-text-secondary)" />
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, flexGrow: 1, wordBreak: 'break-all' }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500, flexGrow: 1, wordBreak: 'break-all' }}>
|
||||
{t('doc_file_meta', { name: document.originalFileName ?? `#${document.id}`, size: formatSize(document.fileSizeBytes) })}
|
||||
</Typography>
|
||||
<AppButton
|
||||
@@ -47,7 +47,7 @@ const DocumentViewer: FunctionComponent<DocumentViewerProps> = ({ document }) =>
|
||||
startIcon="refresh"
|
||||
onClick={() => signed.refetch()}
|
||||
disabled={signed.isFetching}
|
||||
sx={{ m: 0, minWidth: 0 }}
|
||||
sx={{ minWidth: 0 }}
|
||||
>
|
||||
{t('doc_reload')}
|
||||
</AppButton>
|
||||
@@ -60,7 +60,7 @@ const DocumentViewer: FunctionComponent<DocumentViewerProps> = ({ document }) =>
|
||||
<Typography variant="body2" sx={{ color: 'var(--bal-error)' }}>
|
||||
{t('doc_error')}
|
||||
</Typography>
|
||||
<AppButton variant="outlined" color="primary" startIcon="refresh" onClick={() => signed.refetch()} sx={{ m: 0 }}>
|
||||
<AppButton variant="outlined" color="primary" startIcon="refresh" onClick={() => signed.refetch()}>
|
||||
{t('doc_reload')}
|
||||
</AppButton>
|
||||
</Stack>
|
||||
@@ -74,7 +74,7 @@ const DocumentViewer: FunctionComponent<DocumentViewerProps> = ({ document }) =>
|
||||
sx={{ maxWidth: '100%', maxHeight: 320, borderRadius: 1, display: 'block' }}
|
||||
/>
|
||||
) : (
|
||||
<AppButton variant="outlined" color="primary" endIcon="external" href={signed.data.url} openInNewTab sx={{ m: 0 }}>
|
||||
<AppButton variant="outlined" color="primary" endIcon="external" href={signed.data.url} openInNewTab>
|
||||
{t('doc_open_new')}
|
||||
</AppButton>
|
||||
)
|
||||
|
||||
@@ -74,11 +74,11 @@ const PartnerSettlementRow: FunctionComponent<PartnerSettlementRowProps> = ({ in
|
||||
/>
|
||||
{invoice.pdfUrl ? (
|
||||
downloadError ? (
|
||||
<AppButton variant="outlined" color="error" startIcon="refresh" onClick={() => onRetryDownload?.(invoice)} sx={{ m: 0 }}>
|
||||
<AppButton variant="outlined" color="error" startIcon="refresh" onClick={() => onRetryDownload?.(invoice)}>
|
||||
{t('invoice_pdf_error')}
|
||||
</AppButton>
|
||||
) : (
|
||||
<AppButton variant="outlined" color="primary" startIcon="download" onClick={() => onDownloadPdf?.(invoice)} sx={{ m: 0 }}>
|
||||
<AppButton variant="outlined" color="primary" startIcon="download" onClick={() => onDownloadPdf?.(invoice)}>
|
||||
{t('invoice_download')}
|
||||
</AppButton>
|
||||
)
|
||||
|
||||
@@ -160,7 +160,7 @@ const RefundPanel: FunctionComponent<RefundPanelProps> = ({ bookingId, ticketId,
|
||||
minRows={2}
|
||||
/>
|
||||
|
||||
<AppButton variant="contained" color="primary" startIcon="refunds" onClick={() => setConfirmOpen(true)} disabled={busy} sx={{ m: 0, alignSelf: 'flex-start' }}>
|
||||
<AppButton variant="contained" color="primary" startIcon="refunds" onClick={() => setConfirmOpen(true)} disabled={busy} sx={{ alignSelf: 'flex-start' }}>
|
||||
{t('refund_initiate')}
|
||||
</AppButton>
|
||||
</>
|
||||
@@ -170,10 +170,10 @@ const RefundPanel: FunctionComponent<RefundPanelProps> = ({ bookingId, ticketId,
|
||||
{t('refund_provider_failed')}
|
||||
</Alert>
|
||||
<Stack direction="row" sx={{ gap: 1 }}>
|
||||
<AppButton variant="contained" color="primary" startIcon="refresh" onClick={doRetry} disabled={busy} sx={{ m: 0 }}>
|
||||
<AppButton variant="contained" color="primary" startIcon="refresh" onClick={doRetry} disabled={busy}>
|
||||
{t('refund_approve')}
|
||||
</AppButton>
|
||||
<AppButton variant="outlined" color="error" onClick={() => setRejectOpen(true)} disabled={busy} sx={{ m: 0 }}>
|
||||
<AppButton variant="outlined" color="error" onClick={() => setRejectOpen(true)} disabled={busy}>
|
||||
{t('refund_reject')}
|
||||
</AppButton>
|
||||
</Stack>
|
||||
|
||||
@@ -85,11 +85,11 @@ const SupportAlertCard: FunctionComponent<SupportAlertCardProps> = ({ alert, can
|
||||
{canAct && alert.status !== 'resolved' ? (
|
||||
<Stack direction="row" sx={{ gap: 1, mt: 1.5, flexWrap: 'wrap' }}>
|
||||
{alert.status === 'open' ? (
|
||||
<AppButton variant="outlined" color="primary" startIcon="assign" onClick={() => onAssignSelf?.(alert)} sx={{ m: 0 }}>
|
||||
<AppButton variant="outlined" color="primary" startIcon="assign" onClick={() => onAssignSelf?.(alert)}>
|
||||
{t('alert_assign_me')}
|
||||
</AppButton>
|
||||
) : null}
|
||||
<AppButton variant="contained" color="primary" startIcon="verified" onClick={() => onResolve?.(alert)} sx={{ m: 0 }}>
|
||||
<AppButton variant="contained" color="primary" startIcon="verified" onClick={() => onResolve?.(alert)}>
|
||||
{t('alert_resolve')}
|
||||
</AppButton>
|
||||
</Stack>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import AppIcon from '../common/AppIcon';
|
||||
import LogoLockup from '../common/AppIcon/icons/LogoLockup';
|
||||
|
||||
interface BrandMarkProps {
|
||||
/** Show the brand tagline under the wordmark. */
|
||||
@@ -18,7 +18,8 @@ const BrandMark: FunctionComponent<BrandMarkProps> = ({ withTagline }) => {
|
||||
const t = useTranslations('common');
|
||||
return (
|
||||
<Stack sx={{ alignItems: 'center', gap: 1 }}>
|
||||
<AppIcon icon="logo" size={56} color="var(--bal-primary)" />
|
||||
{/* Decorative — the wordmark below already announces "Balinyaar" to AT. */}
|
||||
<LogoLockup size={56} aria-hidden="true" />
|
||||
<Typography variant="h5" component="p" sx={{ fontWeight: 700, color: 'primary.main' }}>
|
||||
{t('brand')}
|
||||
</Typography>
|
||||
|
||||
@@ -129,7 +129,6 @@ const OtpStep: FunctionComponent<OtpStepProps> = ({
|
||||
fullWidth
|
||||
disabled={code.length !== OTP_CODE_LENGTH || verifyOtp.isPending || locked}
|
||||
startIcon={verifyOtp.isPending ? <CircularProgress size={18} color="inherit" /> : undefined}
|
||||
sx={{ m: 0 }}
|
||||
>
|
||||
{isNurse ? t('otp_verify_nurse') : t('otp_verify_customer')}
|
||||
</AppButton>
|
||||
|
||||
@@ -85,7 +85,6 @@ const PhoneStep: FunctionComponent<PhoneStepProps> = ({ intendedRole, onSwitchRo
|
||||
fullWidth
|
||||
disabled={requestOtp.isPending}
|
||||
startIcon={requestOtp.isPending ? <CircularProgress size={18} color="inherit" /> : undefined}
|
||||
sx={{ m: 0 }}
|
||||
>
|
||||
{t('request_code')}
|
||||
</AppButton>
|
||||
|
||||
@@ -84,7 +84,7 @@ const SelectRole: FunctionComponent<SelectRoleProps> = ({ initialRole }) => {
|
||||
>
|
||||
<AppIcon icon={icon} size={28} color="var(--bal-primary)" />
|
||||
<Stack sx={{ gap: 0.25 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
|
||||
{t(`role_${role}`)}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
@@ -103,7 +103,6 @@ const SelectRole: FunctionComponent<SelectRoleProps> = ({ initialRole }) => {
|
||||
onClick={confirm}
|
||||
disabled={selectRole.isPending}
|
||||
startIcon={selectRole.isPending ? <CircularProgress size={18} color="inherit" /> : undefined}
|
||||
sx={{ m: 0 }}
|
||||
>
|
||||
{t('continue')}
|
||||
</AppButton>
|
||||
|
||||
@@ -133,7 +133,7 @@ function HeaderFact({ label, value }: { label: string; value: string }) {
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
||||
{value}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
@@ -45,7 +45,7 @@ const BookingStatusTimeline: FunctionComponent<BookingStatusTimelineProps> = ({
|
||||
sx={{ gap: 1, alignItems: 'center', px: 1.5, py: 1.25, borderRadius: 2, bgcolor: 'var(--bal-primary-soft)' }}
|
||||
>
|
||||
<AppIcon icon="rejected" size={20} color="var(--bal-text-secondary)" />
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, color: 'text.secondary' }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500, color: 'text.secondary' }}>
|
||||
{t('bstatus_cancelled')}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
@@ -89,7 +89,7 @@ const SessionCard: FunctionComponent<SessionCardProps> = ({
|
||||
</Typography>
|
||||
) : null}
|
||||
<Stack direction="row" sx={{ justifyContent: 'space-between', alignItems: 'center', gap: 1 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: title ? 600 : 700, color: title ? 'text.secondary' : 'text.primary' }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: title ? 500 : 700, color: title ? 'text.secondary' : 'text.primary' }}>
|
||||
{t('session_index', { n: sessionIndex })}
|
||||
</Typography>
|
||||
<StatusChip status={SESSION_STATUS_KIND[status]} label={t(`sstatus_${status}`)} />
|
||||
@@ -109,7 +109,7 @@ const SessionCard: FunctionComponent<SessionCardProps> = ({
|
||||
) : null}
|
||||
|
||||
{showPayout && visitPayoutAmount ? (
|
||||
<Typography variant="caption" sx={{ color: 'var(--bal-secondary-dark)', fontWeight: 600 }}>
|
||||
<Typography variant="caption" sx={{ color: 'var(--bal-secondary-dark)', fontWeight: 500 }}>
|
||||
{t('session_payout')}: {formatIrrToToman(visitPayoutAmount, locale)} {tc('currency_toman')}
|
||||
</Typography>
|
||||
) : null}
|
||||
@@ -121,7 +121,7 @@ const SessionCard: FunctionComponent<SessionCardProps> = ({
|
||||
startIcon={acquiringLocation ? 'gps' : 'check_in'}
|
||||
disabled={busy}
|
||||
onClick={onCheckIn}
|
||||
sx={{ m: 0, alignSelf: 'flex-start', py: 1 }}
|
||||
sx={{ alignSelf: 'flex-start', py: 1 }}
|
||||
>
|
||||
{acquiringLocation ? t('evv_acquiring_location') : evvPending ? t('evv_checking_in') : t('evv_check_in')}
|
||||
</AppButton>
|
||||
@@ -134,7 +134,7 @@ const SessionCard: FunctionComponent<SessionCardProps> = ({
|
||||
startIcon={acquiringLocation ? 'gps' : 'check_out'}
|
||||
disabled={busy}
|
||||
onClick={onCheckOut}
|
||||
sx={{ m: 0, alignSelf: 'flex-start', py: 1 }}
|
||||
sx={{ alignSelf: 'flex-start', py: 1 }}
|
||||
>
|
||||
{acquiringLocation ? t('evv_acquiring_location') : evvPending ? t('evv_checking_out') : t('evv_check_out')}
|
||||
</AppButton>
|
||||
|
||||
@@ -60,14 +60,17 @@ describe('<AppButton/> component', () => {
|
||||
expect(button).toHaveAttribute('type', 'button'); // not "submit" or "input" by default
|
||||
});
|
||||
|
||||
it('has .margin style by default', () => {
|
||||
let text = 'button with default margin';
|
||||
it('does not force a default margin, and passes a custom sx through untouched', () => {
|
||||
const testId = randomText(8);
|
||||
render(<ComponentToTest data-testid={testId}>{text}</ComponentToTest>);
|
||||
render(
|
||||
<ComponentToTest data-testid={testId} sx={{ marginInlineStart: 2 }}>
|
||||
button with custom sx
|
||||
</ComponentToTest>
|
||||
);
|
||||
const button = screen.getByTestId(testId);
|
||||
expect(button).toBeDefined();
|
||||
// MUI v9 + cssVariables: spacing is emitted as CSS vars (not resolved in jsdom).
|
||||
// Verify the sx margin is applied via class rather than inline style.
|
||||
// Regression guard for the removed starter DEFAULT_SX_VALUES={ margin: 1 }:
|
||||
// AppButton must not silently drop or merge over a caller-provided sx.
|
||||
expect(button).toHaveClass('MuiButton-root');
|
||||
});
|
||||
|
||||
@@ -127,7 +130,7 @@ describe('<AppButton/> component', () => {
|
||||
let text = 'button with start icon';
|
||||
render(<ComponentToTest text={text} startIcon="default" />);
|
||||
let button = screen.getByText(text);
|
||||
let icon = within(button).getByTestId('MoreHorizIcon'); //Note: this is valid only when "default" icon is <MoreHorizIcon />
|
||||
let icon = within(button).getByTestId('MoreHorizRoundedIcon'); //Note: this is valid only when "default" icon is <MoreHorizRoundedIcon />
|
||||
expect(icon).toBeDefined();
|
||||
let span = icon.closest('span');
|
||||
expect(span).toHaveClass('MuiButton-startIcon');
|
||||
@@ -137,7 +140,7 @@ describe('<AppButton/> component', () => {
|
||||
let text = 'button with end icon as string';
|
||||
render(<ComponentToTest text={text} endIcon="default" />);
|
||||
let button = screen.getByText(text);
|
||||
let icon = within(button).getByTestId('MoreHorizIcon'); //Note: this is valid only when "default" icon is <MoreHorizIcon />
|
||||
let icon = within(button).getByTestId('MoreHorizRoundedIcon'); //Note: this is valid only when "default" icon is <MoreHorizRoundedIcon />
|
||||
expect(icon).toBeDefined();
|
||||
let span = icon.closest('span');
|
||||
expect(span).toHaveClass('MuiButton-endIcon');
|
||||
|
||||
@@ -6,26 +6,20 @@ import { APP_BUTTON_VARIANT } from '../../config';
|
||||
|
||||
const MUI_BUTTON_COLORS = ['inherit', 'primary', 'secondary', 'success', 'error', 'info', 'warning'];
|
||||
|
||||
const DEFAULT_SX_VALUES = {
|
||||
margin: 1, // By default the AppButton has theme.spacing(1) margin on all sides
|
||||
};
|
||||
|
||||
export interface AppButtonProps extends Omit<ButtonProps, 'color' | 'endIcon' | 'startIcon'> {
|
||||
color?: string; // Not only 'inherit' | 'primary' | 'secondary' | 'success' | 'error' | 'info' | 'warning',
|
||||
endIcon?: string | ReactNode;
|
||||
label?: string; // Alternate to .text
|
||||
text?: string; // Alternate to .label
|
||||
startIcon?: string | ReactNode;
|
||||
// Missing props
|
||||
component?: ElementType; // Could be RouterLink, AppLink, <a>, etc.
|
||||
to?: string; // Link prop
|
||||
href?: string; // Link prop
|
||||
openInNewTab?: boolean; // Link prop
|
||||
underline?: 'none' | 'hover' | 'always'; // Link prop
|
||||
}
|
||||
|
||||
/**
|
||||
* Application styled Material UI Button with Box around to specify margins using props
|
||||
* Application-styled Material UI Button.
|
||||
* @component AppButton
|
||||
* @param {string} [color] - when passing MUI value ('primary', 'secondary', and so on), it is color of the button body, otherwise it is color of text and icons
|
||||
* @param {string} [children] - content to render, overrides .label and .text props
|
||||
@@ -37,7 +31,6 @@ export interface AppButtonProps extends Omit<ButtonProps, 'color' | 'endIcon' |
|
||||
* @param {Array<func| object| bool> | func | object} [sx] - additional CSS styles to apply to the button
|
||||
* @param {string} [text] - text to render, alternate to .label
|
||||
* @param {string} [to] - internal link URI
|
||||
* @param {string} [underline] - controls underline style when button used as link, one of 'none', 'hover', or 'always'
|
||||
* @param {string} [variant] - MUI variant of the button, one of 'text', 'outlined', or 'contained'
|
||||
*/
|
||||
const AppButton: FunctionComponent<AppButtonProps> = ({
|
||||
@@ -47,9 +40,7 @@ const AppButton: FunctionComponent<AppButtonProps> = ({
|
||||
endIcon,
|
||||
label,
|
||||
startIcon,
|
||||
sx: propSx = DEFAULT_SX_VALUES,
|
||||
text,
|
||||
underline = 'none',
|
||||
variant = APP_BUTTON_VARIANT,
|
||||
...restOfProps
|
||||
}) => {
|
||||
@@ -69,10 +60,7 @@ const AppButton: FunctionComponent<AppButtonProps> = ({
|
||||
!propComponent && (restOfProps?.href || restOfProps?.to) ? AppLink : propComponent ?? Button;
|
||||
|
||||
const colorToRender = isMuiColor ? (propColor as ButtonProps['color']) : 'inherit';
|
||||
const sxToRender = {
|
||||
...propSx,
|
||||
...(isMuiColor ? {} : { color: propColor }),
|
||||
};
|
||||
const sxToRender = isMuiColor ? restOfProps.sx : { ...restOfProps.sx, color: propColor };
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -80,9 +68,9 @@ const AppButton: FunctionComponent<AppButtonProps> = ({
|
||||
color={colorToRender}
|
||||
endIcon={iconEnd}
|
||||
startIcon={iconStart}
|
||||
sx={sxToRender}
|
||||
variant={variant}
|
||||
{...{ ...restOfProps, underline }}
|
||||
{...restOfProps}
|
||||
sx={sxToRender}
|
||||
>
|
||||
{children || label || text}
|
||||
</Button>
|
||||
|
||||
@@ -16,9 +16,11 @@ describe('<AppIcon/> component', () => {
|
||||
const svg = screen.getByTestId(testId);
|
||||
expect(svg).toBeDefined();
|
||||
expect(svg).toHaveAttribute('data-icon', 'default');
|
||||
expect(svg).toHaveAttribute('size', String(APP_ICON_SIZE)); // default size
|
||||
expect(svg).toHaveAttribute('height', String(APP_ICON_SIZE)); // default size when .size is not set
|
||||
expect(svg).toHaveAttribute('width', String(APP_ICON_SIZE)); // default size when .size is not se
|
||||
// Size is actually driven by fontSize (MUI SvgIcon's `1em` sizing beats
|
||||
// width/height attributes) — never assert a `size` DOM attribute, it's invalid.
|
||||
expect(svg).toHaveStyle(`font-size: ${APP_ICON_SIZE}px`);
|
||||
});
|
||||
|
||||
it('supports .color property', () => {
|
||||
@@ -48,9 +50,11 @@ describe('<AppIcon/> component', () => {
|
||||
const size = Math.floor(Math.random() * 128) + 1;
|
||||
render(<ComponentToTest data-testid={testId} size={size} />);
|
||||
const svg = screen.getByTestId(testId);
|
||||
expect(svg).toHaveAttribute('size', String(size));
|
||||
expect(svg).toHaveAttribute('height', String(size));
|
||||
expect(svg).toHaveAttribute('width', String(size));
|
||||
// The bug this regression-guards: MUI SvgIcon's own CSS sets width/height:1em,
|
||||
// which beats plain width/height attributes — only fontSize actually resizes it.
|
||||
expect(svg).toHaveStyle(`font-size: ${size}px`);
|
||||
});
|
||||
|
||||
it('supports .title property', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ComponentType, FunctionComponent, SVGAttributes } from 'react';
|
||||
import { APP_ICON_SIZE } from '../../config';
|
||||
import { IconName, ICONS } from './config';
|
||||
import { IconName, ICONS, DIRECTIONAL_ICONS } from './config';
|
||||
|
||||
/**
|
||||
* Props of the AppIcon component, also can be used for SVG icons
|
||||
@@ -31,21 +31,35 @@ const AppIcon: FunctionComponent<Props> = ({
|
||||
|
||||
let ComponentToRender: ComponentType = ICONS[iconName];
|
||||
if (!ComponentToRender) {
|
||||
console.warn(`AppIcon: icon "${iconName}" is not found!`);
|
||||
ComponentToRender = ICONS.default; // ICONS['default'];
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.warn(`AppIcon: icon "${iconName}" is not found!`);
|
||||
}
|
||||
ComponentToRender = ICONS.default;
|
||||
}
|
||||
|
||||
const sizeValue = typeof size === 'number' ? `${size}px` : size;
|
||||
|
||||
// MUI's SvgIcon sets width/height:1em via its own class, which beats plain
|
||||
// width/height attributes — driving size through fontSize (the basis for 1em)
|
||||
// is what actually resizes MUI-registry icons. Custom raw <svg> icons (the
|
||||
// brand mark) aren't MUI SvgIcon-based, so they keep scaling off the explicit
|
||||
// width/height attributes below; the extra fontSize style is harmless for them.
|
||||
const propsToRender = {
|
||||
height: size,
|
||||
width: size,
|
||||
color,
|
||||
fill: color && 'currentColor',
|
||||
size,
|
||||
style: { ...style, color },
|
||||
width: size,
|
||||
style: { ...style, color, fontSize: sizeValue },
|
||||
...restOfProps,
|
||||
};
|
||||
|
||||
return <ComponentToRender data-icon={iconName} {...propsToRender} />;
|
||||
// Directional icons (back/chevron_start, …) are authored for LTR and mirror
|
||||
// under RTL via the single CSS rule in tokens.css keyed on this attribute —
|
||||
// registering a name in DIRECTIONAL_ICONS (config.ts) is the only step a
|
||||
// later phase needs; never hand-roll a flip at the call site.
|
||||
const directionalProps = DIRECTIONAL_ICONS.has(iconName) ? { 'data-icon-directional': true } : undefined;
|
||||
|
||||
return <ComponentToRender data-icon={iconName} {...directionalProps} {...propsToRender} />;
|
||||
};
|
||||
|
||||
export default AppIcon;
|
||||
|
||||
@@ -1,101 +1,103 @@
|
||||
// SVG assets
|
||||
import PencilIcon from './icons/PencilIcon';
|
||||
// MUI Icons
|
||||
import DefaultIcon from '@mui/icons-material/MoreHoriz';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import DayNightIcon from '@mui/icons-material/Brightness4';
|
||||
import NightIcon from '@mui/icons-material/Brightness3';
|
||||
import DayIcon from '@mui/icons-material/Brightness5';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
import HomeIcon from '@mui/icons-material/Home';
|
||||
import AccountCircle from '@mui/icons-material/AccountCircle';
|
||||
import PersonAddIcon from '@mui/icons-material/PersonAdd';
|
||||
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';
|
||||
import EditIcon from '@mui/icons-material/EditOutlined';
|
||||
import ArchiveIcon from '@mui/icons-material/Inventory2Outlined';
|
||||
import BankIcon from '@mui/icons-material/AccountBalanceOutlined';
|
||||
import CameraIcon from '@mui/icons-material/PhotoCameraOutlined';
|
||||
import WarningIcon from '@mui/icons-material/WarningAmberOutlined';
|
||||
import LocationIcon from '@mui/icons-material/LocationOnOutlined';
|
||||
import DeleteIcon from '@mui/icons-material/DeleteOutlined';
|
||||
import CoverageIcon from '@mui/icons-material/MapOutlined';
|
||||
// SVG assets — the Balinyaar brand mark
|
||||
import LogoMark from './icons/LogoMark';
|
||||
// MUI Icons — normalized to ONE visual family (Rounded): warmer, softer strokes
|
||||
// than the filled/outlined mix this registry used to carry, fitting the
|
||||
// clinical-but-human tone. Adding an icon = import here + one lowercase key
|
||||
// below; never import a raw MUI icon anywhere else in the app.
|
||||
import DefaultIcon from '@mui/icons-material/MoreHorizRounded';
|
||||
import CloseIcon from '@mui/icons-material/CloseRounded';
|
||||
import MenuIcon from '@mui/icons-material/MenuRounded';
|
||||
import SearchIcon from '@mui/icons-material/SearchRounded';
|
||||
import InfoIcon from '@mui/icons-material/InfoRounded';
|
||||
import HomeIcon from '@mui/icons-material/HomeRounded';
|
||||
import AccountCircleIcon from '@mui/icons-material/AccountCircleRounded';
|
||||
import ExitToAppIcon from '@mui/icons-material/ExitToAppRounded';
|
||||
import NotificationsIcon from '@mui/icons-material/NotificationsRounded';
|
||||
import DangerousIcon from '@mui/icons-material/DangerousRounded';
|
||||
import EventNoteIcon from '@mui/icons-material/EventNoteRounded';
|
||||
import GroupsIcon from '@mui/icons-material/GroupsRounded';
|
||||
import PeopleAltIcon from '@mui/icons-material/PeopleAltRounded';
|
||||
import WalletIcon from '@mui/icons-material/AccountBalanceWalletRounded';
|
||||
import DashboardIcon from '@mui/icons-material/DashboardRounded';
|
||||
import VerifiedUserIcon from '@mui/icons-material/VerifiedUserRounded';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircleRounded';
|
||||
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmptyRounded';
|
||||
import CancelIcon from '@mui/icons-material/CancelRounded';
|
||||
import MedicalServicesIcon from '@mui/icons-material/MedicalServicesRounded';
|
||||
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettingsRounded';
|
||||
import AddIcon from '@mui/icons-material/AddRounded';
|
||||
import EditIcon from '@mui/icons-material/EditRounded';
|
||||
import ArchiveIcon from '@mui/icons-material/Inventory2Rounded';
|
||||
import BankIcon from '@mui/icons-material/AccountBalanceRounded';
|
||||
import CameraIcon from '@mui/icons-material/PhotoCameraRounded';
|
||||
import WarningIcon from '@mui/icons-material/WarningAmberRounded';
|
||||
import LocationIcon from '@mui/icons-material/LocationOnRounded';
|
||||
import DeleteIcon from '@mui/icons-material/DeleteRounded';
|
||||
import CoverageIcon from '@mui/icons-material/MapRounded';
|
||||
// Catalog — nurse services surface + the customer Home service-category grid (f4/b5)
|
||||
import ServicesIcon from '@mui/icons-material/LocalOfferOutlined';
|
||||
import CategoryIcon from '@mui/icons-material/CategoryOutlined';
|
||||
import ElderlyIcon from '@mui/icons-material/ElderlyOutlined';
|
||||
import PostSurgeryIcon from '@mui/icons-material/HealingOutlined';
|
||||
import InfantIcon from '@mui/icons-material/ChildCareOutlined';
|
||||
import ChronicIcon from '@mui/icons-material/MonitorHeartOutlined';
|
||||
import CompanionshipIcon from '@mui/icons-material/VolunteerActivismOutlined';
|
||||
import ServicesIcon from '@mui/icons-material/LocalOfferRounded';
|
||||
import CategoryIcon from '@mui/icons-material/CategoryRounded';
|
||||
import ElderlyIcon from '@mui/icons-material/ElderlyRounded';
|
||||
import PostSurgeryIcon from '@mui/icons-material/HealingRounded';
|
||||
import InfantIcon from '@mui/icons-material/ChildCareRounded';
|
||||
import ChronicIcon from '@mui/icons-material/MonitorHeartRounded';
|
||||
import CompanionshipIcon from '@mui/icons-material/VolunteerActivismRounded';
|
||||
// Verification — nurse trust flow (f5/b6): document upload, credential + identity, re-upload
|
||||
import UploadIcon from '@mui/icons-material/CloudUploadOutlined';
|
||||
import DocumentIcon from '@mui/icons-material/InsertDriveFileOutlined';
|
||||
import RefreshIcon from '@mui/icons-material/RefreshOutlined';
|
||||
import IdentityIcon from '@mui/icons-material/BadgeOutlined';
|
||||
import LicenseIcon from '@mui/icons-material/WorkspacePremiumOutlined';
|
||||
import PublishIcon from '@mui/icons-material/RocketLaunchOutlined';
|
||||
import UploadIcon from '@mui/icons-material/CloudUploadRounded';
|
||||
import DocumentIcon from '@mui/icons-material/InsertDriveFileRounded';
|
||||
import RefreshIcon from '@mui/icons-material/RefreshRounded';
|
||||
import IdentityIcon from '@mui/icons-material/BadgeRounded';
|
||||
import LicenseIcon from '@mui/icons-material/WorkspacePremiumRounded';
|
||||
import PublishIcon from '@mui/icons-material/RocketLaunchRounded';
|
||||
// Search & discovery — the customer nurse-finding flow (f6/b7): rating star, filter controls
|
||||
import StarIcon from '@mui/icons-material/Star';
|
||||
import TuneIcon from '@mui/icons-material/TuneOutlined';
|
||||
import StarIcon from '@mui/icons-material/StarRounded';
|
||||
import StarHalfIcon from '@mui/icons-material/StarHalfRounded';
|
||||
import TuneIcon from '@mui/icons-material/TuneRounded';
|
||||
import SortIcon from '@mui/icons-material/SortRounded';
|
||||
// Booking requests — the pre-payment intent flow (f7/b8): nurse inbox + the pay-&-continue handoff
|
||||
import RequestsIcon from '@mui/icons-material/AssignmentOutlined';
|
||||
import PaymentIcon from '@mui/icons-material/CreditCardOutlined';
|
||||
import RequestsIcon from '@mui/icons-material/AssignmentRounded';
|
||||
import PaymentIcon from '@mui/icons-material/CreditCardRounded';
|
||||
// Bookings, sessions & EVV — the post-payment engagement (f8/b9): check-in/out, GPS, care instructions
|
||||
import CheckInIcon from '@mui/icons-material/LoginOutlined';
|
||||
import CheckOutIcon from '@mui/icons-material/LogoutOutlined';
|
||||
import GpsIcon from '@mui/icons-material/MyLocationOutlined';
|
||||
import ScheduleIcon from '@mui/icons-material/ScheduleOutlined';
|
||||
import ClinicalIcon from '@mui/icons-material/HealthAndSafetyOutlined';
|
||||
import MedicationIcon from '@mui/icons-material/MedicationOutlined';
|
||||
import EmergencyIcon from '@mui/icons-material/LocalPhoneOutlined';
|
||||
import LockIcon from '@mui/icons-material/LockOutlined';
|
||||
import CheckInIcon from '@mui/icons-material/LoginRounded';
|
||||
import CheckOutIcon from '@mui/icons-material/LogoutRounded';
|
||||
import GpsIcon from '@mui/icons-material/MyLocationRounded';
|
||||
import ScheduleIcon from '@mui/icons-material/ScheduleRounded';
|
||||
import ClinicalIcon from '@mui/icons-material/HealthAndSafetyRounded';
|
||||
import MedicationIcon from '@mui/icons-material/MedicationRounded';
|
||||
import EmergencyIcon from '@mui/icons-material/LocalPhoneRounded';
|
||||
import PhoneIcon from '@mui/icons-material/PhoneRounded';
|
||||
import LockIcon from '@mui/icons-material/LockRounded';
|
||||
import NavigateIcon from '@mui/icons-material/NavigationRounded';
|
||||
// BNPL — the installment-checkout surface (f11/b12): installment plans + repayment schedule
|
||||
import InstallmentsIcon from '@mui/icons-material/PaymentsOutlined';
|
||||
import InstallmentsIcon from '@mui/icons-material/PaymentsRounded';
|
||||
// Payouts — the nurse earnings & payout-history surface (f12/b13)
|
||||
import EarningsIcon from '@mui/icons-material/PaidOutlined';
|
||||
import EarningsIcon from '@mui/icons-material/PaidRounded';
|
||||
// Reviews & patient care records (f13/b14): visit-note authoring, record tabs, family-ownership banner
|
||||
import NotesIcon from '@mui/icons-material/NoteAltOutlined';
|
||||
import RoutineIcon from '@mui/icons-material/EventRepeatOutlined';
|
||||
import TasksIcon from '@mui/icons-material/ChecklistOutlined';
|
||||
import HistoryIcon from '@mui/icons-material/HistoryOutlined';
|
||||
import FamilyIcon from '@mui/icons-material/FamilyRestroomOutlined';
|
||||
import NotesIcon from '@mui/icons-material/NoteAltRounded';
|
||||
import RoutineIcon from '@mui/icons-material/EventRepeatRounded';
|
||||
import TasksIcon from '@mui/icons-material/ChecklistRounded';
|
||||
import HistoryIcon from '@mui/icons-material/HistoryRounded';
|
||||
import FamilyIcon from '@mui/icons-material/FamilyRestroomRounded';
|
||||
// Messaging (tickets) & notifications (f14/b15): support inbox + the message-send action
|
||||
import SupportIcon from '@mui/icons-material/SupportAgentOutlined';
|
||||
import SendIcon from '@mui/icons-material/SendOutlined';
|
||||
import SupportIcon from '@mui/icons-material/SupportAgentRounded';
|
||||
import SendIcon from '@mui/icons-material/SendRounded';
|
||||
// Admin backoffice & partner consoles (f15/b15): config, holidays, audit, alerts, moderation, partners, refunds
|
||||
import ConfigIcon from '@mui/icons-material/TuneOutlined';
|
||||
import CalendarIcon from '@mui/icons-material/CalendarMonthOutlined';
|
||||
import AuditIcon from '@mui/icons-material/FactCheckOutlined';
|
||||
import AlertsIcon from '@mui/icons-material/NotificationImportantOutlined';
|
||||
import ModerationIcon from '@mui/icons-material/GavelOutlined';
|
||||
import PartnersIcon from '@mui/icons-material/ApartmentOutlined';
|
||||
import RolesIcon from '@mui/icons-material/ManageAccountsOutlined';
|
||||
import RefundsIcon from '@mui/icons-material/CurrencyExchangeOutlined';
|
||||
import DownloadIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||
import ExpandIcon from '@mui/icons-material/ExpandMoreOutlined';
|
||||
import ExternalIcon from '@mui/icons-material/OpenInNewOutlined';
|
||||
import AssignIcon from '@mui/icons-material/AssignmentIndOutlined';
|
||||
import CalendarIcon from '@mui/icons-material/CalendarMonthRounded';
|
||||
import AuditIcon from '@mui/icons-material/FactCheckRounded';
|
||||
import AlertsIcon from '@mui/icons-material/NotificationImportantRounded';
|
||||
import ModerationIcon from '@mui/icons-material/GavelRounded';
|
||||
import PartnersIcon from '@mui/icons-material/ApartmentRounded';
|
||||
import RolesIcon from '@mui/icons-material/ManageAccountsRounded';
|
||||
import RefundsIcon from '@mui/icons-material/CurrencyExchangeRounded';
|
||||
import DownloadIcon from '@mui/icons-material/FileDownloadRounded';
|
||||
import ExpandIcon from '@mui/icons-material/ExpandMoreRounded';
|
||||
import ExternalIcon from '@mui/icons-material/OpenInNewRounded';
|
||||
import AssignIcon from '@mui/icons-material/AssignmentIndRounded';
|
||||
// Cross-cutting affordances: back navigation, share/copy, attachments
|
||||
import BackIcon from '@mui/icons-material/ArrowBackRounded';
|
||||
import ShareIcon from '@mui/icons-material/ShareRounded';
|
||||
import CopyIcon from '@mui/icons-material/ContentCopyRounded';
|
||||
import AttachmentIcon from '@mui/icons-material/AttachFileRounded';
|
||||
|
||||
/**
|
||||
* List of all available Icon names
|
||||
@@ -112,21 +114,13 @@ export type IconName = keyof typeof ICONS;
|
||||
*/
|
||||
export const ICONS /* Note: Setting type disables property autocomplete :( was - : Record<string, ComponentType> */ = {
|
||||
default: DefaultIcon,
|
||||
logo: PencilIcon,
|
||||
logo: LogoMark,
|
||||
close: CloseIcon,
|
||||
menu: MenuIcon,
|
||||
settings: SettingsIcon,
|
||||
visibilityon: VisibilityIcon,
|
||||
visibilityoff: VisibilityOffIcon,
|
||||
daynight: DayNightIcon,
|
||||
night: NightIcon,
|
||||
day: DayIcon,
|
||||
search: SearchIcon,
|
||||
info: InfoIcon,
|
||||
home: HomeIcon,
|
||||
account: AccountCircle,
|
||||
signup: PersonAddIcon,
|
||||
login: PersonIcon,
|
||||
account: AccountCircleIcon,
|
||||
logout: ExitToAppIcon,
|
||||
notifications: NotificationsIcon,
|
||||
error: DangerousIcon,
|
||||
@@ -134,7 +128,7 @@ export const ICONS /* Note: Setting type disables property autocomplete :( was -
|
||||
patients: GroupsIcon,
|
||||
users: PeopleAltIcon,
|
||||
wallet: WalletIcon,
|
||||
profile: PersonOutlineIcon,
|
||||
profile: AccountCircleIcon,
|
||||
dashboard: DashboardIcon,
|
||||
verification: VerifiedUserIcon,
|
||||
verified: CheckCircleIcon,
|
||||
@@ -165,7 +159,9 @@ export const ICONS /* Note: Setting type disables property autocomplete :( was -
|
||||
license: LicenseIcon,
|
||||
publish: PublishIcon,
|
||||
star: StarIcon,
|
||||
star_half: StarHalfIcon,
|
||||
tune: TuneIcon,
|
||||
sort: SortIcon,
|
||||
requests: RequestsIcon,
|
||||
payment: PaymentIcon,
|
||||
check_in: CheckInIcon,
|
||||
@@ -175,7 +171,9 @@ export const ICONS /* Note: Setting type disables property autocomplete :( was -
|
||||
clinical: ClinicalIcon,
|
||||
medication: MedicationIcon,
|
||||
emergency: EmergencyIcon,
|
||||
phone: PhoneIcon,
|
||||
lock: LockIcon,
|
||||
navigate: NavigateIcon,
|
||||
installments: InstallmentsIcon,
|
||||
earnings: EarningsIcon,
|
||||
notes: NotesIcon,
|
||||
@@ -185,7 +183,7 @@ export const ICONS /* Note: Setting type disables property autocomplete :( was -
|
||||
family: FamilyIcon,
|
||||
support: SupportIcon,
|
||||
send: SendIcon,
|
||||
config: ConfigIcon,
|
||||
config: TuneIcon,
|
||||
calendar: CalendarIcon,
|
||||
audit: AuditIcon,
|
||||
alerts: AlertsIcon,
|
||||
@@ -197,4 +195,16 @@ export const ICONS /* Note: Setting type disables property autocomplete :( was -
|
||||
expand: ExpandIcon,
|
||||
external: ExternalIcon,
|
||||
assign: AssignIcon,
|
||||
back: BackIcon,
|
||||
chevron_start: BackIcon,
|
||||
share: ShareIcon,
|
||||
copy: CopyIcon,
|
||||
attachment: AttachmentIcon,
|
||||
};
|
||||
|
||||
/**
|
||||
* Icons authored for LTR that must mirror horizontally under RTL (back arrows,
|
||||
* chevrons pointing "start"). AppIcon applies the flip via a `data-icon-directional`
|
||||
* attribute + the single CSS rule in globals.css — add a name here, nothing else.
|
||||
*/
|
||||
export const DIRECTIONAL_ICONS = new Set<IconName>(['back', 'chevron_start']);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
import { IconProps } from '../utils';
|
||||
|
||||
/**
|
||||
* The full-color Balinyaar mark: deep-teal rounded-square ground, cream
|
||||
* lowercase "b" glyph, single terracotta dot — the brand identity described
|
||||
* in the frontend-designer skill. Colors are `var(--bal-*)` tokens (never a
|
||||
* hex literal), so the mark tracks the color scheme like everything else.
|
||||
* Used directly by BrandMark next to the translated wordmark — the wordmark
|
||||
* stays real `<Typography>` text (not baked into the SVG) so it still
|
||||
* switches with the locale. For an inline, single-color icon (e.g. inside a
|
||||
* button) use AppIcon's `logo` name (LogoMark) instead.
|
||||
* @component LogoLockup
|
||||
*/
|
||||
const LogoLockup: FunctionComponent<IconProps> = ({ size = 56, ...props }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width={size} height={size} {...props}>
|
||||
<rect width="40" height="40" rx="10" fill="var(--bal-primary)" />
|
||||
<path
|
||||
fill="var(--bal-primary-contrast)"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M21,17 A7.5,7.5 0 1,0 21,32 A7.5,7.5 0 1,0 21,17 Z
|
||||
M21,20.8 A3.7,3.7 0 1,0 21,28.2 A3.7,3.7 0 1,0 21,20.8 Z"
|
||||
/>
|
||||
<rect x="12.9" y="9" width="4.8" height="22" rx="2.4" fill="var(--bal-primary-contrast)" />
|
||||
<circle cx="28.5" cy="10.5" r="2.6" fill="var(--bal-secondary)" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default LogoLockup;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
import { IconProps } from '../utils';
|
||||
|
||||
/**
|
||||
* The Balinyaar monochrome mark — a lowercase "b" (stem + bowl) reduced to a
|
||||
* single `currentColor` fill so it works like any other AppIcon: it recolors
|
||||
* via the `color` prop and therefore for free across the light/dark scheme.
|
||||
* The bowl's counter is a true transparent hole (evenodd fill-rule on two
|
||||
* concentric circles), not a background-color cutout — it reads correctly on
|
||||
* any surface. For the full-color brand lockup (auth splash, TopBar) see
|
||||
* LogoLockup.tsx instead.
|
||||
* @component LogoMark
|
||||
*/
|
||||
const LogoMark: FunctionComponent<IconProps> = ({ color = 'currentColor', ...props }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" {...props}>
|
||||
<path
|
||||
fill={color}
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M19.5,15.3 A7.2,7.2 0 1,0 19.5,29.7 A7.2,7.2 0 1,0 19.5,15.3 Z
|
||||
M19.5,18.9 A3.6,3.6 0 1,0 19.5,26.1 A3.6,3.6 0 1,0 19.5,18.9 Z"
|
||||
/>
|
||||
<rect x="10.9" y="8" width="4.6" height="21" rx="2.3" fill={color} />
|
||||
<circle cx="27" cy="10" r="2.4" fill={color} />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default LogoMark;
|
||||
@@ -1,29 +0,0 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
import { IconProps } from '../utils';
|
||||
|
||||
const PencilIcon: 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 PencilIcon;
|
||||
@@ -36,9 +36,10 @@ describe('<AppIconButton/> component', () => {
|
||||
const svg = button.querySelector('svg');
|
||||
expect(svg).toBeDefined();
|
||||
expect(svg).toHaveAttribute('data-icon', 'default'); // default icon
|
||||
expect(svg).toHaveAttribute('size', String(APP_ICON_SIZE)); // default size
|
||||
expect(svg).toHaveAttribute('height', String(APP_ICON_SIZE)); // default size when .size is not set
|
||||
expect(svg).toHaveAttribute('width', String(APP_ICON_SIZE)); // default size when .size is not se
|
||||
// Size is actually driven by fontSize, not a `size` DOM attribute (invalid on <svg>).
|
||||
expect(svg).toHaveStyle(`font-size: ${APP_ICON_SIZE}px`);
|
||||
});
|
||||
|
||||
it('supports .color property', () => {
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { randomText } from '@/utils';
|
||||
import AppImage from './AppImage';
|
||||
|
||||
const ComponentToTest = AppImage;
|
||||
|
||||
/**
|
||||
* Tests for <AppImage/> component
|
||||
*/
|
||||
describe('<AppImage/> component', () => {
|
||||
const src = 'https:/domain.com/image.jpg';
|
||||
|
||||
it('renders itself', () => {
|
||||
const testId = randomText(8);
|
||||
render(<ComponentToTest data-testid={testId} src={src} />);
|
||||
const image = screen.getByTestId(testId);
|
||||
expect(image).toBeDefined();
|
||||
expect(image).toHaveAttribute('src', src);
|
||||
expect(image).toHaveAttribute('alt', 'Image'); // Default prop value
|
||||
expect(image).toHaveAttribute('height', '256'); // Default prop value
|
||||
expect(image).toHaveAttribute('width', '256'); // Default prop value
|
||||
});
|
||||
|
||||
it('supports .width and .height props', () => {
|
||||
const testId = randomText(8);
|
||||
const height = 345;
|
||||
const width = 123;
|
||||
render(<ComponentToTest data-testid={testId} height={height} src={src} width={width} />);
|
||||
const image = screen.getByTestId(testId);
|
||||
expect(image).toBeDefined();
|
||||
expect(image).toHaveAttribute('height', String(height));
|
||||
expect(image).toHaveAttribute('width', String(width));
|
||||
});
|
||||
|
||||
it('supports .title property', () => {
|
||||
const testId = randomText(8);
|
||||
const title = randomText(16);
|
||||
render(<ComponentToTest data-testid={testId} src={src} title={title} />);
|
||||
const image = screen.getByTestId(testId);
|
||||
expect(image).toBeDefined();
|
||||
expect(image).toHaveAttribute('title', title);
|
||||
expect(image).toHaveAttribute('alt', title); // When title is provided, it is used as alt
|
||||
});
|
||||
|
||||
it('supports .alt property even when .title is provided', () => {
|
||||
const testId = randomText(8);
|
||||
const title = randomText(16);
|
||||
const alt = randomText(32);
|
||||
render(<ComponentToTest alt={alt} data-testid={testId} src={src} title={title} />);
|
||||
const image = screen.getByTestId(testId);
|
||||
expect(image).toBeDefined();
|
||||
expect(image).toHaveAttribute('alt', alt);
|
||||
expect(image).toHaveAttribute('title', title);
|
||||
});
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
import NextImage, { ImageProps } from 'next/image';
|
||||
|
||||
interface AppImageProps extends Omit<ImageProps, 'alt'> {
|
||||
alt?: string; // Make property optional as it was before NextJs v13
|
||||
}
|
||||
|
||||
/**
|
||||
* Application wrapper around NextJS image with some default props
|
||||
* @component AppImage
|
||||
*/
|
||||
const AppImage: FunctionComponent<AppImageProps> = ({
|
||||
title, // Note: value has be destructed before usage as default value for other property
|
||||
alt = title ?? 'Image',
|
||||
height = 256,
|
||||
width = 256,
|
||||
...restOfProps
|
||||
}) => {
|
||||
// Uses custom loader + unoptimized="true" to avoid NextImage warning https://nextjs.org/docs/api-reference/next/image#unoptimized
|
||||
return <NextImage alt={alt} height={height} title={title} unoptimized={true} width={width} {...restOfProps} />;
|
||||
};
|
||||
|
||||
export default AppImage;
|
||||
@@ -1,3 +0,0 @@
|
||||
import AppImage from './AppImage';
|
||||
|
||||
export { AppImage as default, AppImage };
|
||||
@@ -2,9 +2,8 @@ import AppAlert from './AppAlert';
|
||||
import AppButton from './AppButton';
|
||||
import AppIcon from './AppIcon';
|
||||
import AppIconButton from './AppIconButton';
|
||||
import AppImage from './AppImage';
|
||||
import AppLink from './AppLink';
|
||||
import AppLoading from './AppLoading';
|
||||
import ErrorBoundary from './ErrorBoundary';
|
||||
|
||||
export { ErrorBoundary, AppAlert, AppButton, AppIcon, AppIconButton, AppImage, AppLink, AppLoading };
|
||||
export { ErrorBoundary, AppAlert, AppButton, AppIcon, AppIconButton, AppLink, AppLoading };
|
||||
|
||||
@@ -2,19 +2,24 @@
|
||||
* Components configuration
|
||||
*/
|
||||
export const CONTENT_MAX_WIDTH = 800;
|
||||
export const CONTENT_MIN_WIDTH = 320; // CONTENT_MAX_WIDTH - Sidebar width
|
||||
// The reading-width column (800) does not derive from the sidebar width (240,
|
||||
// see layout/config.ts SIDE_BAR_WIDTH) — they're independent decisions. 320 is
|
||||
// a standalone minimum so narrow viewports never squeeze content unreadably.
|
||||
export const CONTENT_MIN_WIDTH = 320;
|
||||
|
||||
/**
|
||||
* AppAlert and AppSnackBarAlert components
|
||||
* AppAlert and AppSnackBarAlert components.
|
||||
* A bare <AppAlert> defaults to a calm, non-alarming baseline — genuinely
|
||||
* error-severity call-sites must pass severity="error" explicitly. `standard`
|
||||
* pairs with the semantic palette (colors.ts) so inline alerts match toasts.
|
||||
*/
|
||||
export const APP_ALERT_SEVERITY = 'error'; // 'error' | 'info'| 'success' | 'warning'
|
||||
export const APP_ALERT_VARIANT = 'filled'; // 'filled' | 'outlined' | 'standard'
|
||||
export const APP_ALERT_SEVERITY = 'info'; // 'error' | 'info'| 'success' | 'warning'
|
||||
export const APP_ALERT_VARIANT = 'standard'; // 'filled' | 'outlined' | 'standard'
|
||||
|
||||
/**
|
||||
* AppButton component
|
||||
*/
|
||||
export const APP_BUTTON_VARIANT = 'contained'; // | 'text' | 'outlined'
|
||||
export const APP_BUTTON_MARGIN = 1;
|
||||
|
||||
/**
|
||||
* AppIcon component
|
||||
|
||||
@@ -70,7 +70,7 @@ const AddressCard: FunctionComponent<AddressCardProps> = ({
|
||||
color="primary"
|
||||
disabled={settingPrimary}
|
||||
onClick={onSetPrimary}
|
||||
sx={{ m: 0, mt: 0.25, alignSelf: 'flex-start' }}
|
||||
sx={{ mt: 0.25, alignSelf: 'flex-start' }}
|
||||
>
|
||||
{setPrimaryLabel}
|
||||
</AppButton>
|
||||
|
||||
@@ -149,11 +149,11 @@ const AddressForm: FunctionComponent<AddressFormProps> = ({ initial, submitting
|
||||
|
||||
<Stack direction="row" sx={{ gap: 1, justifyContent: 'flex-end' }}>
|
||||
{onCancel ? (
|
||||
<AppButton variant="text" onClick={onCancel} disabled={submitting} sx={{ m: 0 }}>
|
||||
<AppButton variant="text" onClick={onCancel} disabled={submitting}>
|
||||
{tc('cancel')}
|
||||
</AppButton>
|
||||
) : null}
|
||||
<AppButton color="primary" variant="contained" onClick={handleSubmit} disabled={submitting} sx={{ m: 0 }}>
|
||||
<AppButton color="primary" variant="contained" onClick={handleSubmit} disabled={submitting}>
|
||||
{submitting ? tc('saving') : tc('save')}
|
||||
</AppButton>
|
||||
</Stack>
|
||||
|
||||
@@ -53,7 +53,6 @@ const BookingSupportEntry: FunctionComponent<BookingSupportEntryProps> = ({ book
|
||||
color="primary"
|
||||
startIcon="support"
|
||||
onClick={() => setDialogOpen(true)}
|
||||
sx={{ m: 0 }}
|
||||
>
|
||||
{t('open_from_booking')}
|
||||
</AppButton>
|
||||
|
||||
@@ -48,7 +48,6 @@ const TicketInboxScreen: FunctionComponent<TicketInboxScreenProps> = ({ role })
|
||||
color="primary"
|
||||
startIcon="support"
|
||||
onClick={() => setDialogOpen(true)}
|
||||
sx={{ m: 0 }}
|
||||
>
|
||||
{t('contact_support')}
|
||||
</AppButton>
|
||||
|
||||
@@ -43,7 +43,7 @@ const TicketThreadScreen: FunctionComponent<TicketThreadScreenProps> = ({ role,
|
||||
|
||||
return (
|
||||
<Stack sx={{ gap: 2, maxWidth: 720, mx: 'auto', width: '100%' }}>
|
||||
<AppButton variant="text" color="primary" onClick={goBack} sx={{ m: 0, alignSelf: 'flex-start' }}>
|
||||
<AppButton variant="text" color="primary" onClick={goBack} sx={{ alignSelf: 'flex-start' }}>
|
||||
{t('back_to_tickets')}
|
||||
</AppButton>
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ const NotificationCenter: FunctionComponent<NotificationCenterProps> = ({ role }
|
||||
color="primary"
|
||||
onClick={() => markAll.mutate()}
|
||||
disabled={markAll.isPending}
|
||||
sx={{ m: 0 }}
|
||||
>
|
||||
{t('mark_all_read')}
|
||||
</AppButton>
|
||||
|
||||
Reference in New Issue
Block a user