Axo dialog design updates & aria checks

This commit is contained in:
Jamie
2025-11-05 15:15:46 -08:00
committed by GitHub
parent 28388578ce
commit 1fcb4d1041
39 changed files with 540 additions and 247 deletions
+1
View File
@@ -150,6 +150,7 @@ const DOM_PACKAGES = new Set([
'blob-util',
'blueimp-load-image',
'copy-text-to-clipboard',
'dom-accessibility-api',
'fabric',
'focus-trap-react',
'radix-ui',
+24
View File
@@ -3643,6 +3643,30 @@ Signal Desktop makes use of the following open source projects.
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
## dom-accessibility-api
MIT License
Copyright (c) 2020 Sebastian Silbermann
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## emoji-datasource
The MIT License (MIT)
+1
View File
@@ -156,6 +156,7 @@
"credit-card-type": "10.0.2",
"dashdash": "2.0.0",
"direction": "1.0.4",
"dom-accessibility-api": "0.7.0",
"emoji-datasource": "16.0.0",
"emoji-datasource-apple": "16.0.0",
"emoji-regex": "10.4.0",
+8
View File
@@ -200,6 +200,9 @@ importers:
direction:
specifier: 1.0.4
version: 1.0.4
dom-accessibility-api:
specifier: 0.7.0
version: 0.7.0
emoji-datasource:
specifier: 16.0.0
version: 16.0.0
@@ -5739,6 +5742,9 @@ packages:
dom-accessibility-api@0.6.3:
resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
dom-accessibility-api@0.7.0:
resolution: {integrity: sha512-LjjdFmd9AITAet3Hy6Y6rwB7Sq1+x5NiwbOpnkLHC1bCXJqJKiV9DyppSSWobuSKvjKXt9G2u3hW402MPt6m+g==}
dom-converter@0.2.0:
resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==}
@@ -16929,6 +16935,8 @@ snapshots:
dom-accessibility-api@0.6.3: {}
dom-accessibility-api@0.7.0: {}
dom-converter@0.2.0:
dependencies:
utila: 0.4.0
+1 -1
View File
@@ -277,7 +277,7 @@
--type-font-weight-title-small: var(--font-weight-semibold);
--type-font-weight-body-large: var(--font-weight-regular);
--type-font-weight-body-medium: var(--font-weight-regular);
--type-font-weight-body-small: var(--font-weight-medium);
--type-font-weight-body-small: var(--font-weight-regular);
--type-font-weight-caption: var(--font-weight-regular);
/* letter-spacing */
--tracking-*: initial; /* reset defaults */
+1 -5
View File
@@ -78,11 +78,7 @@ function CardButton(props: {
}) {
return (
<AriaClickable.SubWidget>
<AxoButton.Root
variant={props.variant}
size="medium"
onClick={props.onClick}
>
<AxoButton.Root variant={props.variant} size="md" onClick={props.onClick}>
{props.children}
</AxoButton.Root>
</AriaClickable.SubWidget>
+7 -3
View File
@@ -20,7 +20,12 @@ const EXAMPLE_TITLE_LONG = (
</>
);
const EXAMPLE_DESCRIPTION = <>Exporting chat</>;
const EXAMPLE_DESCRIPTION = (
<>
Your chat will be downloaded in the background. You can continue to use
Signal during this process.
</>
);
const EXAMPLE_DESCRIPTION_LONG = (
<>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nobis, amet aut
@@ -72,12 +77,11 @@ function Template(props: {
return (
<AxoAlertDialog.Root open={open} onOpenChange={setOpen}>
<AxoAlertDialog.Trigger>
<AxoButton.Root variant="subtle-primary" size="medium">
<AxoButton.Root variant="subtle-primary" size="md">
Open
</AxoButton.Root>
</AxoAlertDialog.Trigger>
<AxoAlertDialog.Content
size="md"
escape={
props.requireExplicitChoice
? 'cancel-is-destructive'
+23 -30
View File
@@ -12,7 +12,7 @@ import type { AxoSymbol } from './AxoSymbol.dom.js';
const Namespace = 'AxoAlertDialog';
const { useContentEscapeBehavior, useContentSize } = AxoBaseDialog;
const { useContentEscapeBehavior } = AxoBaseDialog;
/**
* Displays a menu located at the pointer, triggered by a right click or a long press.
@@ -74,30 +74,29 @@ export namespace AxoAlertDialog {
* --------------------------------
*/
export type ContentSize = AxoBaseDialog.ContentSize;
export type ContentEscape = AxoBaseDialog.ContentEscape;
export type ContentProps = AxoBaseDialog.ContentProps;
export type ContentProps = Readonly<{
escape: ContentEscape;
children: ReactNode;
}>;
export const Content: FC<ContentProps> = memo(props => {
const sizeConfig = AxoBaseDialog.ContentSizes[props.size];
const handleContentEscapeEvent = useContentEscapeBehavior(props.escape);
return (
<AxoBaseDialog.ContentSizeProvider value={props.size}>
<AlertDialog.Portal>
<AlertDialog.Overlay className={AxoBaseDialog.overlayStyles}>
<AlertDialog.Content
onEscapeKeyDown={handleContentEscapeEvent}
className={AxoBaseDialog.contentStyles}
style={{
minWidth: sizeConfig.minWidth,
width: sizeConfig.width,
}}
>
{props.children}
</AlertDialog.Content>
</AlertDialog.Overlay>
</AlertDialog.Portal>
</AxoBaseDialog.ContentSizeProvider>
<AlertDialog.Portal>
<AlertDialog.Overlay className={AxoBaseDialog.overlayStyles}>
<AlertDialog.Content
onEscapeKeyDown={handleContentEscapeEvent}
className={AxoBaseDialog.contentStyles}
style={{
minWidth: 300,
width: 300,
}}
>
{props.children}
</AlertDialog.Content>
</AlertDialog.Overlay>
</AlertDialog.Portal>
);
});
@@ -113,14 +112,8 @@ export namespace AxoAlertDialog {
}>;
export const Body: FC<BodyProps> = memo(props => {
const contentSize = useContentSize();
const contentSizeConfig = AxoBaseDialog.ContentSizes[contentSize];
return (
<AxoScrollArea.Root
maxHeight={contentSizeConfig.maxBodyHeight}
scrollbarWidth="none"
>
<AxoScrollArea.Root maxHeight={440} scrollbarWidth="none">
<AxoScrollArea.Hint edge="bottom" />
<AxoScrollArea.Viewport>
<AxoScrollArea.Content>
@@ -208,7 +201,7 @@ export namespace AxoAlertDialog {
export const Cancel: FC<CancelProps> = memo(props => {
return (
<AlertDialog.Cancel asChild>
<AxoButton.Root variant="secondary" size="medium" width="fill">
<AxoButton.Root variant="secondary" size="md" width="full">
{props.children}
</AxoButton.Root>
</AlertDialog.Cancel>
@@ -239,8 +232,8 @@ export namespace AxoAlertDialog {
variant={props.variant}
symbol={props.symbol}
arrow={props.arrow}
size="medium"
width="fill"
size="md"
width="full"
>
{props.children}
</AxoButton.Root>
+16 -16
View File
@@ -145,7 +145,7 @@ const LONG_TEXT = (
function Fit(props: { longText?: boolean }) {
return (
<AxoButton.Root variant="primary" size="medium" width="fit">
<AxoButton.Root variant="primary" size="md" width="fit">
Fit {props.longText && LONG_TEXT}
</AxoButton.Root>
);
@@ -153,15 +153,15 @@ function Fit(props: { longText?: boolean }) {
function Grow(props: { longText?: boolean }) {
return (
<AxoButton.Root variant="affirmative" size="medium" width="grow">
<AxoButton.Root variant="affirmative" size="md" width="grow">
Grow {props.longText && LONG_TEXT}
</AxoButton.Root>
);
}
function Fill(props: { longText?: boolean }) {
function Full(props: { longText?: boolean }) {
return (
<AxoButton.Root variant="destructive" size="medium" width="fill">
<AxoButton.Root variant="destructive" size="md" width="full">
Fill {props.longText && LONG_TEXT}
</AxoButton.Root>
);
@@ -180,7 +180,7 @@ function WidthTestTemplate(props: {
<>
<Fit />
<Grow />
<Fill />
<Full />
</>
)}
@@ -239,26 +239,26 @@ function WidthTestTemplate(props: {
<p>Fill</p>
{props.children(
<>
<Fill />
<Fill />
<Fill />
<Full />
<Full />
<Full />
</>
)}
<p>Fill: With long text</p>
<p>Full: With long text</p>
{props.children(
<>
<Fill longText />
<Fill longText />
<Fill longText />
<Full longText />
<Full longText />
<Full longText />
</>
)}
<p>Fill: With mixed length texts</p>
<p>Full: With mixed length texts</p>
{props.children(
<>
<Fill />
<Fill />
<Fill longText />
<Full />
<Full />
<Full longText />
</>
)}
</div>
+8 -8
View File
@@ -128,9 +128,9 @@ const AxoButtonVariants = {
};
const AxoButtonSizes = {
large: tw('min-w-16 px-4 py-2 type-body-medium font-medium'),
medium: tw('min-w-14 px-3 py-1.5 type-body-medium font-medium'),
small: tw('min-w-12 px-2 py-1 type-body-small font-medium'),
lg: tw('min-w-16 px-4 py-2 type-body-medium font-medium'),
md: tw('min-w-14 px-3 py-1.5 type-body-medium font-medium'),
sm: tw('min-w-12 px-2 py-1 type-body-small font-medium'),
} as const satisfies Record<string, TailwindStyles>;
type BaseButtonAttrs = Omit<
@@ -171,9 +171,9 @@ const AxoButtonSpinnerSizes: Record<
AxoButtonSize,
{ size: number; strokeWidth: number }
> = {
large: { size: 20, strokeWidth: 2 },
medium: { size: 20, strokeWidth: 2 },
small: { size: 16, strokeWidth: 1.5 },
lg: { size: 20, strokeWidth: 2 },
md: { size: 20, strokeWidth: 2 },
sm: { size: 16, strokeWidth: 1.5 },
};
type ExperimentalButtonSpinnerProps = Readonly<{
@@ -204,7 +204,7 @@ export namespace AxoButton {
export type Variant = AxoButtonVariant;
export type Size = AxoButtonSize;
export type Width = 'fit' | 'grow' | 'fill';
export type Width = 'fit' | 'grow' | 'full';
const Widths: Record<Width, TailwindStyles> = {
/* Always try to fit to the content of the button */
@@ -212,7 +212,7 @@ export namespace AxoButton {
/* Allow the button to grow within a flex container */
grow: tw('grow'),
/* Always try to fill the available space */
fill: tw('w-full'),
full: tw('w-full'),
};
export type RootProps = BaseButtonAttrs &
+1 -1
View File
@@ -25,7 +25,7 @@ export namespace AxoCheckbox {
disabled={props.disabled}
required={props.required}
className={tw(
'flex size-5 items-center justify-center rounded-full',
'flex size-5 items-center justify-center rounded-full leading-none',
'border border-border-primary inset-shadow-on-color',
'data-[state=unchecked]:bg-fill-primary',
'data-[state=unchecked]:pressed:bg-fill-primary-pressed',
+221 -3
View File
@@ -1,12 +1,14 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactNode } from 'react';
import React, { useState } from 'react';
import type { CSSProperties, ReactNode } from 'react';
import React, { useId, useMemo, useState } from 'react';
import type { Meta } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { AxoDialog } from './AxoDialog.dom.js';
import { AxoButton } from './AxoButton.dom.js';
import { tw } from './tw.dom.js';
import { AxoCheckbox } from './AxoCheckbox.dom.js';
import { getScrollbarGutters } from './_internal/scrollbars.dom.js';
export default {
title: 'Axo/AxoDialog',
@@ -46,7 +48,7 @@ function Template(props: {
return (
<AxoDialog.Root open={open} onOpenChange={setOpen}>
<AxoDialog.Trigger>
<AxoButton.Root variant="secondary" size="medium">
<AxoButton.Root variant="secondary" size="md">
Open Dialog
</AxoButton.Root>
</AxoDialog.Trigger>
@@ -143,3 +145,219 @@ export function FooterContentLongAndTight(): JSX.Element {
</Template>
);
}
function Spacer(props: { height: 8 | 12 }) {
return <div style={{ height: props.height }} />;
}
function TextInputField(props: { placeholder: string }) {
const style = useMemo(() => {
const bodyPadding = 24;
const inputPadding = 16;
return { marginInline: inputPadding - bodyPadding };
}, []);
return (
<div className={tw('py-1.5')} style={style}>
<input
placeholder={props.placeholder}
className={tw(
'w-full px-3 py-1.5',
'border-[0.5px] border-border-primary shadow-elevation-0',
'rounded-lg bg-fill-primary',
'placeholder:text-label-placeholder'
)}
/>
</div>
);
}
export function ExampleNicknameAndNoteDialog(): JSX.Element {
const [open, setOpen] = useState(true);
return (
<AxoDialog.Root open={open} onOpenChange={setOpen}>
<AxoDialog.Trigger>
<AxoButton.Root variant="secondary" size="md">
Open Dialog
</AxoButton.Root>
</AxoDialog.Trigger>
<AxoDialog.Content size="sm" escape="cancel-is-destructive">
<AxoDialog.Header>
<AxoDialog.Title>Nickname</AxoDialog.Title>
<AxoDialog.Close aria-label="Close" />
</AxoDialog.Header>
<AxoDialog.Body>
<p className={tw('mb-4 type-body-small text-label-secondary')}>
Nicknames &amp; notes are stored with Signal and end-to-end
encrypted. They are only visible to you.
</p>
<div
className={tw('mx-auto size-20 rounded-full bg-color-fill-primary')}
/>
<Spacer height={12} />
<TextInputField placeholder="First name" />
<TextInputField placeholder="Last name" />
<TextInputField placeholder="Note" />
<Spacer height={12} />
</AxoDialog.Body>
<AxoDialog.Footer>
<AxoDialog.Actions>
<AxoDialog.Action variant="secondary" onClick={action('onCancel')}>
Cancel
</AxoDialog.Action>
<AxoDialog.Action variant="primary" onClick={action('onSave')}>
Save
</AxoDialog.Action>
</AxoDialog.Actions>
</AxoDialog.Footer>
</AxoDialog.Content>
</AxoDialog.Root>
);
}
function CheckboxField(props: { label: string }) {
const id = useId();
const [checked, setChecked] = useState(false);
return (
<div className={tw('flex gap-3 py-2.5')}>
<AxoCheckbox.Root
id={id}
checked={checked}
onCheckedChange={setChecked}
/>
<label
htmlFor={id}
className={tw('truncate type-body-large text-label-primary')}
>
{props.label}
</label>
</div>
);
}
export function ExampleMuteNotificationsDialog(): JSX.Element {
const [open, setOpen] = useState(true);
return (
<AxoDialog.Root open={open} onOpenChange={setOpen}>
<AxoDialog.Trigger>
<AxoButton.Root variant="secondary" size="md">
Open Dialog
</AxoButton.Root>
</AxoDialog.Trigger>
<AxoDialog.Content size="sm" escape="cancel-is-noop">
<AxoDialog.Header>
<AxoDialog.Title>Mute notifications</AxoDialog.Title>
<AxoDialog.Close aria-label="Close" />
</AxoDialog.Header>
<AxoDialog.Body>
<Spacer height={8} />
<CheckboxField label="Mute for 1 hour" />
<CheckboxField label="Mute for 8 hours" />
<CheckboxField label="Mute for 1 day" />
<CheckboxField label="Mute for 1 week" />
<CheckboxField label="Mute always" />
<Spacer height={8} />
</AxoDialog.Body>
<AxoDialog.Footer>
<AxoDialog.Actions>
<AxoDialog.Action variant="secondary" onClick={action('onCancel')}>
Cancel
</AxoDialog.Action>
<AxoDialog.Action variant="primary" onClick={action('onSave')}>
Save
</AxoDialog.Action>
</AxoDialog.Actions>
</AxoDialog.Footer>
</AxoDialog.Content>
</AxoDialog.Root>
);
}
function ExampleItem(props: { label: string; description: string }) {
const labelId = useId();
const descriptionId = useId();
const style = useMemo((): CSSProperties => {
return {
paddingInline: 24 - getScrollbarGutters('thin', 'custom').vertical,
};
}, []);
return (
<div
role="option"
aria-selected={false}
aria-labelledby={labelId}
aria-describedby={descriptionId}
tabIndex={0}
className={tw('rounded-lg py-2.5 hover:bg-fill-secondary')}
style={style}
>
<div
id={labelId}
className={tw('truncate type-body-large text-label-primary')}
>
{props.label}
</div>
<div
id={descriptionId}
className={tw('truncate type-body-small text-label-secondary')}
>
{props.description}
</div>
</div>
);
}
export function ExampleLanguageDialog(): JSX.Element {
const [open, setOpen] = useState(true);
return (
<AxoDialog.Root open={open} onOpenChange={setOpen}>
<AxoDialog.Trigger>
<AxoButton.Root variant="secondary" size="md">
Open Dialog
</AxoButton.Root>
</AxoDialog.Trigger>
<AxoDialog.Content size="sm" escape="cancel-is-noop">
<AxoDialog.Header>
<AxoDialog.Title>Language</AxoDialog.Title>
<AxoDialog.Close aria-label="Close" />
</AxoDialog.Header>
<AxoDialog.ExperimentalSearch>
<input
type="search"
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
placeholder="Search languages"
className={tw('w-full rounded-lg bg-fill-secondary px-3 py-[5px]')}
/>
</AxoDialog.ExperimentalSearch>
<AxoDialog.Body padding="only-scrollbar-gutter">
<div role="listbox">
<ExampleItem label="System Language" description="English" />
<ExampleItem label="Afrikaans" description="Afrikaans" />
<ExampleItem label="Arabic" description="العربية" />
<ExampleItem label="Azerbaijani" description="Azərbaycan dili" />
<ExampleItem label="Bulgarian" description="Български" />
<ExampleItem label="Bangla" description="বাংলা" />
<ExampleItem label="Bosnian" description="bosanski" />
<ExampleItem label="Catalan" description="català" />
<ExampleItem label="Czech" description="Čeština" />
</div>
</AxoDialog.Body>
<AxoDialog.Footer>
<AxoDialog.Actions>
<AxoDialog.Action variant="secondary" onClick={action('onCancel')}>
Cancel
</AxoDialog.Action>
<AxoDialog.Action variant="primary" onClick={action('onSet')}>
Set
</AxoDialog.Action>
</AxoDialog.Actions>
</AxoDialog.Footer>
</AxoDialog.Content>
</AxoDialog.Root>
);
}
+90 -59
View File
@@ -19,30 +19,30 @@ import { AxoButton } from './AxoButton.dom.js';
const Namespace = 'AxoDialog';
const { useContentEscapeBehavior, useContentSize } = AxoBaseDialog;
const { useContentEscapeBehavior } = AxoBaseDialog;
// We want to have 25px of padding on either side of header/body/footer, but
// We want to have 24px of padding on either side of header/body/footer, but
// it's import that we remain aligned with the vertical scrollbar gutters that
// we need to measure in the browser to know the value of.
//
// Chrome currently renders vertical scrollbars as 11px with
// `scrollbar-width: thin` but that could change someday or based on some OS
// settings. So we'll target 24px but we'll tolerate different values.
const SCROLLBAR_WIDTH_EXPECTED = 11; /* (keep in sync with chromium) */
const SCROLLBAR_WIDTH_ACTUAL = getScrollbarGutters('thin', 'custom').vertical;
function getPadding(target: number, scrollbars: boolean): number {
const scrollbarWidthExpected = 11;
const paddingBeforeScrollbarWidth = target - scrollbarWidthExpected;
const DIALOG_PADDING_TARGET = 20;
if (scrollbars) {
// If this element has scrollbars we should just rely on the rendered gutter
return paddingBeforeScrollbarWidth;
}
const DIALOG_PADDING_BEFORE_SCROLLBAR_WIDTH =
DIALOG_PADDING_TARGET - SCROLLBAR_WIDTH_EXPECTED;
const scrollbarWidthActual = getScrollbarGutters('thin', 'custom').vertical;
const DIALOG_PADDING_PLUS_SCROLLBAR_WIDTH =
SCROLLBAR_WIDTH_ACTUAL + DIALOG_PADDING_BEFORE_SCROLLBAR_WIDTH;
const DIALOG_HEADER_PADDING_BLOCK = 10;
const DIALOG_HEADER_ICON_BUTTON_MARGIN =
DIALOG_HEADER_PADDING_BLOCK - DIALOG_PADDING_PLUS_SCROLLBAR_WIDTH;
// If this element doesn't have scrollbars, we need to add the exact value of
// the actual scrollbar gutter
return scrollbarWidthActual + paddingBeforeScrollbarWidth;
}
export namespace AxoDialog {
/**
@@ -86,31 +86,44 @@ export namespace AxoDialog {
* ------------------------------
*/
export type ContentSize = AxoBaseDialog.ContentSize;
type ContentSizeConfig = Readonly<{
width: number;
minWidth: number;
}>;
const ContentSizes: Record<ContentSize, ContentSizeConfig> = {
sm: { width: 360, minWidth: 360 },
md: { width: 420, minWidth: 360 },
lg: { width: 720, minWidth: 360 },
};
export type ContentSize = 'sm' | 'md' | 'lg';
export type ContentEscape = AxoBaseDialog.ContentEscape;
export type ContentProps = AxoBaseDialog.ContentProps;
export type ContentProps = Readonly<{
size: ContentSize;
escape: ContentEscape;
children: ReactNode;
}>;
export const Content: FC<ContentProps> = memo(props => {
const sizeConfig = AxoBaseDialog.ContentSizes[props.size];
const sizeConfig = ContentSizes[props.size];
const handleContentEscapeEvent = useContentEscapeBehavior(props.escape);
return (
<AxoBaseDialog.ContentSizeProvider value={props.size}>
<Dialog.Portal>
<Dialog.Overlay className={AxoBaseDialog.overlayStyles}>
<Dialog.Content
className={AxoBaseDialog.contentStyles}
onEscapeKeyDown={handleContentEscapeEvent}
onInteractOutside={handleContentEscapeEvent}
style={{
width: sizeConfig.width,
minWidth: sizeConfig.minWidth,
}}
>
{props.children}
</Dialog.Content>
</Dialog.Overlay>
</Dialog.Portal>
</AxoBaseDialog.ContentSizeProvider>
<Dialog.Portal>
<Dialog.Overlay className={AxoBaseDialog.overlayStyles}>
<Dialog.Content
className={AxoBaseDialog.contentStyles}
onEscapeKeyDown={handleContentEscapeEvent}
onInteractOutside={handleContentEscapeEvent}
style={{
width: sizeConfig.width,
minWidth: 320,
}}
>
{props.children}
</Dialog.Content>
</Dialog.Overlay>
</Dialog.Portal>
);
});
@@ -128,14 +141,13 @@ export namespace AxoDialog {
export const Header: FC<HeaderProps> = memo(props => {
const style = useMemo(() => {
return {
paddingBlock: DIALOG_HEADER_PADDING_BLOCK,
paddingInline: DIALOG_PADDING_PLUS_SCROLLBAR_WIDTH,
paddingInline: getPadding(10, false),
};
}, []);
return (
<div
className={tw(
'grid items-center',
'grid items-center py-2.5',
'grid-cols-[[back-slot]_1fr_[title-slot]_auto_[close-slot]_1fr]'
)}
style={style}
@@ -167,12 +179,12 @@ export namespace AxoDialog {
type="button"
aria-label={label}
className={tw(
'rounded-full p-1.5',
'rounded-full p-[5px] leading-none',
'hovered:bg-fill-secondary pressed:bg-fill-secondary-pressed',
'outline-0 outline-border-focused focused:outline-[2.5px]'
)}
>
<AxoSymbol.Icon symbol={symbol} size={20} label={null} />
<AxoSymbol.Icon symbol={symbol} size={18} label={null} />
</button>
);
}
@@ -186,17 +198,25 @@ export namespace AxoDialog {
*/
export type TitleProps = Readonly<{
screenReaderOnly?: boolean;
children: ReactNode;
}>;
export const Title: FC<TitleProps> = memo(props => {
const style = useMemo(() => {
return {
paddingInline: 24 - getPadding(10, false),
};
}, []);
return (
<Dialog.Title
className={tw(
'col-[title-slot]',
'col-[title-slot] py-0.5',
'truncate text-center',
'type-title-small text-label-primary'
'type-body-medium font-semibold text-label-primary',
props.screenReaderOnly && 'sr-only'
)}
style={style}
>
{props.children}
</Dialog.Title>
@@ -215,11 +235,8 @@ export namespace AxoDialog {
}>;
export const Back: FC<BackProps> = memo(props => {
const style = useMemo((): CSSProperties => {
return { marginInlineStart: DIALOG_HEADER_ICON_BUTTON_MARGIN };
}, []);
return (
<div className={tw('col-[back-slot] text-start')} style={style}>
<div className={tw('col-[back-slot] text-start')}>
<HeaderIconButton
label={props['aria-label']}
symbol="chevron-[start]"
@@ -240,11 +257,8 @@ export namespace AxoDialog {
}>;
export const Close: FC<CloseProps> = memo(props => {
const style = useMemo((): CSSProperties => {
return { marginInlineEnd: DIALOG_HEADER_ICON_BUTTON_MARGIN };
}, []);
return (
<div className={tw('col-[close-slot] text-end')} style={style}>
<div className={tw('col-[close-slot] text-end')}>
<Dialog.Close asChild>
<HeaderIconButton label={props['aria-label']} symbol="x" />
</Dialog.Close>
@@ -254,6 +268,23 @@ export namespace AxoDialog {
Close.displayName = `${Namespace}.Close`;
export type ExperimentalSearchProps = Readonly<{
children: ReactNode;
}>;
export const ExperimentalSearch: FC<ExperimentalSearchProps> = memo(props => {
const style = useMemo(() => {
return { paddingInline: getPadding(16, false) };
}, []);
return (
<div style={style} className={tw('pb-2')}>
{props.children}
</div>
);
});
ExperimentalSearch.displayName = `${Namespace}.ExperimentalSearch`;
/**
* Component: <AxoDialog.Body>
* ---------------------------
@@ -268,22 +299,18 @@ export namespace AxoDialog {
export const Body: FC<BodyProps> = memo(props => {
const { padding = 'normal' } = props;
const contentSize = useContentSize();
const contentSizeConfig = AxoBaseDialog.ContentSizes[contentSize];
const style = useMemo((): CSSProperties => {
return {
paddingInline:
padding === 'normal'
? DIALOG_PADDING_BEFORE_SCROLLBAR_WIDTH
: undefined,
paddingInline: padding === 'normal' ? getPadding(24, true) : undefined,
};
}, [padding]);
return (
<AxoScrollArea.Root
maxHeight={contentSizeConfig.maxBodyHeight}
maxHeight={440}
scrollbarWidth="thin"
scrollbarVisibility="as-needed"
>
<AxoScrollArea.Hint edge="top" />
<AxoScrollArea.Hint edge="bottom" />
@@ -325,13 +352,13 @@ export namespace AxoDialog {
export const Footer: FC<FooterProps> = memo(props => {
const style = useMemo((): CSSProperties => {
return {
paddingInline: DIALOG_PADDING_PLUS_SCROLLBAR_WIDTH,
paddingInline: getPadding(12, false),
};
}, []);
return (
<div
className={tw('flex flex-wrap items-center gap-3 py-3')}
className={tw('flex flex-wrap items-center gap-3 py-2.5')}
style={style}
>
{props.children}
@@ -351,6 +378,9 @@ export namespace AxoDialog {
}>;
export const FooterContent: FC<FooterContentProps> = memo(props => {
const style = useMemo(() => {
return { paddingInlineStart: 24 - getPadding(12, false) };
}, []);
return (
<div
className={tw(
@@ -364,6 +394,7 @@ export namespace AxoDialog {
'flex-grow',
'type-body-large text-label-primary'
)}
style={style}
>
{props.children}
</div>
@@ -422,7 +453,7 @@ export namespace AxoDialog {
variant={props.variant}
symbol={props.symbol}
arrow={props.arrow}
size="medium"
size="md"
width="grow"
>
{props.children}
+3 -3
View File
@@ -28,7 +28,7 @@ export function Basic(): JSX.Element {
<Container>
<AxoDropdownMenu.Root>
<AxoDropdownMenu.Trigger>
<AxoButton.Root variant="secondary" size="medium">
<AxoButton.Root variant="secondary" size="md">
Open Dropdown Menu
</AxoButton.Root>
</AxoDropdownMenu.Trigger>
@@ -114,7 +114,7 @@ export function WithHeader(): JSX.Element {
<Container>
<AxoDropdownMenu.Root>
<AxoDropdownMenu.Trigger>
<AxoButton.Root variant="secondary" size="medium">
<AxoButton.Root variant="secondary" size="md">
Open Dropdown Menu
</AxoButton.Root>
</AxoDropdownMenu.Trigger>
@@ -222,7 +222,7 @@ export function StressTestLongText(): JSX.Element {
<Container>
<AxoDropdownMenu.Root>
<AxoDropdownMenu.Trigger>
<AxoButton.Root variant="secondary" size="medium">
<AxoButton.Root variant="secondary" size="md">
Open Dropdown Menu
</AxoButton.Root>
</AxoDropdownMenu.Trigger>
+28 -3
View File
@@ -1,8 +1,9 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { memo, useId } from 'react';
import React, { memo, useEffect, useId, useRef } from 'react';
import { DropdownMenu } from 'radix-ui';
import type { FC, ReactNode } from 'react';
import { getRole, computeAccessibleName } from 'dom-accessibility-api';
import { AxoSymbol } from './AxoSymbol.dom.js';
import { AxoBaseMenu } from './_internal/AxoBaseMenu.dom.js';
import { tw } from './tw.dom.js';
@@ -11,6 +12,7 @@ import {
useAriaLabellingContext,
useCreateAriaLabellingContext,
} from './_internal/AriaLabellingContext.dom.js';
import { assert } from './_internal/assert.dom.js';
const Namespace = 'AxoDropdownMenu';
@@ -88,18 +90,41 @@ export namespace AxoDropdownMenu {
export type TriggerProps = AxoBaseMenu.MenuTriggerProps;
const triggerDisplayName = `${Namespace}.Trigger`;
/**
* The button that toggles the dropdown menu.
* By default, the {@link AxoDropdownMenu.Content} will position itself
* against the trigger.
*/
export const Trigger: FC<TriggerProps> = memo(props => {
const ref = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
assert(
ref.current instanceof HTMLElement,
`${triggerDisplayName} child must forward ref`
);
assert(
getRole(ref.current) === 'button',
`${triggerDisplayName} child must be a <button> or role=button`
);
assert(
computeAccessibleName(ref.current) !== '',
`${triggerDisplayName} child must have an accessible name`
);
}
});
return (
<DropdownMenu.Trigger asChild>{props.children}</DropdownMenu.Trigger>
<DropdownMenu.Trigger ref={ref} asChild>
{props.children}
</DropdownMenu.Trigger>
);
});
Trigger.displayName = `${Namespace}.Trigger`;
Trigger.displayName = triggerDisplayName;
/**
* Component: <AxoDropdownMenu.Content>
+1 -1
View File
@@ -100,7 +100,7 @@ export function Basic(): JSX.Element {
return (
<div className={tw('flex h-96 w-full items-center justify-center gap-8')}>
<Template renderer="AxoDropdownMenu">
<AxoButton.Root variant="secondary" size="medium">
<AxoButton.Root variant="secondary" size="md">
Open Dropdown Menu
</AxoButton.Root>
</Template>
+33 -13
View File
@@ -66,9 +66,12 @@ export namespace AxoScrollArea {
export type ScrollBehavior = 'auto' | 'smooth';
export type ScrollbarVisibility = 'auto' | 'as-needed';
type ScrollAreaConfig = Readonly<{
scrollbarWidth: ScrollbarWidth;
scrollbarGutter: ScrollbarGutter;
scrollbarVisibility: ScrollbarVisibility;
scrollBehavior: ScrollBehavior;
}>;
@@ -93,6 +96,7 @@ export namespace AxoScrollArea {
maxHeight?: number;
scrollbarWidth: ScrollbarWidth;
scrollbarGutter?: ScrollbarGutter;
scrollbarVisibility?: ScrollbarVisibility;
scrollBehavior?: ScrollBehavior;
children: ReactNode;
}>;
@@ -104,12 +108,18 @@ export namespace AxoScrollArea {
maxHeight,
scrollbarWidth = 'thin',
scrollbarGutter = 'stable-both-edges',
scrollbarVisibility = 'auto',
scrollBehavior = 'auto',
} = props;
const config = useMemo((): ScrollAreaConfig => {
return { scrollbarWidth, scrollbarGutter, scrollBehavior };
}, [scrollbarWidth, scrollbarGutter, scrollBehavior]);
return {
scrollbarWidth,
scrollbarGutter,
scrollbarVisibility,
scrollBehavior,
};
}, [scrollbarWidth, scrollbarGutter, scrollbarVisibility, scrollBehavior]);
const style = useMemo((): CSSProperties => {
return {
@@ -158,13 +168,6 @@ export namespace AxoScrollArea {
'outline-0'
);
// Note: Use "scroll" for `overflow-x` because scrollbar-gutter doesnt fix the space
const ViewportOrientations: Record<Orientation, TailwindStyles> = {
vertical: tw('overflow-x-hidden overflow-y-auto'),
horizontal: tw('overflow-x-scroll overflow-y-hidden'),
both: tw('overflow-x-scroll overflow-y-auto'),
};
const ViewportScrollbarWidths: Record<ScrollbarWidth, TailwindStyles> = {
wide: tw('scrollbar-width-auto'),
thin: tw('scrollbar-width-thin'),
@@ -177,6 +180,16 @@ export namespace AxoScrollArea {
'stable-both-edges': tw('scrollbar-gutter-stable'),
};
const ViewportScrollbarVisibilities: Record<
ScrollbarVisibility,
TailwindStyles
> = {
auto: tw(),
'as-needed': tw(
'transition-[scrollbar-color] duration-150 not-hover:not-focus-within:scrollbar-thumb-transparent'
),
};
const ViewportScrollBehaviors: Record<ScrollBehavior, TailwindStyles> = {
auto: tw('scroll-auto'),
smooth: tw('scroll-smooth'),
@@ -188,8 +201,12 @@ export namespace AxoScrollArea {
export const Viewport: FC<ViewportProps> = memo(props => {
const orientation = useAxoScrollAreaOrientation();
const { scrollbarWidth, scrollbarGutter, scrollBehavior } =
useAxoScrollAreaConfig();
const {
scrollbarWidth,
scrollbarGutter,
scrollbarVisibility,
scrollBehavior,
} = useAxoScrollAreaConfig();
const style = useMemo((): CSSProperties => {
const hasVerticalScrollbar = orientation !== 'horizontal';
@@ -239,9 +256,9 @@ export namespace AxoScrollArea {
data-axo-scroll-area-viewport
className={tw(
baseViewportStyles,
ViewportOrientations[orientation],
ViewportScrollbarWidths[scrollbarWidth],
ViewportScrollbarGutters[scrollbarGutter],
ViewportScrollbarVisibilities[scrollbarVisibility],
ViewportScrollBehaviors[scrollBehavior]
)}
style={style}
@@ -452,7 +469,10 @@ export namespace AxoScrollArea {
return (
<div
className={tw('flex size-full flex-col', AXO_MASK_CLASS_NAME)}
className={tw(
'flex size-full flex-col overflow-hidden',
AXO_MASK_CLASS_NAME
)}
style={style}
>
{props.children}
+3 -3
View File
@@ -84,7 +84,7 @@ export namespace AxoSelect {
*/
export type TriggerVariant = 'default' | 'floating' | 'borderless';
export type TriggerWidth = 'hug' | 'full';
export type TriggerWidth = 'fit' | 'full';
export type TriggerChevron = 'always' | 'on-hover';
const baseTriggerStyles = tw(
@@ -116,7 +116,7 @@ export namespace AxoSelect {
};
const TriggerWidths: Record<TriggerWidth, TailwindStyles> = {
hug: tw(),
fit: tw(),
full: tw('w-full'),
};
@@ -170,7 +170,7 @@ export namespace AxoSelect {
*/
export const Trigger: FC<TriggerProps> = memo(props => {
const variant = props.variant ?? 'default';
const width = props.width ?? 'hug';
const width = props.width ?? 'fit';
const chevron = props.chevron ?? 'always';
const variantStyles = TriggerVariants[variant];
const widthStyles = TriggerWidths[width];
+3 -2
View File
@@ -65,14 +65,15 @@ export namespace AxoSymbol {
*/
export type IconName = AxoSymbolIconName;
export type IconSize = 12 | 14 | 16 | 20 | 24 | 48;
export type IconSize = 12 | 14 | 16 | 18 | 20 | 24 | 48;
type IconSizeConfig = { size: number; fontSize: number };
const IconSizes: Record<IconSize, IconSizeConfig> = {
12: { size: 12, fontSize: 11 },
12: { size: 12, fontSize: 10 },
14: { size: 14, fontSize: 12 },
16: { size: 16, fontSize: 14 },
18: { size: 18, fontSize: 16 },
20: { size: 20, fontSize: 18 },
24: { size: 24, fontSize: 22 },
48: { size: 48, fontSize: 44 },
+2 -34
View File
@@ -1,10 +1,9 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { createContext, useCallback, useContext } from 'react';
import { useCallback } from 'react';
import type { ReactNode } from 'react';
import { tw } from '../tw.dom.js';
import { assert } from './assert.dom.js';
export namespace AxoBaseDialog {
/**
@@ -46,6 +45,7 @@ export namespace AxoBaseDialog {
*/
export const contentStyles = tw(
'relative',
'max-h-full min-h-fit max-w-full min-w-fit',
'rounded-3xl bg-elevated-background-primary shadow-elevation-3 select-none',
'outline-0 outline-border-focused focused:outline-[2.5px]',
@@ -53,21 +53,6 @@ export namespace AxoBaseDialog {
'animate-scale-98 animate-translate-y-1'
);
export type ContentSize = 'sm' | 'md' | 'lg';
export type ContentSizeConfig = Readonly<{
width: number;
minWidth: number;
maxBodyHeight: number;
}>;
// TODO: These sizes are not finalized
export const ContentSizes: Record<ContentSize, ContentSizeConfig> = {
sm: { width: 320, minWidth: 320, maxBodyHeight: 440 },
md: { width: 440, minWidth: 320, maxBodyHeight: 440 },
lg: { width: 560, minWidth: 440, maxBodyHeight: 440 },
};
export type ContentEscape = 'cancel-is-noop' | 'cancel-is-destructive';
export function useContentEscapeBehavior(
@@ -82,21 +67,4 @@ export namespace AxoBaseDialog {
[escape]
);
}
export type ContentProps = Readonly<{
escape: ContentEscape;
size: ContentSize;
children: ReactNode;
}>;
const ContentSizeContext = createContext<ContentSize | null>(null);
export const ContentSizeProvider = ContentSizeContext.Provider;
export function useContentSize(): ContentSize {
return assert(
useContext(ContentSizeContext),
'Must be wrapped with dialog <Content> component'
);
}
}
+2 -2
View File
@@ -28,7 +28,7 @@ export function AvatarModalButtons({
<Modal.ButtonFooter>
<AxoButton.Root
variant="secondary"
size="large"
size="lg"
onClick={() => {
if (hasChanges) {
setConfirmDiscardAction(() => onCancel);
@@ -41,7 +41,7 @@ export function AvatarModalButtons({
</AxoButton.Root>
<AxoButton.Root
variant="primary"
size="large"
size="lg"
disabled={!hasChanges}
onClick={onSave}
>
+5 -5
View File
@@ -753,17 +753,17 @@ export const CompositionArea = memo(function CompositionArea({
attButton = (
<div className="CompositionArea__button-cell">
<AxoDropdownMenu.Root>
<AxoDropdownMenu.Trigger>
<div className={tw('flex h-8 items-center')}>
<div className={tw('flex h-8 items-center')}>
<AxoDropdownMenu.Trigger>
<AxoButton.Root
variant="borderless-secondary"
size="small"
size="sm"
aria-label={i18n('icu:CompositionArea--attach-plus')}
>
<AxoSymbol.Icon label={null} symbol="plus" size={20} />
</AxoButton.Root>
</div>
</AxoDropdownMenu.Trigger>
</AxoDropdownMenu.Trigger>
</div>
<AxoDropdownMenu.Content>
<AxoDropdownMenu.Item symbol="photo" onSelect={launchMediaPicker}>
{i18n('icu:CompositionArea__AttachMenu__PhotosAndVideos')}
@@ -145,7 +145,7 @@ const Template: StoryFn<Props> = args => {
return (
<AxoDropdownMenu.Root>
<AxoDropdownMenu.Trigger>
<AxoButton.Root size="medium" variant="secondary">
<AxoButton.Root size="md" variant="secondary">
Open
</AxoButton.Root>
</AxoDropdownMenu.Trigger>
+2 -2
View File
@@ -375,12 +375,12 @@ export function PollCreateModal({
</div>
<div className={tw('mt-3 flex justify-end gap-3')}>
<AxoButton.Root variant="secondary" size="large" onClick={onClose}>
<AxoButton.Root variant="secondary" size="lg" onClick={onClose}>
{i18n('icu:cancel')}
</AxoButton.Root>
<AxoButton.Root
variant="primary"
size="large"
size="lg"
onClick={handleSend}
disabled={!allowSend}
>
+9 -9
View File
@@ -949,14 +949,14 @@ export function Preferences({
<>
<AxoButton.Root
variant="secondary"
size="large"
size="lg"
onClick={closeLanguageDialog}
>
{i18n('icu:cancel')}
</AxoButton.Root>
<AxoButton.Root
variant="primary"
size="large"
size="lg"
disabled={selectedLanguageLocale === localeOverride}
onClick={() => {
setLanguageDialog(LanguageDialog.Confirmation);
@@ -1210,7 +1210,7 @@ export function Preferences({
}
right={
<AxoButton.Root
size="large"
size="lg"
variant="secondary"
onClick={() => {
setSettingsLocation({
@@ -1256,7 +1256,7 @@ export function Preferences({
<div className="Preferences__right-button">
<AxoButton.Root
variant="secondary"
size="large"
size="lg"
disabled={nowSyncing}
experimentalSpinner={
nowSyncing ? { 'aria-label': i18n('icu:syncing') } : null
@@ -1556,7 +1556,7 @@ export function Preferences({
right={
<AxoButton.Root
variant="secondary"
size="large"
size="lg"
onClick={() =>
setSettingsLocation({
page: SettingsPage.NotificationProfilesHome,
@@ -1606,7 +1606,7 @@ export function Preferences({
>
<AxoButton.Root
variant="secondary"
size="large"
size="lg"
onClick={() => setSettingsLocation({ page: SettingsPage.PNP })}
>
{i18n('icu:Preferences__pnp__row--button')}
@@ -1765,7 +1765,7 @@ export function Preferences({
<AxoButton.Root
onClick={() => onHasStoriesDisabledChanged(false)}
variant="secondary"
size="large"
size="lg"
>
{i18n('icu:Preferences__turn-stories-on')}
</AxoButton.Root>
@@ -1773,7 +1773,7 @@ export function Preferences({
<AxoButton.Root
onClick={() => setConfirmStoriesOff(true)}
variant="subtle-destructive"
size="large"
size="lg"
>
{i18n('icu:Preferences__turn-stories-off')}
</AxoButton.Root>
@@ -1805,7 +1805,7 @@ export function Preferences({
>
<AxoButton.Root
variant="subtle-destructive"
size="large"
size="lg"
onClick={() => setConfirmDelete(true)}
>
{i18n('icu:clearDataButton')}
+2 -2
View File
@@ -233,7 +233,7 @@ export function PreferencesBackups({
>
<AxoButton.Root
variant="secondary"
size="large"
size="lg"
onClick={() =>
setSettingsLocation({ page: SettingsPage.BackupsDetails })
}
@@ -277,7 +277,7 @@ export function PreferencesBackups({
>
<AxoButton.Root
variant="secondary"
size="large"
size="lg"
disabled={isAuthPending}
onClick={async () => {
setAuthError(undefined);
+2 -2
View File
@@ -518,7 +518,7 @@ function AmountPicker({
const continueButton = (
<AxoButton.Root
variant={isOnline ? 'primary' : 'secondary'}
size="large"
size="lg"
disabled={!isContinueEnabled}
onClick={handleContinueClicked}
>
@@ -763,7 +763,7 @@ function CardForm({
disabled={isDonateDisabled}
onClick={handleDonateClicked}
variant={isOnline ? 'primary' : 'secondary'}
size="large"
size="lg"
>
{i18n('icu:PreferencesDonations__donate-button-with-amount', {
formattedCurrencyAmount,
+1 -1
View File
@@ -239,7 +239,7 @@ function DonationsHome({
<span className={tw('mb-8')}>
<AxoButton.Root
variant={isOnline ? 'primary' : 'secondary'}
size="medium"
size="md"
disabled={!isOnline}
onClick={handleDonateButtonClicked}
>
+6 -6
View File
@@ -241,7 +241,7 @@ export function PreferencesInternal({
>
<AxoButton.Root
variant="secondary"
size="large"
size="lg"
onClick={validateBackup}
disabled={isValidationPending}
experimentalSpinner={
@@ -277,7 +277,7 @@ export function PreferencesInternal({
>
<AxoButton.Root
variant="secondary"
size="large"
size="lg"
onClick={exportLocalBackup}
disabled={isExportPending}
experimentalSpinner={
@@ -309,7 +309,7 @@ export function PreferencesInternal({
>
<AxoButton.Root
variant="secondary"
size="large"
size="lg"
onClick={async () => {
setMessageCountBySchemaVersion(
await getMessageCountBySchemaVersion()
@@ -404,7 +404,7 @@ export function PreferencesInternal({
>
<AxoButton.Root
variant="secondary"
size="large"
size="lg"
onClick={handleAddTestReceipt}
>
Add Test Receipt
@@ -455,7 +455,7 @@ export function PreferencesInternal({
<td style={{ padding: '8px' }}>
<AxoButton.Root
variant="secondary"
size="large"
size="lg"
onClick={() => handleGenerateReceipt(receipt)}
disabled={isGeneratingReceipt}
experimentalSpinner={
@@ -493,7 +493,7 @@ export function PreferencesInternal({
/>
<AxoButton.Root
variant="destructive"
size="large"
size="lg"
onClick={handleReadOnlySqlInputSubmit}
>
Run Query
+9 -13
View File
@@ -132,7 +132,7 @@ export function PreferencesLocalBackups({
>
<AxoButton.Root
variant="secondary"
size="large"
size="lg"
onClick={pickLocalBackupFolder}
>
{i18n('icu:Preferences__local-backups-folder__change')}
@@ -157,7 +157,7 @@ export function PreferencesLocalBackups({
>
<AxoButton.Root
variant="secondary"
size="large"
size="lg"
disabled={isAuthPending}
experimentalSpinner={
isAuthPending ? { 'aria-label': i18n('icu:loading') } : null
@@ -233,7 +233,7 @@ function LocalBackupsSetupFolderPicker({
</div>
<AxoButton.Root
variant="primary"
size="large"
size="lg"
onClick={pickLocalBackupFolder}
>
{i18n('icu:Preferences__button--choose-folder')}
@@ -306,7 +306,7 @@ function LocalBackupsBackupKeyViewer({
footerRight = (
<AxoButton.Root
variant="primary"
size="large"
size="lg"
onClick={() => setStep('confirm')}
>
{i18n('icu:Preferences--local-backups-setup-next')}
@@ -314,11 +314,7 @@ function LocalBackupsBackupKeyViewer({
);
} else {
footerRight = (
<AxoButton.Root
variant="primary"
size="large"
onClick={onBackupKeyViewed}
>
<AxoButton.Root variant="primary" size="lg" onClick={onBackupKeyViewed}>
{i18n('icu:Preferences--local-backups-view-backup-key-done')}
</AxoButton.Root>
);
@@ -331,7 +327,7 @@ function LocalBackupsBackupKeyViewer({
footerLeft = (
<AxoButton.Root
variant="borderless-primary"
size="large"
size="lg"
onClick={() => setStep('view')}
>
{i18n('icu:Preferences--local-backups-see-backup-key-again')}
@@ -340,7 +336,7 @@ function LocalBackupsBackupKeyViewer({
footerRight = (
<AxoButton.Root
variant="primary"
size="large"
size="lg"
disabled={!isBackupKeyConfirmed}
onClick={() => setStep('caution')}
>
@@ -359,7 +355,7 @@ function LocalBackupsBackupKeyViewer({
modalFooter={
<AxoButton.Root
variant="primary"
size="large"
size="lg"
onClick={onBackupKeyViewed}
>
{i18n(
@@ -403,7 +399,7 @@ function LocalBackupsBackupKeyViewer({
<div className="Preferences--LocalBackupsSetupScreenPaneContent">
<AxoButton.Root
variant="secondary"
size="small"
size="sm"
symbol="copy"
onClick={onCopyBackupKey}
>
@@ -669,7 +669,7 @@ function NotificationProfilesOnboardingDialog({
<p className={tw('mt-4 mb-12 max-w-[340px] text-center leading-5')}>
{i18n('icu:NotificationProfiles--setup-description')}
</p>
<AxoButton.Root variant="primary" onClick={onDismiss} size="large">
<AxoButton.Root variant="primary" onClick={onDismiss} size="lg">
{i18n('icu:NotificationProfiles--setup-continue')}
</AxoButton.Root>
</div>
@@ -836,7 +836,7 @@ function NotificationProfilesNamePage({
<ButtonContainer>
<AxoButton.Root
variant="primary"
size="large"
size="lg"
type="button"
form="notificationProfileName"
disabled={!isValid}
@@ -913,7 +913,7 @@ function NotificationProfilesAllowedPage({
onNext();
}}
>
<AxoButton.Root variant="primary" size="large" type="submit">
<AxoButton.Root variant="primary" size="lg" type="submit">
{i18n('icu:next2')}
</AxoButton.Root>
</form>
@@ -1078,7 +1078,7 @@ function NotificationProfilesSchedulePage({
<ButtonContainer>
<AxoButton.Root
variant="primary"
size="large"
size="lg"
type="button"
onClick={onNext}
>
@@ -1113,7 +1113,7 @@ function NotificationProfilesDonePage({
</p>
<AxoButton.Root
variant="primary"
size="large"
size="lg"
type="button"
onClick={onNext}
>
@@ -1408,7 +1408,7 @@ function NotificationProfilesEditPage({
<ButtonContainer>
<AxoButton.Root
variant="primary"
size="large"
size="lg"
type="button"
onClick={onBack}
>
+5 -5
View File
@@ -408,12 +408,12 @@ export function ProfileEditor({
value={stagedProfile.familyName}
/>
<div className="ProfileEditor__button-footer">
<AxoButton.Root variant="secondary" size="large" onClick={handleBack}>
<AxoButton.Root variant="secondary" size="lg" onClick={handleBack}>
{i18n('icu:cancel')}
</AxoButton.Root>
<AxoButton.Root
variant="primary"
size="large"
size="lg"
disabled={shouldDisableSave}
onClick={() => {
if (!stagedProfile.firstName) {
@@ -516,12 +516,12 @@ export function ProfileEditor({
})}
<div className="ProfileEditor__button-footer">
<AxoButton.Root variant="secondary" size="large" onClick={handleBack}>
<AxoButton.Root variant="secondary" size="lg" onClick={handleBack}>
{i18n('icu:cancel')}
</AxoButton.Root>
<AxoButton.Root
variant="primary"
size="large"
size="lg"
disabled={shouldDisableSave}
onClick={() => {
setFullBio({
@@ -724,7 +724,7 @@ export function ProfileEditor({
setEditState(ProfileEditorPage.BetterAvatar);
}}
variant="secondary"
size="small"
size="sm"
>
{i18n('icu:ProfileEditor--edit-photo')}
</AxoButton.Root>
+2 -2
View File
@@ -384,7 +384,7 @@ export function UsernameEditor({
<div className="UsernameEditor__button-footer">
<AxoButton.Root
variant="secondary"
size="large"
size="lg"
disabled={isConfirming}
onClick={onCancel}
>
@@ -392,7 +392,7 @@ export function UsernameEditor({
</AxoButton.Root>
<AxoButton.Root
variant="primary"
size="large"
size="lg"
disabled={!canSave}
onClick={onSave}
experimentalSpinner={
@@ -245,7 +245,7 @@ export function PollMessageContents({
{totalVotes > 0 ? (
<div className={tw('mt-4 flex justify-center scheme-light')}>
<AxoButton.Root
size="medium"
size="md"
variant="floating-secondary"
onClick={() => setShowVotesModal(true)}
>
@@ -165,7 +165,7 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
</h3>
<AxoButton.Root
variant="floating-secondary"
size="medium"
size="md"
onClick={() => {
changeLocation({
tab: NavTab.Settings,
@@ -12,7 +12,7 @@ export function DeleteChatFolderDialog(props: {
onConfirm: () => void;
}): JSX.Element {
return (
<AxoAlertDialog.Content size="sm" escape="cancel-is-noop">
<AxoAlertDialog.Content escape="cancel-is-noop">
<AxoAlertDialog.Body>
<AxoAlertDialog.Title>{props.title}</AxoAlertDialog.Title>
<AxoAlertDialog.Description>
@@ -393,7 +393,7 @@ function ChatFolderPresetItem(props: ChatFolderPresetItemProps) {
<ItemDescription>{props.config.description}</ItemDescription>
</ItemBody>
<AxoButton.Root
size="medium"
size="md"
variant="secondary"
onClick={handleCreateChatFolder}
>
@@ -550,14 +550,14 @@ export function PreferencesEditChatFolderPage(
actions={
<>
<AxoButton.Root
size="large"
size="lg"
variant="secondary"
onClick={handleDiscardAndBack}
>
{i18n('icu:Preferences__EditChatFolderPage__CancelButton')}
</AxoButton.Root>
<AxoButton.Root
size="large"
size="lg"
variant="primary"
onClick={handleSaveChangesAndBack}
disabled={!(isChanged && isValid)}
+7
View File
@@ -825,6 +825,13 @@
"reasonCategory": "usageTrusted",
"updated": "2025-08-28T23:36:44.974Z"
},
{
"rule": "React-useRef",
"path": "ts/axo/AxoDropdownMenu.dom.tsx",
"line": " const ref = useRef<HTMLButtonElement>(null);",
"reasonCategory": "usageTrusted",
"updated": "2025-11-05T21:09:43.372Z"
},
{
"rule": "React-useRef",
"path": "ts/calling/useGetCallingFrameBuffer.std.ts",