ui phase 0

This commit is contained in:
hamid
2026-07-17 14:00:22 +03:30
parent 9051bb3e18
commit f1cba6cf74
139 changed files with 1242 additions and 667 deletions
@@ -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;
+110 -100
View File
@@ -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 };
+1 -2
View File
@@ -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 };