diff --git a/.eslintrc.js b/.eslintrc.js index a74991b169..66d145e08e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -254,6 +254,43 @@ const typescriptRules = { 'import/no-cycle': 'off', }; +const TAILWIND_REPLACEMENTS = [ + // inset + { pattern: 'left-*', fix: 'start-*' }, + { pattern: 'right-*', fix: 'end-*' }, + // margin + { pattern: 'ml-*', fix: 'ms-*' }, + { pattern: 'mr-*', fix: 'me-*' }, + // padding + { pattern: 'pl-*', fix: 'ps-*' }, + { pattern: 'pr-*', fix: 'pe-*' }, + // border + { pattern: 'border-l-*', fix: 'border-s-*' }, + { pattern: 'border-r-*', fix: 'border-e-*' }, + // border-radius + { pattern: 'rounded-l', fix: 'rounded-s' }, + { pattern: 'rounded-r', fix: 'rounded-e' }, + { pattern: 'rounded-tl', fix: 'rounded-ss' }, + { pattern: 'rounded-tr', fix: 'rounded-se' }, + { pattern: 'rounded-bl', fix: 'rounded-es' }, + { pattern: 'rounded-br', fix: 'rounded-ee' }, + { pattern: 'rounded-l-*', fix: 'rounded-s-*' }, + { pattern: 'rounded-r-*', fix: 'rounded-e-*' }, + { pattern: 'rounded-tl-*', fix: 'rounded-ss-*' }, + { pattern: 'rounded-tr-*', fix: 'rounded-se-*' }, + { pattern: 'rounded-bl-*', fix: 'rounded-es-*' }, + { pattern: 'rounded-br-*', fix: 'rounded-ee-*' }, + // text-align + { pattern: 'text-left', fix: 'text-start' }, + { pattern: 'text-right', fix: 'text-end' }, + // float + { pattern: 'float-left', fix: 'float-start' }, + { pattern: 'float-right', fix: 'float-end' }, + // clear + { pattern: 'clear-left', fix: 'clear-start' }, + { pattern: 'clear-right', fix: 'clear-end' }, +]; + module.exports = { root: true, settings: { @@ -377,6 +414,15 @@ module.exports = { pattern: '^\\*+:.*', // ex: "*:mx-0", message: 'No child variants', }, + ...TAILWIND_REPLACEMENTS.map(item => { + const pattern = item.pattern.replace('*', '(.*)'); + const fix = item.fix.replace('*', '$2'); + return { + message: `Use logical property ${item.fix} instead of ${item.pattern}`, + pattern: `^(.*:)?${pattern}$`, + fix: `$1${fix}`, + }; + }), ], }, ], diff --git a/.prettierrc.js b/.prettierrc.js index 3500e8cd2c..ebff2fb615 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -3,17 +3,11 @@ /** @type {import("prettier").Config} */ module.exports = { + plugins: ['prettier-plugin-tailwindcss'], singleQuote: true, arrowParens: 'avoid', trailingComma: 'es5', - overrides: [ - { - files: ['./ts/axo/**.tsx'], - plugins: ['prettier-plugin-tailwindcss'], - options: { - tailwindStylesheet: './stylesheets/tailwind-config.css', - tailwindFunctions: ['tw'], - }, - }, - ], + tailwindStylesheet: './stylesheets/tailwind-config.css', + tailwindFunctions: ['tw'], + tailwindAttributes: [], }; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index cb9b3bbc09..5815815f44 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -17,6 +17,7 @@ import { HourCyclePreference } from '../ts/types/I18N'; import { Provider } from 'react-redux'; import { Store, combineReducers, createStore } from 'redux'; import { Globals } from '@react-spring/web'; +import { AxoProvider } from '../ts/axo/AxoProvider'; import { StateType } from '../ts/state/reducer'; import { ScrollerLockContext, @@ -254,8 +255,17 @@ function withFunProvider(Story, context) { ); } +function withAxoProvider(Story, context) { + return ( + + + + ); +} + export const decorators = [ withStrictMode, + withAxoProvider, withGlobalTypesProvider, withMockStoreProvider, withScrollLockProvider, diff --git a/stylesheets/tailwind-config.css b/stylesheets/tailwind-config.css index f2f8435bff..84d36cdf25 100644 --- a/stylesheets/tailwind-config.css +++ b/stylesheets/tailwind-config.css @@ -6,9 +6,9 @@ */ @custom-variant dark (&:where(.dark-theme, .dark-theme *)); -@custom-variant hovered (&:hover:not(:disabled)); -@custom-variant pressed (&:active:not(:disabled)); -@custom-variant focused (.keyboard-mode &:focus); +@custom-variant hovered (&:where(:hover:not(:disabled))); +@custom-variant pressed (&:where(:active:not(:disabled))); +@custom-variant focused (:where(.keyboard-mode) &:where(:focus)); /** * Color @@ -325,6 +325,9 @@ /* box-shadow: inset */ --inset-shadow-*: initial; /* reset defaults */ + --inset-shadow-on-color: + inset 0 0.5px 1px 0 --alpha(#000 / 12%); + /* filter: drop-shadow() */ --drop-shadow-*: initial; /* reset defaults */ --drop-shadow-elevation-0: var(--shadow-elevation-0); diff --git a/ts/axo/AxoButton.tsx b/ts/axo/AxoButton.tsx index 21befdae64..059cdcc058 100644 --- a/ts/axo/AxoButton.tsx +++ b/ts/axo/AxoButton.tsx @@ -11,7 +11,8 @@ const Namespace = 'AxoButton'; const baseAxoButtonStyles = tw( 'flex items-center-safe justify-center-safe gap-1 truncate rounded-full select-none', - 'outline-0 outline-border-focused focused:outline-[2.5px]' + 'outline-0 outline-border-focused focused:outline-[2.5px]', + 'forced-colors:border' ); const AxoButtonTypes = { diff --git a/ts/axo/AxoCheckbox.stories.tsx b/ts/axo/AxoCheckbox.stories.tsx new file mode 100644 index 0000000000..322129fd09 --- /dev/null +++ b/ts/axo/AxoCheckbox.stories.tsx @@ -0,0 +1,40 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +import React, { useState } from 'react'; +import type { Meta } from '@storybook/react'; +import { AxoCheckbox } from './AxoCheckbox'; +import { tw } from './tw'; + +export default { + title: 'Axo/AxoCheckbox', +} satisfies Meta; + +function Template(props: { + label: string; + defaultChecked: boolean; + disabled?: boolean; +}): JSX.Element { + const [checked, setChecked] = useState(props.defaultChecked); + return ( + + ); +} + +export function Basic(): JSX.Element { + return ( + <> +

AxoCheckbox

+