Rework AxoList/AxoItem CSS layout and component structure

This commit is contained in:
Jamie
2026-08-28 12:38:43 -07:00
committed by GitHub
parent 9ff5b4e3ad
commit 9b4a5e6f63
15 changed files with 880 additions and 684 deletions
+77
View File
@@ -0,0 +1,77 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactNode, FC } from 'react';
import { memo } from 'react';
import { tw } from '../tw.dom.tsx';
import { AxoSymbol } from '../AxoSymbol.dom.tsx';
export namespace Story {
/**
* <Story.Hint>
* --------------------------------------------------------------------------
*/
export type HintProps = Readonly<{
children?: ReactNode;
}>;
export const Hint: FC<HintProps> = memo(props => {
return (
<p className={tw('type-caption text-secondary')}>{props.children}</p>
);
});
Hint.displayName = 'Story.Hint';
/**
* <Story.Callout>
* --------------------------------------------------------------------------
*/
export type CalloutProps = Readonly<{
children?: ReactNode;
}>;
export const Callout: FC<HintProps> = memo(props => {
return (
<div
className={tw(
'my-2 flex gap-2 p-2',
'bg-warning-tint text-warning',
'border border-dashed',
'type-caption font-medium',
'font-mono'
)}
>
<AxoSymbol.InlineGlyph symbol="error-circle-fill" label={null} />
<div className={tw('flex-1')}>{props.children}</div>
</div>
);
});
Callout.displayName = 'Story.Callout';
/**
* <Story.Legend>
* --------------------------------------------------------------------------
*/
export type LegendProps = Readonly<{
label: string;
children?: ReactNode;
}>;
export const Legend: FC<LegendProps> = memo(props => {
return (
<fieldset className={tw('my-2 border border-dashed border-selected p-2')}>
<legend className={tw('px-2 font-mono type-caption text-accent')}>
{props.label}
</legend>
{props.children}
</fieldset>
);
});
Legend.displayName = 'Story.Legend';
}
+1
View File
@@ -25,3 +25,4 @@
@import './AxoDragRegions.css';
@import './AxoScrollArea.css';
@import './AxoSelect.css';
@import './items/_AxoBaseItem.css';
@@ -178,7 +178,6 @@ export function ItemActions(): ReactNode {
<AxoContactList.Item
avatar={<LegacyAvatar title="Adrian" />}
title="Adrian"
value="Admin"
onClick={action('onClick')}
accessory={
<AxoContactList.ItemAction variant="subtle-secondary">
@@ -235,7 +234,6 @@ export function ItemIconActions(): ReactNode {
<AxoContactList.Item
avatar={<LegacyAvatar title="Adrian" />}
title="Adrian"
value="Admin"
onClick={action('onClick')}
accessory={
<AxoContactList.ItemIconAction
+12 -10
View File
@@ -26,7 +26,7 @@ export namespace AxoContactList {
<AxoList.Root>
{props.title != null && (
<AxoList.Header>
<AxoList.Title>{props.title}</AxoList.Title>
<AxoList.Label>{props.title}</AxoList.Label>
</AxoList.Header>
)}
<AxoList.Body>
@@ -58,11 +58,11 @@ export namespace AxoContactList {
return (
<AriaList.Item asChild>
<AxoBaseItem.Root>
<AxoBaseItem.LeadingSlot>{props.avatar}</AxoBaseItem.LeadingSlot>
<AxoBaseItem.Leading>{props.avatar}</AxoBaseItem.Leading>
<AxoBaseItem.Content>
<AxoBaseItem.Body>
<AriaList.Label asChild id={id}>
<AxoBaseItem.Title>{props.title}</AxoBaseItem.Title>
<AxoBaseItem.Label>{props.title}</AxoBaseItem.Label>
</AriaList.Label>
{props.value != null && (
<AriaList.Label asChild>
@@ -82,10 +82,10 @@ export namespace AxoContactList {
onClick={props.onClick}
/>
)}
{props.accessory != null && (
<AxoBaseItem.Accessory>{props.accessory}</AxoBaseItem.Accessory>
)}
</AxoBaseItem.Body>
{props.accessory != null && (
<AxoBaseItem.Accessory>{props.accessory}</AxoBaseItem.Accessory>
)}
</AxoBaseItem.Content>
</AxoBaseItem.Root>
</AriaList.Item>
@@ -173,11 +173,13 @@ export namespace AxoContactList {
return (
<AriaList.Item asChild>
<AxoBaseItem.Root>
<AxoBaseItem.IconAvatar size={32} symbol={props.symbol} />
<AxoBaseItem.Leading>
<AxoBaseItem.IconAvatar size={32} symbol={props.symbol} />
</AxoBaseItem.Leading>
<AxoBaseItem.Content>
<AxoBaseItem.Body>
<AriaList.Label asChild id={id}>
<AxoBaseItem.Title>{props.title}</AxoBaseItem.Title>
<AxoBaseItem.Label>{props.title}</AxoBaseItem.Label>
</AriaList.Label>
{props.onClick != null && (
<AxoBaseItem.HiddenTrigger
@@ -210,9 +212,9 @@ export namespace AxoContactList {
<AxoBaseItem.Content>
<AxoBaseItem.Body>
<AriaList.Label asChild>
<AxoBaseItem.Title truncate>
<AxoBaseItem.Label truncate>
<span className={tw('text-secondary')}>{props.title}</span>
</AxoBaseItem.Title>
</AxoBaseItem.Label>
</AriaList.Label>
</AxoBaseItem.Body>
</AxoBaseItem.Content>
+2 -2
View File
@@ -27,7 +27,7 @@ export namespace AxoFieldList {
<AxoList.Root>
{props.title != null && (
<AxoList.Header>
<AxoList.Title>{props.title}</AxoList.Title>
<AxoList.Label>{props.title}</AxoList.Label>
{/* We probably don't ever want to show a description without a title */}
{props.description != null && (
<AxoList.Description>{props.description}</AxoList.Description>
@@ -37,7 +37,7 @@ export namespace AxoFieldList {
<AxoList.Body>{props.children}</AxoList.Body>
{props.help != null && (
<AxoList.Footer>
<AxoList.Help>{props.help}</AxoList.Help>
<AxoList.FooterDescription>{props.help}</AxoList.FooterDescription>
</AxoList.Footer>
)}
</AxoList.Root>
+296 -327
View File
@@ -9,19 +9,32 @@ import { tw } from '../tw.dom.tsx';
import { AxoSwitch } from '../AxoSwitch.dom.tsx';
import { AxoSelect } from '../AxoSelect.dom.tsx';
import { AxoDropdownMenu } from '../AxoDropdownMenu.dom.tsx';
import { Story } from '../_storybook-helpers/Story.dom.tsx';
export default {
title: 'Axo/Items/AxoItem',
} satisfies Meta;
export function Title(): ReactNode {
const LONG_TEXT = (
<>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Ipsa magni rerum
consequatur vero enim, laborum ullam voluptate expedita tempora amet natus
nisi praesentium alias earum accusantium ex doloribus et quidem.
</>
);
const LONG_LABEL = <>Very long label: {LONG_TEXT}</>;
const LONG_VALUE = <>Very long value: {LONG_TEXT}</>;
const LONG_DESCRIPTION = <>Very long description: {LONG_TEXT}</>;
export function Label(): ReactNode {
return (
<div className={tw('mx-auto max-w-150')}>
<AxoItem.Group>
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Title of item</AxoItem.Title>
<AxoItem.Label>Label of item</AxoItem.Label>
<AxoItem.HiddenTrigger
label="Trigger"
onClick={action('onClick')}
@@ -33,7 +46,7 @@ export function Title(): ReactNode {
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Another title</AxoItem.Title>
<AxoItem.Label>Another label</AxoItem.Label>
<AxoItem.HiddenTrigger
label="Trigger"
onClick={action('onClick')}
@@ -45,7 +58,7 @@ export function Title(): ReactNode {
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Yet another title</AxoItem.Title>
<AxoItem.Label>{LONG_LABEL}</AxoItem.Label>
<AxoItem.HiddenTrigger
label="Trigger"
onClick={action('onClick')}
@@ -65,7 +78,7 @@ export function Description(): ReactNode {
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Title of item</AxoItem.Title>
<AxoItem.Label>Label of item</AxoItem.Label>
<AxoItem.Description>Description of the item</AxoItem.Description>
<AxoItem.HiddenTrigger
label="Trigger"
@@ -78,7 +91,7 @@ export function Description(): ReactNode {
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Another title</AxoItem.Title>
<AxoItem.Label>Another label</AxoItem.Label>
<AxoItem.Description>
Description with more detail about the item
</AxoItem.Description>
@@ -93,10 +106,8 @@ export function Description(): ReactNode {
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Yet another title</AxoItem.Title>
<AxoItem.Description>
Description that explains what this means
</AxoItem.Description>
<AxoItem.Label>Yet another label</AxoItem.Label>
<AxoItem.Description>{LONG_DESCRIPTION}</AxoItem.Description>
<AxoItem.HiddenTrigger
label="Trigger"
onClick={action('onClick')}
@@ -114,10 +125,12 @@ export function Icon(): ReactNode {
<div className={tw('mx-auto max-w-150')}>
<AxoItem.Group>
<AxoItem.Root>
<AxoItem.Icon symbol="settings" />
<AxoItem.Leading>
<AxoItem.Icon symbol="settings" />
</AxoItem.Leading>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Title of item</AxoItem.Title>
<AxoItem.Label>Label of item</AxoItem.Label>
<AxoItem.HiddenTrigger
label="Trigger"
onClick={action('onClick')}
@@ -127,13 +140,13 @@ export function Icon(): ReactNode {
</AxoItem.Root>
<AxoItem.Root>
<AxoItem.Icon symbol="appearance" />
<AxoItem.Leading>
<AxoItem.Icon symbol="appearance" />
</AxoItem.Leading>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Another title</AxoItem.Title>
<AxoItem.Description>
Description with more detail about the item
</AxoItem.Description>
<AxoItem.Label>Another label</AxoItem.Label>
<AxoItem.Description>{LONG_DESCRIPTION}</AxoItem.Description>
<AxoItem.HiddenTrigger
label="Trigger"
onClick={action('onClick')}
@@ -145,7 +158,7 @@ export function Icon(): ReactNode {
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Item without icon</AxoItem.Title>
<AxoItem.Label>Item without icon</AxoItem.Label>
<AxoItem.Description>
Notice it stays aligned with the other items
</AxoItem.Description>
@@ -161,6 +174,71 @@ export function Icon(): ReactNode {
);
}
function IconAvatarTemplate(props: { size: AxoItem.IconAvatarSize }) {
return (
<Story.Legend label={`size=${props.size}`}>
<AxoItem.Group>
<AxoItem.Root>
<AxoItem.Leading>
<AxoItem.IconAvatar symbol="settings" size={props.size} />
</AxoItem.Leading>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Label>Label of item</AxoItem.Label>
<AxoItem.HiddenTrigger
label="Trigger"
onClick={action('onClick')}
/>
</AxoItem.Body>
</AxoItem.Content>
</AxoItem.Root>
<AxoItem.Root>
<AxoItem.Leading>
<AxoItem.IconAvatar symbol="appearance" size={props.size} />
</AxoItem.Leading>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Label>Another label</AxoItem.Label>
<AxoItem.Description>{LONG_DESCRIPTION}</AxoItem.Description>
<AxoItem.HiddenTrigger
label="Trigger"
onClick={action('onClick')}
/>
</AxoItem.Body>
</AxoItem.Content>
</AxoItem.Root>
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Label>Item without icon avatar</AxoItem.Label>
<AxoItem.Description>
Notice it stays aligned with the other items
</AxoItem.Description>
<AxoItem.HiddenTrigger
label="Trigger"
onClick={action('onClick')}
/>
</AxoItem.Body>
</AxoItem.Content>
</AxoItem.Root>
</AxoItem.Group>
</Story.Legend>
);
}
export function IconAvatar(): ReactNode {
return (
<div className={tw('mx-auto max-w-150')}>
<IconAvatarTemplate size={32} />
<IconAvatarTemplate size={36} />
<IconAvatarTemplate size={38} />
<IconAvatarTemplate size={48} />
</div>
);
}
export function Value(): ReactNode {
return (
<div className={tw('mx-auto max-w-150')}>
@@ -168,7 +246,7 @@ export function Value(): ReactNode {
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Title of item</AxoItem.Title>
<AxoItem.Label>Label of item</AxoItem.Label>
<AxoItem.Value>+1 555 555-5555</AxoItem.Value>
<AxoItem.HiddenTrigger
label="Trigger"
@@ -181,11 +259,9 @@ export function Value(): ReactNode {
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Another title</AxoItem.Title>
<AxoItem.Label>Another label</AxoItem.Label>
<AxoItem.Value>Jamie-MacBook-Pro.local</AxoItem.Value>
<AxoItem.Description>
Description with more detail about the item
</AxoItem.Description>
<AxoItem.Description>{LONG_DESCRIPTION}</AxoItem.Description>
<AxoItem.HiddenTrigger
label="Trigger"
onClick={action('onClick')}
@@ -197,7 +273,7 @@ export function Value(): ReactNode {
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Yet another title</AxoItem.Title>
<AxoItem.Label>Yet another label</AxoItem.Label>
<AxoItem.Value>System Language</AxoItem.Value>
<AxoItem.Description>
Description that explains what this means
@@ -210,6 +286,11 @@ export function Value(): ReactNode {
</AxoItem.Content>
</AxoItem.Root>
</AxoItem.Group>
<Story.Callout>
<strong>Note:</strong> If you have a value, you cannot have any other
type of accessory.
</Story.Callout>
</div>
);
}
@@ -219,54 +300,60 @@ export function Arrow(): ReactNode {
<div className={tw('mx-auto max-w-150')}>
<AxoItem.Group>
<AxoItem.Root>
<AxoItem.Icon symbol="settings" />
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Title of item</AxoItem.Title>
<AxoItem.Label>Label of item</AxoItem.Label>
<AxoItem.HiddenTrigger
label="Trigger"
onClick={action('onClick')}
/>
</AxoItem.Body>
<AxoItem.Trailing>
<AxoItem.Arrow />
</AxoItem.Trailing>
</AxoItem.Content>
<AxoItem.Arrow />
</AxoItem.Root>
<AxoItem.Root>
<AxoItem.Icon symbol="appearance" />
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Another title</AxoItem.Title>
<AxoItem.Value>Jamie-MacBook-Pro.local</AxoItem.Value>
<AxoItem.Description>
Description with more detail about the item
</AxoItem.Description>
<AxoItem.HiddenTrigger
label="Trigger"
onClick={action('onClick')}
/>
</AxoItem.Body>
</AxoItem.Content>
<AxoItem.Arrow />
</AxoItem.Root>
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Yet another title</AxoItem.Title>
<AxoItem.Value>System Language</AxoItem.Value>
<AxoItem.Description>
Description that explains what this means
</AxoItem.Description>
<AxoItem.Label>Another label</AxoItem.Label>
<AxoItem.Description>{LONG_DESCRIPTION}</AxoItem.Description>
<AxoItem.Value>Item value</AxoItem.Value>
<AxoItem.HiddenTrigger
label="Trigger"
onClick={action('onClick')}
/>
</AxoItem.Body>
<AxoItem.Trailing>
<AxoItem.Arrow />
</AxoItem.Trailing>
</AxoItem.Content>
</AxoItem.Root>
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Label>Item without arrow</AxoItem.Label>
<AxoItem.Description>{LONG_DESCRIPTION}</AxoItem.Description>
<AxoItem.HiddenTrigger
label="Trigger"
onClick={action('onClick')}
/>
</AxoItem.Body>
</AxoItem.Content>
<AxoItem.Arrow />
</AxoItem.Root>
</AxoItem.Group>
<Story.Callout>
<strong>Note:</strong> Arrows should only be used when the item is
clickable and needs an extra hint.
</Story.Callout>
<Story.Callout>
<strong>Note:</strong> Arrows should not be used with any accessories
except for values.
</Story.Callout>
</div>
);
}
@@ -278,42 +365,56 @@ export function Action(): ReactNode {
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Title of item</AxoItem.Title>
<AxoItem.Label>Label of item</AxoItem.Label>
<AxoItem.Accessory>
<AxoItem.Action variant="subtle-secondary">
Action
</AxoItem.Action>
</AxoItem.Accessory>
</AxoItem.Body>
<AxoItem.Accessory>
<AxoItem.Action variant="subtle-secondary">Action</AxoItem.Action>
</AxoItem.Accessory>
</AxoItem.Content>
</AxoItem.Root>
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Title of item</AxoItem.Title>
<AxoItem.Label>Label of item</AxoItem.Label>
<AxoItem.Description>Description of the item</AxoItem.Description>
<AxoItem.Accessory>
<AxoItem.Action variant="subtle-secondary">
Action
</AxoItem.Action>
</AxoItem.Accessory>
</AxoItem.Body>
<AxoItem.Accessory>
<AxoItem.Action variant="subtle-secondary">Action</AxoItem.Action>
</AxoItem.Accessory>
</AxoItem.Content>
</AxoItem.Root>
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Another title</AxoItem.Title>
<AxoItem.Description>
Description with more detail about the item
</AxoItem.Description>
<AxoItem.Label>Another label</AxoItem.Label>
<AxoItem.Description>{LONG_DESCRIPTION}</AxoItem.Description>
<AxoItem.Accessory>
<AxoItem.Action
symbol="phone-fill"
variant="strong-affirmative"
>
Join
</AxoItem.Action>
</AxoItem.Accessory>
</AxoItem.Body>
<AxoItem.Accessory>
<AxoItem.Action symbol="phone-fill" variant="strong-affirmative">
Join
</AxoItem.Action>
</AxoItem.Accessory>
</AxoItem.Content>
</AxoItem.Root>
</AxoItem.Group>
<Story.Callout>
<strong>Note:</strong> There should only be one text action at a time,
if you need more than one action you can use an icon action or menu.
</Story.Callout>
<Story.Callout>
<strong>Note:</strong> Items with actions should not be clickable
</Story.Callout>
</div>
);
}
@@ -359,40 +460,36 @@ export function IconActions(): ReactNode {
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Title of item</AxoItem.Title>
<AxoItem.Label>Label of item</AxoItem.Label>
</AxoItem.Body>
<AxoItem.Accessory>
<AxoItem.Trailing>
<DownloadIconAction />
</AxoItem.Accessory>
</AxoItem.Trailing>
</AxoItem.Content>
</AxoItem.Root>
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Another title</AxoItem.Title>
<AxoItem.Description>
Description with more detail about the item
</AxoItem.Description>
<AxoItem.Label>Another label</AxoItem.Label>
<AxoItem.Description>{LONG_DESCRIPTION}</AxoItem.Description>
</AxoItem.Body>
<AxoItem.Accessory>
<AxoItem.Trailing>
<MoreIconActionWithMenu />
</AxoItem.Accessory>
</AxoItem.Trailing>
</AxoItem.Content>
</AxoItem.Root>
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Yet another title</AxoItem.Title>
<AxoItem.Description>
Description that explains what this means
</AxoItem.Description>
<AxoItem.Label>Yet another label</AxoItem.Label>
<AxoItem.Description>{LONG_DESCRIPTION}</AxoItem.Description>
</AxoItem.Body>
<AxoItem.Accessory>
<AxoItem.Trailing>
<DownloadIconAction />
<MoreIconActionWithMenu />
</AxoItem.Accessory>
</AxoItem.Trailing>
</AxoItem.Content>
</AxoItem.Root>
</AxoItem.Group>
@@ -425,55 +522,53 @@ function Select() {
);
}
export function Accessories(): ReactNode {
export function OtherAccessories(): ReactNode {
return (
<div className={tw('mx-auto max-w-150')}>
<AxoItem.Group>
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Title of item</AxoItem.Title>
<AxoItem.Label>Label of item</AxoItem.Label>
</AxoItem.Body>
<AxoItem.Accessory>
<AxoItem.Trailing>
<Switch />
</AxoItem.Accessory>
</AxoItem.Trailing>
</AxoItem.Content>
</AxoItem.Root>
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Another title</AxoItem.Title>
<AxoItem.Label>Another label</AxoItem.Label>
<AxoItem.Accessory>
<Select />
</AxoItem.Accessory>
</AxoItem.Body>
<AxoItem.Accessory>
<Select />
</AxoItem.Accessory>
</AxoItem.Content>
</AxoItem.Root>
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Title of item</AxoItem.Title>
<AxoItem.Label>Label of item</AxoItem.Label>
<AxoItem.Description>Description of the item</AxoItem.Description>
</AxoItem.Body>
<AxoItem.Accessory>
<AxoItem.Trailing>
<Switch />
</AxoItem.Accessory>
</AxoItem.Trailing>
</AxoItem.Content>
</AxoItem.Root>
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Another title</AxoItem.Title>
<AxoItem.Description>
Description with more detail about the item
</AxoItem.Description>
<AxoItem.Label>Another label</AxoItem.Label>
<AxoItem.Description>{LONG_DESCRIPTION}</AxoItem.Description>
<AxoItem.Accessory>
<Select />
</AxoItem.Accessory>
</AxoItem.Body>
<AxoItem.Accessory>
<Select />
</AxoItem.Accessory>
</AxoItem.Content>
</AxoItem.Root>
</AxoItem.Group>
@@ -481,243 +576,117 @@ export function Accessories(): ReactNode {
);
}
function Header(props: { children: ReactNode }): ReactNode {
return <h2 className={tw('type-title-small')}>{props.children}</h2>;
function StressTest(props: {
label: ReactNode;
value?: ReactNode;
description?: ReactNode;
}): ReactNode {
return (
<AxoItem.Group>
<AxoItem.Root>
<AxoItem.Leading>
<AxoItem.Icon symbol="settings" />
</AxoItem.Leading>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Label>{props.label}</AxoItem.Label>
{props.value != null && (
<AxoItem.Value>{props.value}</AxoItem.Value>
)}
{props.description != null && (
<AxoItem.Description>{props.description}</AxoItem.Description>
)}
<AxoItem.HiddenTrigger
label="Trigger"
onClick={action('onClick')}
/>
</AxoItem.Body>
<AxoItem.Trailing>
<AxoItem.Arrow />
</AxoItem.Trailing>
</AxoItem.Content>
</AxoItem.Root>
</AxoItem.Group>
);
}
export function StressTests(): ReactNode {
return (
<div className={tw('mx-auto max-w-150')}>
<Header>Kitchen Sink</Header>
<AxoItem.Group>
<AxoItem.Root>
<AxoItem.Icon symbol="settings" />
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Title</AxoItem.Title>
<AxoItem.Value>Value</AxoItem.Value>
<AxoItem.Description>Description</AxoItem.Description>
<AxoItem.HiddenTrigger
label="Trigger"
onClick={action('onClick')}
/>
</AxoItem.Body>
<AxoItem.Accessory>
<DownloadIconAction />
</AxoItem.Accessory>
</AxoItem.Content>
<AxoItem.Arrow />
</AxoItem.Root>
<AxoItem.Root>
<AxoItem.Icon symbol="settings" />
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Title</AxoItem.Title>
<AxoItem.Value>Value</AxoItem.Value>
<AxoItem.Description>Description</AxoItem.Description>
<AxoItem.HiddenTrigger
label="Trigger"
onClick={action('onClick')}
/>
</AxoItem.Body>
<AxoItem.Accessory>
<DownloadIconAction />
<MoreIconActionWithMenu />
</AxoItem.Accessory>
</AxoItem.Content>
<AxoItem.Arrow />
</AxoItem.Root>
<AxoItem.Root>
<AxoItem.Icon symbol="settings" />
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Title</AxoItem.Title>
<AxoItem.Value>Value</AxoItem.Value>
<AxoItem.Description>Description</AxoItem.Description>
<AxoItem.HiddenTrigger
label="Trigger"
onClick={action('onClick')}
/>
</AxoItem.Body>
<AxoItem.Accessory>
<AxoItem.Action variant="subtle-secondary">Action</AxoItem.Action>
</AxoItem.Accessory>
</AxoItem.Content>
<AxoItem.Arrow />
</AxoItem.Root>
<AxoItem.Root>
<AxoItem.Icon symbol="settings" />
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Title</AxoItem.Title>
<AxoItem.Value>Value</AxoItem.Value>
<AxoItem.Description>Description</AxoItem.Description>
<AxoItem.HiddenTrigger
label="Trigger"
onClick={action('onClick')}
/>
</AxoItem.Body>
<AxoItem.Accessory>
<AxoItem.Action variant="subtle-secondary">Action</AxoItem.Action>
<AxoItem.Action variant="subtle-secondary">Action</AxoItem.Action>
</AxoItem.Accessory>
</AxoItem.Content>
<AxoItem.Arrow />
</AxoItem.Root>
<AxoItem.Root>
<AxoItem.Icon symbol="settings" />
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Title</AxoItem.Title>
<AxoItem.Value>Value</AxoItem.Value>
<AxoItem.Description>Description</AxoItem.Description>
<AxoItem.HiddenTrigger
label="Trigger"
onClick={action('onClick')}
/>
</AxoItem.Body>
<AxoItem.Accessory>
<Switch />
</AxoItem.Accessory>
</AxoItem.Content>
<AxoItem.Arrow />
</AxoItem.Root>
<AxoItem.Root>
<AxoItem.Icon symbol="settings" />
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Title</AxoItem.Title>
<AxoItem.Value>Value</AxoItem.Value>
<AxoItem.Description>Description</AxoItem.Description>
<AxoItem.HiddenTrigger
label="Trigger"
onClick={action('onClick')}
/>
</AxoItem.Body>
<AxoItem.Accessory>
<Select />
</AxoItem.Accessory>
</AxoItem.Content>
<AxoItem.Arrow />
</AxoItem.Root>
</AxoItem.Group>
<Story.Callout>These should all fit in their normal place.</Story.Callout>
<Header>Long Title: Title should be clamped to two lines</Header>
<AxoItem.Group>
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Odio
pariatur ipsum non officia laboriosam amet omnis autem
architecto, expedita dolores officiis laborum iste cum porro,
fugiat sapiente sequi dolor. Excepturi.
</AxoItem.Title>
</AxoItem.Body>
</AxoItem.Content>
</AxoItem.Root>
</AxoItem.Group>
<Story.Legend label="Baseline">
<StressTest label="Label" />
<StressTest label="Label" value="Value" />
<StressTest label="Label" description="Description" />
<StressTest label="Label" value="Value" description="Description" />
</Story.Legend>
<Header>Long Title: Value should be forced to the next line</Header>
<AxoItem.Group>
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Odio
pariatur ipsum non officia laboriosam amet omnis autem
architecto, expedita dolores officiis laborum iste cum porro,
fugiat sapiente sequi dolor. Excepturi.
</AxoItem.Title>
<AxoItem.Value>Value</AxoItem.Value>
</AxoItem.Body>
</AxoItem.Content>
</AxoItem.Root>
</AxoItem.Group>
<Story.Legend label="Long label">
<Story.Callout>
These should all break into a stacked layout.
</Story.Callout>
<Header>
Long Title: Wrapped value should be on separate line from description
</Header>
<AxoItem.Group>
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Odio
pariatur ipsum non officia laboriosam amet omnis autem
architecto, expedita dolores officiis laborum iste cum porro,
fugiat sapiente sequi dolor. Excepturi.
</AxoItem.Title>
<AxoItem.Value>Value</AxoItem.Value>
<AxoItem.Description>Description</AxoItem.Description>
</AxoItem.Body>
</AxoItem.Content>
</AxoItem.Root>
</AxoItem.Group>
<StressTest label={LONG_LABEL} />
<StressTest label={LONG_LABEL} value="Value" />
<StressTest label={LONG_LABEL} description="Description" />
<StressTest
label={LONG_LABEL}
value="Value"
description="Description"
/>
</Story.Legend>
<Header>
Long Title: Actions and arrow should stay on the same line
</Header>
<AxoItem.Group>
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Odio
pariatur ipsum non officia laboriosam amet omnis autem
architecto, expedita dolores officiis laborum iste cum porro,
fugiat sapiente sequi dolor. Excepturi.
</AxoItem.Title>
</AxoItem.Body>
<AxoItem.Accessory>
<DownloadIconAction />
<MoreIconActionWithMenu />
</AxoItem.Accessory>
</AxoItem.Content>
<AxoItem.Arrow />
</AxoItem.Root>
</AxoItem.Group>
<Story.Legend label="Long value">
<Story.Callout>
These should all break into a stacked layout.
</Story.Callout>
<Header>Long Value: Value should be forced to next line</Header>
<AxoItem.Group>
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Title</AxoItem.Title>
<AxoItem.Value>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Odio
pariatur ipsum non officia laboriosam amet omnis autem
architecto, expedita dolores officiis laborum iste cum porro,
fugiat sapiente sequi dolor. Excepturi.
</AxoItem.Value>
</AxoItem.Body>
</AxoItem.Content>
</AxoItem.Root>
</AxoItem.Group>
<StressTest label="Label" value={LONG_VALUE} />
<StressTest
label="Label"
value={LONG_VALUE}
description="Description"
/>
</Story.Legend>
<Header>
Long Value: Actions and arrow should stay on the same line
</Header>
<AxoItem.Group>
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Title</AxoItem.Title>
<AxoItem.Value>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Odio
pariatur ipsum non officia laboriosam amet omnis autem
architecto, expedita dolores officiis laborum iste cum porro,
fugiat sapiente sequi dolor. Excepturi.
</AxoItem.Value>
</AxoItem.Body>
<AxoItem.Accessory>
<DownloadIconAction />
<MoreIconActionWithMenu />
</AxoItem.Accessory>
</AxoItem.Content>
<AxoItem.Arrow />
</AxoItem.Root>
</AxoItem.Group>
<Story.Legend label="Long description">
<Story.Callout>
Long descriptions should <em>not</em> break into a stacked layout.
</Story.Callout>
<StressTest label="Label" description={LONG_DESCRIPTION} />
<StressTest
label="Label"
value="Value"
description={LONG_DESCRIPTION}
/>
</Story.Legend>
<Story.Legend label="Long label + value">
<Story.Callout>
These should all break into a stacked layout.
</Story.Callout>
<StressTest label={LONG_LABEL} value={LONG_VALUE} />
<StressTest
label={LONG_LABEL}
value={LONG_VALUE}
description="Description"
/>
</Story.Legend>
<Story.Legend label="Long everything">
<Story.Callout>
These should all break into a stacked layout.
</Story.Callout>
<StressTest
label={LONG_LABEL}
value={LONG_VALUE}
description={LONG_DESCRIPTION}
/>
</Story.Legend>
</div>
);
}
+68 -16
View File
@@ -13,24 +13,28 @@ import { AriaList } from '../aria/AriaList.dom.tsx';
* ```tsx
* <AxoItem.Group>
* <AxoItem.Root>
* <AxoItem.Icon />
* <AxoItem.Leading>
* <AxoItem.Icon />
* </AxoItem.Leading>
* <AxoItem.Content>
* <AxoItem.Body>
* <AxoItem.Title />
* <AxoItem.Value />
* <AxoItem.Label />
* <AxoItem.Value/>
* <AxoItem.Description />
* <AxoItem.HiddenTrigger />
* <AxoItem.Accessory>
* <AxoItem.Action />
* <AxoItem.IconAction />
* <AxoSelect.Root />
* <AxoSwitch.Root />
* </AxoItem.Accessory>
* </AxoItem.Body>
* <AxoItem.Accessory>
* <AxoItem.Action />
* <AxoItem.IconAction />
* <AxoSelect.Root />
* <AxoSwitch.Root />
* </AxoItem.Accessory>
* <AxoItem.Trailing>
* <AxoItem.Arrow />
* </AxoItem.Trailing>
* </AxoItem.Content>
* <AxoItem.Arrow />
* </AxoItem.Root>
* </AxoItem.Layout>
* </AxoItem.Group>
* ```
*/
export namespace AxoItem {
@@ -73,6 +77,21 @@ export namespace AxoItem {
Root.displayName = 'AxoItem.Root';
/**
* <AxoItem.Leading>
* --------------------------------------------------------------------------
*/
export type LeadingProps = Readonly<{
children: ReactNode;
}>;
export const Leading: FC<LeadingProps> = memo(props => {
return <AxoBaseItem.Leading>{props.children}</AxoBaseItem.Leading>;
});
Leading.displayName = 'AxoItem.Leading';
/**
* <AxoItem.Icon>
* --------------------------------------------------------------------------
@@ -88,6 +107,24 @@ export namespace AxoItem {
Icon.displayName = 'AxoItem.Icon';
/**
* <AxoItem.IconAvatar>
* --------------------------------------------------------------------------
*/
export type IconAvatarSize = AxoBaseItem.IconAvatarSize;
export type IconAvatarProps = Readonly<{
size: IconAvatarSize;
symbol: AxoSymbol.Name;
}>;
export const IconAvatar: FC<IconAvatarProps> = memo(props => {
return <AxoBaseItem.IconAvatar size={props.size} symbol={props.symbol} />;
});
IconAvatar.displayName = 'AxoItem.IconAvatar';
/**
* <AxoItem.Content>
* --------------------------------------------------------------------------
@@ -119,24 +156,24 @@ export namespace AxoItem {
Body.displayName = 'AxoItem.Body';
/**
* <AxoItem.Title>
* <AxoItem.Label>
* --------------------------------------------------------------------------
*/
export type TitleProps = Readonly<{
export type LabelProps = Readonly<{
id?: string;
children: ReactNode;
}>;
export const Title: FC<TitleProps> = memo(props => {
export const Label: FC<LabelProps> = memo(props => {
return (
<AriaList.Label asChild id={props.id}>
<AxoBaseItem.Title>{props.children}</AxoBaseItem.Title>
<AxoBaseItem.Label>{props.children}</AxoBaseItem.Label>
</AriaList.Label>
);
});
Title.displayName = 'AxoItem.Title';
Label.displayName = 'AxoItem.Label';
/**
* <AxoItem.Value>
@@ -286,6 +323,21 @@ export namespace AxoItem {
IconAction.displayName = 'AxoItem.IconAction';
/**
* <AxoItem.Trailing>
* --------------------------------------------------------------------------
*/
export type TrailingProps = Readonly<{
children: ReactNode;
}>;
export const Trailing: FC<TrailingProps> = memo(props => {
return <AxoBaseItem.Trailing>{props.children}</AxoBaseItem.Trailing>;
});
Trailing.displayName = 'AxoItem.Trailing';
/**
* <AxoItem.Arrow>
* --------------------------------------------------------------------------
+23 -15
View File
@@ -18,10 +18,12 @@ function ExampleItems() {
return (
<AxoItem.Group>
<AxoItem.Root>
<AxoItem.Icon symbol="info" />
<AxoItem.Leading>
<AxoItem.Icon symbol="info-circle" />
</AxoItem.Leading>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>First Item</AxoItem.Title>
<AxoItem.Label>First Item</AxoItem.Label>
<AxoItem.Description>
Description of the first item
</AxoItem.Description>
@@ -30,10 +32,12 @@ function ExampleItems() {
</AxoItem.Content>
</AxoItem.Root>
<AxoItem.Root>
<AxoItem.Icon symbol="info" />
<AxoItem.Leading>
<AxoItem.Icon symbol="info-circle" />
</AxoItem.Leading>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Second Item</AxoItem.Title>
<AxoItem.Label>Second Item</AxoItem.Label>
<AxoItem.Description>
Description of the second item
</AxoItem.Description>
@@ -42,10 +46,12 @@ function ExampleItems() {
</AxoItem.Content>
</AxoItem.Root>
<AxoItem.Root>
<AxoItem.Icon symbol="info" />
<AxoItem.Leading>
<AxoItem.Icon symbol="info-circle" />
</AxoItem.Leading>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>Third Item</AxoItem.Title>
<AxoItem.Label>Third Item</AxoItem.Label>
<AxoItem.Description>
Description of the third item
</AxoItem.Description>
@@ -74,7 +80,7 @@ export function WithTitle(): React.JSX.Element {
<div className={tw('bg-surface-secondary p-8')}>
<AxoList.Root>
<AxoList.Header>
<AxoList.Title>List Title</AxoList.Title>
<AxoList.Label>List Title</AxoList.Label>
</AxoList.Header>
<AxoList.Body>
<ExampleItems />
@@ -89,7 +95,7 @@ export function WithDescription(): React.JSX.Element {
<div className={tw('bg-surface-secondary p-8')}>
<AxoList.Root>
<AxoList.Header>
<AxoList.Title>List Title</AxoList.Title>
<AxoList.Label>List Title</AxoList.Label>
<AxoList.Description>
Lorem ipsum dolor sit amet consectetur adipisicing elit.
</AxoList.Description>
@@ -102,20 +108,20 @@ export function WithDescription(): React.JSX.Element {
);
}
export function WithHelp(): React.JSX.Element {
export function WithFooterDescription(): React.JSX.Element {
return (
<div className={tw('bg-surface-secondary p-8')}>
<AxoList.Root>
<AxoList.Header>
<AxoList.Title>List Title</AxoList.Title>
<AxoList.Label>List Title</AxoList.Label>
</AxoList.Header>
<AxoList.Body>
<ExampleItems />
</AxoList.Body>
<AxoList.Footer>
<AxoList.Help>
<AxoList.FooterDescription>
This is some helpful text that describes the section above.
</AxoList.Help>
</AxoList.FooterDescription>
</AxoList.Footer>
</AxoList.Root>
</div>
@@ -133,19 +139,21 @@ export function MultipleLists(): React.JSX.Element {
<AxoList.Root>
<AxoList.Header>
<AxoList.Title>Second Section</AxoList.Title>
<AxoList.Label>Second Section</AxoList.Label>
</AxoList.Header>
<AxoList.Body>
<ExampleItems />
</AxoList.Body>
<AxoList.Footer>
<AxoList.Help>Help text for the second section.</AxoList.Help>
<AxoList.FooterDescription>
Help text for the second section.
</AxoList.FooterDescription>
</AxoList.Footer>
</AxoList.Root>
<AxoList.Root>
<AxoList.Header>
<AxoList.Title>Third Section</AxoList.Title>
<AxoList.Label>Third Section</AxoList.Label>
</AxoList.Header>
<AxoList.Body>
<ExampleItems />
+9 -9
View File
@@ -42,15 +42,15 @@ export namespace AxoList {
Header.displayName = 'AxoList.Header';
/**
* <AxoList.Title>
* <AxoList.Label>
* --------------------------------------------------------------------------
*/
export type TitleProps = Readonly<{
export type LabelProps = Readonly<{
children: ReactNode;
}>;
export const Title: FC<TitleProps> = memo(props => {
export const Label: FC<LabelProps> = memo(props => {
return (
<AriaLabelled.Label asChild>
<h2 className={tw('type-body-medium font-semibold')}>
@@ -60,7 +60,7 @@ export namespace AxoList {
);
});
Title.displayName = 'AxoList.Title';
Label.displayName = 'AxoList.Label';
/**
* <AxoList.Description>
@@ -95,7 +95,7 @@ export namespace AxoList {
<div
className={tw(
'min-w-fit',
'curved-20 bg-surface-card p-1.5 shadow-elevation-0',
'curved-2xl bg-surface-card p-1 shadow-elevation-0',
'forced-colors:bg-[Canvas] forced-colors:text-[CanvasText]',
'forced-colors:border forced-colors:border-[ButtonBorder]'
)}
@@ -123,15 +123,15 @@ export namespace AxoList {
Footer.displayName = 'AxoList.Footer';
/**
* <AxoList.Help>
* <AxoList.FooterDescription>
* --------------------------------------------------------------------------
*/
export type HelpProps = Readonly<{
export type FooterDescriptionProps = Readonly<{
children: ReactNode;
}>;
export const Help: FC<HelpProps> = memo(props => {
export const FooterDescription: FC<FooterDescriptionProps> = memo(props => {
return (
<AriaLabelled.Description asChild>
<p
@@ -146,5 +146,5 @@ export namespace AxoList {
);
});
Help.displayName = 'AxoList.Help';
FooterDescription.displayName = 'AxoList.FooterDescription';
}
+5 -5
View File
@@ -31,7 +31,7 @@ export namespace AxoRadioGroupList {
<AxoList.Root>
{props.title != null && (
<AxoList.Header>
<AxoList.Title>{props.title}</AxoList.Title>
<AxoList.Label>{props.title}</AxoList.Label>
{/* We probably don't ever want to show a description without a title */}
{props.description != null && (
<AxoList.Description>{props.description}</AxoList.Description>
@@ -50,7 +50,7 @@ export namespace AxoRadioGroupList {
</AxoList.Body>
{props.help != null && (
<AxoList.Footer>
<AxoList.Help>{props.help}</AxoList.Help>
<AxoList.FooterDescription>{props.help}</AxoList.FooterDescription>
</AxoList.Footer>
)}
</AxoList.Root>
@@ -78,9 +78,9 @@ export namespace AxoRadioGroupList {
asChild
>
<AxoBaseItem.Root>
<AxoBaseItem.LeadingSlot>
<AxoBaseItem.Leading>
<AxoBaseRadioGroup.Indicator />
</AxoBaseItem.LeadingSlot>
</AxoBaseItem.Leading>
<AxoBaseItem.Content>
<AxoBaseItem.Body>{props.children}</AxoBaseItem.Body>
</AxoBaseItem.Content>
@@ -103,7 +103,7 @@ export namespace AxoRadioGroupList {
export const Label: FC<LabelProps> = memo(props => {
return (
<AxoBaseRadioGroup.Label asChild>
<AxoBaseItem.Title>{props.children}</AxoBaseItem.Title>
<AxoBaseItem.Label>{props.children}</AxoBaseItem.Label>
</AxoBaseRadioGroup.Label>
);
});
+138
View File
@@ -0,0 +1,138 @@
/**
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
@utility axo-item-centering-to-max-height {
@apply flex h-full min-h-fit items-center;
/* --axo-leading-body-medium + --axo-leading-body-small + (description margin top) */
max-height: 38px;
}
@layer components {
.axo-item-group {
@apply grid;
@apply min-w-90;
grid-template-columns:
[axo-item-leading] min-content
[axo-item-content] auto;
}
:where(.axo-item-group) .axo-item-root:where(.group) {
@apply p-0.5;
/* Forward Grid */
@apply col-span-full grid grid-cols-subgrid;
}
:where(.axo-item-root) .axo-item-root-inner {
/* Forward Grid */
@apply col-span-full grid grid-cols-subgrid;
@apply gap-x-3 curved-xl px-3 text-primary;
/* Hovered */
@apply group-data-hovered:bg-secondary;
/* Pressed */
@apply group-data-pressed:bg-secondary-pressed;
/* Focus ring */
@apply outline-none;
@apply keyboard-mode:group-data-focused:axo-focus-ring;
@apply keyboard-mode:group-focus:axo-focus-ring;
}
:where(.axo-item-root-inner) .axo-item-leading {
grid-column: axo-item-leading;
@apply self-start;
@apply axo-item-centering-to-max-height;
}
:where(.axo-item-root-inner) .axo-item-content {
grid-column: axo-item-content;
@apply self-center;
@apply min-w-0;
/* Leave room for focus rings */
@apply -m-1;
}
:where(.axo-item-content) .axo-item-content-inner {
@apply flex;
@apply p-1;
@apply h-full;
}
:where(.axo-item-content-inner) .axo-item-body {
@apply shrink grow;
@apply grid;
}
:where(.axo-item-body) .axo-item-label {
@apply type-body-medium text-primary;
}
:where(.axo-item-body) .axo-item-value {
@apply type-body-medium text-secondary;
}
:where(.axo-item-body) .axo-item-description {
@apply mt-0.5;
@apply type-body-small text-secondary forced-colors:text-[GrayText];
@apply min-w-[min(max-content,120px)];
@apply wrap-break-word [word-break:auto-phrase];
}
:where(.axo-item-body) .axo-item-accessory {
@apply flex items-center gap-1.5;
}
:where(.axo-item-content-inner) .axo-item-trailing {
@apply axo-item-centering-to-max-height;
@apply ml-1.5;
}
:where(.axo-item-trailing) .axo-item-arrow {
@apply type-body-medium text-placeholder;
}
/* When not overflowing container */
@container scroll-state(scrollable: none) {
.axo-item-body {
grid-template:
'axo-item-label axo-item-value axo-item-accessory' 1fr
'axo-item-description axo-item-value axo-item-accessory' min-content / auto min-content min-content;
}
.axo-item-label {
grid-area: axo-item-label;
@apply self-center;
@apply text-nowrap;
}
.axo-item-value {
grid-area: axo-item-value;
@apply w-fit text-nowrap;
@apply ms-3;
@apply axo-item-centering-to-max-height;
}
.axo-item-description {
grid-area: axo-item-description;
@apply contain-inline-size contain-layout;
}
.axo-item-accessory {
grid-area: axo-item-accessory;
@apply ms-3;
}
}
/* When overflowing container */
@container scroll-state(scrollable: inline) {
.axo-item-content-inner {
width: calc(100% + 1px);
padding-inline-end: 1px;
}
.axo-item-accessory {
@apply mt-0.5;
@apply flex-col items-stretch;
}
}
}
+84 -153
View File
@@ -15,39 +15,49 @@ import { AxoButton } from '../AxoButton.dom.tsx';
import { AxoCheckbox } from '../AxoCheckbox.dom.tsx';
import { AxoAvatar } from '../AxoAvatar.dom.tsx';
import { variants } from '../_internal/variants.dom.tsx';
import { FlexWrapDetector } from '../_internal/FlexWrapDetector.dom.tsx';
const LEADING_SLOT = 'axo-item-leading-slot';
const CONTENT_SLOT = 'axo-item-content-slot';
const TRAILING_SLOT = 'axo-item-trailing-slot';
const GRID_TEMPLATE_COLUMNS =
`[${LEADING_SLOT}] min-content ` +
`[${CONTENT_SLOT}] auto ` +
`[${TRAILING_SLOT}] min-content`;
const AXO_ITEM_GROUP_CLASS = 'axo-item-group';
const AXO_ITEM_ROOT_CLASS = 'axo-item-root';
const AXO_ITEM_ROOT_INNER_CLASS = 'axo-item-root-inner';
const AXO_ITEM_LEADING_CLASS = 'axo-item-leading';
const AXO_ITEM_CONTENT_CLASS = 'axo-item-content';
const AXO_ITEM_CONTENT_INNER_CLASS = 'axo-item-content-inner';
const AXO_ITEM_BODY_CLASS = 'axo-item-body';
const AXO_ITEM_LABEL_CLASS = 'axo-item-label';
const AXO_ITEM_ACCESSORY_CLASS = 'axo-item-accessory';
const AXO_ITEM_VALUE_CLASS = 'axo-item-value';
const AXO_ITEM_DESCRIPTION_CLASS = 'axo-item-description';
const AXO_ITEM_TRAILING_CLASS = 'axo-item-trailing';
const AXO_ITEM_ARROW_CLASS = 'axo-item-arrow';
/**
* @example Anatomy
* ```tsx
* <AxoBaseItem.Group>
* <AxoBaseItem.Root>
* <AxoBaseItem.Icon />
* <AxoBaseItem.Leading>
* <AxoBaseItem.Icon />
* </AxoBaseItem.Leading>
* <AxoBaseItem.Content>
* <AxoBaseItem.Body>
* <AxoBaseItem.Title />
* <AxoBaseItem.Label />
* <AxoBaseItem.Value />
* <AxoBaseItem.Description />
* <AxoBaseItem.HiddenTrigger />
* <AxoBaseItem.Accessory>
* <AxoBaseItem.Action />
* <AxoBaseItem.IconAction />
* <AxoSelect.Root />
* <AxoSwitch.Root />
* </AxoBaseItem.Accessory>
* </AxoBaseItem.Body>
* <AxoBaseItem.Accessory>
* <AxoBaseItem.Action />
* <AxoBaseItem.IconAction />
* <AxoSelect.Root />
* <AxoSwitch.Root />
* </AxoBaseItem.Accessory>
* <AxoBaseItem.Trailing>
* <AxoBaseItem.Arrow />
* </AxoBaseItem.Trailing>
* </AxoBaseItem.Content>
* <AxoBaseItem.Arrow />
* </AxoBaseItem.Root>
* </AxoBaseItem.Layout>
* </AxoBaseItem.Group>
* ```
*/
export namespace AxoBaseItem {
@@ -82,13 +92,8 @@ export namespace AxoBaseItem {
<GroupContext value={context}>
<div
ref={ref}
className={tw('grid min-w-90')}
className={AXO_ITEM_GROUP_CLASS}
{...forwardExtraPropsForRadix(rest)}
// Style needs to come after forwarded props. Long-term we should
// figure out how to automatically merge props like these
style={{
gridTemplateColumns: GRID_TEMPLATE_COLUMNS,
}}
>
{children}
</div>
@@ -119,28 +124,13 @@ export namespace AxoBaseItem {
return (
<AriaClickable.Root asChild>
<div
className={tw(
'group',
// forward grid
'col-span-full grid grid-cols-subgrid',
'p-0.5',
'outline-none'
)}
className={tw(AXO_ITEM_ROOT_CLASS, 'group')}
{...forwardExtraPropsForRadix(rest)}
>
<div
className={tw(
// forward grid
'col-span-full grid grid-cols-subgrid',
'gap-x-3 px-3',
RootSpacing.get(groupContext.spacing),
'items-baseline',
'text-primary',
'curved-14',
'group-data-hovered:bg-secondary',
'group-data-pressed:bg-secondary-pressed',
'outline-none keyboard-mode:group-data-focused:axo-focus-ring',
'keyboard-mode:group-focus:axo-focus-ring'
AXO_ITEM_ROOT_INNER_CLASS,
RootSpacing.get(groupContext.spacing)
)}
>
{children}
@@ -153,46 +143,19 @@ export namespace AxoBaseItem {
Root.displayName = 'AxoBaseItem.Root';
/**
* <AxoBaseItem.LeadingSlot>
* <AxoBaseItem.Leading>
* --------------------------------------------------------------------------
*/
export type LeadingSlotProps = Readonly<{
className?: string;
export type LeadingProps = Readonly<{
children: ReactNode;
}>;
export const LeadingSlot: FC<LeadingSlotProps> = memo(props => {
return (
<div style={{ gridColumn: LEADING_SLOT }} className={props.className}>
{props.children}
</div>
);
export const Leading: FC<LeadingProps> = memo(props => {
return <div className={AXO_ITEM_LEADING_CLASS}>{props.children}</div>;
});
LeadingSlot.displayName = 'AxoBaseItem.LeadingSlot';
/**
* <AxoBaseItem.TrailingSlot>
* --------------------------------------------------------------------------
*/
/** @internal */
type TrailingSlotProps = Readonly<{
className?: string;
children: ReactNode;
}>;
/** @internal */
const TrailingSlot: FC<TrailingSlotProps> = memo(props => {
return (
<div style={{ gridColumn: TRAILING_SLOT }} className={props.className}>
{props.children}
</div>
);
});
TrailingSlot.displayName = 'AxoBaseItem.TrailingSlot';
Leading.displayName = 'AxoBaseItem.Leading';
/**
* <AxoBaseItem.Icon>
@@ -204,11 +167,7 @@ export namespace AxoBaseItem {
}>;
export const Icon: FC<IconProps> = memo(props => {
return (
<LeadingSlot>
<AxoSymbol.Icon size={18} symbol={props.symbol} label={null} />
</LeadingSlot>
);
return <AxoSymbol.Icon size={18} symbol={props.symbol} label={null} />;
});
Icon.displayName = 'AxoBaseItem.Icon';
@@ -228,14 +187,12 @@ export namespace AxoBaseItem {
export const Checkbox: FC<CheckboxProps> = memo(props => {
return (
<LeadingSlot>
<AxoCheckbox.Root
id={props.id}
variant="square"
checked={props.checked}
onCheckedChange={props.onCheckedChange}
/>
</LeadingSlot>
<AxoCheckbox.Root
id={props.id}
variant="square"
checked={props.checked}
onCheckedChange={props.onCheckedChange}
/>
);
});
@@ -255,13 +212,11 @@ export namespace AxoBaseItem {
export const IconAvatar: FC<IconAvatarProps> = memo(props => {
return (
<LeadingSlot>
<AxoAvatar.Root size={props.size}>
<AxoAvatar.Content label={null}>
<AxoAvatar.Icon symbol={props.symbol} />
</AxoAvatar.Content>
</AxoAvatar.Root>
</LeadingSlot>
<AxoAvatar.Root size={props.size}>
<AxoAvatar.Content label={null}>
<AxoAvatar.Icon symbol={props.symbol} />
</AxoAvatar.Content>
</AxoAvatar.Root>
);
});
@@ -278,16 +233,10 @@ export namespace AxoBaseItem {
export const Content: FC<ContentProps> = memo(props => {
return (
<div
style={{ gridColumn: CONTENT_SLOT }}
className={tw(
'flex min-w-50 grow basis-0 flex-wrap',
'self-stretch',
'items-baseline',
'gap-x-3 gap-y-2'
)}
>
{props.children}
<div className={AXO_ITEM_CONTENT_CLASS}>
<FlexWrapDetector>
<div className={AXO_ITEM_CONTENT_INNER_CLASS}>{props.children}</div>
</FlexWrapDetector>
</div>
);
});
@@ -304,18 +253,7 @@ export namespace AxoBaseItem {
}>;
export const Body: FC<BodyProps> = memo(props => {
return (
<div
className={tw(
'flex shrink grow basis-0 flex-wrap',
'min-w-50', // Note: We need an absolute length for min-width for description to truncate (even if its 0)
'self-center-safe',
'gap-x-3 gap-y-0.5'
)}
>
{props.children}
</div>
);
return <div className={AXO_ITEM_BODY_CLASS}>{props.children}</div>;
});
Body.displayName = 'AxoBaseItem.Body';
@@ -325,24 +263,18 @@ export namespace AxoBaseItem {
* --------------------------------------------------------------------------
*/
export type TitleProps = Readonly<{
export type LabelProps = Readonly<{
ref?: Ref<HTMLDivElement>;
truncate?: boolean;
children: ReactNode;
}>;
export const Title: FC<TitleProps> = memo(props => {
export const Label: FC<LabelProps> = memo(props => {
const { ref, truncate, children, ...rest } = props;
return (
<div
ref={ref}
className={tw(
'min-w-50', // force value to next line if there's not much space
'grow-[calc(infinity)]',
'type-body-medium text-primary',
truncate ? 'truncate' : 'line-clamp-2',
'-my-0.75 py-0.75' // extra space for focus rings
)}
className={tw(AXO_ITEM_LABEL_CLASS, truncate && 'truncate')}
{...forwardExtraPropsForRadix(rest)}
>
{children}
@@ -350,7 +282,7 @@ export namespace AxoBaseItem {
);
});
Title.displayName = 'AxoBaseItem.Title';
Label.displayName = 'AxoBaseItem.Label';
/**
* <AxoBaseItem.Value>
@@ -367,7 +299,7 @@ export namespace AxoBaseItem {
return (
<div
ref={ref}
className={tw('w-fit grow type-body-medium text-secondary')}
className={AXO_ITEM_VALUE_CLASS}
{...forwardExtraPropsForRadix(rest)}
>
{children}
@@ -391,23 +323,13 @@ export namespace AxoBaseItem {
export const Description: FC<DescriptionProps> = memo(props => {
const { ref, truncate, children, ...rest } = props;
return (
<>
{/* Force description to its own line */}
<div className={tw('basis-full')} />
<div
ref={ref}
className={tw(
'min-w-0',
'type-body-small text-secondary',
'forced-colors:text-[GrayText]',
truncate && 'truncate',
'-my-0.5 py-0.5' // extra space for focus rings
)}
{...forwardExtraPropsForRadix(rest)}
>
{children}
</div>
</>
<div
ref={ref}
className={tw(AXO_ITEM_DESCRIPTION_CLASS, truncate && 'truncate')}
{...forwardExtraPropsForRadix(rest)}
>
{children}
</div>
);
});
@@ -446,11 +368,7 @@ export namespace AxoBaseItem {
}>;
export const Accessory: FC<AccessoryProps> = memo(props => {
return (
<AriaClickable.DeadArea className={tw('flex gap-1.5')}>
{props.children}
</AriaClickable.DeadArea>
);
return <div className={AXO_ITEM_ACCESSORY_CLASS}>{props.children}</div>;
});
Accessory.displayName = 'AxoBaseItem.Accessory';
@@ -527,6 +445,21 @@ export namespace AxoBaseItem {
IconAction.displayName = 'AxoBaseItem.IconAction';
/**
* <AxoBaseItem.Trailing>
* --------------------------------------------------------------------------
*/
export type TrailingProps = Readonly<{
children: ReactNode;
}>;
export const Trailing: FC<TrailingProps> = memo(props => {
return <div className={AXO_ITEM_TRAILING_CLASS}>{props.children}</div>;
});
Trailing.displayName = 'AxoBaseItem.Trailing';
/**
* <AxoBaseItem.Arrow>
* --------------------------------------------------------------------------
@@ -534,11 +467,9 @@ export namespace AxoBaseItem {
export const Arrow: FC = memo(() => {
return (
<TrailingSlot
className={tw('shrink-0 type-body-medium text-placeholder')}
>
<div className={AXO_ITEM_ARROW_CLASS}>
<AxoSymbol.InlineGlyph label={null} symbol="chevron-[end]" />
</TrailingSlot>
</div>
);
});
+152 -132
View File
@@ -864,30 +864,32 @@ export function Preferences({
} else if (settingsLocation.page === SettingsPage.General) {
const pageContents = (
<ListGroup>
<List help={i18n('icu:Preferences--device-name__description')}>
<List
footerDescription={i18n('icu:Preferences--device-name__description')}
>
<ValueItem
title={i18n('icu:Preferences--phone-number')}
label={i18n('icu:Preferences--phone-number')}
value={phoneNumber}
/>
<ValueItem
title={i18n('icu:Preferences--device-name')}
label={i18n('icu:Preferences--device-name')}
value={deviceName}
/>
</List>
{weArePrimaryDevice && (
<List title={i18n('icu:Preferences--signal-pin')}>
<List label={i18n('icu:Preferences--signal-pin')}>
<SwitchItem
title={i18n('icu:Preferences--pin-reminders--header')}
label={i18n('icu:Preferences--pin-reminders--header')}
description={i18n('icu:Preferences--pin-reminders--description')}
checked={hasPinReminders ?? false}
onCheckedChange={onPinRemindersChange}
/>
</List>
)}
<List title={i18n('icu:Preferences--system')}>
<List label={i18n('icu:Preferences--system')}>
{isAutoLaunchSupported && (
<SwitchItem
title={i18n('icu:autoLaunchDescription')}
label={i18n('icu:autoLaunchDescription')}
disabled={hasAutoLaunch === undefined}
checked={hasAutoLaunch ?? false}
onCheckedChange={onAutoLaunchChange}
@@ -895,7 +897,7 @@ export function Preferences({
)}
{isHideMenuBarSupported && (
<SwitchItem
title={i18n('icu:hideMenuBar')}
label={i18n('icu:hideMenuBar')}
checked={hasHideMenuBar ?? false}
onCheckedChange={onHideMenuBarChange}
/>
@@ -903,14 +905,14 @@ export function Preferences({
{isSystemTraySupported && (
<>
<SwitchItem
title={i18n('icu:SystemTraySetting__minimize-to-system-tray')}
label={i18n('icu:SystemTraySetting__minimize-to-system-tray')}
disabled={hasMinimizeToSystemTray === undefined}
checked={hasMinimizeToSystemTray ?? false}
onCheckedChange={onMinimizeToSystemTrayChange}
/>
{isMinimizeToAndStartInSystemTraySupported && (
<SwitchItem
title={i18n(
label={i18n(
'icu:SystemTraySetting__minimize-to-and-start-in-system-tray'
)}
disabled={
@@ -924,24 +926,24 @@ export function Preferences({
</>
)}
</List>
<List title={i18n('icu:permissions')}>
<List label={i18n('icu:permissions')}>
<SwitchItem
title={i18n('icu:mediaPermissionsDescription')}
label={i18n('icu:mediaPermissionsDescription')}
disabled={hasMediaPermissions === undefined}
checked={hasMediaPermissions ?? false}
onCheckedChange={onMediaPermissionsChange}
/>
<SwitchItem
title={i18n('icu:mediaCameraPermissionsDescription')}
label={i18n('icu:mediaCameraPermissionsDescription')}
disabled={hasMediaCameraPermissions === undefined}
checked={hasMediaCameraPermissions ?? false}
onCheckedChange={onMediaCameraPermissionsChange}
/>
</List>
{isAutoDownloadUpdatesSupported && (
<List title={i18n('icu:Preferences--updates')}>
<List label={i18n('icu:Preferences--updates')}>
<SwitchItem
title={i18n('icu:Preferences__download-update')}
label={i18n('icu:Preferences__download-update')}
checked={hasAutoDownloadUpdate}
onCheckedChange={onAutoDownloadUpdateChange}
/>
@@ -951,7 +953,7 @@ export function Preferences({
<List>
{!weArePrimaryDevice && (
<ItemWithAction
title={i18n('icu:clearDataHeader')}
label={i18n('icu:clearDataHeader')}
description={i18n('icu:clearDataExplanation')}
action={
<AxoItem.Action
@@ -980,7 +982,7 @@ export function Preferences({
{weArePrimaryDevice && (
<ItemWithAction
title={i18n('icu:deleteAccountHeader')}
label={i18n('icu:deleteAccountHeader')}
description={i18n('icu:deleteAccountExplanation')}
action={
<AxoItem.Action
@@ -1050,7 +1052,7 @@ export function Preferences({
<List>
<ClickableItem
symbol="globe"
title={i18n('icu:Preferences__Language__Label')}
label={i18n('icu:Preferences__Language__Label')}
arrow
value={
<span
@@ -1176,7 +1178,7 @@ export function Preferences({
)}
<SelectItem
symbol="contrast"
title={i18n('icu:Preferences--theme')}
label={i18n('icu:Preferences--theme')}
disabled={themeSetting === undefined}
value={themeSetting ?? null}
onValueChange={value => {
@@ -1200,8 +1202,8 @@ export function Preferences({
/>
<ClickableItem
symbol="palette"
title={i18n('icu:showChatColorEditor')}
arrow={false}
label={i18n('icu:showChatColorEditor')}
arrow
onClick={() => {
setSettingsLocation({ page: SettingsPage.ChatColor });
}}
@@ -1218,7 +1220,7 @@ export function Preferences({
/>
<SelectItem
symbol="zoom-in"
title={i18n('icu:Preferences--zoom')}
label={i18n('icu:Preferences--zoom')}
disabled={zoomFactor === undefined}
value={zoomFactor != null ? String(zoomFactor) : null}
onValueChange={onZoomSelectChange}
@@ -1258,7 +1260,7 @@ export function Preferences({
<ListGroup>
<List accessibilityLabel={i18n('icu:Preferences__button--chats')}>
<SwitchItem
title={i18n('icu:Preferences__address-book-photos--title')}
label={i18n('icu:Preferences__address-book-photos--title')}
description={i18n(
'icu:Preferences__address-book-photos--description'
)}
@@ -1266,7 +1268,7 @@ export function Preferences({
onCheckedChange={onPreferContactAvatarsChange}
/>
<SwitchItem
title={i18n('icu:Preferences__keep-muted-chats-archived--title')}
label={i18n('icu:Preferences__keep-muted-chats-archived--title')}
description={i18n(
'icu:Preferences__keep-muted-chats-archived--description'
)}
@@ -1274,21 +1276,21 @@ export function Preferences({
onCheckedChange={onKeepMutedChatsArchivedChange}
/>
</List>
<List title={i18n('icu:Preferences__Chats__TextInputSection__Title')}>
<List label={i18n('icu:Preferences__Chats__TextInputSection__Title')}>
<SwitchItem
title={i18n('icu:spellCheckDescription')}
label={i18n('icu:spellCheckDescription')}
description={spellCheckDirtyText}
disabled={hasSpellCheck === undefined}
checked={hasSpellCheck ?? false}
onCheckedChange={onSpellCheckChange}
/>
<SwitchItem
title={i18n('icu:textFormattingDescription')}
label={i18n('icu:textFormattingDescription')}
checked={hasTextFormatting}
onCheckedChange={onTextFormattingChange}
/>
<SwitchItem
title={i18n('icu:Preferences__link-previews--title')}
label={i18n('icu:Preferences__link-previews--title')}
description={i18n(
'icu:Preferences__link-previews--new-description'
)}
@@ -1296,7 +1298,7 @@ export function Preferences({
onCheckedChange={onLinkPreviewsChange}
/>
<SwitchItem
title={i18n('icu:Preferences__auto-convert-emoji--title')}
label={i18n('icu:Preferences__auto-convert-emoji--title')}
description={
<I18n
i18n={i18n}
@@ -1308,11 +1310,9 @@ export function Preferences({
/>
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>
{i18n('icu:Preferences__EmojiSkinToneDefaultSetting__Label')}
</AxoItem.Title>
</AxoItem.Body>
<AxoItem.Label>
{i18n('icu:Preferences__EmojiSkinToneDefaultSetting__Label')}
</AxoItem.Label>
<AxoItem.Accessory>
<FunSkinTonesList
i18n={i18n}
@@ -1324,9 +1324,9 @@ export function Preferences({
</AxoItem.Content>
</AxoItem.Root>
</List>
<List title={i18n('icu:Preferences__Chats__ChatFoldersSection__Title')}>
<List label={i18n('icu:Preferences__Chats__ChatFoldersSection__Title')}>
<ClickableItem
title={
label={
hasAnyCurrentCustomChatFolders
? i18n(
'icu:Preferences__ChatsPage__ChatFoldersSection__AddChatFolderItem__Title--WithChatFolders'
@@ -1358,7 +1358,7 @@ export function Preferences({
<List>
<ItemWithAction
title={i18n('icu:PlaintextExport--PreferencesRow--Header')}
label={i18n('icu:PlaintextExport--PreferencesRow--Header')}
description={i18n(
'icu:PlaintextExport--PreferencesRow--Description'
)}
@@ -1376,7 +1376,7 @@ export function Preferences({
{isSyncSupported && (
<List>
<ItemWithAction
title={i18n('icu:sync')}
label={i18n('icu:sync')}
description={
<>
<div>
@@ -1430,19 +1430,19 @@ export function Preferences({
<ListGroup>
<List accessibilityLabel={i18n('icu:calling')}>
<SwitchItem
title={i18n('icu:incomingCallNotificationDescription')}
label={i18n('icu:incomingCallNotificationDescription')}
checked={hasIncomingCallNotifications}
onCheckedChange={onIncomingCallNotificationsChange}
/>
<SwitchItem
title={i18n('icu:callRingtoneNotificationDescription')}
label={i18n('icu:callRingtoneNotificationDescription')}
checked={hasCallRingtoneNotification}
onCheckedChange={onCallRingtoneNotificationChange}
/>
</List>
<List title={i18n('icu:Preferences__devices')}>
<List label={i18n('icu:Preferences__devices')}>
<SelectItem
title={i18n('icu:callingDeviceSelection__label--video')}
label={i18n('icu:callingDeviceSelection__label--video')}
disabled={!availableCameras.length}
emptyOptionsLabel={i18n(
'icu:callingDeviceSelection__select--no-device'
@@ -1458,7 +1458,7 @@ export function Preferences({
})}
/>
<SelectItem
title={i18n('icu:callingDeviceSelection__label--audio-input')}
label={i18n('icu:callingDeviceSelection__label--audio-input')}
disabled={!availableMicrophones.length}
value={
selectedMicrophone != null
@@ -1479,7 +1479,7 @@ export function Preferences({
/>
<SelectItem
title={i18n('icu:callingDeviceSelection__label--audio-output')}
label={i18n('icu:callingDeviceSelection__label--audio-output')}
disabled={!availableSpeakers.length}
value={
selectedSpeaker != null ? String(selectedSpeaker.index) : null
@@ -1497,10 +1497,10 @@ export function Preferences({
})}
/>
</List>
<List title={i18n('icu:Preferences--advanced')}>
<List label={i18n('icu:Preferences--advanced')}>
<SwitchItem
description={i18n('icu:alwaysRelayCallsDetail')}
title={i18n('icu:alwaysRelayCallsDescription')}
label={i18n('icu:alwaysRelayCallsDescription')}
checked={hasRelayCalls ?? false}
onCheckedChange={onRelayCallsChange}
/>
@@ -1519,17 +1519,17 @@ export function Preferences({
<ListGroup>
<List>
<SwitchItem
title={i18n('icu:Preferences__enable-notifications')}
label={i18n('icu:Preferences__enable-notifications')}
checked={hasNotifications}
onCheckedChange={onNotificationsChange}
/>
<SwitchItem
title={i18n('icu:callSystemNotificationDescription')}
label={i18n('icu:callSystemNotificationDescription')}
checked={hasCallNotifications}
onCheckedChange={onCallNotificationsChange}
/>
<SwitchItem
title={i18n('icu:Preferences__reaction-notifications-title')}
label={i18n('icu:Preferences__reaction-notifications-title')}
description={i18n(
'icu:Preferences__reaction-notifications-description'
)}
@@ -1538,13 +1538,13 @@ export function Preferences({
/>
{isNotificationAttentionSupported && (
<SwitchItem
title={i18n('icu:notificationDrawAttention')}
label={i18n('icu:notificationDrawAttention')}
checked={hasNotificationAttention}
onCheckedChange={onNotificationAttentionChange}
/>
)}
<SelectItem
title={i18n('icu:Preferences--notification-content')}
label={i18n('icu:Preferences--notification-content')}
disabled={!hasNotifications}
value={notificationContent}
onValueChange={onNotificationContentChange}
@@ -1564,7 +1564,7 @@ export function Preferences({
]}
/>
<ClickableItem
title={i18n('icu:WhileMuted__title')}
label={i18n('icu:WhileMuted__title')}
value={getNotifyWhileMutedSummary(notifyWhileMuted, i18n)}
description={i18n('icu:Preferences__WhileMuted__description')}
arrow
@@ -1576,15 +1576,15 @@ export function Preferences({
</List>
<List
title={i18n('icu:Preferences__Notifications__SoundsSection__Title')}
label={i18n('icu:Preferences__Notifications__SoundsSection__Title')}
>
<SwitchItem
title={i18n('icu:audioNotificationDescription')}
label={i18n('icu:audioNotificationDescription')}
checked={hasAudioNotifications ?? false}
onCheckedChange={onAudioNotificationsChange}
/>
<SwitchItem
title={i18n('icu:Preferences__message-audio-title')}
label={i18n('icu:Preferences__message-audio-title')}
description={i18n('icu:Preferences__message-audio-description')}
checked={hasMessageAudio}
onCheckedChange={onMessageAudioChange}
@@ -1592,10 +1592,10 @@ export function Preferences({
</List>
<List
title={i18n('icu:Preferences__Notifications__AppBadgeSection__Title')}
label={i18n('icu:Preferences__Notifications__AppBadgeSection__Title')}
>
<SwitchItem
title={i18n('icu:countMutedConversationsDescription')}
label={i18n('icu:countMutedConversationsDescription')}
checked={hasCountMutedConversations}
onCheckedChange={onCountMutedConversationsChange}
/>
@@ -1603,7 +1603,7 @@ export function Preferences({
<List>
<ClickableItem
title={i18n('icu:NotificationProfiles--setting')}
label={i18n('icu:NotificationProfiles--setting')}
description={i18n('icu:NotificationProfiles--manage-description')}
arrow
onClick={() =>
@@ -1616,7 +1616,7 @@ export function Preferences({
<List>
<ItemWithAction
title={i18n('icu:Preferences__Notifications__Reset__title')}
label={i18n('icu:Preferences__Notifications__Reset__title')}
action={
<AxoItem.Action
variant="subtle-destructive"
@@ -1688,7 +1688,7 @@ export function Preferences({
<ListGroup>
<List>
<ClickableItem
title={i18n('icu:Preferences__pnp__row--title')}
label={i18n('icu:Preferences__pnp__row--title')}
description={i18n('icu:Preferences__pnp__row--body')}
arrow
onClick={() => setSettingsLocation({ page: SettingsPage.PNP })}
@@ -1696,7 +1696,7 @@ export function Preferences({
</List>
<List>
<ClickableItem
title={i18n('icu:Preferences--blocked')}
label={i18n('icu:Preferences--blocked')}
description={blockedDescription}
arrow
disabled={!blockedContacts.length && !blockedGroups.length}
@@ -1704,16 +1704,16 @@ export function Preferences({
/>
</List>
<List
title={i18n('icu:Preferences--messaging')}
help={i18n('icu:Preferences--messaging-help')}
label={i18n('icu:Preferences--messaging')}
footerDescription={i18n('icu:Preferences--messaging-help')}
>
<SwitchItem
title={i18n('icu:Preferences--read-receipts')}
label={i18n('icu:Preferences--read-receipts')}
checked={hasReadReceipts}
onCheckedChange={onReadReceiptsChange}
/>
<SwitchItem
title={i18n('icu:Preferences--typing-indicators')}
label={i18n('icu:Preferences--typing-indicators')}
checked={hasTypingIndicators}
onCheckedChange={onTypingIndicatorsChange}
/>
@@ -1726,9 +1726,9 @@ export function Preferences({
onSubmit={onUniversalExpireTimerChange}
/>
)}
<List title={i18n('icu:disappearingMessages')}>
<List label={i18n('icu:disappearingMessages')}>
<SelectItem
title={i18n('icu:settings__DisappearingMessages__timer__label')}
label={i18n('icu:settings__DisappearingMessages__timer__label')}
description={i18n('icu:settings__DisappearingMessages__footer')}
value={String(universalExpireTimer)}
onValueChange={value => {
@@ -1760,12 +1760,12 @@ export function Preferences({
/>
</List>
{isContentProtectionSupported && (
<List title={i18n('icu:Preferences__Privacy__Application')}>
<List label={i18n('icu:Preferences__Privacy__Application')}>
<SwitchItem
description={i18n(
'icu:Preferences__content-protection--description'
)}
title={i18n('icu:Preferences__content-protection--label')}
label={i18n('icu:Preferences__content-protection--label')}
disabled={hasContentProtection === undefined}
checked={hasContentProtection ?? false}
onCheckedChange={handleContentProtectionChange}
@@ -1790,9 +1790,9 @@ export function Preferences({
</AxoConfirmDialog.Action>
</AxoConfirmDialog.Root>
) : null}
<List title={i18n('icu:Stories__title')}>
<List label={i18n('icu:Stories__title')}>
<ItemWithAction
title={i18n('icu:Stories__settings-toggle--title')}
label={i18n('icu:Stories__settings-toggle--title')}
description={i18n('icu:Stories__settings-toggle--description')}
action={
hasStoriesDisabled ? (
@@ -1813,9 +1813,9 @@ export function Preferences({
}
/>
</List>
<List title={i18n('icu:Preferences--advanced')}>
<List label={i18n('icu:Preferences--advanced')}>
<SwitchItem
title={
label={
<>
{i18n('icu:Preferences__PrivacyPage__ShowStatusIcon__Label')}
<div className="Preferences__Privacy__StatusIcon" />
@@ -1829,7 +1829,7 @@ export function Preferences({
/>
{isKeyTransparencyAvailable && (
<SwitchItem
title={i18n(
label={i18n(
'icu:Preferences__PrivacyPage__KeyTransparency__Label'
)}
description={
@@ -1888,11 +1888,13 @@ export function Preferences({
const pageContents = (
<ListGroup>
<List
title={i18n('icu:Preferences__media-auto-download')}
help={i18n('icu:Preferences__media-auto-download__description')}
label={i18n('icu:Preferences__media-auto-download')}
footerDescription={i18n(
'icu:Preferences__media-auto-download__description'
)}
>
<SwitchItem
title={i18n('icu:Preferences__media-auto-download__photos')}
label={i18n('icu:Preferences__media-auto-download__photos')}
checked={autoDownloadAttachment.photos}
onCheckedChange={(newValue: boolean) => {
onAutoDownloadAttachmentChange({
@@ -1902,7 +1904,7 @@ export function Preferences({
}}
/>
<SwitchItem
title={i18n('icu:Preferences__media-auto-download__videos')}
label={i18n('icu:Preferences__media-auto-download__videos')}
checked={autoDownloadAttachment.videos}
onCheckedChange={(newValue: boolean) => {
onAutoDownloadAttachmentChange({
@@ -1912,7 +1914,7 @@ export function Preferences({
}}
/>
<SwitchItem
title={i18n('icu:Preferences__media-auto-download__audio')}
label={i18n('icu:Preferences__media-auto-download__audio')}
checked={autoDownloadAttachment.audio}
onCheckedChange={(newValue: boolean) => {
onAutoDownloadAttachmentChange({
@@ -1922,7 +1924,7 @@ export function Preferences({
}}
/>
<SwitchItem
title={i18n('icu:Preferences__media-auto-download__documents')}
label={i18n('icu:Preferences__media-auto-download__documents')}
checked={autoDownloadAttachment.documents}
onCheckedChange={(newValue: boolean) => {
onAutoDownloadAttachmentChange({
@@ -1934,7 +1936,7 @@ export function Preferences({
</List>
<List>
<SelectItem
title={i18n('icu:Preferences__sent-media-quality')}
label={i18n('icu:Preferences__sent-media-quality')}
description={i18n(
'icu:Preferences__sent-media-quality__description'
)}
@@ -2319,7 +2321,7 @@ export function Preferences({
<List>
<SwitchItem
symbol="phone"
title={i18n('icu:WhileMuted__calls__title')}
label={i18n('icu:WhileMuted__calls__title')}
description={i18n(
'icu:Preferences__WhileMuted__calls__description'
)}
@@ -2330,7 +2332,7 @@ export function Preferences({
/>
<SwitchItem
symbol="at"
title={i18n('icu:WhileMuted__mentions__title')}
label={i18n('icu:WhileMuted__mentions__title')}
description={i18n(
'icu:Preferences__WhileMuted__mentions__description'
)}
@@ -2341,7 +2343,7 @@ export function Preferences({
/>
<SwitchItem
symbol="reply"
title={i18n('icu:WhileMuted__replies__title')}
label={i18n('icu:WhileMuted__replies__title')}
description={i18n(
'icu:Preferences__WhileMuted__replies__description'
)}
@@ -2725,25 +2727,27 @@ function ListGroup(props: ListGroupProps): ReactNode {
type ListProps = Readonly<{
accessibilityLabel?: string;
title?: string;
help?: ReactNode;
label?: string;
footerDescription?: ReactNode;
children: ReactNode;
}>;
function List(props: ListProps): ReactNode {
return (
<AxoList.Root accessibilityLabel={props.accessibilityLabel}>
{props.title != null && (
{props.label != null && (
<AxoList.Header>
<AxoList.Title>{props.title}</AxoList.Title>
<AxoList.Label>{props.label}</AxoList.Label>
</AxoList.Header>
)}
<AxoList.Body>
<AxoItem.Group>{props.children}</AxoItem.Group>
</AxoList.Body>
{props.help != null && (
{props.footerDescription != null && (
<AxoList.Footer>
<AxoList.Help>{props.help}</AxoList.Help>
<AxoList.FooterDescription>
{props.footerDescription}
</AxoList.FooterDescription>
</AxoList.Footer>
)}
</AxoList.Root>
@@ -2752,7 +2756,7 @@ function List(props: ListProps): ReactNode {
type SwitchItemProps = Readonly<{
symbol?: AxoSymbol.Name;
title: ReactNode;
label: ReactNode;
description?: ReactNode;
disabled?: boolean;
checked: boolean;
@@ -2762,21 +2766,25 @@ type SwitchItemProps = Readonly<{
function SwitchItem(props: SwitchItemProps): ReactNode {
return (
<AxoItem.Root>
{props.symbol != null && <AxoItem.Icon symbol={props.symbol} />}
{props.symbol != null && (
<AxoItem.Leading>
<AxoItem.Icon symbol={props.symbol} />
</AxoItem.Leading>
)}
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>{props.title}</AxoItem.Title>
<AxoItem.Label>{props.label}</AxoItem.Label>
{props.description != null && (
<AxoItem.Description>{props.description}</AxoItem.Description>
)}
</AxoItem.Body>
<AxoItem.Accessory>
<AxoItem.Trailing>
<AxoSwitch.Root
disabled={props.disabled}
checked={props.checked}
onCheckedChange={props.onCheckedChange}
/>
</AxoItem.Accessory>
</AxoItem.Trailing>
</AxoItem.Content>
</AxoItem.Root>
);
@@ -2790,7 +2798,7 @@ type SelectItemOption<T extends string> = Readonly<{
type SelectItemProps<T extends string> = Readonly<{
symbol?: AxoSymbol.Name;
title: ReactNode;
label: ReactNode;
description?: ReactNode;
disabled?: boolean;
placeholder?: string;
@@ -2835,43 +2843,47 @@ function SelectItem<T extends string>(props: SelectItemProps<T>): ReactNode {
return (
<AxoItem.Root>
{props.symbol != null && <AxoItem.Icon symbol={props.symbol} />}
{props.symbol != null && (
<AxoItem.Leading>
<AxoItem.Icon symbol={props.symbol} />
</AxoItem.Leading>
)}
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>{props.title}</AxoItem.Title>
<AxoItem.Label>{props.label}</AxoItem.Label>
{props.description != null && (
<AxoItem.Description>{props.description}</AxoItem.Description>
)}
<AxoItem.Accessory>
<AxoSelect.Root
disabled={props.disabled}
value={props.value}
onValueChange={handleValueChange}
>
<AxoSelect.Trigger placeholder={renderPlaceholder} />
<AxoSelect.Content>
{options.map(item => {
return (
<AxoSelect.Item
key={item.value}
value={item.value}
disabled={item.disabled}
>
<AxoSelect.ItemText>{item.label}</AxoSelect.ItemText>
</AxoSelect.Item>
);
})}
</AxoSelect.Content>
</AxoSelect.Root>
</AxoItem.Accessory>
</AxoItem.Body>
<AxoItem.Accessory>
<AxoSelect.Root
disabled={props.disabled}
value={props.value}
onValueChange={handleValueChange}
>
<AxoSelect.Trigger placeholder={renderPlaceholder} />
<AxoSelect.Content>
{options.map(item => {
return (
<AxoSelect.Item
key={item.value}
value={item.value}
disabled={item.disabled}
>
<AxoSelect.ItemText>{item.label}</AxoSelect.ItemText>
</AxoSelect.Item>
);
})}
</AxoSelect.Content>
</AxoSelect.Root>
</AxoItem.Accessory>
</AxoItem.Content>
</AxoItem.Root>
);
}
type ValueItemProps = Readonly<{
title: ReactNode;
label: ReactNode;
value: ReactNode;
}>;
@@ -2881,7 +2893,7 @@ function ValueItem(props: ValueItemProps): ReactNode {
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title id={id}>{props.title}</AxoItem.Title>
<AxoItem.Label id={id}>{props.label}</AxoItem.Label>
<AxoItem.Value>{props.value}</AxoItem.Value>
</AxoItem.Body>
</AxoItem.Content>
@@ -2891,7 +2903,7 @@ function ValueItem(props: ValueItemProps): ReactNode {
type ClickableItemProps = Readonly<{
symbol?: AxoSymbol.Name;
title: ReactNode;
label: ReactNode;
description?: ReactNode;
value?: ReactNode;
accessory?: ReactNode;
@@ -2904,10 +2916,14 @@ function ClickableItem(props: ClickableItemProps): ReactNode {
const id = useId();
return (
<AxoItem.Root>
{props.symbol != null && <AxoItem.Icon symbol={props.symbol} />}
{props.symbol != null && (
<AxoItem.Leading>
<AxoItem.Icon symbol={props.symbol} />
</AxoItem.Leading>
)}
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title id={id}>{props.title}</AxoItem.Title>
<AxoItem.Label id={id}>{props.label}</AxoItem.Label>
{props.value != null && <AxoItem.Value>{props.value}</AxoItem.Value>}
{props.description != null && (
<AxoItem.Description>{props.description}</AxoItem.Description>
@@ -2915,18 +2931,22 @@ function ClickableItem(props: ClickableItemProps): ReactNode {
{!props.disabled && (
<AxoItem.HiddenTrigger labelledby={id} onClick={props.onClick} />
)}
{props.accessory != null && (
<AxoItem.Accessory>{props.accessory}</AxoItem.Accessory>
)}
</AxoItem.Body>
{props.accessory != null && (
<AxoItem.Accessory>{props.accessory}</AxoItem.Accessory>
)}
</AxoItem.Content>
{props.arrow && !props.disabled && <AxoItem.Arrow />}
{props.arrow && !props.disabled && (
<AxoItem.Trailing>
<AxoItem.Arrow />
</AxoItem.Trailing>
)}
</AxoItem.Root>
);
}
type ItemWithActionProps = Readonly<{
title: ReactNode;
label: ReactNode;
description?: ReactNode;
action: ReactNode;
}>;
@@ -2937,12 +2957,12 @@ function ItemWithAction(props: ItemWithActionProps): ReactNode {
<AxoItem.Root>
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title id={id}>{props.title}</AxoItem.Title>
<AxoItem.Label id={id}>{props.label}</AxoItem.Label>
{props.description != null && (
<AxoItem.Description>{props.description}</AxoItem.Description>
)}
<AxoItem.Accessory>{props.action}</AxoItem.Accessory>
</AxoItem.Body>
<AxoItem.Accessory>{props.action}</AxoItem.Accessory>
</AxoItem.Content>
</AxoItem.Root>
);
@@ -37,7 +37,7 @@ export function ConversationNotificationsSettings({
onOpenWhileMutedSettings,
setMuteExpiration,
}: PropsType): JSX.Element {
const whileMutedTitleId = useId();
const whileMutedLabelId = useId();
const mutedUntilText =
muteExpiresAt != null && isConversationMuted({ muteExpiresAt })
@@ -81,9 +81,9 @@ export function ConversationNotificationsSettings({
<AxoItem.Icon symbol="bell-slash" />
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>
<AxoItem.Label>
{i18n('icu:muteNotificationsTitle')}
</AxoItem.Title>
</AxoItem.Label>
<AxoItem.Description>
{mutedUntilText ?? i18n('icu:notMuted')}
</AxoItem.Description>
@@ -115,15 +115,15 @@ export function ConversationNotificationsSettings({
<AxoItem.Icon symbol="bell-badge" />
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title id={whileMutedTitleId}>
<AxoItem.Label id={whileMutedLabelId}>
{i18n('icu:WhileMuted__title')}
</AxoItem.Title>
</AxoItem.Label>
<AxoItem.Value>{whileMutedSummary}</AxoItem.Value>
<AxoItem.Description>
{i18n('icu:WhileMuted__description')}
</AxoItem.Description>
<AxoItem.HiddenTrigger
labelledby={whileMutedTitleId}
labelledby={whileMutedLabelId}
onClick={onOpenWhileMutedSettings}
/>
</AxoItem.Body>
@@ -36,7 +36,7 @@ export function WhileMutedSettings({
<WhileMutedSwitch
settingKey="calls"
symbol="phone"
title={i18n('icu:WhileMuted__calls__title')}
label={i18n('icu:WhileMuted__calls__title')}
description={i18n('icu:WhileMuted__calls__description')}
checked={notifyWhileMuted.calls}
setNotifyWhileMuted={setNotifyWhileMuted}
@@ -46,7 +46,7 @@ export function WhileMutedSettings({
<WhileMutedSwitch
settingKey="mentions"
symbol="at"
title={i18n('icu:WhileMuted__mentions__title')}
label={i18n('icu:WhileMuted__mentions__title')}
description={i18n('icu:WhileMuted__mentions__description')}
checked={notifyWhileMuted.mentions}
setNotifyWhileMuted={setNotifyWhileMuted}
@@ -54,7 +54,7 @@ export function WhileMutedSettings({
<WhileMutedSwitch
settingKey="replies"
symbol="reply"
title={i18n('icu:WhileMuted__replies__title')}
label={i18n('icu:WhileMuted__replies__title')}
description={i18n('icu:WhileMuted__replies__description')}
checked={notifyWhileMuted.replies}
setNotifyWhileMuted={setNotifyWhileMuted}
@@ -71,7 +71,7 @@ export function WhileMutedSettings({
type WhileMutedSwitchProps = Readonly<{
settingKey: NotifyWhileMutedKey;
symbol: AxoSymbol.Name;
title: string;
label: string;
description: string;
checked: boolean;
setNotifyWhileMuted: PropsType['setNotifyWhileMuted'];
@@ -80,7 +80,7 @@ type WhileMutedSwitchProps = Readonly<{
function WhileMutedSwitch({
settingKey,
symbol,
title,
label,
description,
checked,
setNotifyWhileMuted,
@@ -97,12 +97,12 @@ function WhileMutedSwitch({
<AxoItem.Icon symbol={symbol} />
<AxoItem.Content>
<AxoItem.Body>
<AxoItem.Title>{title}</AxoItem.Title>
<AxoItem.Label>{label}</AxoItem.Label>
<AxoItem.Description>{description}</AxoItem.Description>
</AxoItem.Body>
<AxoItem.Accessory>
<label>
<span className={tw('sr-only')}>{title}</span>
<span className={tw('sr-only')}>{label}</span>
<AxoSwitch.Root
checked={checked}
onCheckedChange={handleCheckedChange}