diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 202a2a3c13..da4c2f967b 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -269,8 +269,10 @@ function withFunProvider(Story, context) { } function withAxoProvider(Story, context) { + const globalValue = context.globals.direction ?? 'ltr'; + const dir = globalValue === 'auto' ? 'ltr' : globalValue; return ( - + ); diff --git a/stylesheets/tailwind-config.css b/stylesheets/tailwind-config.css index 061f88791a..9372918f2d 100644 --- a/stylesheets/tailwind-config.css +++ b/stylesheets/tailwind-config.css @@ -1,4 +1,9 @@ @import 'tailwindcss' source(none); +@import './tailwind-plugins/animate-general.css'; +@import './tailwind-plugins/animate-enter-exit.css'; +@import './tailwind-plugins/scrollbar.css'; + +@import '../ts/axo/_styles.css'; @source "../ts"; @source "../test"; @@ -23,6 +28,7 @@ /* prettier-ignore */ @theme { --color-*: initial; /* reset defaults */ + --color-transparent: transparent; /* Colors/Labels */ --color-label-primary: light-dark(--alpha(#000 / 85%), --alpha(#FFF / 85%)); @@ -368,24 +374,30 @@ --east-in-out-cubic: cubic-bezier(0.65, 0, 0.35, 1); } +/** + * Transitions + * ---------------------------------------------------------------------------- + */ + +@theme { + --default-transition-duration: 120ms; + --default-transition-timing-function: var(--ease-out-cubic); +} + /** * Animations * ---------------------------------------------------------------------------- */ @theme { + --default-animation-duration: 120ms; + --default-animation-timing-function: var(--ease-out-cubic); + --animate-*: initial; /* reset defaults */ - --animate-fade-out: animate-fade-out 120ms var(--ease-out-cubic); --animate-spinner-v2-rotate: animate-spinner-v2-rotate 2s linear infinite; --animate-spinner-v2-dash: animate-spinner-v2-dash 1.5s ease-in-out infinite; } @layer base { - @keyframes animate-fade-out { - to { - opacity: 0; - } - } - @keyframes animate-spinner-v2-rotate { 0% { transform: rotate(-180deg); @@ -416,3 +428,12 @@ inherits: false; initial-value: transparent; } + +/** + * Scrollbars + */ + +@theme { + --default-scrollbar-track: transparent; + --default-scrollbar-thumb: #b9b9b9; +} diff --git a/stylesheets/tailwind-plugins/animate-enter-exit.css b/stylesheets/tailwind-plugins/animate-enter-exit.css new file mode 100644 index 0000000000..667ab3191b --- /dev/null +++ b/stylesheets/tailwind-plugins/animate-enter-exit.css @@ -0,0 +1,142 @@ +/** + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/** + * Properties + */ + +@property --tw-animate-opacity { + syntax: '*'; + inherits: false; +} +@property --tw-animate-rotate { + syntax: '*'; + inherits: false; +} +@property --tw-animate-scale { + syntax: '*'; + inherits: false; +} +@property --tw-animate-translate-x { + syntax: '*'; + inherits: false; +} +@property --tw-animate-translate-y { + syntax: '*'; + inherits: false; +} + +/** + * Utilities + */ + +@utility animate-enter { + animation-name: tw-animate-enter; + animation-duration: var( + --tw-animate-duration, + var(--default-animation-duration) + ); + animation-timing-function: var( + --tw-animate-ease, + var(--default-animation-timing-function) + ); +} + +@utility animate-exit { + animation-name: tw-animate-exit; + animation-duration: var( + --tw-animate-duration, + var(--default-animation-duration) + ); + animation-timing-function: var( + --tw-animate-ease, + var(--default-animation-timing-function) + ); +} + +/** + * animate-opacity + */ + +@utility animate-opacity-* { + --tw-animate-opacity: calc(--value(integer) * 1%); +} + +/** + * animate-rotate + */ + +@utility animate-rotate-* { + --tw-animate-rotate: rotate(calc(--value(integer) * 1deg)); +} +@utility -animate-rotate-* { + --tw-animate-rotate: rotate(calc(--value(integer) * -1deg)); +} + +/** + * animate-scale + */ + +@utility animate-scale-* { + --tw-animate-scale: scale(calc(--value(number) * 1%)); +} +@utility -animate-scale-* { + --tw-animate-scale: scale(calc(--value(number) * -1%)); +} + +/** + * animate-translate + */ + +@utility animate-translate-x-* { + --tw-animate-translate-x: translateX(--spacing(--value(integer))); + --tw-animate-translate-x: translateX(--value([percentage], [length])); +} +@utility -animate-translate-x-* { + --tw-animate-translate-x: translateX(--spacing(--value(integer) * -1)); + --tw-animate-translate-x: translateX( + calc(--value([percentage], [length]) * -1) + ); +} +@utility animate-translate-y-* { + --tw-animate-translate-y: translateY(--spacing(--value(integer))); + --tw-animate-translate-y: translateY(--value([percentage], [length])); +} +@utility -animate-translate-y-* { + --tw-animate-translate-y: translateY(--spacing(--value(integer) * -1)); + --tw-animate-translate-y: translateY( + calc(--value([percentage], [length]) * -1) + ); +} + +/** + * Keyframes + */ + +@layer utilities { + @keyframes tw-animate-enter { + from { + opacity: var(--tw-animate-opacity); + /* prettier-ignore */ + transform: + var(--tw-animate-rotate,) + var(--tw-animate-scale,) + var(--tw-animate-translate-x,) + var(--tw-animate-translate-y,); + } + } + + @keyframes tw-animate-exit { + to { + opacity: var(--tw-animate-opacity); + /* prettier-ignore */ + transform: + var(--tw-animate-rotate,) + var(--tw-animate-scale,) + var(--tw-animate-translate-x,) + var(--tw-animate-translate-y,); + } + } +} diff --git a/stylesheets/tailwind-plugins/animate-general.css b/stylesheets/tailwind-plugins/animate-general.css new file mode 100644 index 0000000000..1b06b47039 --- /dev/null +++ b/stylesheets/tailwind-plugins/animate-general.css @@ -0,0 +1,85 @@ +/** + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/** + * Theme + */ + +@theme { + --default-animation-duration: var(--default-transition-duration); + --default-animation-timing-function: var(--default-animation-timing-function); +} + +/** + * Properties + */ + +@property --tw-animate-duration { + syntax: '*'; + inherits: false; +} +@property --tw-animate-ease { + syntax: '*'; + inherits: false; +} + +/** + * Utilities + */ + +/** `animation-duration` */ +@utility animate-duration-* { + --tw-animate-duration: calc(--value(integer) * 1ms); + animation-duration: calc(--value(integer) * 1ms); +} + +/** `animation-delay` */ +@utility animate-delay-* { + animation-delay: calc(--value(integer) * 1ms); +} + +/** `animation-timing-function` */ +@utility animate-ease-* { + /* prettier-ignore */ + --tw-animate-ease: --value(--ease-*); + /* prettier-ignore */ + animation-timing-function: --value(--ease-*); +} + +/** `animation-fill-mode` */ +@utility animate-forwards { + animation-fill-mode: forwards; +} +@utility animate-backwards { + animation-fill-mode: backwards; +} +@utility animate-both { + animation-fill-mode: both; +} +@utility animate-none { + animation-fill-mode: none; +} + +/** `animation-play-state` */ +@utility paused { + animation-play-state: paused; +} +@utility running { + animation-play-state: running; +} + +/** `animation-direction` */ +@utility animate-normal { + animation-direction: normal; +} +@utility animate-reverse { + animation-direction: reverse; +} +@utility animate-alternate { + animation-direction: alternate; +} +@utility animate-alternate-reverse { + animation-direction: alternate-reverse; +} diff --git a/stylesheets/tailwind-plugins/scrollbar.css b/stylesheets/tailwind-plugins/scrollbar.css new file mode 100644 index 0000000000..797e310bec --- /dev/null +++ b/stylesheets/tailwind-plugins/scrollbar.css @@ -0,0 +1,79 @@ +/** + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/** + * Theme + */ + +@theme { + --default-scrollbar-track: transparent; + --default-scrollbar-thumb: currentColor; +} + +/** + * Properties + */ + +@property --tw-scrollbar-track { + syntax: '*'; + inherits: false; +} +@property --tw-scrollbar-thumb { + syntax: '*'; + inherits: false; +} + +/** + * Utilities + */ + +@utility scrollbar-track-* { + /* prettier-ignore */ + --tw-scrollbar-track: --value(--color-*); + /* prettier-ignore */ + --tw-scrollbar-track: --value([*]); + /* prettier-ignore */ + scrollbar-color: + var(--tw-scrollbar-thumb, var(--default-scrollbar-thumb)) + var(--tw-scrollbar-track, var(--default-scrollbar-track)); +} + +@utility scrollbar-thumb-* { + /* prettier-ignore */ + --tw-scrollbar-thumb: --value(--color-*); + /* prettier-ignore */ + --tw-scrollbar-thumb: --value([*]); + /* prettier-ignore */ + scrollbar-color: + var(--tw-scrollbar-thumb, var(--default-scrollbar-thumb)) + var(--tw-scrollbar-track, var(--default-scrollbar-track)); +} + +@utility scrollbar-width-auto { + scrollbar-width: auto; +} +@utility scrollbar-width-thin { + scrollbar-width: thin; +} +@utility scrollbar-width-none { + scrollbar-width: none; +} + +@utility scrollbar-gutter-auto { + scrollbar-gutter: auto; +} +@utility scrollbar-gutter-stable { + scrollbar-gutter: stable; +} +@utility scrollbar-gutter-stable-both-edges { + scrollbar-gutter: stable both-edges; +} + +@layer base { + :root { + scrollbar-color: var(--default-scrollbar-thumb) + var(--default-scrollbar-track); + } +} diff --git a/ts/axo/AxoAlertDialog.dom.stories.tsx b/ts/axo/AxoAlertDialog.dom.stories.tsx new file mode 100644 index 0000000000..2b15c4f20a --- /dev/null +++ b/ts/axo/AxoAlertDialog.dom.stories.tsx @@ -0,0 +1,129 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { Meta } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import React, { useState } from 'react'; +import { AxoAlertDialog } from './AxoAlertDialog.dom.js'; +import { AxoButton } from './AxoButton.dom.js'; + +export default { + title: 'Axo/AxoAlertDialog', +} satisfies Meta; + +const EXAMPLE_TITLE = <>Exporting chat; +const EXAMPLE_TITLE_LONG = ( + <> + Lorem ipsum dolor, sit amet consectetur adipisicing elit. Est vel + repudiandae magnam tempora temporibus nihil repellendus ullam. Ex veniam + ipsa voluptate, quae ullam qui eius enim explicabo laborum modi minima! + +); + +const EXAMPLE_DESCRIPTION = <>Exporting chat; +const EXAMPLE_DESCRIPTION_LONG = ( + <> + Lorem ipsum dolor sit amet consectetur adipisicing elit. Nobis, amet aut + quasi possimus repudiandae accusamus dolore. Iure, ad neque qui recusandae + quod asperiores! Facere nulla illum suscipit dolores sint libero! Quibusdam + hic, facilis soluta quae voluptatum eius voluptates alias ipsa, autem sed + tempore atque nesciunt illum blanditiis tempora fugiat. Quidem odit optio + sint! Iste rerum, molestias doloremque asperiores ipsa nostrum! Provident + impedit quam aspernatur libero veniam sint et tempore maiores! Porro + incidunt numquam sapiente deserunt id possimus atque at. Repudiandae + recusandae blanditiis autem ad numquam animi omnis eos perspiciatis harum! + Accusantium nesciunt eligendi laboriosam ipsam reprehenderit voluptate, + minima necessitatibus molestias reiciendis repellendus maiores assumenda + alias atque odit, voluptatum facere voluptas excepturi, nostrum quidem + beatae quasi quis? Provident, quaerat autem! Numquam. Laborum, aut quidem + molestias beatae eius, id molestiae officiis, dolores perspiciatis ratione + doloremque eligendi? Aut facilis temporibus inventore beatae nihil dolores + quidem alias ab expedita, quas fugit recusandae at dignissimos. Ullam + veritatis eligendi dicta asperiores minus quisquam! Odit dolorem ipsum + repudiandae enim excepturi omnis quisquam molestias ullam placeat delectus + necessitatibus eligendi illo, pariatur mollitia, alias sit ad amet eveniet + tenetur. Rem debitis, aperiam iusto officia fugiat consectetur hic voluptate + reprehenderit. Est quisquam, saepe fuga odit ex recusandae vero earum + asperiores aspernatur at, fugit temporibus eligendi tempore nemo obcaecati + libero dolore. Tenetur illum facere delectus sapiente architecto, minima + accusamus officia sed quos. Ipsum odit exercitationem ullam iure deleniti ea + eius, quia illum debitis cum quae pariatur assumenda officia dolores. Quasi, + temporibus? Distinctio iure quis nihil eaque ut cum quibusdam officiis, + eveniet maxime, debitis eos asperiores itaque voluptatem aliquam expedita? + Sint, animi eos. Repudiandae deleniti beatae quam dolores optio ipsa totam + perferendis. Nulla nostrum laudantium provident est itaque inventore neque, + eveniet facere vero voluptatibus alias nisi repellat placeat ipsa ea, amet + numquam iusto voluptates dolorem, sint odit optio quam. Dolores, molestiae! + Dolorem? + +); + +const EXAMPLE_ACTION = <>OK; +const EXAMPLE_ACTION_LONG = <>Consectetur adipisicing elit; +const EXAMPLE_CANCEL = <>Cancel; +const EXAMPLE_CANCEL_LONG = <>Lorem ipsum dolor sit amet; + +function Template(props: { + visuallyHiddenTitle?: boolean; + requireExplicitChoice?: boolean; + extraLongText?: boolean; +}) { + const [open, setOpen] = useState(true); + return ( + + + + Open + + + + + + {props.extraLongText ? EXAMPLE_TITLE_LONG : EXAMPLE_TITLE} + + + {props.extraLongText + ? EXAMPLE_DESCRIPTION_LONG + : EXAMPLE_DESCRIPTION} + + + + + {props.extraLongText ? EXAMPLE_CANCEL_LONG : EXAMPLE_CANCEL} + + + {props.extraLongText ? EXAMPLE_ACTION_LONG : EXAMPLE_ACTION} + + + + + ); +} + +export function Basic(): JSX.Element { + return