mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-20 02:08:57 +00:00
Init AxoIconButton
This commit is contained in:
@@ -11,6 +11,8 @@ import { SpinnerV2 } from '../components/SpinnerV2.dom.js';
|
|||||||
|
|
||||||
const Namespace = 'AxoButton';
|
const Namespace = 'AxoButton';
|
||||||
|
|
||||||
|
type GenericButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
|
||||||
|
|
||||||
const baseAxoButtonStyles = tw(
|
const baseAxoButtonStyles = tw(
|
||||||
'relative inline-flex max-w-full items-center-safe justify-center-safe rounded-full select-none',
|
'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]',
|
'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'),
|
sm: tw('min-w-12 px-2 py-1 type-body-small font-medium'),
|
||||||
} as const satisfies Record<string, TailwindStyles>;
|
} as const satisfies Record<string, TailwindStyles>;
|
||||||
|
|
||||||
type BaseButtonAttrs = Omit<
|
|
||||||
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
||||||
'className' | 'style' | 'children'
|
|
||||||
>;
|
|
||||||
|
|
||||||
type AxoButtonVariant = keyof typeof AxoButtonVariants;
|
type AxoButtonVariant = keyof typeof AxoButtonVariants;
|
||||||
type AxoButtonSize = keyof typeof AxoButtonSizes;
|
type AxoButtonSize = keyof typeof AxoButtonSizes;
|
||||||
|
|
||||||
@@ -215,16 +212,19 @@ export namespace AxoButton {
|
|||||||
full: tw('w-full'),
|
full: tw('w-full'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RootProps = BaseButtonAttrs &
|
export type RootProps = Readonly<{
|
||||||
Readonly<{
|
variant: AxoButtonVariant;
|
||||||
variant: AxoButtonVariant;
|
size: AxoButtonSize;
|
||||||
size: AxoButtonSize;
|
width?: Width;
|
||||||
width?: Width;
|
symbol?: AxoSymbol.InlineGlyphName;
|
||||||
symbol?: AxoSymbol.InlineGlyphName;
|
arrow?: boolean;
|
||||||
arrow?: boolean;
|
experimentalSpinner?: { 'aria-label': string } | null;
|
||||||
experimentalSpinner?: { 'aria-label': string } | null;
|
disabled?: GenericButtonProps['disabled'];
|
||||||
children: ReactNode;
|
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(
|
export const Root: FC<RootProps> = memo(
|
||||||
forwardRef((props, ref: ForwardedRef<HTMLButtonElement>) => {
|
forwardRef((props, ref: ForwardedRef<HTMLButtonElement>) => {
|
||||||
@@ -251,9 +251,9 @@ export namespace AxoButton {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
{...rest}
|
||||||
type="button"
|
type="button"
|
||||||
className={tw(variantStyles, sizeStyles, widthStyles)}
|
className={tw(variantStyles, sizeStyles, widthStyles)}
|
||||||
{...rest}
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={tw(
|
className={tw(
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ function Template(props: {
|
|||||||
back?: boolean;
|
back?: boolean;
|
||||||
contentSize: AxoDialog.ContentSize;
|
contentSize: AxoDialog.ContentSize;
|
||||||
bodyPadding?: AxoDialog.BodyPadding;
|
bodyPadding?: AxoDialog.BodyPadding;
|
||||||
|
iconAction?: boolean;
|
||||||
footerContent?: ReactNode;
|
footerContent?: ReactNode;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
@@ -68,12 +69,26 @@ function Template(props: {
|
|||||||
</AxoDialog.FooterContent>
|
</AxoDialog.FooterContent>
|
||||||
)}
|
)}
|
||||||
<AxoDialog.Actions>
|
<AxoDialog.Actions>
|
||||||
<AxoDialog.Action variant="secondary" onClick={action('onCancel')}>
|
{props.iconAction ? (
|
||||||
Cancel
|
<AxoDialog.IconAction
|
||||||
</AxoDialog.Action>
|
aria-label="Send"
|
||||||
<AxoDialog.Action variant="primary" onClick={action('onSave')}>
|
variant="primary"
|
||||||
Save
|
symbol="send-fill"
|
||||||
</AxoDialog.Action>
|
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.Actions>
|
||||||
</AxoDialog.Footer>
|
</AxoDialog.Footer>
|
||||||
</AxoDialog.Content>
|
</AxoDialog.Content>
|
||||||
@@ -102,6 +117,14 @@ export function Large(): JSX.Element {
|
|||||||
return <Template contentSize="lg">{TEXT_LONG}</Template>;
|
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 {
|
export function LongContent(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Template contentSize="md">
|
<Template contentSize="md">
|
||||||
@@ -166,7 +189,8 @@ function TextInputField(props: { placeholder: string }) {
|
|||||||
'w-full px-3 py-1.5',
|
'w-full px-3 py-1.5',
|
||||||
'border-[0.5px] border-border-primary shadow-elevation-0',
|
'border-[0.5px] border-border-primary shadow-elevation-0',
|
||||||
'rounded-lg bg-fill-primary',
|
'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>
|
</div>
|
||||||
@@ -193,7 +217,10 @@ export function ExampleNicknameAndNoteDialog(): JSX.Element {
|
|||||||
encrypted. They are only visible to you.
|
encrypted. They are only visible to you.
|
||||||
</p>
|
</p>
|
||||||
<div
|
<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} />
|
<Spacer height={12} />
|
||||||
<TextInputField placeholder="First name" />
|
<TextInputField placeholder="First name" />
|
||||||
@@ -331,7 +358,10 @@ export function ExampleLanguageDialog(): JSX.Element {
|
|||||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||||
autoFocus
|
autoFocus
|
||||||
placeholder="Search languages"
|
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.ExperimentalSearch>
|
||||||
<AxoDialog.Body padding="only-scrollbar-gutter">
|
<AxoDialog.Body padding="only-scrollbar-gutter">
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { tw } from './tw.dom.js';
|
|||||||
import { AxoScrollArea } from './AxoScrollArea.dom.js';
|
import { AxoScrollArea } from './AxoScrollArea.dom.js';
|
||||||
import { getScrollbarGutters } from './_internal/scrollbars.dom.js';
|
import { getScrollbarGutters } from './_internal/scrollbars.dom.js';
|
||||||
import { AxoButton } from './AxoButton.dom.js';
|
import { AxoButton } from './AxoButton.dom.js';
|
||||||
|
import { AxoIconButton } from './AxoIconButton.dom.js';
|
||||||
|
|
||||||
const Namespace = 'AxoDialog';
|
const Namespace = 'AxoDialog';
|
||||||
|
|
||||||
@@ -237,9 +238,11 @@ export namespace AxoDialog {
|
|||||||
export const Back: FC<BackProps> = memo(props => {
|
export const Back: FC<BackProps> = memo(props => {
|
||||||
return (
|
return (
|
||||||
<div className={tw('col-[back-slot] text-start')}>
|
<div className={tw('col-[back-slot] text-start')}>
|
||||||
<HeaderIconButton
|
<AxoIconButton.Root
|
||||||
label={props['aria-label']}
|
size="sm"
|
||||||
|
variant="borderless-secondary"
|
||||||
symbol="chevron-[start]"
|
symbol="chevron-[start]"
|
||||||
|
aria-label={props['aria-label']}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -260,7 +263,12 @@ export namespace AxoDialog {
|
|||||||
return (
|
return (
|
||||||
<div className={tw('col-[close-slot] text-end')}>
|
<div className={tw('col-[close-slot] text-end')}>
|
||||||
<Dialog.Close asChild>
|
<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>
|
</Dialog.Close>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -462,4 +470,31 @@ export namespace AxoDialog {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Action.displayName = `${Namespace}.Action`;
|
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',
|
'rounded-[2px] outline-border-focused',
|
||||||
// Move the outline from the viewport to the parent
|
// Move the outline from the viewport to the parent
|
||||||
// so it doesn't get cut off by <Mask>
|
// so it doesn't get cut off by <Mask>
|
||||||
'[:where(.keyboard-mode)_&:has([data-axo-scroll-area-viewport]:focus)]:outline-[2.5px]',
|
'[:where(.keyboard-mode)_&:has([data-axo-scroll-area-viewport]:focus)]:outline-[2.5px]'
|
||||||
'forced-colors:border forced-colors:border-[ButtonBorder]'
|
|
||||||
)}
|
)}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
@@ -310,15 +309,16 @@ export namespace AxoScrollArea {
|
|||||||
'absolute z-10',
|
'absolute z-10',
|
||||||
'opacity-0',
|
'opacity-0',
|
||||||
'from-shadow-outline to-transparent dark:from-shadow-elevation-1',
|
'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`
|
// Need `animation-fill-mode` so we can customize the `animation-range`
|
||||||
const edgeStartStyles = tw('animate-forwards');
|
const edgeStartStyles = tw('animate-both');
|
||||||
const edgeEndStyles = tw('animate-backwards animate-reverse');
|
const edgeEndStyles = tw('animate-both animate-reverse');
|
||||||
|
|
||||||
const edgeYStyles = tw('inset-x-0 h-0.5');
|
const edgeYStyles = tw('inset-x-0 h-0.5 forced-colors:h-px');
|
||||||
const edgeXStyles = tw('inset-y-0 w-0.5');
|
const edgeXStyles = tw('inset-y-0 w-0.5 forced-colors:w-px');
|
||||||
|
|
||||||
const HintEdges: Record<Edge, TailwindStyles> = {
|
const HintEdges: Record<Edge, TailwindStyles> = {
|
||||||
top: tw(
|
top: tw(
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ export namespace AxoBaseDialog {
|
|||||||
// Allow the entire overlay to be scrolled in case the window is extremely small
|
// Allow the entire overlay to be scrolled in case the window is extremely small
|
||||||
'overflow-auto scrollbar-width-none',
|
'overflow-auto scrollbar-width-none',
|
||||||
'data-[state=closed]:animate-exit data-[state=open]:animate-enter',
|
'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',
|
'relative',
|
||||||
'max-h-full min-h-fit max-w-full min-w-fit',
|
'max-h-full min-h-fit max-w-full min-w-fit',
|
||||||
'rounded-3xl bg-elevated-background-primary shadow-elevation-3 select-none',
|
'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',
|
'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';
|
export type ContentEscape = 'cancel-is-noop' | 'cancel-is-destructive';
|
||||||
|
|||||||
@@ -837,8 +837,6 @@ function NotificationProfilesNamePage({
|
|||||||
<AxoButton.Root
|
<AxoButton.Root
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="lg"
|
size="lg"
|
||||||
type="button"
|
|
||||||
form="notificationProfileName"
|
|
||||||
disabled={!isValid}
|
disabled={!isValid}
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
>
|
>
|
||||||
@@ -907,16 +905,9 @@ function NotificationProfilesAllowedPage({
|
|||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
<form
|
<AxoButton.Root variant="primary" size="lg" onClick={onNext}>
|
||||||
onSubmit={e => {
|
{i18n('icu:next2')}
|
||||||
e.preventDefault();
|
</AxoButton.Root>
|
||||||
onNext();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AxoButton.Root variant="primary" size="lg" type="submit">
|
|
||||||
{i18n('icu:next2')}
|
|
||||||
</AxoButton.Root>
|
|
||||||
</form>
|
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -1076,12 +1067,7 @@ function NotificationProfilesSchedulePage({
|
|||||||
</FullWidthRow>
|
</FullWidthRow>
|
||||||
</Container>
|
</Container>
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
<AxoButton.Root
|
<AxoButton.Root variant="primary" size="lg" onClick={onNext}>
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
type="button"
|
|
||||||
onClick={onNext}
|
|
||||||
>
|
|
||||||
{isEditing ? i18n('icu:done') : i18n('icu:next2')}
|
{isEditing ? i18n('icu:done') : i18n('icu:next2')}
|
||||||
</AxoButton.Root>
|
</AxoButton.Root>
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
@@ -1111,12 +1097,7 @@ function NotificationProfilesDonePage({
|
|||||||
<p className={tw('mt-4 mb-6 max-w-[350px] text-center leading-5')}>
|
<p className={tw('mt-4 mb-6 max-w-[350px] text-center leading-5')}>
|
||||||
{i18n('icu:NotificationProfiles--done-description')}
|
{i18n('icu:NotificationProfiles--done-description')}
|
||||||
</p>
|
</p>
|
||||||
<AxoButton.Root
|
<AxoButton.Root variant="primary" size="lg" onClick={onNext}>
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
type="button"
|
|
||||||
onClick={onNext}
|
|
||||||
>
|
|
||||||
{i18n('icu:done')}
|
{i18n('icu:done')}
|
||||||
</AxoButton.Root>
|
</AxoButton.Root>
|
||||||
</MidFloatingContainer>
|
</MidFloatingContainer>
|
||||||
@@ -1406,12 +1387,7 @@ function NotificationProfilesEditPage({
|
|||||||
</FullWidthButton>
|
</FullWidthButton>
|
||||||
</Container>
|
</Container>
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
<AxoButton.Root
|
<AxoButton.Root variant="primary" size="lg" onClick={onBack}>
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
type="button"
|
|
||||||
onClick={onBack}
|
|
||||||
>
|
|
||||||
{i18n('icu:done')}
|
{i18n('icu:done')}
|
||||||
</AxoButton.Root>
|
</AxoButton.Root>
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
|
|||||||
Reference in New Issue
Block a user