Update AxoBadge component

This commit is contained in:
Jamie
2026-07-13 21:13:52 -07:00
committed by GitHub
parent bc43719ffc
commit 4fd8ab741b
10 changed files with 309 additions and 185 deletions
+8
View File
@@ -31,6 +31,10 @@
"messageformat": "Clear",
"description": "Axo Design System > Text Field Component > Clear Button > Generic accessibility label"
},
"icu:AxoBadge.MaxOverflow": {
"messageformat": "{max, number}+",
"description": "Axo Design System > Badge Component > When numeric value is greater than max (Ex: 99 -> '99', 100 -> '99+')"
},
"icu:AddUserToAnotherGroupModal__title": {
"messageformat": "Add to a group",
"description": "Shown as the title of the dialog that allows you to add a contact to an group"
@@ -435,6 +439,10 @@
"messageformat": "{maxCount, number}+",
"description": "Left Pane > Inbox > Chat List > Chat Folders > Item > Badge Count > When over the max count (Example: 1000 or more would be 999+)"
},
"icu:LeftPaneChatFolders__ItemUnreadBadge__AccessibleLabel": {
"messageformat": "{count, plural, one {# Unread Message} other {# Unread Messages}}",
"description": "Left Pane > Inbox > Chat List > Chat Folders > Item > Badge Count > Accessible Label"
},
"icu:LeftPaneChatFolders__Item__ContextMenu__MarkAllRead": {
"messageformat": "Mark all read",
"description": "Left Pane > Inbox > Chat List > Chat Folders > Item > Right-Click Context Menu > Mark all unread chats in chat folder as read"
+84 -45
View File
@@ -3,56 +3,95 @@
import type { JSX } from 'react';
import type { Meta } from '@storybook/react';
import { ExperimentalAxoBadge } from './AxoBadge.dom.tsx';
import { AxoBadge } from './AxoBadge.dom.tsx';
import { tw } from './tw.dom.tsx';
export default {
title: 'Axo/AriaBadge (Experimental)',
title: 'Axo/AxoBadge',
} satisfies Meta;
export function All(): JSX.Element {
const values: ReadonlyArray<ExperimentalAxoBadge.Value> = [
-1,
0,
1,
10,
123,
1234,
12345,
'mention',
'unread',
];
function Template(props: Partial<AxoBadge.RootProps>) {
return (
<table className={tw('border-separate border-spacing-2 text-center')}>
<thead>
<th>size</th>
{values.map(value => {
return <th key={value}>{value}</th>;
})}
</thead>
<tbody>
{ExperimentalAxoBadge._getAllSizes().map(size => {
return (
<tr key={size}>
<th>{size}</th>
{values.map(value => {
return (
<td key={value} className={tw('')}>
<ExperimentalAxoBadge.Root
size={size}
value={value}
max={99}
maxDisplay="99+"
label={null}
/>
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
<AxoBadge.Root
variant="primary"
size="lg"
value={1}
max={99}
label={null}
{...props}
/>
);
}
export function Basic(): JSX.Element {
return <Template />;
}
export function Digits(): JSX.Element {
return (
<div className={tw('flex gap-4')}>
<Template value={10} max={Infinity} />
<Template value={99} max={Infinity} />
<Template value={999} max={Infinity} />
<Template value={9999} max={Infinity} />
<Template value={999999999} max={Infinity} />
</div>
);
}
export function Max(): JSX.Element {
return (
<div className={tw('flex gap-4')}>
<Template value={9} max={9} />
<Template value={10} max={9} />
<Template value={99} max={99} />
<Template value={100} max={99} />
<Template value={999} max={999} />
<Template value={1000} max={999} />
<Template value={9999} max={9999} />
<Template value={10000} max={9999} />
</div>
);
}
export function Mention(): JSX.Element {
return <Template value="mention" />;
}
export function Unread(): JSX.Element {
return <Template value="unread" />;
}
export function Error(): JSX.Element {
return <Template variant="destructive" value="error" />;
}
export function Variants(): JSX.Element {
return (
<div className={tw('flex gap-4')}>
<Template variant="primary" />
<Template variant="destructive" />
<Template variant="secondary" />
</div>
);
}
export function Sizes(): JSX.Element {
return (
<div className={tw('flex gap-4')}>
<Template size="dot" />
<Template size="sm" />
<Template size="md" />
<Template size="lg" />
</div>
);
}
export function Dot(): JSX.Element {
return (
<div className={tw('flex gap-4')}>
<Template size="dot" variant="primary" value="unread" />
<Template size="dot" variant="destructive" value="unread" />
</div>
);
}
+93 -26
View File
@@ -6,6 +6,7 @@ import { AxoSymbol } from './AxoSymbol.dom.tsx';
import { tw } from './tw.dom.tsx';
import { unreachable } from './_internal/assert.std.tsx';
import { variants } from './_internal/variants.dom.tsx';
import { type AxoIntl, useAxoIntl } from './_internal/AxoIntl.dom.tsx';
/**
* @example Anatomy
@@ -21,14 +22,19 @@ import { variants } from './_internal/variants.dom.tsx';
* </AxoBadge.Root>
* ````
*/
export namespace ExperimentalAxoBadge {
export namespace AxoBadge {
/**
* Visual style of the badge.
*/
export type Variant = 'primary' | 'secondary' | 'destructive';
/**
* Visual size of the badge.
* - `sm`: 14px height
* - `md`: 16px height
* - `lg`: 18px height
*/
export type Size = 'sm' | 'md' | 'lg';
export type Size = 'dot' | 'sm' | 'md' | 'lg';
/**
* What the badge represents.
@@ -36,25 +42,39 @@ export namespace ExperimentalAxoBadge {
* - `'mention'`: Shows an `@`-sign icon.
* - `'unread'`: A dot with no text content.
*/
export type Value = number | 'mention' | 'unread';
export type Value = number | 'mention' | 'unread' | 'error';
const baseStyles = tw(
'flex size-fit items-center justify-center-safe overflow-clip',
'flex items-center justify-center-safe overflow-clip',
'rounded-full font-semibold',
'bg-accent text-primary-oncolor',
'forced-color-adjust-none forced-colors:bg-[Mark] forced-colors:text-[MarkText]'
);
const Variants = variants<Variant>('AxoBadge.Variant', {
primary: tw('bg-accent text-primary-oncolor'),
secondary: tw('bg-primary text-secondary'),
destructive: tw('bg-destructive text-primary-oncolor'),
});
const Sizes = variants<Size>('AxoBadge.Size', {
sm: tw(baseStyles, 'min-h-3.5 min-w-3.5 text-[8px] leading-3.5'),
md: tw(baseStyles, 'min-h-4 min-w-4 text-[11px] leading-4'),
lg: tw(baseStyles, 'min-h-4.5 min-w-4.5 text-[11px] leading-4.5'),
dot: tw('size-1.5'),
sm: tw('size-fit min-h-3.5 min-w-3.5'),
md: tw('size-fit min-h-4 min-w-4'),
lg: tw('size-fit min-h-4.5 min-w-4.5'),
});
const TextSizes = variants<Size>('AxoBadge.Size', {
dot: tw('sr-only'),
sm: tw('text-[8px] leading-3.5'),
md: tw('text-[11px] leading-4'),
lg: tw('text-[11px] leading-4.5'),
});
const CountSizes = variants<Size>('AxoBadge.Size', {
sm: tw('px-[3px]'),
md: tw('px-[4px]'),
lg: tw('px-[5px]'),
dot: tw(),
sm: tw('px-0.75'),
md: tw('px-1'),
lg: tw('px-1.25'),
});
/** @testexport */
@@ -67,10 +87,10 @@ export namespace ExperimentalAxoBadge {
function formatBadgeCount(
value: number,
max: number,
maxDisplay: string
intl: AxoIntl.ContextType
): string {
if (value > max) {
return maxDisplay;
return intl.get('AxoBadge.MaxOverflow')(max);
}
cachedNumberFormat ??= new Intl.NumberFormat();
return cachedNumberFormat.format(value);
@@ -82,14 +102,14 @@ export namespace ExperimentalAxoBadge {
*/
export type RootProps = Readonly<{
/** Visual style of the badge. */
variant: Variant;
/** Visual size of the badge. */
size: Size;
/** What the badge represents. */
value: Value;
/** When `value` is a number, values above this are replaced with `maxDisplay`. */
/** When `value` is a number, values above this are formatted `{max}+`. */
max: number;
/** The string shown when the numeric `value` exceeds `max` (e.g. `"999+"`). */
maxDisplay: string;
/** Accessible label for screen readers. Pass `null` if the badge is purely decorative. */
label: string | null;
}>;
@@ -99,41 +119,88 @@ export namespace ExperimentalAxoBadge {
*
* @example Count with overflow
* ```tsx
* <ExperimentalAxoBadge.Root size="md" value={42} max={99} maxDisplay="99+" label="42 unread messages" />
* <AxoBadge.Root
* variant="primary"
* size="md"
* value={42}
* max={99}
* label="42 unread messages"
* />
* ```
*
* @example Mention
* ```tsx
* <ExperimentalAxoBadge.Root size="md" value="mention" max={0} maxDisplay="" label="You were mentioned" />
* <AxoBadge.Root
* variant="primary"
* size="md"
* value="mention"
* max={99}
* label="You were mentioned"
* />
* ```
*
* @example Unread dot
* @example Unread
* ```tsx
* <ExperimentalAxoBadge.Root size="md" value="unread" max={0} maxDisplay="" label="Marked unread" />
* <AxoBadge.Root
* variant="primary"
* size="md"
* value="unread"
* max={99}
* label="Marked unread"
* />
* ```
*
* * @example Update Dot
* ```tsx
* <AxoBadge.Root
* variant="primary"
* size="dot"
* value={0}
* max={0}
* label="Update Available"
* />
* ```
*/
export const Root: FC<RootProps> = memo(props => {
const { size, value, max, maxDisplay } = props;
const { variant, size, value, max } = props;
const intl = useAxoIntl();
const children = useMemo(() => {
if (value === 'unread') {
return null;
}
if (value === 'error') {
return (
<span className={TextSizes.get(size)}>
<AxoSymbol.InlineGlyph symbol="error-fill" label={null} />
</span>
);
}
if (value === 'mention') {
return <AxoSymbol.InlineGlyph symbol="at" label={null} />;
return (
<span className={TextSizes.get(size)}>
<AxoSymbol.InlineGlyph symbol="at" label={null} />
</span>
);
}
if (typeof value === 'number') {
return (
<span aria-hidden className={CountSizes.get(size)}>
{formatBadgeCount(value, max, maxDisplay)}
<span
aria-hidden
className={tw(TextSizes.get(size), CountSizes.get(size))}
>
{formatBadgeCount(value, max, intl)}
</span>
);
}
unreachable(value);
}, [size, value, max, maxDisplay]);
}, [size, value, max, intl]);
return (
<span aria-label={props.label ?? undefined} className={Sizes.get(size)}>
<span
aria-label={props.label ?? undefined}
className={tw(baseStyles, Variants.get(variant), Sizes.get(size))}
>
{children}
</span>
);
+9 -9
View File
@@ -42,11 +42,11 @@ function Template(props: {
Inbox
</ExperimentalAxoSegmentedControl.ItemText>
{props.includeBadges && (
<ExperimentalAxoSegmentedControl.ExperimentalItemBadge
<ExperimentalAxoSegmentedControl.ItemBadge
variant="primary"
value={42}
max={99}
maxDisplay="99+"
label={null}
label="42 unread messages"
/>
)}
</ExperimentalAxoSegmentedControl.Item>
@@ -56,11 +56,11 @@ function Template(props: {
Drafts
</ExperimentalAxoSegmentedControl.ItemText>
{props.includeBadges && (
<ExperimentalAxoSegmentedControl.ExperimentalItemBadge
<ExperimentalAxoSegmentedControl.ItemBadge
variant="primary"
value="mention"
max={99}
maxDisplay="99+"
label={null}
label="You were mentioned"
/>
)}
</ExperimentalAxoSegmentedControl.Item>
@@ -69,11 +69,11 @@ function Template(props: {
Sent
</ExperimentalAxoSegmentedControl.ItemText>
{props.includeBadges && (
<ExperimentalAxoSegmentedControl.ExperimentalItemBadge
<ExperimentalAxoSegmentedControl.ItemBadge
variant="primary"
value="unread"
max={99}
maxDisplay="99+"
label={null}
label="Marked unread"
/>
)}
</ExperimentalAxoSegmentedControl.Item>
+9 -13
View File
@@ -147,7 +147,7 @@ export namespace ExperimentalAxoSegmentedControl {
*/
value: string;
/**
* Should be an `ItemText`, optionally followed by an `ExperimentalItemBadge`.
* Should be an `ItemText`, optionally followed by an `ItemBadge`.
*/
children: ReactNode;
}>;
@@ -156,9 +156,9 @@ export namespace ExperimentalAxoSegmentedControl {
* An item in the group.
*/
export const Item: FC<ItemProps> = memo(props => {
const { value, children, ...rest } = props;
const { ref, value, children, ...rest } = props;
return (
<ToggleGroup.Item asChild ref={props.ref} value={value} {...rest}>
<ToggleGroup.Item asChild ref={ref} value={value} {...rest}>
<ExperimentalAxoBaseSegmentedControl.Item value={value}>
{children}
</ExperimentalAxoBaseSegmentedControl.Item>
@@ -203,19 +203,15 @@ export namespace ExperimentalAxoSegmentedControl {
* --------------------------------------------------------------------------
*/
export type ExperimentalItemBadgeProps =
ExperimentalAxoBaseSegmentedControl.ExperimentalItemBadgeProps;
export type ItemBadgeProps =
ExperimentalAxoBaseSegmentedControl.ItemBadgeProps;
/**
* A badge shown on an item, typically for unread counts.
*/
export const ExperimentalItemBadge: FC<ExperimentalItemBadgeProps> = memo(
props => {
return (
<ExperimentalAxoBaseSegmentedControl.ExperimentalItemBadge {...props} />
);
}
);
export const ItemBadge: FC<ItemBadgeProps> = memo(props => {
return <ExperimentalAxoBaseSegmentedControl.ItemBadge {...props} />;
});
ExperimentalItemBadge.displayName = 'AxoSegmentedControl.ItemBadge';
ItemBadge.displayName = 'AxoSegmentedControl.ItemBadge';
}
+16 -21
View File
@@ -6,7 +6,7 @@ import { Select } from 'radix-ui';
import { AxoBaseMenu } from './_internal/AxoBaseMenu.dom.tsx';
import { AxoSymbol } from './AxoSymbol.dom.tsx';
import { tw } from './tw.dom.tsx';
import { ExperimentalAxoBadge } from './AxoBadge.dom.tsx';
import { AxoBadge } from './AxoBadge.dom.tsx';
import { AxoTheme } from './AxoTheme.dom.tsx';
import { variants } from './_internal/variants.dom.tsx';
@@ -462,31 +462,26 @@ export namespace AxoSelect {
* --------------------------------------------------------------------------
*/
export type ExperimentalItemBadgeProps = Omit<
ExperimentalAxoBadge.RootProps,
'size'
>;
export type ItemBadgeProps = Omit<AxoBadge.RootProps, 'size'>;
/**
* A badge shown at the trailing edge of an item, typically for unread counts.
*/
export const ExperimentalItemBadge = memo(
(props: ExperimentalItemBadgeProps) => {
return (
<span className={tw('ms-[5px]')}>
<ExperimentalAxoBadge.Root
size="sm"
value={props.value}
max={props.max}
maxDisplay={props.maxDisplay}
label={props.label}
/>
</span>
);
}
);
export const ItemBadge: FC<ItemBadgeProps> = memo((props: ItemBadgeProps) => {
return (
<span className={tw('ms-[5px]')}>
<AxoBadge.Root
variant={props.variant}
size="sm"
value={props.value}
max={props.max}
label={props.label}
/>
</span>
);
});
ExperimentalItemBadge.displayName = 'AxoSelect.ItemBadge';
ItemBadge.displayName = 'AxoSelect.ItemBadge';
/**
* <AxoSelect.Group>
@@ -5,9 +5,14 @@ import { memo, useId, useMemo } from 'react';
import type { Transition } from 'motion/react';
import { motion } from 'motion/react';
import { tw } from '../tw.dom.tsx';
import { ExperimentalAxoBadge } from '../AxoBadge.dom.tsx';
import { AxoBadge } from '../AxoBadge.dom.tsx';
import { createStrictContext, useStrictContext } from './StrictContext.dom.tsx';
import { variants } from './variants.dom.tsx';
import {
AriaLabellingProvider,
useAriaLabellingContext,
useCreateAriaLabellingContext,
} from './AriaLabellingContext.dom.tsx';
/**
* Used to share styles/animations for SegmentedControls, Toolbar ToggleGroups,
@@ -165,8 +170,13 @@ export namespace ExperimentalAxoBaseSegmentedControl {
}>;
export const Item: FC<ItemProps> = memo(props => {
const { value, children, ...rest } = props;
const { ref, value, children, ...rest } = props;
const context = useStrictContext(RootContext);
const {
context: labellingContext,
labelId,
descriptionId,
} = useCreateAriaLabellingContext();
const isSelected = useMemo(() => {
if (context.value == null) {
@@ -181,35 +191,39 @@ export namespace ExperimentalAxoBaseSegmentedControl {
}, [value, context.value]);
return (
<button
ref={props.ref}
type="button"
className={tw(
'relative flex min-w-0 items-center justify-center px-3 py-[5px]',
'cursor-pointer rounded-full type-body-medium font-medium text-primary',
'outline-focused-inner not-forced-colors:outline-none not-forced-colors:keyboard-mode:focus:axo-focus-ring',
'forced-colors:bg-[ButtonFace] forced-colors:text-[ButtonText]',
'forced-colors:data-[axo-contextmenu-state=open]:text-[HighlightText]',
ItemWidths.get(context.itemWidth),
isSelected && tw('forced-colors:text-[SelectedItemText]'),
!isSelected &&
tw(
'data-[axo-contextmenu-state=open]:bg-primary',
'forced-colors:data-[axo-contextmenu-state=open]:bg-[Highlight]'
)
)}
{...rest}
>
{children}
{isSelected && (
<motion.span
layoutId={`${context.id}.Indicator`}
className={IndicatorStyles.get(context.variant)}
transition={IndicatorTransition}
style={{ borderRadius: 14 }}
/>
)}
</button>
<AriaLabellingProvider value={labellingContext}>
<button
ref={ref}
type="button"
className={tw(
'relative flex min-w-0 items-center justify-center px-3 py-[5px]',
'cursor-pointer rounded-full type-body-medium font-medium text-primary',
'outline-focused-inner not-forced-colors:outline-none not-forced-colors:keyboard-mode:focus:axo-focus-ring',
'forced-colors:bg-[ButtonFace] forced-colors:text-[ButtonText]',
'forced-colors:data-[axo-contextmenu-state=open]:text-[HighlightText]',
ItemWidths.get(context.itemWidth),
isSelected && tw('forced-colors:text-[SelectedItemText]'),
!isSelected &&
tw(
'data-[axo-contextmenu-state=open]:bg-primary',
'forced-colors:data-[axo-contextmenu-state=open]:bg-[Highlight]'
)
)}
aria-labelledby={labelId}
aria-describedby={descriptionId}
{...rest}
>
{children}
{isSelected && (
<motion.span
layoutId={`${context.id}.Indicator`}
className={IndicatorStyles.get(context.variant)}
transition={IndicatorTransition}
style={{ borderRadius: 14 }}
/>
)}
</button>
</AriaLabellingProvider>
);
});
@@ -231,8 +245,12 @@ export namespace ExperimentalAxoBaseSegmentedControl {
/** Truncated label text inside a segmented control item. */
export const ItemText: FC<ItemTextProps> = memo(props => {
const id = useId();
const { labelRef } = useAriaLabellingContext('AxoBaseSegmentControl.Item');
return (
<span
ref={labelRef}
id={id}
className={tw('relative z-20 block truncate forced-color-adjust-none')}
style={{ maxWidth: props.maxWidth }}
>
@@ -248,27 +266,30 @@ export namespace ExperimentalAxoBaseSegmentedControl {
* --------------------------------------------------------------------------
*/
export type ExperimentalItemBadgeProps = Omit<
ExperimentalAxoBadge.RootProps,
'size'
>;
export type ItemBadgeProps = Omit<AxoBadge.RootProps, 'size'>;
/** A badge rendered to the right of the item label. */
export const ExperimentalItemBadge = memo(
(props: ExperimentalItemBadgeProps) => {
return (
<span className={tw('relative z-20 ms-[5px]')}>
<ExperimentalAxoBadge.Root
size="md"
value={props.value}
max={props.max}
maxDisplay={props.maxDisplay}
label={props.label}
/>
</span>
);
}
);
export const ItemBadge: FC<ItemBadgeProps> = memo((props: ItemBadgeProps) => {
const id = useId();
const { descriptionRef } = useAriaLabellingContext(
'AxoBaseSegmentControl.Item'
);
return (
<span
ref={descriptionRef}
id={id}
className={tw('relative z-20 ms-[5px]')}
>
<AxoBadge.Root
variant={props.variant}
size="md"
value={props.value}
max={props.max}
label={props.label}
/>
</span>
);
});
ExperimentalItemBadge.displayName = 'AxoBaseSegmentedControl.ItemBadge';
ItemBadge.displayName = 'AxoBaseSegmentedControl.ItemBadge';
}
+6 -5
View File
@@ -16,20 +16,21 @@ export namespace AxoIntl {
'AxoDialog.Back': 'Back',
'AxoDialog.Close': 'Close',
'AxoTextField.Clear': 'Clear',
'AxoBadge.MaxOverflow': (max: number) => `${max}+`,
};
/** A key for a built-in Axo UI string. */
export type MessageKey = keyof typeof DefaultMessages;
/** Map of all message keys to their translated strings. */
export type Messages = Record<MessageKey, string>;
export type Messages = typeof DefaultMessages;
/** A key for a built-in Axo UI string. */
export type MessageKey = keyof Messages;
/** Directionality of text */
export type Direction = 'ltr' | 'rtl';
/** The intl API available via `useAxoIntl`. */
export type ContextType = Readonly<{
get: (key: MessageKey) => string;
get: <const K extends MessageKey>(key: K) => Messages[K];
}>;
function createIntlContext(messages: Messages): ContextType {
@@ -44,9 +44,7 @@ export type LeftPaneChatFoldersProps = Readonly<{
onChatFolderOpenSettings: (chatFolderId: ChatFolderId) => void;
}>;
function getBadgeValue(
unreadStats: UnreadStats | null
): ExperimentalAxoSegmentedControl.ExperimentalItemBadgeProps['value'] | null {
function getBadgeValue(unreadStats: UnreadStats | null): number | null {
if (unreadStats == null) {
return null;
}
@@ -210,16 +208,14 @@ function ChatFolderSelectItem(props: {
{getChatFolderLabel(i18n, props.chatFolder, true)}
</AxoSelect.ItemText>
{badgeValue != null && (
<AxoSelect.ExperimentalItemBadge
<AxoSelect.ItemBadge
variant="primary"
value={badgeValue}
max={UNREAD_BADGE_MAX_COUNT}
maxDisplay={i18n(
'icu:LeftPaneChatFolders__ItemUnreadBadge__MaxCount',
{
maxCount: UNREAD_BADGE_MAX_COUNT,
}
label={i18n(
'icu:LeftPaneChatFolders__ItemUnreadBadge__AccessibleLabel',
{ count: badgeValue }
)}
label={null}
/>
)}
</AxoSelect.Item>
@@ -256,14 +252,14 @@ function ChatFolderSegmentedControlItem(props: {
{getChatFolderLabel(i18n, props.chatFolder, false)}
</ExperimentalAxoSegmentedControl.ItemText>
{badgeValue != null && (
<ExperimentalAxoSegmentedControl.ExperimentalItemBadge
<ExperimentalAxoSegmentedControl.ItemBadge
variant="primary"
value={badgeValue}
max={UNREAD_BADGE_MAX_COUNT}
maxDisplay={i18n(
'icu:LeftPaneChatFolders__ItemUnreadBadge__MaxCount',
{ maxCount: UNREAD_BADGE_MAX_COUNT }
label={i18n(
'icu:LeftPaneChatFolders__ItemUnreadBadge__AccessibleLabel',
{ count: badgeValue }
)}
label={null}
/>
)}
</ExperimentalAxoSegmentedControl.Item>
+1
View File
@@ -20,6 +20,7 @@ export const AppProvider: FC<AppProviderProps> = memo(
'AxoDialog.Back': i18n('icu:AxoDialog.Back'),
'AxoDialog.Close': i18n('icu:AxoDialog.Close'),
'AxoTextField.Clear': i18n('icu:AxoTextField.Clear'),
'AxoBadge.MaxOverflow': max => i18n('icu:AxoBadge.MaxOverflow', { max }),
};
return (