From 03a555a54d04ecb77b2fa8bdf2ac8adb60b26507 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 16 Sep 2026 19:45:49 -0400 Subject: [PATCH 1/3] feat(ui): mirror direction-aware Mosaic icons under rtl --- .changeset/mosaic-rtl-mirror-icons.md | 5 ++ .claude/skills/mosaic/references/stylex.md | 13 +++- .../steps/reverification-method-picker.tsx | 12 ++- .../features/user-button/user-button.view.tsx | 3 + .../user-profile-api-keys-panel.view.tsx | 11 ++- ...r-profile-billing-history-section.view.tsx | 11 ++- packages/mosaic/src/utils/rtl.styles.ts | 7 ++ packages/mosaic/src/utils/rtl.test.tsx | 35 +++++++++ packages/swingset/CLAUDE.md | 1 + .../swingset/src/components/ClientRoot.tsx | 73 ++++++++++--------- .../src/components/DirectionProvider.tsx | 25 +++++++ .../src/components/DirectionToggle.tsx | 26 +++++++ references/mosaic-architecture.md | 2 +- 13 files changed, 182 insertions(+), 42 deletions(-) create mode 100644 .changeset/mosaic-rtl-mirror-icons.md create mode 100644 packages/mosaic/src/utils/rtl.styles.ts create mode 100644 packages/mosaic/src/utils/rtl.test.tsx create mode 100644 packages/swingset/src/components/DirectionProvider.tsx create mode 100644 packages/swingset/src/components/DirectionToggle.tsx diff --git a/.changeset/mosaic-rtl-mirror-icons.md b/.changeset/mosaic-rtl-mirror-icons.md new file mode 100644 index 00000000000..b4384210b9f --- /dev/null +++ b/.changeset/mosaic-rtl-mirror-icons.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': patch +--- + +Direction-aware icons in Mosaic (the forward chevrons on menu rows and the previous/next arrows in paginated lists) now mirror horizontally when rendered under an ancestor with `dir="rtl"`. diff --git a/.claude/skills/mosaic/references/stylex.md b/.claude/skills/mosaic/references/stylex.md index 49a9041db4a..a760afbba98 100644 --- a/.claude/skills/mosaic/references/stylex.md +++ b/.claude/skills/mosaic/references/stylex.md @@ -52,8 +52,17 @@ rule reserves the `.stylex.ts` extension for StyleX define-primitives: **a components render the same visual surface — e.g. inputs (`TextInput`, `NumberInput`, date fields, `Selector`) sharing one `inputWrapper` / `inputStatusBorder` / `inputStatusFocusWithin` set instead of redefining the - border/focus treatment five times. Three exist today: `reset.styles.ts`, - `typography.styles.ts` and `focus-outline.styles.ts`. + border/focus treatment five times. Four exist today: `reset.styles.ts`, + `typography.styles.ts`, `focus-outline.styles.ts` and `rtl.styles.ts`. +- **DO** compose `rtl.mirror` from `utils/rtl.styles.ts` onto a direction-aware + icon — a chevron that means "forward"/"back", a pagination arrow, the + log-out arrow leaving its frame — via its + `xstyle`. It flips the glyph with `scaleX(-1)` only under an ancestor carrying + `dir="rtl"`, so the same `chevron-right` reads as "forward" in both directions. + Pick it by meaning, not by shape: a chevron that points at a dropdown or an + external-link arrow stays unmirrored. +- **DON'T** register a mirrored twin (`chevron-forward`) in the icon registry or + swap the name at the call site by direction. The direction lives in CSS. - **DON'T** put a shared style file under `components/`. That directory holds one subdirectory per component and nothing else, so a loose file there reads as a component that lost its folder. diff --git a/packages/mosaic/src/features/reverification/steps/reverification-method-picker.tsx b/packages/mosaic/src/features/reverification/steps/reverification-method-picker.tsx index 244ccb2bb2a..67b78173a51 100644 --- a/packages/mosaic/src/features/reverification/steps/reverification-method-picker.tsx +++ b/packages/mosaic/src/features/reverification/steps/reverification-method-picker.tsx @@ -8,6 +8,7 @@ import { Icon, IconFrame } from '../../../components/icon'; import { Item } from '../../../components/item'; import { Spinner } from '../../../components/spinner'; import { Text } from '../../../components/text'; +import { rtl } from '../../../utils/rtl.styles'; const styles = stylex.create({ helpText: { @@ -81,7 +82,16 @@ export function ReverificationMethodPicker({ {method.label} {method.description ? {method.description} : null} - {isPending ? : } + + {isPending ? ( + + ) : ( + + )} + ); })} diff --git a/packages/mosaic/src/features/user-button/user-button.view.tsx b/packages/mosaic/src/features/user-button/user-button.view.tsx index 33a6f5dfefd..d3e8594244c 100644 --- a/packages/mosaic/src/features/user-button/user-button.view.tsx +++ b/packages/mosaic/src/features/user-button/user-button.view.tsx @@ -21,6 +21,7 @@ import type { IconName } from '../../icons/registry'; import { applyOrder } from '../../utils/apply-order'; import { focusOutline } from '../../utils/focus-outline.styles'; import { fill, plural } from '../../utils/messages'; +import { rtl } from '../../utils/rtl.styles'; import { truncationStyles } from '../../utils/typography.styles'; import type { UserButtonLayout } from './user-button.layout'; import { resolveUserButtonLayout } from './user-button.layout'; @@ -794,6 +795,7 @@ function SwitchAccountRow() { @@ -957,6 +959,7 @@ function Footer() { } label={m.accounts.signOutAll} diff --git a/packages/mosaic/src/features/user-profile/user-profile-api-keys-panel.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-api-keys-panel.view.tsx index 189eda2d84c..782fe7af056 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-api-keys-panel.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-api-keys-panel.view.tsx @@ -8,6 +8,7 @@ import { Input } from '../../components/input'; import { Menu } from '../../components/menu'; import { Profile } from '../../components/profile'; import { mergeStyleProps, themeProps } from '../../props'; +import { rtl } from '../../utils/rtl.styles'; import { styles } from './user-profile-api-keys-panel.styles'; export interface UserProfileAPIKey { @@ -202,7 +203,10 @@ export function UserProfileApiKeysPanelView({ variant='ghost' onClick={() => onPageChange?.(pagination.page - 1)} > - +