mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-19 17:58:48 +00:00
Init AxoIconButton
This commit is contained in:
@@ -11,6 +11,8 @@ import { SpinnerV2 } from '../components/SpinnerV2.dom.js';
|
||||
|
||||
const Namespace = 'AxoButton';
|
||||
|
||||
type GenericButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
const baseAxoButtonStyles = tw(
|
||||
'relative inline-flex max-w-full items-center-safe justify-center-safe rounded-full select-none',
|
||||
'outline-0 outline-border-focused focused:outline-[2.5px]',
|
||||
@@ -133,11 +135,6 @@ const AxoButtonSizes = {
|
||||
sm: tw('min-w-12 px-2 py-1 type-body-small font-medium'),
|
||||
} as const satisfies Record<string, TailwindStyles>;
|
||||
|
||||
type BaseButtonAttrs = Omit<
|
||||
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
'className' | 'style' | 'children'
|
||||
>;
|
||||
|
||||
type AxoButtonVariant = keyof typeof AxoButtonVariants;
|
||||
type AxoButtonSize = keyof typeof AxoButtonSizes;
|
||||
|
||||
@@ -215,16 +212,19 @@ export namespace AxoButton {
|
||||
full: tw('w-full'),
|
||||
};
|
||||
|
||||
export type RootProps = BaseButtonAttrs &
|
||||
Readonly<{
|
||||
variant: AxoButtonVariant;
|
||||
size: AxoButtonSize;
|
||||
width?: Width;
|
||||
symbol?: AxoSymbol.InlineGlyphName;
|
||||
arrow?: boolean;
|
||||
experimentalSpinner?: { 'aria-label': string } | null;
|
||||
children: ReactNode;
|
||||
}>;
|
||||
export type RootProps = Readonly<{
|
||||
variant: AxoButtonVariant;
|
||||
size: AxoButtonSize;
|
||||
width?: Width;
|
||||
symbol?: AxoSymbol.InlineGlyphName;
|
||||
arrow?: boolean;
|
||||
experimentalSpinner?: { 'aria-label': string } | null;
|
||||
disabled?: GenericButtonProps['disabled'];
|
||||
onClick?: GenericButtonProps['onClick'];
|
||||
children: ReactNode;
|
||||
// Note: Technically we forward all props for Radix, but we restrict the
|
||||
// props that the type accepts
|
||||
}>;
|
||||
|
||||
export const Root: FC<RootProps> = memo(
|
||||
forwardRef((props, ref: ForwardedRef<HTMLButtonElement>) => {
|
||||
@@ -251,9 +251,9 @@ export namespace AxoButton {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
{...rest}
|
||||
type="button"
|
||||
className={tw(variantStyles, sizeStyles, widthStyles)}
|
||||
{...rest}
|
||||
>
|
||||
<span
|
||||
className={tw(
|
||||
|
||||
@@ -41,6 +41,7 @@ function Template(props: {
|
||||
back?: boolean;
|
||||
contentSize: AxoDialog.ContentSize;
|
||||
bodyPadding?: AxoDialog.BodyPadding;
|
||||
iconAction?: boolean;
|
||||
footerContent?: ReactNode;
|
||||
children: ReactNode;
|
||||
}): JSX.Element {
|
||||
@@ -68,12 +69,26 @@ function Template(props: {
|
||||
</AxoDialog.FooterContent>
|
||||
)}
|
||||
<AxoDialog.Actions>
|
||||
<AxoDialog.Action variant="secondary" onClick={action('onCancel')}>
|
||||
Cancel
|
||||
</AxoDialog.Action>
|
||||
<AxoDialog.Action variant="primary" onClick={action('onSave')}>
|
||||
Save
|
||||
</AxoDialog.Action>
|
||||
{props.iconAction ? (
|
||||
<AxoDialog.IconAction
|
||||
aria-label="Send"
|
||||
variant="primary"
|
||||
symbol="send-fill"
|
||||
onClick={action('onSend')}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<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>
|
||||
@@ -102,6 +117,14 @@ export function Large(): JSX.Element {
|
||||
return <Template contentSize="lg">{TEXT_LONG}</Template>;
|
||||
}
|
||||
|
||||
export function IconAction(): JSX.Element {
|
||||
return (
|
||||
<Template contentSize="sm" iconAction>
|
||||
{TEXT_SHORT}
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
export function LongContent(): JSX.Element {
|
||||
return (
|
||||
<Template contentSize="md">
|
||||
@@ -166,7 +189,8 @@ function TextInputField(props: { placeholder: string }) {
|
||||
'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'
|
||||
'placeholder:text-label-placeholder',
|
||||
'forced-colors:border forced-colors:border-[ButtonBorder] forced-colors:bg-[ButtonFace] forced-colors:text-[ButtonText]'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -193,7 +217,10 @@ export function ExampleNicknameAndNoteDialog(): JSX.Element {
|
||||
encrypted. They are only visible to you.
|
||||
</p>
|
||||
<div
|
||||
className={tw('mx-auto size-20 rounded-full bg-color-fill-primary')}
|
||||
className={tw(
|
||||
'mx-auto size-20 rounded-full bg-color-fill-primary',
|
||||
'forced-colors:border'
|
||||
)}
|
||||
/>
|
||||
<Spacer height={12} />
|
||||
<TextInputField placeholder="First name" />
|
||||
@@ -331,7 +358,10 @@ export function ExampleLanguageDialog(): JSX.Element {
|
||||
// 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]')}
|
||||
className={tw(
|
||||
'w-full rounded-lg bg-fill-secondary px-3 py-[5px]',
|
||||
'forced-colors:border forced-colors:border-[ButtonBorder] forced-colors:bg-[ButtonFace] forced-colors:text-[ButtonText]'
|
||||
)}
|
||||
/>
|
||||
</AxoDialog.ExperimentalSearch>
|
||||
<AxoDialog.Body padding="only-scrollbar-gutter">
|
||||
|
||||
@@ -16,6 +16,7 @@ import { tw } from './tw.dom.js';
|
||||
import { AxoScrollArea } from './AxoScrollArea.dom.js';
|
||||
import { getScrollbarGutters } from './_internal/scrollbars.dom.js';
|
||||
import { AxoButton } from './AxoButton.dom.js';
|
||||
import { AxoIconButton } from './AxoIconButton.dom.js';
|
||||
|
||||
const Namespace = 'AxoDialog';
|
||||
|
||||
@@ -237,9 +238,11 @@ export namespace AxoDialog {
|
||||
export const Back: FC<BackProps> = memo(props => {
|
||||
return (
|
||||
<div className={tw('col-[back-slot] text-start')}>
|
||||
<HeaderIconButton
|
||||
label={props['aria-label']}
|
||||
<AxoIconButton.Root
|
||||
size="sm"
|
||||
variant="borderless-secondary"
|
||||
symbol="chevron-[start]"
|
||||
aria-label={props['aria-label']}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -260,7 +263,12 @@ export namespace AxoDialog {
|
||||
return (
|
||||
<div className={tw('col-[close-slot] text-end')}>
|
||||
<Dialog.Close asChild>
|
||||
<HeaderIconButton label={props['aria-label']} symbol="x" />
|
||||
<AxoIconButton.Root
|
||||
size="sm"
|
||||
variant="borderless-secondary"
|
||||
symbol="x"
|
||||
aria-label={props['aria-label']}
|
||||
/>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
);
|
||||
@@ -462,4 +470,31 @@ export namespace AxoDialog {
|
||||
});
|
||||
|
||||
Action.displayName = `${Namespace}.Action`;
|
||||
|
||||
/**
|
||||
* Component: <AxoDialog.Actions>
|
||||
* ------------------------------
|
||||
*/
|
||||
|
||||
export type IconActionVariant = 'primary' | 'destructive' | 'secondary';
|
||||
|
||||
export type IconActionProps = Readonly<{
|
||||
'aria-label': string;
|
||||
variant: ActionVariant;
|
||||
symbol: AxoSymbol.IconName;
|
||||
onClick: () => void;
|
||||
}>;
|
||||
|
||||
export const IconAction: FC<IconActionProps> = memo(props => {
|
||||
return (
|
||||
<AxoIconButton.Root
|
||||
aria-label={props['aria-label']}
|
||||
variant={props.variant}
|
||||
size="md"
|
||||
symbol={props.symbol}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
IconAction.displayName = `${Namespace}.IconAction`;
|
||||
}
|
||||
|
||||
224
ts/axo/AxoIconButton.dom.stories.tsx
Normal file
224
ts/axo/AxoIconButton.dom.stories.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { Fragment } from 'react';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { AxoIconButton } from './AxoIconButton.dom.js';
|
||||
import type { TailwindStyles } from './tw.dom.js';
|
||||
import { tw } from './tw.dom.js';
|
||||
|
||||
export default {
|
||||
title: 'Axo/AxoIconButton',
|
||||
} satisfies Meta;
|
||||
|
||||
export function Basic(): JSX.Element {
|
||||
return (
|
||||
<AxoIconButton.Root
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
symbol="more"
|
||||
aria-label="More"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const Backgrounds: Record<string, TailwindStyles> = {
|
||||
'background-primary': tw('bg-background-primary'),
|
||||
'background-secondary': tw('bg-background-secondary'),
|
||||
'background-overlay': tw('bg-background-overlay'),
|
||||
'elevated-background-primary': tw('bg-elevated-background-primary'),
|
||||
'elevated-background-secondary': tw('bg-elevated-background-secondary'),
|
||||
'elevated-background-tertiary': tw('bg-elevated-background-tertiary'),
|
||||
'elevated-background-quaternary': tw('bg-elevated-background-quaternary'),
|
||||
};
|
||||
|
||||
const Themes: Record<string, TailwindStyles> = {
|
||||
light: tw('scheme-only-light'),
|
||||
dark: tw('scheme-only-dark'),
|
||||
};
|
||||
|
||||
function getRows() {
|
||||
return Object.keys(Themes).flatMap(theme => {
|
||||
return Object.keys(Backgrounds).map(background => {
|
||||
return { theme, background };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function Variants(): JSX.Element {
|
||||
const variants = AxoIconButton._getAllVariants();
|
||||
return (
|
||||
<div className={tw('grid min-w-full')}>
|
||||
{getRows().map((row, rowIndex) => {
|
||||
return (
|
||||
<Fragment key={rowIndex}>
|
||||
<div
|
||||
className={tw('flex items-center p-2 text-end')}
|
||||
style={{
|
||||
gridRow: rowIndex + 1,
|
||||
gridColumn: 1,
|
||||
}}
|
||||
>
|
||||
<code>
|
||||
{row.background} ({row.theme})
|
||||
</code>
|
||||
</div>
|
||||
{variants.map((variant, variantIndex) => {
|
||||
return (
|
||||
<div
|
||||
key={variant}
|
||||
className={tw(
|
||||
'p-2 text-center',
|
||||
Themes[row.theme],
|
||||
Backgrounds[row.background]
|
||||
)}
|
||||
style={{
|
||||
gridRow: rowIndex + 1,
|
||||
gridColumn: variantIndex + 2,
|
||||
}}
|
||||
>
|
||||
<AxoIconButton.Root
|
||||
variant={variant}
|
||||
size="lg"
|
||||
symbol="more"
|
||||
aria-label="More"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Sizes(): JSX.Element {
|
||||
return (
|
||||
<div className={tw('grid min-w-full')}>
|
||||
{AxoIconButton._getAllSizes().map((size, sizeIndex) => {
|
||||
return (
|
||||
<Fragment key={size}>
|
||||
<div
|
||||
className={tw('flex items-center p-2 text-end')}
|
||||
style={{
|
||||
gridRow: sizeIndex + 1,
|
||||
gridColumn: 1,
|
||||
}}
|
||||
>
|
||||
<code>{size}</code>
|
||||
</div>
|
||||
{AxoIconButton._getAllVariants().map((variant, variantIndex) => {
|
||||
return (
|
||||
<div
|
||||
key={variant}
|
||||
className={tw('p-2 text-center')}
|
||||
style={{
|
||||
gridRow: sizeIndex + 1,
|
||||
gridColumn: variantIndex + 2,
|
||||
}}
|
||||
>
|
||||
<AxoIconButton.Root
|
||||
variant={variant}
|
||||
size={size}
|
||||
symbol="more"
|
||||
aria-label="More"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const AllStates: Record<string, Partial<AxoIconButton.RootProps>> = {
|
||||
'disabled=true': { disabled: true },
|
||||
'aria-pressed=false': { 'aria-pressed': false },
|
||||
'aria-pressed=true': { 'aria-pressed': true },
|
||||
};
|
||||
|
||||
export function States(): JSX.Element {
|
||||
return (
|
||||
<div className={tw('grid min-w-full')}>
|
||||
{Object.keys(AllStates).map((state, stateIndex) => {
|
||||
return (
|
||||
<Fragment key={stateIndex}>
|
||||
<div
|
||||
className={tw('flex items-center p-2 text-end')}
|
||||
style={{
|
||||
gridRow: stateIndex + 1,
|
||||
gridColumn: 1,
|
||||
}}
|
||||
>
|
||||
<code>{state}</code>
|
||||
</div>
|
||||
{AxoIconButton._getAllVariants().map((variant, variantIndex) => {
|
||||
return (
|
||||
<div
|
||||
key={variant}
|
||||
className={tw('p-2 text-center')}
|
||||
style={{
|
||||
gridRow: stateIndex + 1,
|
||||
gridColumn: variantIndex + 2,
|
||||
}}
|
||||
>
|
||||
<AxoIconButton.Root
|
||||
variant={variant}
|
||||
size="lg"
|
||||
symbol="more"
|
||||
aria-label="More"
|
||||
{...AllStates[state]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Spinners(): JSX.Element {
|
||||
return (
|
||||
<div className={tw('grid min-w-full')}>
|
||||
{AxoIconButton._getAllSizes().map((size, sizeIndex) => {
|
||||
return (
|
||||
<Fragment key={size}>
|
||||
<div
|
||||
className={tw('flex items-center p-2 text-end')}
|
||||
style={{
|
||||
gridRow: sizeIndex + 1,
|
||||
gridColumn: 1,
|
||||
}}
|
||||
>
|
||||
<code>{size}</code>
|
||||
</div>
|
||||
{AxoIconButton._getAllVariants().map((variant, variantIndex) => {
|
||||
return (
|
||||
<div
|
||||
key={variant}
|
||||
className={tw('p-2 text-center')}
|
||||
style={{
|
||||
gridRow: sizeIndex + 1,
|
||||
gridColumn: variantIndex + 2,
|
||||
}}
|
||||
>
|
||||
<AxoIconButton.Root
|
||||
variant={variant}
|
||||
size={size}
|
||||
symbol="more"
|
||||
aria-label="More"
|
||||
experimentalSpinner={{ 'aria-label': 'Loading' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
196
ts/axo/AxoIconButton.dom.tsx
Normal file
196
ts/axo/AxoIconButton.dom.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ButtonHTMLAttributes, FC, ForwardedRef } from 'react';
|
||||
import React, { forwardRef, memo } from 'react';
|
||||
import { AxoSymbol } from './AxoSymbol.dom.js';
|
||||
import type { TailwindStyles } from './tw.dom.js';
|
||||
import { tw } from './tw.dom.js';
|
||||
import type { SpinnerVariant } from '../components/SpinnerV2.dom.js';
|
||||
import { SpinnerV2 } from '../components/SpinnerV2.dom.js';
|
||||
|
||||
const Namespace = 'AxoIconButton';
|
||||
|
||||
type GenericButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
export namespace AxoIconButton {
|
||||
const baseStyles = tw(
|
||||
'relative rounded-full align-top leading-none select-none',
|
||||
'outline-border-focused not-forced-colors:outline-0 not-forced-colors:focused:outline-[2.5px]',
|
||||
'forced-colors:border forced-colors:border-[ButtonBorder] forced-colors:bg-[ButtonFace] forced-colors:text-[ButtonText]',
|
||||
'forced-colors:disabled:text-[GrayText]',
|
||||
'forced-colors:aria-pressed:bg-[SelectedItem] forced-colors:aria-pressed:text-[SelectedItemText]'
|
||||
);
|
||||
|
||||
const pressedStyles = {
|
||||
fillInverted: tw(
|
||||
'aria-pressed:bg-fill-inverted aria-pressed:pressed:bg-fill-inverted-pressed',
|
||||
'aria-pressed:text-label-primary-inverted aria-pressed:disabled:text-label-disabled-inverted'
|
||||
),
|
||||
colorFillPrimary: tw(
|
||||
'aria-pressed:bg-color-fill-primary aria-pressed:pressed:bg-color-fill-primary-pressed',
|
||||
'aria-pressed:text-label-primary-on-color aria-pressed:disabled:text-label-disabled-on-color'
|
||||
),
|
||||
};
|
||||
|
||||
const Variants: Record<Variant, TailwindStyles> = {
|
||||
secondary: tw(
|
||||
'bg-fill-secondary pressed:bg-fill-secondary-pressed',
|
||||
'text-label-primary disabled:text-label-disabled',
|
||||
pressedStyles.fillInverted
|
||||
),
|
||||
primary: tw(
|
||||
'bg-color-fill-primary pressed:bg-color-fill-primary-pressed',
|
||||
'text-label-primary-on-color disabled:text-label-disabled-on-color',
|
||||
pressedStyles.fillInverted
|
||||
),
|
||||
affirmative: tw(
|
||||
'bg-color-fill-affirmative pressed:bg-color-fill-affirmative-pressed',
|
||||
'text-label-primary-on-color disabled:text-label-disabled-on-color',
|
||||
pressedStyles.fillInverted
|
||||
),
|
||||
destructive: tw(
|
||||
'bg-color-fill-destructive pressed:bg-color-fill-destructive-pressed',
|
||||
'text-label-primary-on-color disabled:text-label-disabled-on-color',
|
||||
pressedStyles.fillInverted
|
||||
),
|
||||
'borderless-secondary': tw(
|
||||
'hovered:bg-fill-secondary pressed:bg-fill-secondary-pressed',
|
||||
'text-label-primary disabled:text-label-disabled',
|
||||
pressedStyles.colorFillPrimary
|
||||
),
|
||||
'floating-secondary': tw(
|
||||
'bg-fill-floating pressed:bg-fill-floating-pressed',
|
||||
'text-label-primary disabled:text-label-disabled',
|
||||
'shadow-elevation-1',
|
||||
pressedStyles.fillInverted
|
||||
),
|
||||
};
|
||||
|
||||
export function _getAllVariants(): ReadonlyArray<Variant> {
|
||||
return Object.keys(Variants) as ReadonlyArray<Variant>;
|
||||
}
|
||||
|
||||
type SizeConfig = Readonly<{
|
||||
buttonStyles: TailwindStyles;
|
||||
iconSize: AxoSymbol.IconSize;
|
||||
}>;
|
||||
|
||||
const Sizes: Record<Size, SizeConfig> = {
|
||||
sm: { buttonStyles: tw('p-[5px]'), iconSize: 18 },
|
||||
md: { buttonStyles: tw('p-1.5'), iconSize: 20 },
|
||||
lg: { buttonStyles: tw('p-2'), iconSize: 20 },
|
||||
};
|
||||
|
||||
export function _getAllSizes(): ReadonlyArray<Size> {
|
||||
return Object.keys(Sizes) as ReadonlyArray<Size>;
|
||||
}
|
||||
|
||||
export type Variant =
|
||||
| 'secondary'
|
||||
| 'primary'
|
||||
| 'affirmative'
|
||||
| 'destructive'
|
||||
| 'borderless-secondary'
|
||||
| 'floating-secondary';
|
||||
|
||||
export type Size = 'sm' | 'md' | 'lg';
|
||||
|
||||
export type RootProps = Readonly<{
|
||||
// required: Should describe the purpose of the button, not the icon.
|
||||
'aria-label': string;
|
||||
|
||||
variant: Variant;
|
||||
size: Size;
|
||||
symbol: AxoSymbol.IconName;
|
||||
|
||||
experimentalSpinner?: { 'aria-label': string } | null;
|
||||
|
||||
disabled?: GenericButtonProps['disabled'];
|
||||
onClick?: GenericButtonProps['onClick'];
|
||||
'aria-pressed'?: GenericButtonProps['aria-pressed'];
|
||||
// Note: Technically we forward all props for Radix, but we restrict the
|
||||
// props that the type accepts
|
||||
}>;
|
||||
|
||||
export const Root: FC<RootProps> = memo(
|
||||
forwardRef((props, ref: ForwardedRef<HTMLButtonElement>) => {
|
||||
const { variant, size, symbol, experimentalSpinner, ...rest } = props;
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
{...rest}
|
||||
type="button"
|
||||
className={tw(
|
||||
baseStyles,
|
||||
Variants[variant],
|
||||
Sizes[size].buttonStyles
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={tw(
|
||||
'forced-color-adjust-none',
|
||||
experimentalSpinner != null ? 'opacity-0' : null
|
||||
)}
|
||||
>
|
||||
<AxoSymbol.Icon
|
||||
size={Sizes[size].iconSize}
|
||||
symbol={symbol}
|
||||
label={null}
|
||||
/>
|
||||
</span>
|
||||
{experimentalSpinner != null && (
|
||||
<Spinner
|
||||
buttonVariant={variant}
|
||||
buttonSize={size}
|
||||
aria-label={experimentalSpinner['aria-label']}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
Root.displayName = `${Namespace}.Root`;
|
||||
|
||||
const SpinnerVariants: Record<Variant, SpinnerVariant> = {
|
||||
primary: 'axo-button-spinner-on-color',
|
||||
secondary: 'axo-button-spinner-secondary',
|
||||
affirmative: 'axo-button-spinner-on-color',
|
||||
destructive: 'axo-button-spinner-on-color',
|
||||
'floating-secondary': 'axo-button-spinner-secondary',
|
||||
'borderless-secondary': 'axo-button-spinner-secondary',
|
||||
};
|
||||
|
||||
type SpinnerSizeConfig = { size: number; strokeWidth: number };
|
||||
|
||||
const SpinnerSizes: Record<Size, SpinnerSizeConfig> = {
|
||||
lg: { size: 20, strokeWidth: 2 },
|
||||
md: { size: 20, strokeWidth: 2 },
|
||||
sm: { size: 16, strokeWidth: 1.5 },
|
||||
};
|
||||
|
||||
type SpinnerProps = Readonly<{
|
||||
buttonVariant: Variant;
|
||||
buttonSize: Size;
|
||||
'aria-label': string;
|
||||
}>;
|
||||
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
function Spinner(props: SpinnerProps): JSX.Element {
|
||||
const variant = SpinnerVariants[props.buttonVariant];
|
||||
const sizeConfig = SpinnerSizes[props.buttonSize];
|
||||
return (
|
||||
<span className={tw('absolute inset-0 flex items-center justify-center')}>
|
||||
<SpinnerV2
|
||||
size={sizeConfig.size}
|
||||
strokeWidth={sizeConfig.strokeWidth}
|
||||
variant={variant}
|
||||
value="indeterminate"
|
||||
ariaLabel={props['aria-label']}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -141,8 +141,7 @@ export namespace AxoScrollArea {
|
||||
'rounded-[2px] outline-border-focused',
|
||||
// Move the outline from the viewport to the parent
|
||||
// so it doesn't get cut off by <Mask>
|
||||
'[:where(.keyboard-mode)_&:has([data-axo-scroll-area-viewport]:focus)]:outline-[2.5px]',
|
||||
'forced-colors:border forced-colors:border-[ButtonBorder]'
|
||||
'[:where(.keyboard-mode)_&:has([data-axo-scroll-area-viewport]:focus)]:outline-[2.5px]'
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
@@ -310,15 +309,16 @@ export namespace AxoScrollArea {
|
||||
'absolute z-10',
|
||||
'opacity-0',
|
||||
'from-shadow-outline to-transparent dark:from-shadow-elevation-1',
|
||||
'animate-duration-1 [animation-name:axo-scroll-area-hint-reveal]'
|
||||
'animate-duration-1 [animation-name:axo-scroll-area-hint-reveal]',
|
||||
'forced-colors:bg-[ButtonBorder]'
|
||||
);
|
||||
|
||||
// Need `animation-fill-mode` so we can customize the `animation-range`
|
||||
const edgeStartStyles = tw('animate-forwards');
|
||||
const edgeEndStyles = tw('animate-backwards animate-reverse');
|
||||
const edgeStartStyles = tw('animate-both');
|
||||
const edgeEndStyles = tw('animate-both animate-reverse');
|
||||
|
||||
const edgeYStyles = tw('inset-x-0 h-0.5');
|
||||
const edgeXStyles = tw('inset-y-0 w-0.5');
|
||||
const edgeYStyles = tw('inset-x-0 h-0.5 forced-colors:h-px');
|
||||
const edgeXStyles = tw('inset-y-0 w-0.5 forced-colors:w-px');
|
||||
|
||||
const HintEdges: Record<Edge, TailwindStyles> = {
|
||||
top: tw(
|
||||
|
||||
@@ -36,7 +36,8 @@ export namespace AxoBaseDialog {
|
||||
// Allow the entire overlay to be scrolled in case the window is extremely small
|
||||
'overflow-auto scrollbar-width-none',
|
||||
'data-[state=closed]:animate-exit data-[state=open]:animate-enter',
|
||||
'animate-opacity-0'
|
||||
'animate-opacity-0',
|
||||
'forced-colors:bg-[Canvas]'
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -48,9 +49,10 @@ export namespace AxoBaseDialog {
|
||||
'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]',
|
||||
'outline-border-focused not-forced-colors:outline-0 not-forced-colors:focused:outline-[2.5px]',
|
||||
'data-[state=closed]:animate-exit data-[state=open]:animate-enter',
|
||||
'animate-scale-98 animate-translate-y-1'
|
||||
'animate-scale-98 animate-translate-y-1',
|
||||
'forced-colors:border forced-colors:border-[ButtonBorder] forced-colors:bg-[Canvas] forced-colors:text-[CanvasText]'
|
||||
);
|
||||
|
||||
export type ContentEscape = 'cancel-is-noop' | 'cancel-is-destructive';
|
||||
|
||||
@@ -837,8 +837,6 @@ function NotificationProfilesNamePage({
|
||||
<AxoButton.Root
|
||||
variant="primary"
|
||||
size="lg"
|
||||
type="button"
|
||||
form="notificationProfileName"
|
||||
disabled={!isValid}
|
||||
onClick={onNext}
|
||||
>
|
||||
@@ -907,16 +905,9 @@ function NotificationProfilesAllowedPage({
|
||||
/>
|
||||
</Container>
|
||||
<ButtonContainer>
|
||||
<form
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
onNext();
|
||||
}}
|
||||
>
|
||||
<AxoButton.Root variant="primary" size="lg" type="submit">
|
||||
{i18n('icu:next2')}
|
||||
</AxoButton.Root>
|
||||
</form>
|
||||
<AxoButton.Root variant="primary" size="lg" onClick={onNext}>
|
||||
{i18n('icu:next2')}
|
||||
</AxoButton.Root>
|
||||
</ButtonContainer>
|
||||
</>
|
||||
);
|
||||
@@ -1076,12 +1067,7 @@ function NotificationProfilesSchedulePage({
|
||||
</FullWidthRow>
|
||||
</Container>
|
||||
<ButtonContainer>
|
||||
<AxoButton.Root
|
||||
variant="primary"
|
||||
size="lg"
|
||||
type="button"
|
||||
onClick={onNext}
|
||||
>
|
||||
<AxoButton.Root variant="primary" size="lg" onClick={onNext}>
|
||||
{isEditing ? i18n('icu:done') : i18n('icu:next2')}
|
||||
</AxoButton.Root>
|
||||
</ButtonContainer>
|
||||
@@ -1111,12 +1097,7 @@ function NotificationProfilesDonePage({
|
||||
<p className={tw('mt-4 mb-6 max-w-[350px] text-center leading-5')}>
|
||||
{i18n('icu:NotificationProfiles--done-description')}
|
||||
</p>
|
||||
<AxoButton.Root
|
||||
variant="primary"
|
||||
size="lg"
|
||||
type="button"
|
||||
onClick={onNext}
|
||||
>
|
||||
<AxoButton.Root variant="primary" size="lg" onClick={onNext}>
|
||||
{i18n('icu:done')}
|
||||
</AxoButton.Root>
|
||||
</MidFloatingContainer>
|
||||
@@ -1406,12 +1387,7 @@ function NotificationProfilesEditPage({
|
||||
</FullWidthButton>
|
||||
</Container>
|
||||
<ButtonContainer>
|
||||
<AxoButton.Root
|
||||
variant="primary"
|
||||
size="lg"
|
||||
type="button"
|
||||
onClick={onBack}
|
||||
>
|
||||
<AxoButton.Root variant="primary" size="lg" onClick={onBack}>
|
||||
{i18n('icu:done')}
|
||||
</AxoButton.Root>
|
||||
</ButtonContainer>
|
||||
|
||||
Reference in New Issue
Block a user