From e5d1f8f54cca136cbf59f1e1393c9934ad77b1e2 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 01:16:13 -0300 Subject: [PATCH 01/84] feat(fuselage): Add Tamagui v3 foundation (config, tokens, themes, primitives, provider) Fresh start with all learnings from v1 attempt. Includes: - tamagui.config.ts with 72 colors, space/size/radius/zIndex tokens, fonts (15 scales), and 12 media queries - 3 themes (light, dark, high-contrast) with ~140 semantic tokens each - Base primitives (RcxView, RcxText, RcxInteractive, RcxInteractiveText) - FuselageProvider wrapping TamaguiProvider - Box.scss with real CSS classes for rcx-box resets - Storybook preview with FuselageProvider + process polyfill - Webpack transpileOnly + tsconfig skipLibCheck Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/fuselage/.storybook/main.ts | 24 +- packages/fuselage/.storybook/preview.tsx | 9 +- packages/fuselage/package.json | 5 +- packages/fuselage/src/index.ts | 6 + packages/fuselage/src/primitives/index.ts | 30 + .../src/providers/FuselageProvider.tsx | 21 + .../fuselage/src/styles/primitives/box.scss | 12 + packages/fuselage/src/tamagui.config.ts | 377 +++ packages/fuselage/src/themes/dark.ts | 175 ++ packages/fuselage/src/themes/highContrast.ts | 175 ++ packages/fuselage/src/themes/index.ts | 3 + packages/fuselage/src/themes/light.ts | 176 ++ packages/fuselage/src/types/tamagui.d.ts | 91 + packages/fuselage/tsconfig.json | 12 +- packages/fuselage/webpack.config.js | 1 + yarn.lock | 2130 +++++++++++++++-- 16 files changed, 3011 insertions(+), 236 deletions(-) create mode 100644 packages/fuselage/src/primitives/index.ts create mode 100644 packages/fuselage/src/providers/FuselageProvider.tsx create mode 100644 packages/fuselage/src/tamagui.config.ts create mode 100644 packages/fuselage/src/themes/dark.ts create mode 100644 packages/fuselage/src/themes/highContrast.ts create mode 100644 packages/fuselage/src/themes/index.ts create mode 100644 packages/fuselage/src/themes/light.ts create mode 100644 packages/fuselage/src/types/tamagui.d.ts diff --git a/packages/fuselage/.storybook/main.ts b/packages/fuselage/.storybook/main.ts index 56ba1ee845..014c7c66fe 100644 --- a/packages/fuselage/.storybook/main.ts +++ b/packages/fuselage/.storybook/main.ts @@ -1,16 +1,9 @@ import { dirname, join } from 'path'; import type { StorybookConfig } from '@storybook/react-webpack5'; +import webpack from 'webpack'; const config: StorybookConfig = { - webpackFinal: async (config) => { - config.module?.rules?.push({ - test: /\.woff2$/, - type: 'asset/resource', - }); - - return config; - }, addons: [ getAbsolutePath('@storybook/addon-a11y'), getAbsolutePath('@rocket.chat/storybook-dark-mode'), @@ -95,8 +88,21 @@ const config: StorybookConfig = { typescript: { reactDocgen: 'react-docgen-typescript', }, -}; + webpackFinal: async (config) => { + config.plugins.push( + new webpack.ProvidePlugin({ + process: 'process/browser', + }), + ); + config.module?.rules?.push({ + test: /\.woff2$/, + type: 'asset/resource', + }); + + return config; + }, +} satisfies StorybookConfig; export default config; function getAbsolutePath(value: string): any { diff --git a/packages/fuselage/.storybook/preview.tsx b/packages/fuselage/.storybook/preview.tsx index dfd5d65d6c..642c7e86a8 100644 --- a/packages/fuselage/.storybook/preview.tsx +++ b/packages/fuselage/.storybook/preview.tsx @@ -5,7 +5,7 @@ import type { Preview } from '@storybook/react-webpack5'; import { themes } from 'storybook/theming'; import manifest from '../package.json'; -import { PaletteStyleTag } from '../src'; +import { FuselageProvider, PaletteStyleTag } from '../src'; import DocsContainer from './DocsContainer'; import logo from './logo.svg'; @@ -78,12 +78,13 @@ export default { decorators: [ (Story) => { const dark = useDarkMode(); + const theme = dark ? 'dark' : 'light'; return ( - <> - + + - + ); }, ], diff --git a/packages/fuselage/package.json b/packages/fuselage/package.json index 31c71415ff..fcbcdac9fe 100644 --- a/packages/fuselage/package.json +++ b/packages/fuselage/package.json @@ -42,10 +42,13 @@ "@rocket.chat/fuselage-tokens": "workspace:~", "@rocket.chat/memo": "workspace:~", "@rocket.chat/styled": "workspace:~", + "@tamagui/core": "2.0.0-rc.31", + "@tamagui/web": "2.0.0-rc.31", "invariant": "^2.2.4", "react-aria": "~3.37.0", "react-keyed-flatten-children": "^1.3.0", - "react-stately": "~3.35.0" + "react-stately": "~3.35.0", + "tamagui": "2.0.0-rc.31" }, "devDependencies": { "@babel/core": "~7.29.0", diff --git a/packages/fuselage/src/index.ts b/packages/fuselage/src/index.ts index d3c1ff0a20..9198c37cbc 100644 --- a/packages/fuselage/src/index.ts +++ b/packages/fuselage/src/index.ts @@ -5,3 +5,9 @@ export * from './styleTokens'; export { Palette, __setThrowErrorOnInvalidToken__ } from './Theme'; export { useArrayLikeClassNameProp } from './hooks/useArrayLikeClassNameProp'; + +// Tamagui v3 exports +export { FuselageProvider } from './providers/FuselageProvider'; +export { tamaguiConfig, tokens } from './tamagui.config'; +export { lightTheme, darkTheme, highContrastTheme } from './themes'; +export { RcxView, RcxText, RcxInteractive, RcxInteractiveText } from './primitives'; diff --git a/packages/fuselage/src/primitives/index.ts b/packages/fuselage/src/primitives/index.ts new file mode 100644 index 0000000000..7844987e83 --- /dev/null +++ b/packages/fuselage/src/primitives/index.ts @@ -0,0 +1,30 @@ +import { styled, Text, View } from 'tamagui'; + +// Only use 'rcx-box' (NOT 'rcx-box--full') to avoid margin/padding/border +// conflicts with SCSS classes from non-migrated components. +// Tamagui already resets margin/padding via .is_View/.is_Text. +// rcx-box provides: flex:0 1 auto, box-sizing, font-variant-numeric, outline, font-smoothing. + +export const RcxView = styled(View, { + name: 'RcxView', + className: 'rcx-box', +}); + +export const RcxText = styled(Text, { + name: 'RcxText', + className: 'rcx-box', +}); + +// NOTE: Do NOT use focusable:true — Tamagui v2 RC forces tabIndex:-1 which +// overrides our tabIndex:0. Use only tabIndex for keyboard accessibility. +export const RcxInteractive = styled(View, { + name: 'RcxInteractive', + className: 'rcx-box', + cursor: 'pointer', +}); + +export const RcxInteractiveText = styled(Text, { + name: 'RcxInteractiveText', + className: 'rcx-box', + cursor: 'pointer', +}); diff --git a/packages/fuselage/src/providers/FuselageProvider.tsx b/packages/fuselage/src/providers/FuselageProvider.tsx new file mode 100644 index 0000000000..c484f36014 --- /dev/null +++ b/packages/fuselage/src/providers/FuselageProvider.tsx @@ -0,0 +1,21 @@ +import type { ReactNode } from 'react'; +import { TamaguiProvider } from 'tamagui'; + +import { tamaguiConfig } from '../tamagui.config'; + +type FuselageProviderProps = { + children: ReactNode; + theme?: 'light' | 'dark' | 'high-contrast'; +}; + +export const FuselageProvider = ({ + children, + theme = 'light', +}: FuselageProviderProps) => ( + + {children} + +); diff --git a/packages/fuselage/src/styles/primitives/box.scss b/packages/fuselage/src/styles/primitives/box.scss index 7a9dad52c7..82b9b2c4b5 100644 --- a/packages/fuselage/src/styles/primitives/box.scss +++ b/packages/fuselage/src/styles/primitives/box.scss @@ -35,3 +35,15 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } + +.rcx-box { + @extend %box; +} + +.rcx-box--full { + @extend %box--full; +} + +.rcx-box--animated { + @extend %box--animated; +} diff --git a/packages/fuselage/src/tamagui.config.ts b/packages/fuselage/src/tamagui.config.ts new file mode 100644 index 0000000000..8169cc6186 --- /dev/null +++ b/packages/fuselage/src/tamagui.config.ts @@ -0,0 +1,377 @@ +import colors from '@rocket.chat/fuselage-tokens/colors.json'; +import { createFont, createTamagui, createTokens } from 'tamagui'; + +import { darkTheme, highContrastTheme, lightTheme } from './themes'; + +// --------------------------------------------------------------------------- +// Tokens +// --------------------------------------------------------------------------- + +const space = { + x0: 0, + x1: 1, + x2: 2, + x4: 4, + x6: 6, + x8: 8, + x10: 10, + x12: 12, + x14: 14, + x16: 16, + x18: 18, + x20: 20, + x24: 24, + x28: 28, + x32: 32, + x36: 36, + x40: 40, + x44: 44, + x48: 48, + x52: 52, + x56: 56, + x60: 60, + x64: 64, + x68: 68, + x72: 72, + x76: 76, + x80: 80, + x84: 84, + x88: 88, + x92: 92, + x96: 96, + x100: 100, + x104: 104, + x108: 108, + x112: 112, + x116: 116, + x120: 120, + x124: 124, + x128: 128, + x132: 132, + x136: 136, + x140: 140, + x144: 144, + x148: 148, + x152: 152, + x156: 156, + x160: 160, + x164: 164, + x168: 168, + x172: 172, + x176: 176, + x180: 180, + x184: 184, + x188: 188, + x192: 192, + x196: 196, + x200: 200, + x204: 204, + x208: 208, + x212: 212, + x216: 216, + x220: 220, + x224: 224, + x228: 228, + x232: 232, + x236: 236, + x240: 240, + x244: 244, + x248: 248, + x252: 252, + x256: 256, + x260: 260, + x264: 264, + x268: 268, + x272: 272, + x276: 276, + x280: 280, + x284: 284, + x288: 288, + x292: 292, + x296: 296, + x300: 300, + x304: 304, + x308: 308, + x312: 312, + x316: 316, + x320: 320, + x324: 324, + x328: 328, + x332: 332, + x336: 336, + x340: 340, + x344: 344, + x348: 348, + x352: 352, + x356: 356, + x360: 360, + x364: 364, + x368: 368, + true: 16, +} as const; + +const size = { + ...space, + none: 0, + full: '100%', + sw: '100vw', + sh: '100vh', +} as const; + +const radius = { + none: 0, + x2: 2, + x4: 4, + x8: 8, + x16: 16, + full: 9999, + true: 4, +} as const; + +const zIndex = { + x0: 0, + x1: 1, + x2: 2, + x10: 10, + x100: 100, +} as const; + +export const tokens = createTokens({ + color: { + white: colors.white, + n100: colors.n100, + n200: colors.n200, + n250: colors.n250, + n300: colors.n300, + n400: colors.n400, + n450: colors.n450, + n500: colors.n500, + n600: colors.n600, + n700: colors.n700, + n800: colors.n800, + n900: colors.n900, + r100: colors.r100, + r200: colors.r200, + r300: colors.r300, + r400: colors.r400, + r500: colors.r500, + r600: colors.r600, + r700: colors.r700, + r800: colors.r800, + r900: colors.r900, + r1000: colors.r1000, + o100: colors.o100, + o200: colors.o200, + o300: colors.o300, + o400: colors.o400, + o500: colors.o500, + o600: colors.o600, + o700: colors.o700, + o800: colors.o800, + o900: colors.o900, + o1000: colors.o1000, + p100: colors.p100, + p200: colors.p200, + p300: colors.p300, + p400: colors.p400, + p500: colors.p500, + p600: colors.p600, + p700: colors.p700, + p800: colors.p800, + p900: colors.p900, + y100: colors.y100, + y200: colors.y200, + y300: colors.y300, + y400: colors.y400, + y500: colors.y500, + y600: colors.y600, + y700: colors.y700, + y800: colors.y800, + y900: colors.y900, + y1000: colors.y1000, + g100: colors.g100, + g200: colors.g200, + g300: colors.g300, + g400: colors.g400, + g500: colors.g500, + g600: colors.g600, + g700: colors.g700, + g800: colors.g800, + g900: colors.g900, + g1000: colors.g1000, + b100: colors.b100, + b200: colors.b200, + b300: colors.b300, + b400: colors.b400, + b500: colors.b500, + b600: colors.b600, + b700: colors.b700, + b800: colors.b800, + b900: colors.b900, + }, + space, + size, + radius, + zIndex, +}); + +// --------------------------------------------------------------------------- +// Fonts +// --------------------------------------------------------------------------- + +const sansFontFamily = + 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Meiryo UI", Arial, sans-serif'; + +const monoFontFamily = + 'Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'; + +const fontSizes = { + hero: 48, + h1: 32, + h2: 24, + h3: 20, + h4: 16, + h5: 14, + p1: 16, + p1m: 16, + p1b: 16, + p2: 14, + p2m: 14, + p2b: 14, + c1: 12, + c2: 12, + micro: 10, +} as const; + +const fontLineHeights = { + hero: 64, + h1: 40, + h2: 32, + h3: 28, + h4: 24, + h5: 20, + p1: 24, + p1m: 24, + p1b: 24, + p2: 20, + p2m: 20, + p2b: 20, + c1: 16, + c2: 16, + micro: 12, +} as const; + +const fontWeights = { + hero: '800', + h1: '700', + h2: '700', + h3: '700', + h4: '700', + h5: '700', + p1: '400', + p1m: '500', + p1b: '700', + p2: '400', + p2m: '500', + p2b: '700', + c1: '400', + c2: '700', + micro: '700', +} as const; + +const fontLetterSpacings = { + hero: 0, + h1: 0, + h2: 0, + h3: 0, + h4: 0, + h5: 0, + p1: 0, + p1m: 0, + p1b: 0, + p2: 0, + p2m: 0, + p2b: 0, + c1: 0, + c2: 0, + micro: 0, +} as const; + +const sansFont = createFont({ + family: sansFontFamily, + size: fontSizes, + lineHeight: fontLineHeights, + weight: fontWeights, + letterSpacing: fontLetterSpacings, + face: { + 400: { normal: 'Inter' }, + 500: { normal: 'Inter' }, + 700: { normal: 'Inter' }, + 800: { normal: 'Inter' }, + }, +}); + +const monoFont = createFont({ + family: monoFontFamily, + size: fontSizes, + lineHeight: fontLineHeights, + weight: fontWeights, + letterSpacing: fontLetterSpacings, + face: { + 400: { normal: 'Menlo' }, + 500: { normal: 'Menlo' }, + 700: { normal: 'Menlo' }, + 800: { normal: 'Menlo' }, + }, +}); + +// --------------------------------------------------------------------------- +// Media queries +// --------------------------------------------------------------------------- + +const media = { + xs: { maxWidth: 439 }, + gtXs: { minWidth: 440 }, + sm: { maxWidth: 767 }, + gtSm: { minWidth: 768 }, + md: { maxWidth: 1023 }, + gtMd: { minWidth: 1024 }, + lg: { maxWidth: 1439 }, + gtLg: { minWidth: 1440 }, + xl: { maxWidth: 1919 }, + gtXl: { minWidth: 1920 }, + xxl: { maxWidth: 99999 }, + gtXxl: { minWidth: 100000 }, +} as const; + +// --------------------------------------------------------------------------- +// Config +// --------------------------------------------------------------------------- + +export const tamaguiConfig = createTamagui({ + tokens, + themes: { + light: lightTheme, + dark: darkTheme, + 'high-contrast': highContrastTheme, + }, + fonts: { + body: sansFont, + heading: sansFont, + mono: monoFont, + }, + media, + defaultFont: 'body', + shouldAddPrefersColorThemes: false, + themeClassNameOnRoot: false, +}); + +// --------------------------------------------------------------------------- +// Module augmentation +// --------------------------------------------------------------------------- + +export type AppConfig = typeof tamaguiConfig; + +declare module 'tamagui' { + interface TamaguiCustomConfig extends AppConfig {} +} diff --git a/packages/fuselage/src/themes/dark.ts b/packages/fuselage/src/themes/dark.ts new file mode 100644 index 0000000000..fb4a35d567 --- /dev/null +++ b/packages/fuselage/src/themes/dark.ts @@ -0,0 +1,175 @@ +import colors from '@rocket.chat/fuselage-tokens/colors.json'; + +export const darkTheme = { + // --- Surface tokens (12) --- + surfaceLight: '#262931', + surfaceTint: '#1F2329', + surfaceRoom: '#1F2329', + surfaceNeutral: '#2D3039', + surfaceDisabled: '#24272E', + surfaceHover: '#1A1E23', + surfaceSelected: '#4C5362', + surfaceDark: '#E4E7EA', + surfaceFeatured: '#5F1477', + surfaceFeaturedHover: '#4A105D', + surfaceSidebar: '#2F343D', + surfaceOverlay: 'rgba(0, 0, 0, 0.6)', + + // --- Stroke tokens (9) --- + strokeExtraLight: '#333842', + strokeLight: '#404754', + strokeMedium: '#4B5362', + strokeDark: '#9EA2A8', + strokeExtraDark: '#CBCED1', + strokeExtraLightHighlight: colors.b200, + strokeHighlight: '#6292DA', + strokeExtraLightError: '#F49AA6', + strokeError: '#BB3E4E', + + // --- Font tokens (11) --- + fontWhite: '#2F343D', + fontDisabled: '#60646C', + fontAnnotation: '#9EA2A8', + fontHint: '#9EA2A8', + fontSecondaryInfo: '#9EA2A8', + fontDefault: '#C1C7D0', + fontTitlesLabels: '#F2F3F5', + fontInfo: '#739EDE', + fontDanger: '#D88892', + fontPureWhite: colors.white, + fontPureBlack: colors.n900, + + // --- Badge tokens (5) --- + badgeLevel0: '#404754', + badgeLevel1: '#484C51', + badgeLevel2: '#2C65BA', + badgeLevel3: '#955828', + badgeLevel4: '#B43C4C', + + // --- Status background tokens (8) --- + statusBgInfo: '#A8C3EB', + statusBgSuccess: '#C1EBDD', + statusBgDanger: '#F7CFD4', + statusBgWarning: '#FEEFBE', + statusBgWarning2: '#3C3625', + statusBgService1: '#FCE3CF', + statusBgService2: '#EDD0F7', + statusBgService3: '#5F1477', + + // --- Status font tokens (8) --- + statusFontOnInfo: '#739EDE', + statusFontOnSuccess: '#58AD90', + statusFontOnDanger: '#D88892', + statusFontOnWarning: '#C7AA66', + statusFontOnWarning2: '#FFFFFF', + statusFontOnService1: '#CA9163', + statusFontOnService2: '#C393D2', + statusFontOnService3: '#FFFFFF', + + // --- Shadow tokens (6) --- + shadowHighlight: colors.b200, + shadowDanger: colors.r100, + shadowElevationBorder: '#2F343D', + shadowElevation1: 'rgba(9, 9, 9, 0.35)', + shadowElevation2x: 'rgba(9, 9, 9, 0.3)', + shadowElevation2y: 'rgba(9, 9, 9, 0.45)', + + // --- Button Primary (7) --- + buttonPrimaryBg: '#095AD2', + buttonPrimaryHoverBg: '#10529E', + buttonPrimaryPressBg: '#01336B', + buttonPrimaryFocusBg: '#095AD2', + buttonPrimaryDisabledBg: '#012247', + buttonPrimaryColor: '#FFFFFF', + buttonPrimaryDisabledColor: '#6C727A', + + // --- Button Secondary (7) --- + buttonSecondaryBg: '#353B45', + buttonSecondaryHoverBg: '#404754', + buttonSecondaryPressBg: '#4C5362', + buttonSecondaryFocusBg: '#353B45', + buttonSecondaryDisabledBg: '#353B45', + buttonSecondaryColor: '#E4E7EA', + buttonSecondaryDisabledColor: '#6C727A', + + // --- Button Danger (7) --- + buttonDangerBg: '#BB3E4E', + buttonDangerHoverBg: '#95323F', + buttonDangerPressBg: '#822C37', + buttonDangerFocusBg: '#BB3E4E', + buttonDangerDisabledBg: '#3D2126', + buttonDangerColor: '#FFFFFF', + buttonDangerDisabledColor: '#757575', + + // --- Button Secondary Danger (7) --- + buttonSecondaryDangerBg: '#353B45', + buttonSecondaryDangerHoverBg: '#404754', + buttonSecondaryDangerPressBg: '#4C5362', + buttonSecondaryDangerFocusBg: '#353B45', + buttonSecondaryDangerDisabledBg: '#353B45', + buttonSecondaryDangerColor: '#FFC1C9', + buttonSecondaryDangerDisabledColor: '#6B0513', + + // --- Button Success (7) --- + buttonSuccessBg: '#1D7256', + buttonSuccessHoverBg: '#175943', + buttonSuccessPressBg: '#134937', + buttonSuccessFocusBg: '#1D7256', + buttonSuccessDisabledBg: '#1E4B40', + buttonSuccessColor: '#FFFFFF', + buttonSuccessDisabledColor: '#757575', + + // --- Button Warning (7) --- + buttonWarningBg: '#C7AA66', + buttonWarningHoverBg: '#A8903E', + buttonWarningPressBg: '#8E7A35', + buttonWarningFocusBg: '#C7AA66', + buttonWarningDisabledBg: '#3C3625', + buttonWarningColor: '#1F2329', + buttonWarningDisabledColor: '#6C727A', + + // --- Button Secondary Success (7) --- + buttonSecondarySuccessBg: '#353B45', + buttonSecondarySuccessHoverBg: '#404754', + buttonSecondarySuccessPressBg: '#4C5362', + buttonSecondarySuccessFocusBg: '#353B45', + buttonSecondarySuccessDisabledBg: '#353B45', + buttonSecondarySuccessColor: '#58AD90', + buttonSecondarySuccessDisabledColor: '#1E4B40', + + // --- Button Secondary Warning (7) --- + buttonSecondaryWarningBg: '#353B45', + buttonSecondaryWarningHoverBg: '#404754', + buttonSecondaryWarningPressBg: '#4C5362', + buttonSecondaryWarningFocusBg: '#353B45', + buttonSecondaryWarningDisabledBg: '#353B45', + buttonSecondaryWarningColor: '#C7AA66', + buttonSecondaryWarningDisabledColor: '#3C3625', + + // --- Tag tokens (8) --- + tagDefaultBg: '#353B45', + tagDefaultColor: '#9EA2A8', + tagPrimaryBg: '#1A3A6B', + tagPrimaryColor: '#739EDE', + tagDangerBg: '#3D2126', + tagDangerColor: '#D88892', + tagWarningBg: '#3C3625', + tagWarningColor: '#C7AA66', + + // --- Tamagui standard tokens (15) --- + background: '#1F2329', + backgroundHover: '#1A1E23', + backgroundPress: '#262931', + backgroundFocus: '#1A1E23', + backgroundStrong: '#262931', + backgroundTransparent: 'rgba(0, 0, 0, 0)', + color: '#C1C7D0', + colorHover: '#F2F3F5', + colorPress: '#F2F3F5', + colorFocus: '#C1C7D0', + borderColor: '#333842', + borderColorHover: '#404754', + borderColorPress: '#4B5362', + borderColorFocus: '#6292DA', + placeholderColor: '#60646C', +} as const; diff --git a/packages/fuselage/src/themes/highContrast.ts b/packages/fuselage/src/themes/highContrast.ts new file mode 100644 index 0000000000..6df2c2fdac --- /dev/null +++ b/packages/fuselage/src/themes/highContrast.ts @@ -0,0 +1,175 @@ +import colors from '@rocket.chat/fuselage-tokens/colors.json'; + +export const highContrastTheme = { + // --- Surface tokens (12) --- + surfaceLight: colors.white, + surfaceTint: colors.n100, + surfaceRoom: colors.white, + surfaceNeutral: colors.n400, + surfaceDisabled: colors.n100, + surfaceHover: colors.n200, + surfaceSelected: colors.n450, + surfaceDark: colors.n900, + surfaceFeatured: colors.p700, + surfaceFeaturedHover: colors.p800, + surfaceSidebar: colors.n400, + surfaceOverlay: 'rgba(47, 52, 61, 0.5)', + + // --- Stroke tokens (9) --- + strokeExtraLight: colors.n250, + strokeLight: colors.n500, + strokeMedium: colors.n600, + strokeDark: colors.n700, + strokeExtraDark: colors.n800, + strokeExtraLightHighlight: colors.b200, + strokeHighlight: colors.b500, + strokeExtraLightError: colors.r200, + strokeError: colors.r500, + + // --- Font tokens (11) --- + fontWhite: colors.white, + fontDisabled: colors.n500, + fontAnnotation: colors.n900, + fontHint: colors.n900, + fontSecondaryInfo: colors.n900, + fontDefault: colors.n900, + fontTitlesLabels: colors.n900, + fontInfo: colors.b800, + fontDanger: colors.r800, + fontPureWhite: colors.white, + fontPureBlack: colors.n900, + + // --- Badge tokens (5) --- + badgeLevel0: colors.n400, + badgeLevel1: colors.n800, + badgeLevel2: colors.b700, + badgeLevel3: colors.o900, + badgeLevel4: colors.r800, + + // --- Status background tokens (8) --- + statusBgInfo: colors.b200, + statusBgSuccess: colors.g200, + statusBgDanger: colors.r200, + statusBgWarning: colors.y200, + statusBgWarning2: colors.y100, + statusBgService1: colors.o200, + statusBgService2: colors.p200, + statusBgService3: colors.p700, + + // --- Status font tokens (8) --- + statusFontOnInfo: colors.b700, + statusFontOnSuccess: colors.g1000, + statusFontOnDanger: colors.r1000, + statusFontOnWarning: colors.y1000, + statusFontOnWarning2: colors.n800, + statusFontOnService1: colors.o1000, + statusFontOnService2: colors.p600, + statusFontOnService3: colors.white, + + // --- Shadow tokens (6) --- + shadowHighlight: colors.b200, + shadowDanger: colors.r100, + shadowElevationBorder: colors.n250, + shadowElevation1: 'rgba(47, 52, 61, 0.1)', + shadowElevation2x: 'rgba(47, 52, 61, 0.08)', + shadowElevation2y: 'rgba(47, 52, 61, 0.12)', + + // --- Button Primary (7) --- + buttonPrimaryBg: colors.b700, + buttonPrimaryHoverBg: colors.b800, + buttonPrimaryPressBg: colors.b900, + buttonPrimaryFocusBg: colors.b700, + buttonPrimaryDisabledBg: colors.b200, + buttonPrimaryColor: colors.white, + buttonPrimaryDisabledColor: colors.white, + + // --- Button Secondary (7) --- + buttonSecondaryBg: colors.n400, + buttonSecondaryHoverBg: colors.n500, + buttonSecondaryPressBg: colors.n600, + buttonSecondaryFocusBg: colors.n400, + buttonSecondaryDisabledBg: colors.n300, + buttonSecondaryColor: colors.n900, + buttonSecondaryDisabledColor: colors.n500, + + // --- Button Danger (7) --- + buttonDangerBg: colors.r800, + buttonDangerHoverBg: colors.r900, + buttonDangerPressBg: colors.r900, + buttonDangerFocusBg: colors.r800, + buttonDangerDisabledBg: colors.r200, + buttonDangerColor: colors.white, + buttonDangerDisabledColor: colors.white, + + // --- Button Secondary Danger (7) --- + buttonSecondaryDangerBg: colors.n400, + buttonSecondaryDangerHoverBg: colors.n500, + buttonSecondaryDangerPressBg: colors.n600, + buttonSecondaryDangerFocusBg: colors.n400, + buttonSecondaryDangerDisabledBg: colors.n300, + buttonSecondaryDangerColor: colors.r900, + buttonSecondaryDangerDisabledColor: colors.r300, + + // --- Button Success (7) --- + buttonSuccessBg: colors.g500, + buttonSuccessHoverBg: colors.g600, + buttonSuccessPressBg: colors.g700, + buttonSuccessFocusBg: colors.g500, + buttonSuccessDisabledBg: colors.g200, + buttonSuccessColor: colors.n900, + buttonSuccessDisabledColor: colors.white, + + // --- Button Warning (7) --- + buttonWarningBg: colors.y700, + buttonWarningHoverBg: colors.y800, + buttonWarningPressBg: colors.y900, + buttonWarningFocusBg: colors.y700, + buttonWarningDisabledBg: colors.y200, + buttonWarningColor: colors.n900, + buttonWarningDisabledColor: colors.n700, + + // --- Button Secondary Success (7) --- + buttonSecondarySuccessBg: colors.n400, + buttonSecondarySuccessHoverBg: colors.n500, + buttonSecondarySuccessPressBg: colors.n600, + buttonSecondarySuccessFocusBg: colors.n400, + buttonSecondarySuccessDisabledBg: colors.n300, + buttonSecondarySuccessColor: colors.g1000, + buttonSecondarySuccessDisabledColor: colors.g200, + + // --- Button Secondary Warning (7) --- + buttonSecondaryWarningBg: colors.n400, + buttonSecondaryWarningHoverBg: colors.n500, + buttonSecondaryWarningPressBg: colors.n600, + buttonSecondaryWarningFocusBg: colors.n400, + buttonSecondaryWarningDisabledBg: colors.n300, + buttonSecondaryWarningColor: colors.y1000, + buttonSecondaryWarningDisabledColor: colors.y300, + + // --- Tag tokens (8) --- + tagDefaultBg: colors.n200, + tagDefaultColor: colors.n800, + tagPrimaryBg: colors.b200, + tagPrimaryColor: colors.b800, + tagDangerBg: colors.r200, + tagDangerColor: colors.r900, + tagWarningBg: colors.y200, + tagWarningColor: colors.y1000, + + // --- Tamagui standard tokens (15) --- + background: colors.white, + backgroundHover: colors.n200, + backgroundPress: colors.n300, + backgroundFocus: colors.n200, + backgroundStrong: colors.n100, + backgroundTransparent: 'rgba(0, 0, 0, 0)', + color: colors.n900, + colorHover: colors.n900, + colorPress: colors.n900, + colorFocus: colors.n900, + borderColor: colors.n250, + borderColorHover: colors.n500, + borderColorPress: colors.n600, + borderColorFocus: colors.b500, + placeholderColor: colors.n600, +} as const; diff --git a/packages/fuselage/src/themes/index.ts b/packages/fuselage/src/themes/index.ts new file mode 100644 index 0000000000..7b2c3cc4b2 --- /dev/null +++ b/packages/fuselage/src/themes/index.ts @@ -0,0 +1,3 @@ +export { lightTheme } from './light'; +export { darkTheme } from './dark'; +export { highContrastTheme } from './highContrast'; diff --git a/packages/fuselage/src/themes/light.ts b/packages/fuselage/src/themes/light.ts new file mode 100644 index 0000000000..69635f14dd --- /dev/null +++ b/packages/fuselage/src/themes/light.ts @@ -0,0 +1,176 @@ +import colors from '@rocket.chat/fuselage-tokens/colors.json'; + +export const lightTheme = { + // --- Surface tokens (12) --- + surfaceLight: colors.white, + surfaceTint: colors.n100, + surfaceRoom: colors.white, + surfaceNeutral: colors.n400, + surfaceDisabled: colors.n100, + surfaceHover: colors.n200, + surfaceSelected: colors.n450, + surfaceDark: colors.n900, + surfaceFeatured: colors.p700, + surfaceFeaturedHover: colors.p800, + surfaceSidebar: colors.n400, + surfaceOverlay: 'rgba(47, 52, 61, 0.5)', + + // --- Stroke tokens (9) --- + strokeExtraLight: colors.n250, + strokeLight: colors.n500, + strokeMedium: colors.n600, + strokeDark: colors.n700, + strokeExtraDark: colors.n800, + strokeExtraLightHighlight: colors.b200, + strokeHighlight: colors.b500, + strokeExtraLightError: colors.r200, + strokeError: colors.r500, + + // --- Font tokens (11) --- + fontWhite: colors.white, + fontDisabled: colors.n500, + fontAnnotation: colors.n600, + fontHint: colors.n700, + fontSecondaryInfo: colors.n700, + fontDefault: colors.n800, + fontTitlesLabels: colors.n900, + fontInfo: colors.b600, + fontDanger: colors.r600, + fontPureWhite: colors.white, + fontPureBlack: colors.n800, + + // --- Badge tokens (5) --- + badgeLevel0: colors.n400, + badgeLevel1: colors.n700, + badgeLevel2: colors.b500, + badgeLevel3: colors.o500, + badgeLevel4: colors.r500, + + // --- Status background tokens (8) --- + statusBgInfo: colors.b200, + statusBgSuccess: colors.g200, + statusBgDanger: colors.r200, + statusBgWarning: colors.y200, + statusBgWarning2: colors.y100, + statusBgService1: colors.o200, + statusBgService2: colors.p200, + statusBgService3: colors.p700, + + // --- Status font tokens (8) --- + statusFontOnInfo: colors.b600, + statusFontOnSuccess: colors.g800, + statusFontOnDanger: colors.r800, + statusFontOnWarning: colors.y900, + statusFontOnWarning2: colors.n800, + statusFontOnService1: colors.o800, + statusFontOnService2: colors.p600, + statusFontOnService3: colors.white, + + // --- Shadow tokens (6) --- + shadowHighlight: colors.b200, + shadowDanger: colors.r100, + shadowElevationBorder: colors.n250, + shadowElevation1: 'rgba(47, 52, 61, 0.1)', + shadowElevation2x: 'rgba(47, 52, 61, 0.08)', + shadowElevation2y: 'rgba(47, 52, 61, 0.12)', + + // --- Button Primary (7) --- + buttonPrimaryBg: colors.b500, + buttonPrimaryHoverBg: colors.b600, + buttonPrimaryPressBg: colors.b700, + buttonPrimaryFocusBg: colors.b500, + buttonPrimaryDisabledBg: colors.b200, + buttonPrimaryColor: colors.white, + buttonPrimaryDisabledColor: colors.white, + + // --- Button Secondary (7) --- + buttonSecondaryBg: colors.n400, + buttonSecondaryHoverBg: colors.n500, + buttonSecondaryPressBg: colors.n600, + buttonSecondaryFocusBg: colors.n400, + buttonSecondaryDisabledBg: colors.n300, + buttonSecondaryColor: colors.n900, + buttonSecondaryDisabledColor: colors.n500, + + // --- Button Danger (7) --- + buttonDangerBg: colors.r500, + buttonDangerHoverBg: colors.r600, + buttonDangerPressBg: colors.r700, + buttonDangerFocusBg: colors.r500, + buttonDangerDisabledBg: colors.r200, + buttonDangerColor: colors.white, + buttonDangerDisabledColor: colors.white, + + // --- Button Secondary Danger (7) --- + buttonSecondaryDangerBg: colors.n400, + buttonSecondaryDangerHoverBg: colors.n500, + buttonSecondaryDangerPressBg: colors.n600, + buttonSecondaryDangerFocusBg: colors.n400, + buttonSecondaryDangerDisabledBg: colors.n300, + buttonSecondaryDangerColor: colors.r700, + buttonSecondaryDangerDisabledColor: colors.r300, + + // --- Button Success (7) --- + buttonSuccessBg: colors.g800, + buttonSuccessHoverBg: colors.g900, + buttonSuccessPressBg: colors.g1000, + buttonSuccessFocusBg: colors.g800, + buttonSuccessDisabledBg: colors.g200, + buttonSuccessColor: colors.white, + buttonSuccessDisabledColor: colors.white, + + // --- Button Warning (7) --- + buttonWarningBg: colors.y600, + buttonWarningHoverBg: colors.y700, + buttonWarningPressBg: colors.y800, + buttonWarningFocusBg: colors.y600, + buttonWarningDisabledBg: colors.y200, + buttonWarningColor: colors.n900, + buttonWarningColor: colors.n900, // $-button-fonts on-warning = neutral(900) + buttonWarningDisabledColor: colors.n700, + + // --- Button Secondary Success (7) --- + buttonSecondarySuccessBg: colors.n400, + buttonSecondarySuccessHoverBg: colors.n500, + buttonSecondarySuccessPressBg: colors.n600, + buttonSecondarySuccessFocusBg: colors.n400, + buttonSecondarySuccessDisabledBg: colors.n300, + buttonSecondarySuccessColor: colors.g800, + buttonSecondarySuccessDisabledColor: colors.g200, + + // --- Button Secondary Warning (7) --- + buttonSecondaryWarningBg: colors.n400, + buttonSecondaryWarningHoverBg: colors.n500, + buttonSecondaryWarningPressBg: colors.n600, + buttonSecondaryWarningFocusBg: colors.n400, + buttonSecondaryWarningDisabledBg: colors.n300, + buttonSecondaryWarningColor: colors.y900, + buttonSecondaryWarningDisabledColor: colors.y300, + + // --- Tag tokens (8) --- + tagDefaultBg: colors.n200, + tagDefaultColor: colors.n700, + tagPrimaryBg: colors.b200, + tagPrimaryColor: colors.b600, + tagDangerBg: colors.r200, + tagDangerColor: colors.r700, + tagWarningBg: colors.y200, + tagWarningColor: colors.y900, + + // --- Tamagui standard tokens (15) --- + background: colors.white, + backgroundHover: colors.n200, + backgroundPress: colors.n300, + backgroundFocus: colors.n200, + backgroundStrong: colors.n100, + backgroundTransparent: 'rgba(0, 0, 0, 0)', + color: colors.n800, + colorHover: colors.n900, + colorPress: colors.n900, + colorFocus: colors.n800, + borderColor: colors.n250, + borderColorHover: colors.n500, + borderColorPress: colors.n600, + borderColorFocus: colors.b500, + placeholderColor: colors.n600, +} as const; diff --git a/packages/fuselage/src/types/tamagui.d.ts b/packages/fuselage/src/types/tamagui.d.ts new file mode 100644 index 0000000000..574d7a310a --- /dev/null +++ b/packages/fuselage/src/types/tamagui.d.ts @@ -0,0 +1,91 @@ +declare module 'tamagui' { + import type { ComponentType, ReactNode } from 'react'; + + // Token types + export function createTokens>>( + tokens: T + ): T; + + // Font types + interface FontConfig { + family: string; + size: Record; + lineHeight: Record; + weight: Record; + letterSpacing: Record; + face: Array<{ normal: string; bold?: string }> | Record; + } + + export function createFont(config: FontConfig): FontConfig; + + // Tamagui config + interface TamaguiConfig { + tokens: any; + themes: Record; + fonts: Record; + media: Record; + defaultFont?: string; + shouldAddPrefersColorThemes?: boolean; + themeClassNameOnRoot?: boolean; + } + + interface TamaguiInternalConfig extends TamaguiConfig { + parsed: boolean; + } + + export function createTamagui(config: TamaguiConfig): TamaguiInternalConfig; + + // Provider + interface TamaguiProviderProps { + config: TamaguiInternalConfig; + defaultTheme?: string; + children?: ReactNode; + disableInjectCSS?: boolean; + disableRootThemeClass?: boolean; + } + + export const TamaguiProvider: ComponentType; + + // styled + type StyledOptions = { + name?: string; + className?: string; + focusable?: boolean; + cursor?: string; + variants?: Record; + defaultVariants?: Record; + context?: any; + acceptsClassName?: boolean; + } & Record; + + export function styled

>( + component: P, + options?: StyledOptions + ): P; + + // Primitives + export const View: ComponentType; + export const Text: ComponentType; + + // Utility types + export type GetProps = T extends ComponentType ? P : never; + + // Hooks + export function useTheme(): Record; + export function useMedia(): Record; + + // Context + export function createStyledContext(defaultValue: T): { + Provider: ComponentType<{ children?: ReactNode } & Partial>; + useStyledContext: () => T; + }; + + // HOC + export function withStaticProperties< + A extends ComponentType, + B extends Record, + >( + component: A, + staticProps: B + ): A & B; +} diff --git a/packages/fuselage/tsconfig.json b/packages/fuselage/tsconfig.json index 1376b120eb..57417ad1a8 100644 --- a/packages/fuselage/tsconfig.json +++ b/packages/fuselage/tsconfig.json @@ -1,8 +1,9 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "lib": ["DOM"], - "outDir": "./dist" + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "skipLibCheck": true }, "include": [ "./src", @@ -10,5 +11,10 @@ "./jest.config.ts", "./jest-setup.ts" ], - "exclude": ["./dist", "./storybook-static/", "./*.js"] + "exclude": [ + "./dist", + "./storybook-static/", + "./*.js", + "./node_modules/**/tamagui/src/**" + ] } diff --git a/packages/fuselage/webpack.config.js b/packages/fuselage/webpack.config.js index a05e7415b6..9ec387251e 100644 --- a/packages/fuselage/webpack.config.js +++ b/packages/fuselage/webpack.config.js @@ -38,6 +38,7 @@ export default (env, { mode = 'production' }) => loader: 'ts-loader', options: { configFile: resolve(import.meta.dirname, './tsconfig.build.json'), + transpileOnly: true, }, }, }, diff --git a/yarn.lock b/yarn.lock index a1c8e19119..6d97575caf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2655,6 +2655,56 @@ __metadata: languageName: node linkType: hard +"@floating-ui/core@npm:^1.0.0, @floating-ui/core@npm:^1.7.5": + version: 1.7.5 + resolution: "@floating-ui/core@npm:1.7.5" + dependencies: + "@floating-ui/utils": "npm:^0.2.11" + checksum: 10/fecdc9b3ce93f02bf78a6114b93730a4cb9fa8234c62f9a949016186297a039c9f9cd3c5c81ff74b93ebddf0b32048c4af7a528afe7904b75423ed2e7491b888 + languageName: node + linkType: hard + +"@floating-ui/dom@npm:^1.7.6": + version: 1.7.6 + resolution: "@floating-ui/dom@npm:1.7.6" + dependencies: + "@floating-ui/core": "npm:^1.7.5" + "@floating-ui/utils": "npm:^0.2.11" + checksum: 10/84dff2ffdf85c8b92d7edafc543c55869abbeaeb3007fa983159467e050153b507a0f5fe8e84f88c3f28c35a82de9df9c20a6eef5560cbba3afae19141444ff2 + languageName: node + linkType: hard + +"@floating-ui/react-dom@npm:^2.1.6": + version: 2.1.8 + resolution: "@floating-ui/react-dom@npm:2.1.8" + dependencies: + "@floating-ui/dom": "npm:^1.7.6" + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 10/39c3e3e5538a111a3eadf1b9ca486d7dc17c7eb24b83a0ea9b4c189fa7dbe5abe01357388d8cf6a4badb2d3fec2b1090e10529537bde91acbcfe19b0a8d10f90 + languageName: node + linkType: hard + +"@floating-ui/react-native@npm:^0.10.7": + version: 0.10.9 + resolution: "@floating-ui/react-native@npm:0.10.9" + dependencies: + "@floating-ui/core": "npm:^1.0.0" + peerDependencies: + react: ">=16.8.0" + react-native: ">=0.64.0" + checksum: 10/a93d7738fa61d21837dd9a90ed65680afb36f4ccec26159cac8810c267b1da494a6df7643c4c2ccddc9d6e5cfae004555d96608534875aa10ac7572e9ad7cad3 + languageName: node + linkType: hard + +"@floating-ui/utils@npm:^0.2.11": + version: 0.2.11 + resolution: "@floating-ui/utils@npm:0.2.11" + checksum: 10/72150138ba1c274d757a1da85233202fa9fdfd2272ec1fb0883eb0ffdf138863af81573049ed2c20b98adb4b7ae2236065541ce14037fe328955089831a678d5 + languageName: node + linkType: hard + "@formatjs/ecma402-abstract@npm:1.12.0": version: 1.12.0 resolution: "@formatjs/ecma402-abstract@npm:1.12.0" @@ -4940,6 +4990,13 @@ __metadata: languageName: node linkType: hard +"@react-native/normalize-color@npm:^2.1.0": + version: 2.1.0 + resolution: "@react-native/normalize-color@npm:2.1.0" + checksum: 10/a72b98538e6b7e265fb0669b8767d5f788777fb1a0ac1df7b0c82d8b3a804c8122aa7b819688c5e36fcf90b5ba93050b0070e29d3f0d70ab9530c2abd2bb9f9e + languageName: node + linkType: hard + "@react-stately/calendar@npm:^3.7.0": version: 3.7.0 resolution: "@react-stately/calendar@npm:3.7.0" @@ -5877,6 +5934,8 @@ __metadata: "@storybook/addon-styling-webpack": "npm:~2.0.0" "@storybook/addon-webpack5-compiler-swc": "npm:~4.0.3" "@storybook/react-webpack5": "npm:~9.1.17" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" "@testing-library/dom": "npm:~10.4.1" "@testing-library/jest-dom": "npm:~6.9.1" "@testing-library/react": "npm:~16.3.2" @@ -5923,6 +5982,7 @@ __metadata: stylelint-order: "npm:~7.0.1" stylelint-prettier: "npm:~5.0.3" stylelint-scss: "npm:~6.12.1" + tamagui: "npm:2.0.0-rc.31" testing-utils: "workspace:~" ts-jest: "npm:~29.4.6" ts-loader: "npm:~9.5.4" @@ -6613,320 +6673,1868 @@ __metadata: version: 4.0.3 resolution: "@storybook/addon-webpack5-compiler-swc@npm:4.0.3" dependencies: - "@swc/core": "npm:^1.13.5" - swc-loader: "npm:^0.2.6" + "@swc/core": "npm:^1.13.5" + swc-loader: "npm:^0.2.6" + peerDependencies: + storybook: ^9.0.0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 + checksum: 10/3c062c08254cf896030dded773e5f80e9471733a6b3c3d6afa467ff59fe991a06a52155b3ae9936f741beb9bacfd8f11ff908e38dc6ee31955db628e67260486 + languageName: node + linkType: hard + +"@storybook/builder-webpack5@npm:9.1.17": + version: 9.1.17 + resolution: "@storybook/builder-webpack5@npm:9.1.17" + dependencies: + "@storybook/core-webpack": "npm:9.1.17" + case-sensitive-paths-webpack-plugin: "npm:^2.4.0" + cjs-module-lexer: "npm:^1.2.3" + css-loader: "npm:^6.7.1" + es-module-lexer: "npm:^1.5.0" + fork-ts-checker-webpack-plugin: "npm:^8.0.0" + html-webpack-plugin: "npm:^5.5.0" + magic-string: "npm:^0.30.5" + style-loader: "npm:^3.3.1" + terser-webpack-plugin: "npm:^5.3.1" + ts-dedent: "npm:^2.0.0" + webpack: "npm:5" + webpack-dev-middleware: "npm:^6.1.2" + webpack-hot-middleware: "npm:^2.25.1" + webpack-virtual-modules: "npm:^0.6.0" + peerDependencies: + storybook: ^9.1.17 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/92071488b6d8a696e1f704ba710ff5ab56a3f13e730179cf6a5a201a4489878dce2d42c97231db614f552e96727d256b8cf2730e406cb99e1756c47fce65ad34 + languageName: node + linkType: hard + +"@storybook/core-webpack@npm:9.1.17": + version: 9.1.17 + resolution: "@storybook/core-webpack@npm:9.1.17" + dependencies: + ts-dedent: "npm:^2.0.0" + peerDependencies: + storybook: ^9.1.17 + checksum: 10/67c7d9cd31ec4ab1f00ec618ef6ad803562f464854755450f4cd29e922040b73bb08a5c4b798822fea67ff864c4c45ec4b1944e8fcba44c3eae5fcb49555a4fb + languageName: node + linkType: hard + +"@storybook/csf-plugin@npm:9.1.17": + version: 9.1.17 + resolution: "@storybook/csf-plugin@npm:9.1.17" + dependencies: + unplugin: "npm:^1.3.1" + peerDependencies: + storybook: ^9.1.17 + checksum: 10/64236ee22130bf23998b933dc6d630225eac5eedacb0ce61356ee746e3ce5ee84717251021ff8e38f9c9d76bc190a74f1a361bfd0e16e75e3ff624aed7b2e7d2 + languageName: node + linkType: hard + +"@storybook/global@npm:^5.0.0, @storybook/global@npm:~5.0.0": + version: 5.0.0 + resolution: "@storybook/global@npm:5.0.0" + checksum: 10/0e7b495f4fe7f36447e793926f1c0460ec07fd66f0da68e3150da5878f6043c9eeb9b41614a45c5ec0d48d5d383c59ca8f88b6dc7882a2a784ac9b20375d8edb + languageName: node + linkType: hard + +"@storybook/icons@npm:^1.4.0": + version: 1.6.0 + resolution: "@storybook/icons@npm:1.6.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + checksum: 10/f9036ca3b0d2904778eb4e202305f2780b549434380f9760f0bc704fe3ee19d7332f9560a66435ebb2156346cee9a863e40fa5e4b27790bf993b0c1180a3146d + languageName: node + linkType: hard + +"@storybook/icons@npm:~1.2.12": + version: 1.2.12 + resolution: "@storybook/icons@npm:1.2.12" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 10/5df56f0856764ed7e4bb24ef7a08a8a9c93f8eedcb16dac062f1dfd3bd1fe6cb4a0aa5a0794083d95e31c04960d126a4d2028cfb4c53681bf05513bb38eae9d2 + languageName: node + linkType: hard + +"@storybook/preset-react-webpack@npm:9.1.17": + version: 9.1.17 + resolution: "@storybook/preset-react-webpack@npm:9.1.17" + dependencies: + "@storybook/core-webpack": "npm:9.1.17" + "@storybook/react-docgen-typescript-plugin": "npm:1.0.6--canary.9.0c3f3b7.0" + "@types/semver": "npm:^7.3.4" + find-up: "npm:^7.0.0" + magic-string: "npm:^0.30.5" + react-docgen: "npm:^7.1.1" + resolve: "npm:^1.22.8" + semver: "npm:^7.3.7" + tsconfig-paths: "npm:^4.2.0" + webpack: "npm:5" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.1.17 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/fc86bcc956494bba2f21511048c6326c9358b5660bed480167ff5fc2be39b6e4f367cfa4a63e340158bd2303b956439e2ccb456f18fd57d0579108695a6bec20 + languageName: node + linkType: hard + +"@storybook/react-docgen-typescript-plugin@npm:1.0.6--canary.9.0c3f3b7.0": + version: 1.0.6--canary.9.0c3f3b7.0 + resolution: "@storybook/react-docgen-typescript-plugin@npm:1.0.6--canary.9.0c3f3b7.0" + dependencies: + debug: "npm:^4.1.1" + endent: "npm:^2.0.1" + find-cache-dir: "npm:^3.3.1" + flat-cache: "npm:^3.0.4" + micromatch: "npm:^4.0.2" + react-docgen-typescript: "npm:^2.2.2" + tslib: "npm:^2.0.0" + peerDependencies: + typescript: ">= 4.x" + webpack: ">= 4" + checksum: 10/5d3c64b022d10d7316b600a41eec2cf38ba460e11bf9a01ae976e8d0efe6959633423d2a3546a5d3f9f04bcc946e83774e8efdf9ccfedb76a7065ec08e7ec809 + languageName: node + linkType: hard + +"@storybook/react-dom-shim@npm:9.1.17": + version: 9.1.17 + resolution: "@storybook/react-dom-shim@npm:9.1.17" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.1.17 + checksum: 10/ef0d4eeca593db889d4e78ed1b5a353012cfdec3ee0f73ca31abbe207952cdedc0c291d4ba5bd6d7529f5ea430dc3a90e631844534d2aa972b45c4cde11ae33d + languageName: node + linkType: hard + +"@storybook/react-webpack5@npm:~9.1.17": + version: 9.1.17 + resolution: "@storybook/react-webpack5@npm:9.1.17" + dependencies: + "@storybook/builder-webpack5": "npm:9.1.17" + "@storybook/preset-react-webpack": "npm:9.1.17" + "@storybook/react": "npm:9.1.17" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.1.17 + typescript: ">= 4.9.x" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/b4a4a815e69ce0dc51d4e8d885597f4f0faf8b533c3f994dfd32ddffdd07c83da3f0994b3fbd5a7c448c12bb6e68673d4cdc50d6d303906a0e05801e2f0d54d5 + languageName: node + linkType: hard + +"@storybook/react@npm:9.1.17": + version: 9.1.17 + resolution: "@storybook/react@npm:9.1.17" + dependencies: + "@storybook/global": "npm:^5.0.0" + "@storybook/react-dom-shim": "npm:9.1.17" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.1.17 + typescript: ">= 4.9.x" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/8b6be8cc93c53ce2ccc942a54ae76f4f3652060887f46649f8ea1311fe271753435d675ac3aae76ec2f41a8f8bb0c6a88389d2bbb9a2e6875f2a0b2e655df63a + languageName: node + linkType: hard + +"@swc/core-darwin-arm64@npm:1.13.20": + version: 1.13.20 + resolution: "@swc/core-darwin-arm64@npm:1.13.20" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@swc/core-darwin-x64@npm:1.13.20": + version: 1.13.20 + resolution: "@swc/core-darwin-x64@npm:1.13.20" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@swc/core-linux-arm-gnueabihf@npm:1.13.20": + version: 1.13.20 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.13.20" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@swc/core-linux-arm64-gnu@npm:1.13.20": + version: 1.13.20 + resolution: "@swc/core-linux-arm64-gnu@npm:1.13.20" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@swc/core-linux-arm64-musl@npm:1.13.20": + version: 1.13.20 + resolution: "@swc/core-linux-arm64-musl@npm:1.13.20" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@swc/core-linux-x64-gnu@npm:1.13.20": + version: 1.13.20 + resolution: "@swc/core-linux-x64-gnu@npm:1.13.20" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@swc/core-linux-x64-musl@npm:1.13.20": + version: 1.13.20 + resolution: "@swc/core-linux-x64-musl@npm:1.13.20" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@swc/core-win32-arm64-msvc@npm:1.13.20": + version: 1.13.20 + resolution: "@swc/core-win32-arm64-msvc@npm:1.13.20" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@swc/core-win32-ia32-msvc@npm:1.13.20": + version: 1.13.20 + resolution: "@swc/core-win32-ia32-msvc@npm:1.13.20" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@swc/core-win32-x64-msvc@npm:1.13.20": + version: 1.13.20 + resolution: "@swc/core-win32-x64-msvc@npm:1.13.20" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@swc/core@npm:^1.13.5": + version: 1.13.20 + resolution: "@swc/core@npm:1.13.20" + dependencies: + "@swc/core-darwin-arm64": "npm:1.13.20" + "@swc/core-darwin-x64": "npm:1.13.20" + "@swc/core-linux-arm-gnueabihf": "npm:1.13.20" + "@swc/core-linux-arm64-gnu": "npm:1.13.20" + "@swc/core-linux-arm64-musl": "npm:1.13.20" + "@swc/core-linux-x64-gnu": "npm:1.13.20" + "@swc/core-linux-x64-musl": "npm:1.13.20" + "@swc/core-win32-arm64-msvc": "npm:1.13.20" + "@swc/core-win32-ia32-msvc": "npm:1.13.20" + "@swc/core-win32-x64-msvc": "npm:1.13.20" + "@swc/counter": "npm:^0.1.3" + "@swc/types": "npm:^0.1.25" + peerDependencies: + "@swc/helpers": ">=0.5.17" + dependenciesMeta: + "@swc/core-darwin-arm64": + optional: true + "@swc/core-darwin-x64": + optional: true + "@swc/core-linux-arm-gnueabihf": + optional: true + "@swc/core-linux-arm64-gnu": + optional: true + "@swc/core-linux-arm64-musl": + optional: true + "@swc/core-linux-x64-gnu": + optional: true + "@swc/core-linux-x64-musl": + optional: true + "@swc/core-win32-arm64-msvc": + optional: true + "@swc/core-win32-ia32-msvc": + optional: true + "@swc/core-win32-x64-msvc": + optional: true + peerDependenciesMeta: + "@swc/helpers": + optional: true + checksum: 10/fdbd34f98d894cc765e44bab27da5fa95e604660ae061f5eafeaf459418e1d32e8b8f2333966a6c32fef4770e9948f9cec3bdacc8a35d6b1b32bc48677777261 + languageName: node + linkType: hard + +"@swc/counter@npm:^0.1.3": + version: 0.1.3 + resolution: "@swc/counter@npm:0.1.3" + checksum: 10/df8f9cfba9904d3d60f511664c70d23bb323b3a0803ec9890f60133954173047ba9bdeabce28cd70ba89ccd3fd6c71c7b0bd58be85f611e1ffbe5d5c18616598 + languageName: node + linkType: hard + +"@swc/helpers@npm:^0.5.0": + version: 0.5.15 + resolution: "@swc/helpers@npm:0.5.15" + dependencies: + tslib: "npm:^2.8.0" + checksum: 10/e3f32c6deeecfb0fa3f22edff03a7b358e7ce16d27b0f1c8b5cdc3042c5c4ce4da6eac0b781ab7cc4f54696ece657467d56734fb26883439fb00017385364c4c + languageName: node + linkType: hard + +"@swc/types@npm:^0.1.25": + version: 0.1.25 + resolution: "@swc/types@npm:0.1.25" + dependencies: + "@swc/counter": "npm:^0.1.3" + checksum: 10/f6741450224892d12df43e5ca7f3cc0287df644dcd672626eb0cc2a3a8e3e875f4b29eb11336f37c7240cf6e010ba59eb3a79f4fb8bee5cbd168dfc1326ff369 + languageName: node + linkType: hard + +"@tamagui/accordion@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/accordion@npm:2.0.0-rc.31" + dependencies: + "@tamagui/collapsible": "npm:2.0.0-rc.31" + "@tamagui/collection": "npm:2.0.0-rc.31" + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/polyfill-dev": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/text": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + "@tamagui/use-direction": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/e78e7090cf979cef1533f5af7b026ec3a5d409f7ad8325733bc0b8540700a1c8b19a85eefe7559d531a0274c037b321ffed370931357d4e261b2f611dc5df2b4 + languageName: node + linkType: hard + +"@tamagui/adapt@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/adapt@npm:2.0.0-rc.31" + dependencies: + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/portal": "npm:2.0.0-rc.31" + "@tamagui/z-index-stack": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/d00e61b5fa27b6ff05c2a3b0e3fa134b05e753af1ee17bd8efde4d241c8b4a9183fd7bd43ad2a73d2e7d6e3a7bc8ceaa0579071b847df87382e0b2416c790e0a + languageName: node + linkType: hard + +"@tamagui/alert-dialog@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/alert-dialog@npm:2.0.0-rc.31" + dependencies: + "@tamagui/animate-presence": "npm:2.0.0-rc.31" + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/dialog": "npm:2.0.0-rc.31" + "@tamagui/dismissable": "npm:2.0.0-rc.31" + "@tamagui/focus-scope": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/polyfill-dev": "npm:2.0.0-rc.31" + "@tamagui/portal": "npm:2.0.0-rc.31" + "@tamagui/remove-scroll": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/text": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/1ee7fab4aabb660e3bee1c43338597b41ec4bea39bc9e15a85e064e91ced25b86c9dc59a036ce638627e498e353aace48a76d3a467266443dd7a65a3d470e023 + languageName: node + linkType: hard + +"@tamagui/animate-presence@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/animate-presence@npm:2.0.0-rc.31" + dependencies: + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/use-constant": "npm:2.0.0-rc.31" + "@tamagui/use-force-update": "npm:2.0.0-rc.31" + "@tamagui/use-presence": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/396b7035b4ef8b09b013045a6f677ea411777561daadb968c8ce1487fa9c4c996a04a3b99c81352c35fdaec02a691c0264226f26a30dd2b529c091896c4c79b9 + languageName: node + linkType: hard + +"@tamagui/animate@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/animate@npm:2.0.0-rc.31" + dependencies: + "@tamagui/animate-presence": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/ebbaed1380c72420404b4508debb2a3cacb98819895f0462a384b4151230ff0eb58ba323abf607b69887f4aed5e16d0123dcff3b29d1e50edb40a0366bd48ca2 + languageName: node + linkType: hard + +"@tamagui/animation-helpers@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/animation-helpers@npm:2.0.0-rc.31" + checksum: 10/6b8e4c6e6b24c48123a6cfe6eee600cf048c3af643d3dfd75ea774904e0ce4302563414440eb8463390ebaf5ec5cb494e62b24448e2727bf8259c4e914337f55 + languageName: node + linkType: hard + +"@tamagui/animations-css@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/animations-css@npm:2.0.0-rc.31" + dependencies: + "@tamagui/animation-helpers": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/cubic-bezier-animator": "npm:2.0.0-rc.31" + "@tamagui/use-presence": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-dom: "*" + checksum: 10/21c678229fa5bc095416a693cdc06f05d3388b4b7f95bce37e4dd46ab508ae31e413e2b0bae85442b4a6ffc8e09eedaf82f212cb0f833ac0139ff5ecfa2d4bec + languageName: node + linkType: hard + +"@tamagui/animations-react-native@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/animations-react-native@npm:2.0.0-rc.31" + dependencies: + "@tamagui/animation-helpers": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/use-presence": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/4e3e9e4700ce74e76671af66f4a003fa42cc02143f457cbb75d73c141609caf4f95b8a0818b2621ec9584645b5319b6ccdac2d83b12978cc6a8b5d55501940f4 + languageName: node + linkType: hard + +"@tamagui/avatar@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/avatar@npm:2.0.0-rc.31" + dependencies: + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/image": "npm:2.0.0-rc.31" + "@tamagui/shapes": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/text": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/4310760233bb3cc9c2491c49afd4e777ece04d1de1e790f5e7a418287530bc1fc45498e796ca969dd25aad0b93cb7d3b164f248cc8c431c6b8ca8914b233d687 + languageName: node + linkType: hard + +"@tamagui/button@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/button@npm:2.0.0-rc.31" + dependencies: + "@tamagui/config-default": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/font-size": "npm:2.0.0-rc.31" + "@tamagui/get-button-sized": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/helpers-tamagui": "npm:2.0.0-rc.31" + "@tamagui/spacer": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/text": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/c82eec872d8d9f0c9abef9034aea25995db8e2f81e5ad17ec5e8796263af23edf061f39f98c4ab67fe90958fbb7e66ef2b0ff62d3fd56f3cbafebf41d8a1ad06 + languageName: node + linkType: hard + +"@tamagui/card@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/card@npm:2.0.0-rc.31" + dependencies: + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/6952a8a90ce6447f73cbf813a1f9187c02fe34a01b2705cdcd9601a71280c6274078b3d2ce984a21918b320fd4c166a157d2eaa5a8f25021120b9d6a90b477c6 + languageName: node + linkType: hard + +"@tamagui/checkbox-headless@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/checkbox-headless@npm:2.0.0-rc.31" + dependencies: + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/focusable": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/label": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + "@tamagui/use-previous": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/72d6063d74b9abfcc469e9e76f8254039492ea6abd158d6e4b63c265f7a477687c2f42adff0d709f5fcc4be835471245ad99c7ceb6c78757c35b1f5e23e399d0 + languageName: node + linkType: hard + +"@tamagui/checkbox@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/checkbox@npm:2.0.0-rc.31" + dependencies: + "@tamagui/checkbox-headless": "npm:2.0.0-rc.31" + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/focusable": "npm:2.0.0-rc.31" + "@tamagui/font-size": "npm:2.0.0-rc.31" + "@tamagui/get-token": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/helpers-tamagui": "npm:2.0.0-rc.31" + "@tamagui/label": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + "@tamagui/use-previous": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/fa792f1c0e414d612c698aabfbd1849765d35d753deaa85f404f7d08043c01a19090d58c20a2b445a5bc28bf99c5a97ac9af788a767b2bc69db170b120a7b0fd + languageName: node + linkType: hard + +"@tamagui/collapsible@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/collapsible@npm:2.0.0-rc.31" + dependencies: + "@tamagui/animate-presence": "npm:2.0.0-rc.31" + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/polyfill-dev": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/2f89895e01158701d1e5a6cd9a466caf37c37a0b10a7c77ba68a5a08d2cd6699dffb3a099f3275c6fa962eb484b27d411e965d496580dd45629cf641ff59c3b7 + languageName: node + linkType: hard + +"@tamagui/collection@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/collection@npm:2.0.0-rc.31" + dependencies: + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/polyfill-dev": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/5c82e7433cd0da80f7273b522dc646b328347f95feb2f464ddd23607a3e254d294f975d1bd2a6b666ed12b52f9259dd6960c8881bde9c4a0efee2320f9676107 + languageName: node + linkType: hard + +"@tamagui/compose-refs@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/compose-refs@npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/0e6d050febd76917a5335ed719a8625fdb7a5db19a7f8442126d437a462f3b462e4a3f31520faaa730555ff11a009129f8233a6098eb187af3ecd864a7722c8f + languageName: node + linkType: hard + +"@tamagui/config-default@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/config-default@npm:2.0.0-rc.31" + dependencies: + "@tamagui/animations-css": "npm:2.0.0-rc.31" + "@tamagui/animations-react-native": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/shorthands": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + checksum: 10/e30b6d8138e47e0dc781eabf790ed61c00ca3e93acf5c7b2203357f5578498f0158858eb1863acfe22aa0eb3b4b8ca619e4ece881918c1771f883de3d1326742 + languageName: node + linkType: hard + +"@tamagui/constants@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/constants@npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/3f26e3c12254a92802da7a55ef97bbbffb6958c1aae673d75065fd7d7f59254541da317587284ad020075e78877730b5c2776a1d2c6fe5d197e298914bad649c + languageName: node + linkType: hard + +"@tamagui/context-menu@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/context-menu@npm:2.0.0-rc.31" + dependencies: + "@tamagui/create-menu": "npm:2.0.0-rc.31" + "@tamagui/popover": "npm:2.0.0-rc.31" + "@tamagui/use-callback-ref": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/ec7e994c34e8d289a2bb1ba888381201a1fa21bf131ce52146a469cb711a0e16ac65897b874a05f977688b097b6dcde3b6cf92f1de0053c5f1cc58732742e625 + languageName: node + linkType: hard + +"@tamagui/core@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/core@npm:2.0.0-rc.31" + dependencies: + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/react-native-media-driver": "npm:2.0.0-rc.31" + "@tamagui/react-native-use-pressable": "npm:2.0.0-rc.31" + "@tamagui/use-element-layout": "npm:2.0.0-rc.31" + "@tamagui/use-event": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/922918333e08a2edd26ac469a38a4ec94b11108fbef753fa78395beef15f482c7ee89f8e4a6acc5a5a3c429379f411d1d7209e183cd9625a897c5a3f4e4a00fb + languageName: node + linkType: hard + +"@tamagui/create-context@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/create-context@npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/058381cf4db414c28b1d9778df45d0ea37b1c287b27c1d1f83daa4376d6c948cc376582a8aa4bee58757e85d77d7801da1da515b635eb25d12382b19fcd71429 + languageName: node + linkType: hard + +"@tamagui/create-menu@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/create-menu@npm:2.0.0-rc.31" + dependencies: + "@tamagui/animate": "npm:2.0.0-rc.31" + "@tamagui/animate-presence": "npm:2.0.0-rc.31" + "@tamagui/collection": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/dismissable": "npm:2.0.0-rc.31" + "@tamagui/focus-guard": "npm:2.0.0-rc.31" + "@tamagui/focus-scope": "npm:2.0.0-rc.31" + "@tamagui/get-token": "npm:2.0.0-rc.31" + "@tamagui/image": "npm:2.0.0-rc.31" + "@tamagui/native": "npm:2.0.0-rc.31" + "@tamagui/popover": "npm:2.0.0-rc.31" + "@tamagui/popper": "npm:2.0.0-rc.31" + "@tamagui/portal": "npm:2.0.0-rc.31" + "@tamagui/remove-scroll": "npm:2.0.0-rc.31" + "@tamagui/roving-focus": "npm:2.0.0-rc.31" + "@tamagui/text": "npm:2.0.0-rc.31" + "@tamagui/use-callback-ref": "npm:2.0.0-rc.31" + "@tamagui/use-direction": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/d198e4ebfd5167eca71d6e5e84559b3cc03b566c2a69014621debab2852e2988b500c12995b8c77e2f7713b9bdb851acb56b8b6373b13aa9b37cdab5495e1733 + languageName: node + linkType: hard + +"@tamagui/cubic-bezier-animator@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/cubic-bezier-animator@npm:2.0.0-rc.31" + checksum: 10/e17ec507ebce545bbb54a4545a116c82ae9e83124946dab3626217cc3243c9d1ec8059a6224017116d3e8ef677245ccd2024388226ed48ef796154b4eba103ac + languageName: node + linkType: hard + +"@tamagui/dialog@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/dialog@npm:2.0.0-rc.31" + dependencies: + "@tamagui/adapt": "npm:2.0.0-rc.31" + "@tamagui/animate": "npm:2.0.0-rc.31" + "@tamagui/animate-presence": "npm:2.0.0-rc.31" + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/dismissable": "npm:2.0.0-rc.31" + "@tamagui/focus-scope": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/polyfill-dev": "npm:2.0.0-rc.31" + "@tamagui/portal": "npm:2.0.0-rc.31" + "@tamagui/remove-scroll": "npm:2.0.0-rc.31" + "@tamagui/sheet": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/text": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + "@tamagui/z-index-stack": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/6cd7344dc007d5f455bed21025cba807e48eaa7e351c062eeaabf4cdd9e1d6de732154701615ff5dcb6eae6ef62f21b903bec4baf1c7e6c2c29f1b6ece9fb29d + languageName: node + linkType: hard + +"@tamagui/dismissable@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/dismissable@npm:2.0.0-rc.31" + dependencies: + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/use-escape-keydown": "npm:2.0.0-rc.31" + "@tamagui/use-event": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-dom: "*" + checksum: 10/4c75abb1597243303212c4897b4a355364df56399af27a530435678148b9a58d003220f4987b3b8c2bef002595e2ba697da90843f03349b53926eeb240cf2bc7 + languageName: node + linkType: hard + +"@tamagui/element@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/element@npm:2.0.0-rc.31" + dependencies: + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10/066f4239f1c3d22ea343e12bc4ba057d8c0bddbfc6bd4e826157f2aac87f9e24569c9df1ad139a664e5ff6693c56d13b4961a4703d7f2fdeb9c5db5aeb485555 + languageName: node + linkType: hard + +"@tamagui/elements@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/elements@npm:2.0.0-rc.31" + dependencies: + "@tamagui/core": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/a5960d1df5d7b35b80ccd0118e463d910bb202997a74cde056e5c6144133649ee35807aa9f54d938ad4fe9802ca3746d13aa699f7e8aa4cf6c56663578d6fce1 + languageName: node + linkType: hard + +"@tamagui/fake-react-native@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/fake-react-native@npm:2.0.0-rc.31" + checksum: 10/33057936d715075bc038020cf85d855ed8fd10e3476dff330621751dc2666aa34ea64d9d168c9c74cdeb1ef77ca482f368eced5ee13bea89cb144fd637ac0915 + languageName: node + linkType: hard + +"@tamagui/floating@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/floating@npm:2.0.0-rc.31" + dependencies: + "@floating-ui/react-dom": "npm:^2.1.6" + "@floating-ui/react-native": "npm:^0.10.7" + "@tamagui/use-event": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/1fc12d322ad3fe0297557667316c4560ee443401c24aff322e8f64510f2b211dcedf7bd832b520d146bb8cba43710153a4e0f7b641fb70d178aee09d809a9cee + languageName: node + linkType: hard + +"@tamagui/focus-guard@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/focus-guard@npm:2.0.0-rc.31" + dependencies: + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/use-event": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/4fe242abb631c9d2e36b2a41d6bab2c25aa21efef685215afa0494c67a32f85044ccf4643dfda86f40c39e6ad4c2239eee05928f4a7b15258ce3110074345790 + languageName: node + linkType: hard + +"@tamagui/focus-scope@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/focus-scope@npm:2.0.0-rc.31" + dependencies: + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/start-transition": "npm:2.0.0-rc.31" + "@tamagui/use-async": "npm:2.0.0-rc.31" + "@tamagui/use-event": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/3b7a7420c5c86870f79eced2c32ab97b367b65a978aaa854b6657a97c93e436e766ea04c436a79199e327cd671fe021cff5797d60ed75fb55fcb3edcc58d1261 + languageName: node + linkType: hard + +"@tamagui/focusable@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/focusable@npm:2.0.0-rc.31" + dependencies: + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/858a6e50d5d353a9bb4d23156eba32c5aef021f90eb1da190aadac397ea1bbe99145cb1e8a0fd7b7d760923fbbf3372b66af1ee0cae2dfb207df3190681c039e + languageName: node + linkType: hard + +"@tamagui/font-size@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/font-size@npm:2.0.0-rc.31" + dependencies: + "@tamagui/core": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/3e3160f088aa9f069eb68939bb81483d60b41d8121963aa6e6467d0888b2efee71f63044c7c3748cb1a2c3bf960bc625b7abff5012ca27fb0cb03f429794511e + languageName: node + linkType: hard + +"@tamagui/form@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/form@npm:2.0.0-rc.31" + dependencies: + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/4e7a5221f97a5a8ff29d5ac8a5e0e0e4fc0979784ad6d4cf7a6d5b8164e28b2ef1c21c259523ee69254c246bae80bca906999ae320e3a586df1495c038165248 + languageName: node + linkType: hard + +"@tamagui/get-button-sized@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/get-button-sized@npm:2.0.0-rc.31" + dependencies: + "@tamagui/get-token": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/4aad701671b3dcd86ae6bd21f3a1d9ab7cb7e7ad5a56f4f0caa9e843e248072760e73da5220b90d1be156a68485d6695c04f34125e7f800a09db9084c4a5bf5f + languageName: node + linkType: hard + +"@tamagui/get-font-sized@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/get-font-sized@npm:2.0.0-rc.31" + dependencies: + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/0c5b9dad6ae000a639a51a09c53c4f858e9350b779f48c75d148b4b4e581ebca3d9e7eab071283d309561b00f5084051083e5f4962d30a6c7ddde9dcab39ae01 + languageName: node + linkType: hard + +"@tamagui/get-token@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/get-token@npm:2.0.0-rc.31" + dependencies: + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/cd8eddea1e7c6d2122c19d2b946653b7650730f0af89f513b607aaa8a99b1a34971cbb002fbc50a5b523c7aad6ebc9e98ddba81595d6ff165e019eefa5d2e385 + languageName: node + linkType: hard + +"@tamagui/group@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/group@npm:2.0.0-rc.31" + dependencies: + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/spacer": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/c0139bbac524b2132cd06f171003c09edd06507d11eaa34000f0d8dd96b79ebaf2bac82cba7ca80ef11aeb42d402d3b3b31fa65815ba9033b280684154e0fbdf + languageName: node + linkType: hard + +"@tamagui/helpers-tamagui@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/helpers-tamagui@npm:2.0.0-rc.31" + dependencies: + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/6693d3939ddcbd9cda0c093e1db2a4c934e15594a00b1162bd60611d65b76f2cdb9e4c6680ec28148894ce81816189878202e6d42d6685f6859c23d6207c760e + languageName: node + linkType: hard + +"@tamagui/helpers@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/helpers@npm:2.0.0-rc.31" + dependencies: + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/simple-hash": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/27f43e744520941a8cc3df24f2d40380a5d51331561724cbc123b73dd67e1c6af641b76db1ba679e84e843e77faffc4eb6842ce52ca0777a3c85c006ae0d2382 + languageName: node + linkType: hard + +"@tamagui/image@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/image@npm:2.0.0-rc.31" + dependencies: + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/0019aef5a13ef0ae871f44861a2df21a7da0db8284629abe0d84c213bccde823f97c880658653c153286bc39e8efa8f004b5b06275580d44339eeba0ab8a90be + languageName: node + linkType: hard + +"@tamagui/input@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/input@npm:2.0.0-rc.31" + dependencies: + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/element": "npm:2.0.0-rc.31" + "@tamagui/focusable": "npm:2.0.0-rc.31" + "@tamagui/font-size": "npm:2.0.0-rc.31" + "@tamagui/get-button-sized": "npm:2.0.0-rc.31" + "@tamagui/get-font-sized": "npm:2.0.0-rc.31" + "@tamagui/get-token": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/helpers-tamagui": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/text": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/9fda4c12a2b81073c962ac796ba00ff8d34d483b43ccb698889658db67d4f35d877e2b172293884e51cf986c0f60a12c782494c6f1ec5ddab96bf2fc52cbbbec + languageName: node + linkType: hard + +"@tamagui/is-equal-shallow@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/is-equal-shallow@npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/8bc2e6791541b98f9f9f7e3d03a80b51dd2fe3ba5124f188f576ba80772bd20c8eeef1baf20c5e497e40b846f9f1b5b51cca3e99eb976e0212f3d5d9c736627f + languageName: node + linkType: hard + +"@tamagui/label@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/label@npm:2.0.0-rc.31" + dependencies: + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/focusable": "npm:2.0.0-rc.31" + "@tamagui/get-button-sized": "npm:2.0.0-rc.31" + "@tamagui/get-font-sized": "npm:2.0.0-rc.31" + "@tamagui/text": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/7693a28310980c2c0e6e45845df4d4bc66018ac8bcd340f13271fb7d07cc7ea632fdaf79e215a4d102905006740028d6ac7d4ddef2743af3a2d6f4067daeba68 + languageName: node + linkType: hard + +"@tamagui/linear-gradient@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/linear-gradient@npm:2.0.0-rc.31" + dependencies: + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/native": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/ea63075e1c0de97dd3f79f6251d74fbeeb20741174a35a19ba3721a0c26ec9453c7d7b691571dd20aaf44ede291907d24d46531e305d16906bc1647f0f9b6742 + languageName: node + linkType: hard + +"@tamagui/list-item@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/list-item@npm:2.0.0-rc.31" + dependencies: + "@tamagui/font-size": "npm:2.0.0-rc.31" + "@tamagui/get-font-sized": "npm:2.0.0-rc.31" + "@tamagui/get-token": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/helpers-tamagui": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/text": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/2824baa7aea395dfc61b9a7f7ab31f81f4e1d007db31a29c4e5eb3c461f2d3f5b8754740586eb8cb9394a7a6c5f1d11d61a21ff1ba2e61e51d577650c5b5cc91 + languageName: node + linkType: hard + +"@tamagui/menu@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/menu@npm:2.0.0-rc.31" + dependencies: + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/create-menu": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/popover": "npm:2.0.0-rc.31" + "@tamagui/popper": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/1ee5e89819c309fd524a1c34cd0fc226d24acef50c24d97a3eb1a3f8dd4513bbd1cc8baef6e44d9282702df9f38f32b0336dd22fe928819808b26182daa5d03d + languageName: node + linkType: hard + +"@tamagui/native@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/native@npm:2.0.0-rc.31" + peerDependencies: + "@react-native-menu/menu": ">=2.0.0" + burnt: ">=0.12.0" + react: "*" + react-native: "*" + react-native-gesture-handler: ">=2.0.0" + react-native-ios-context-menu: ">=3.0.0" + react-native-ios-utilities: ">=5.0.0" + react-native-keyboard-controller: ">=1.0.0" + react-native-safe-area-context: ">=4.0.0" + react-native-teleport: ">=0.5.0" + react-native-worklets-core: ">=1.0.0" + sf-symbols-typescript: "*" + zeego: ">=3.0.0" + peerDependenciesMeta: + "@react-native-menu/menu": + optional: true + burnt: + optional: true + react-native-gesture-handler: + optional: true + react-native-ios-context-menu: + optional: true + react-native-ios-utilities: + optional: true + react-native-keyboard-controller: + optional: true + react-native-safe-area-context: + optional: true + react-native-teleport: + optional: true + react-native-worklets-core: + optional: true + sf-symbols-typescript: + optional: true + zeego: + optional: true + checksum: 10/b3b90757116bfa63587f536e7a8f9f8b8cbdf837662e750060b29b9cf1dd212429f059113371b92076f516d0018f1f6dd678dc2fc5f0814737bd81c2527206de + languageName: node + linkType: hard + +"@tamagui/normalize-css-color@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/normalize-css-color@npm:2.0.0-rc.31" + dependencies: + "@react-native/normalize-color": "npm:^2.1.0" + checksum: 10/a0ea2ba845bbcabce213677a46901a443673517855a8043c2d0e6ca3789976983fc970157a740626b3b5f6dc1cb796bbb1768510e0e04a796e1d478465c23a85 + languageName: node + linkType: hard + +"@tamagui/polyfill-dev@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/polyfill-dev@npm:2.0.0-rc.31" + checksum: 10/c0b5027edadbe67fc3ba413f2a0ddc4697d61e7c1af309a902112c625f226374ea69b9cf984572c309f1b2ec6ed81a6cb03089c2776e85229bce939a93180aba + languageName: node + linkType: hard + +"@tamagui/popover@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/popover@npm:2.0.0-rc.31" + dependencies: + "@tamagui/adapt": "npm:2.0.0-rc.31" + "@tamagui/animate": "npm:2.0.0-rc.31" + "@tamagui/animate-presence": "npm:2.0.0-rc.31" + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/dismissable": "npm:2.0.0-rc.31" + "@tamagui/floating": "npm:2.0.0-rc.31" + "@tamagui/focus-scope": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/polyfill-dev": "npm:2.0.0-rc.31" + "@tamagui/popper": "npm:2.0.0-rc.31" + "@tamagui/portal": "npm:2.0.0-rc.31" + "@tamagui/remove-scroll": "npm:2.0.0-rc.31" + "@tamagui/scroll-view": "npm:2.0.0-rc.31" + "@tamagui/sheet": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + "@tamagui/z-index-stack": "npm:2.0.0-rc.31" + react-freeze: "npm:^1.0.3" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/b9b0bd3799790d13c9427f9950d04da9fa1f4aec187012d4b5bbe43f1e6b7a025590577c354a36ad0a581ce1a35be3fefb8ee06096e6c7cb7a4fe3d9fa7dcc89 + languageName: node + linkType: hard + +"@tamagui/popper@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/popper@npm:2.0.0-rc.31" + dependencies: + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/floating": "npm:2.0.0-rc.31" + "@tamagui/get-token": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/start-transition": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/08e7162a3fb0d229e7b1895e6223d15b04b9786c10cc65ac565d7b2299dbfe55016cc8611ecbd2f1e1ec59d2f7b1ae5cd907a2744c72c3ed686f6d3591a15f0d + languageName: node + linkType: hard + +"@tamagui/portal@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/portal@npm:2.0.0-rc.31" + dependencies: + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/native": "npm:2.0.0-rc.31" + "@tamagui/start-transition": "npm:2.0.0-rc.31" + "@tamagui/use-event": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + "@tamagui/z-index-stack": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-dom: "*" + react-native: "*" + checksum: 10/5ab1908be8dab90b6aa282f51b5584bccc3975b690132d38a9bb41afea4ffad9d9f2606bc756a255e501a5d32dd20146f62f465b26a7f9ea24f3bee42c29add6 + languageName: node + linkType: hard + +"@tamagui/progress@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/progress@npm:2.0.0-rc.31" + dependencies: + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/get-token": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/c5db977634ab572c9789b7f45d1526b7eb2d8da9437e171ad5d35cca39ade1c44116ba4db4e3ed669036d61efedbd09a6eb693a9a5abb2320a30baa9ecc582dc + languageName: node + linkType: hard + +"@tamagui/radio-group@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/radio-group@npm:2.0.0-rc.31" + dependencies: + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/focusable": "npm:2.0.0-rc.31" + "@tamagui/get-token": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/label": "npm:2.0.0-rc.31" + "@tamagui/radio-headless": "npm:2.0.0-rc.31" + "@tamagui/roving-focus": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + "@tamagui/use-previous": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/4a6e7ebbdf8e04a592734775c059d29d979fd5ffbd8afd360ee8b222b66d455c9ce28f3b985002640e60d59b17e334840feb8d232980db2bca6dde8710c6e901 + languageName: node + linkType: hard + +"@tamagui/radio-headless@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/radio-headless@npm:2.0.0-rc.31" + dependencies: + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/focusable": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/label": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + "@tamagui/use-previous": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/1470f30a64b0cbe46b879916723826a6809fac810f195464d21810cfb549ad7a2f803e64825ccc33fe30ed10f4c4c402e19999cb8e0e3a4630535121af4c88fd + languageName: node + linkType: hard + +"@tamagui/react-native-media-driver@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/react-native-media-driver@npm:2.0.0-rc.31" + dependencies: + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react-native: "*" + checksum: 10/d611da6cbedb8d64159dde12fb227ca1944d91de77fcbce54d677bc6cae792d5037cfc3b30b4796ec4285a59828ec19b4a60f49390b89d3e350734575dc8c99b + languageName: node + linkType: hard + +"@tamagui/react-native-use-pressable@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/react-native-use-pressable@npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/b7d90ce6682c3ad9a2f6442ef4d8d5149d9530eac9fe173aa4796abc63cad0578d71f51822abffe73e1a8ac41a61cd02a06d9198b8b5538486aab05331e0c624 + languageName: node + linkType: hard + +"@tamagui/remove-scroll@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/remove-scroll@npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/c897db47df749792467b294b377cbae9ffe207b8deb23c540b687888a2dce9fad4158b3b09381ebc6468872da35cb6a31e82d00371310f7cd382e300df39bc0f + languageName: node + linkType: hard + +"@tamagui/roving-focus@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/roving-focus@npm:2.0.0-rc.31" + dependencies: + "@tamagui/collection": "npm:2.0.0-rc.31" + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + "@tamagui/use-direction": "npm:2.0.0-rc.31" + "@tamagui/use-event": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/356d3fb7b320f056e6212d98e823c8e1b651d3e5d269cfc3b5c7757eabc64e7d08b3b870b8c2d080263896b33cbb317d26d1e94bca01c54fb2e541527713e5c6 + languageName: node + linkType: hard + +"@tamagui/scroll-view@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/scroll-view@npm:2.0.0-rc.31" + dependencies: + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/681c835ab51002c78d00e0591b87c9cdc62f01f4cfaf8c00faee97cbee6fa6f152f11ab0447ec502b433481ef01126ff7231ccd28160a039d1d1a396a015e080 + languageName: node + linkType: hard + +"@tamagui/select@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/select@npm:2.0.0-rc.31" + dependencies: + "@tamagui/adapt": "npm:2.0.0-rc.31" + "@tamagui/animate-presence": "npm:2.0.0-rc.31" + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/dismissable": "npm:2.0.0-rc.31" + "@tamagui/floating": "npm:2.0.0-rc.31" + "@tamagui/focus-scope": "npm:2.0.0-rc.31" + "@tamagui/focusable": "npm:2.0.0-rc.31" + "@tamagui/get-token": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/list-item": "npm:2.0.0-rc.31" + "@tamagui/native": "npm:2.0.0-rc.31" + "@tamagui/portal": "npm:2.0.0-rc.31" + "@tamagui/remove-scroll": "npm:2.0.0-rc.31" + "@tamagui/separator": "npm:2.0.0-rc.31" + "@tamagui/sheet": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/text": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + "@tamagui/use-debounce": "npm:2.0.0-rc.31" + "@tamagui/use-event": "npm:2.0.0-rc.31" + "@tamagui/use-previous": "npm:2.0.0-rc.31" + "@tamagui/z-index-stack": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-dom: "*" + react-native: "*" + checksum: 10/4b041496dbb62d60e580442061e2794d2c66680d77b7fe077218e7092ec6c755373d658923f59e980e1219692d69f4f6654bf8675fb51589c008c8d88d19c554 + languageName: node + linkType: hard + +"@tamagui/separator@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/separator@npm:2.0.0-rc.31" + dependencies: + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/3238a8d1044bb5368f260a298c84436fb8c82292dab09d44c93c1ac635f9ee8d1e336aa00f0b917d0a4b78ff3bd31bf88fb323a93c8a1dab453b51ef2682189e + languageName: node + linkType: hard + +"@tamagui/shapes@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/shapes@npm:2.0.0-rc.31" + dependencies: + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/c02453ae418ef18a2a2b7dca0aa606d82351e6ab81ec65d46828d277a89c728432fe2dc1330614db891ee6814162e634a33e03b6ae059f08c744ab0e536e7d21 + languageName: node + linkType: hard + +"@tamagui/sheet@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/sheet@npm:2.0.0-rc.31" + dependencies: + "@tamagui/adapt": "npm:2.0.0-rc.31" + "@tamagui/animate-presence": "npm:2.0.0-rc.31" + "@tamagui/animations-react-native": "npm:2.0.0-rc.31" + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/native": "npm:2.0.0-rc.31" + "@tamagui/portal": "npm:2.0.0-rc.31" + "@tamagui/remove-scroll": "npm:2.0.0-rc.31" + "@tamagui/scroll-view": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/use-constant": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + "@tamagui/use-did-finish-ssr": "npm:2.0.0-rc.31" + "@tamagui/use-keyboard-visible": "npm:2.0.0-rc.31" + "@tamagui/z-index-stack": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + react-native-gesture-handler: ">=2.0.0" + peerDependenciesMeta: + react-native-gesture-handler: + optional: true + checksum: 10/43cd6eb1b63f9bde4040fb2a29c41b8928a94c899a72fe11d93ff2d5b9c3485c419af2bbb9626727dab75199d8fd7b4e692d7221181dff30f5e5b120f617528b + languageName: node + linkType: hard + +"@tamagui/shorthands@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/shorthands@npm:2.0.0-rc.31" + dependencies: + "@tamagui/web": "npm:2.0.0-rc.31" + checksum: 10/b5f3bc153e2edf3b0136f75fc28403f30ed43b8cd0d0e1c825517c40f95e313962ed76bd6241989f667ce61935e637cbbe72e0cebf646be4f5d7de9c7c41bc1b + languageName: node + linkType: hard + +"@tamagui/simple-hash@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/simple-hash@npm:2.0.0-rc.31" + checksum: 10/6ae05414d0e32fc13b762aa22372a3280dafc0a16c8de2abd35f0308100787572f07752db1d86ff4c53a8f5e7a7c0c0ca7a5dd963d6d6f0df41089c8ed146edf + languageName: node + linkType: hard + +"@tamagui/sizable-context@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/sizable-context@npm:2.0.0-rc.31" + dependencies: + "@tamagui/core": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/2a83894647e09b67ea671d929ad8a88494003afab2daae376f60267b6153fb380cba85d54e1bbefb41ac8996af467f2b09c57615c1bafc9e88e03c9546a1b7c9 + languageName: node + linkType: hard + +"@tamagui/slider@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/slider@npm:2.0.0-rc.31" + dependencies: + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/get-token": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + "@tamagui/use-debounce": "npm:2.0.0-rc.31" + "@tamagui/use-direction": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/12ae235cb557da7a1684108ab1503f2584faaebcba3407f33ddc05ee50c879be9c380907a62f2de0926c28af6453348dbc11a5c21b4c19429aaba7979565c82a + languageName: node + linkType: hard + +"@tamagui/spacer@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/spacer@npm:2.0.0-rc.31" + dependencies: + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/94cbf8ed90599c814b87d9d99c4b894c28dd83de9580c6efa57f52265652c28d4a28f653b91d31ff98c621e44d5426262a2b4e602cc134b0e32506ed65eff7dc + languageName: node + linkType: hard + +"@tamagui/spinner@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/spinner@npm:2.0.0-rc.31" + dependencies: + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/3a9b7e7b5a6a0c103f38cddd42a7d4aa0f62c9fc5acb5987a007a3369e5a0a3a63255a38394a3dc5ce64397c016127c8c831b0f2e91ea3ef16ef4c8051e77f36 + languageName: node + linkType: hard + +"@tamagui/stacks@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/stacks@npm:2.0.0-rc.31" + dependencies: + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/get-button-sized": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/5cabce8290fb049ce13068e270a680d0647eefae0bcc23e20ed499901609a26a9900b2895ea46c3b32ecedc14a0101e2ba6b07fe254c8d5e0d8fada5286b654c + languageName: node + linkType: hard + +"@tamagui/start-transition@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/start-transition@npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/4fd9d70718448fee539d68d42dda4123a186c57cdf5f1eb6c18ab65dc7f4fd5015d774e67502efa2b7d9ef9f6f395eb879c22c0a8b2ce01ede7e86673b53020c + languageName: node + linkType: hard + +"@tamagui/switch-headless@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/switch-headless@npm:2.0.0-rc.31" + dependencies: + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/label": "npm:2.0.0-rc.31" + "@tamagui/use-previous": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/d473048ac0acbf39debb412b202a7c4e738128f221ae780e68370e2df10ded5a300ea35737e47353ef8ffb5d68339b5dc69f332b9533be64910fe0605a0fa9c0 + languageName: node + linkType: hard + +"@tamagui/switch@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/switch@npm:2.0.0-rc.31" + dependencies: + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/focusable": "npm:2.0.0-rc.31" + "@tamagui/get-token": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/label": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/switch-headless": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + "@tamagui/use-previous": "npm:2.0.0-rc.31" peerDependencies: - storybook: ^9.0.0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 - checksum: 10/3c062c08254cf896030dded773e5f80e9471733a6b3c3d6afa467ff59fe991a06a52155b3ae9936f741beb9bacfd8f11ff908e38dc6ee31955db628e67260486 + react: ">=19" + react-native: "*" + checksum: 10/509e52868d376a1177323661edad0e2a3f52ec1e0f336aa9673c6bcab154776e8086b12d394f19e1f0fe16dda999c9a41c10ebef0ef79d05128463bb1998b42f languageName: node linkType: hard -"@storybook/builder-webpack5@npm:9.1.17": - version: 9.1.17 - resolution: "@storybook/builder-webpack5@npm:9.1.17" +"@tamagui/tabs@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/tabs@npm:2.0.0-rc.31" dependencies: - "@storybook/core-webpack": "npm:9.1.17" - case-sensitive-paths-webpack-plugin: "npm:^2.4.0" - cjs-module-lexer: "npm:^1.2.3" - css-loader: "npm:^6.7.1" - es-module-lexer: "npm:^1.5.0" - fork-ts-checker-webpack-plugin: "npm:^8.0.0" - html-webpack-plugin: "npm:^5.5.0" - magic-string: "npm:^0.30.5" - style-loader: "npm:^3.3.1" - terser-webpack-plugin: "npm:^5.3.1" - ts-dedent: "npm:^2.0.0" - webpack: "npm:5" - webpack-dev-middleware: "npm:^6.1.2" - webpack-hot-middleware: "npm:^2.25.1" - webpack-virtual-modules: "npm:^0.6.0" + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/element": "npm:2.0.0-rc.31" + "@tamagui/get-button-sized": "npm:2.0.0-rc.31" + "@tamagui/group": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/roving-focus": "npm:2.0.0-rc.31" + "@tamagui/sizable-context": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + "@tamagui/use-direction": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" peerDependencies: - storybook: ^9.1.17 - peerDependenciesMeta: - typescript: - optional: true - checksum: 10/92071488b6d8a696e1f704ba710ff5ab56a3f13e730179cf6a5a201a4489878dce2d42c97231db614f552e96727d256b8cf2730e406cb99e1756c47fce65ad34 + react: ">=19" + react-native: "*" + checksum: 10/b7baf5e9e5a82804e299c4664cab43a3c5d3658817c5d3c34b23f44993ba4f96eac1bd141791ed3f8d7b6fa104a2eda063f7d165bb7426f0e26047a69f0f55e1 languageName: node linkType: hard -"@storybook/core-webpack@npm:9.1.17": - version: 9.1.17 - resolution: "@storybook/core-webpack@npm:9.1.17" +"@tamagui/text@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/text@npm:2.0.0-rc.31" dependencies: - ts-dedent: "npm:^2.0.0" + "@tamagui/get-font-sized": "npm:2.0.0-rc.31" + "@tamagui/helpers-tamagui": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" peerDependencies: - storybook: ^9.1.17 - checksum: 10/67c7d9cd31ec4ab1f00ec618ef6ad803562f464854755450f4cd29e922040b73bb08a5c4b798822fea67ff864c4c45ec4b1944e8fcba44c3eae5fcb49555a4fb + react: ">=19" + checksum: 10/49ec211b5e25f60afb4f5bab1d7d7fe4004c3df9a150b72b0dd3c06fd3dd0639302494dd7f4361fd05dee1715ef19fc60d31999b9a3d0637c80ebeef503b6aa3 languageName: node linkType: hard -"@storybook/csf-plugin@npm:9.1.17": - version: 9.1.17 - resolution: "@storybook/csf-plugin@npm:9.1.17" +"@tamagui/theme@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/theme@npm:2.0.0-rc.31" dependencies: - unplugin: "npm:^1.3.1" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/start-transition": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" peerDependencies: - storybook: ^9.1.17 - checksum: 10/64236ee22130bf23998b933dc6d630225eac5eedacb0ce61356ee746e3ce5ee84717251021ff8e38f9c9d76bc190a74f1a361bfd0e16e75e3ff624aed7b2e7d2 + react: ">=19" + checksum: 10/e7797ea6ca9f8ca1e36afe6d44d327329f777082e10e4c91a57fd688926f224239bd6eea2bd2d91a08b1640433eeaa3a1c0a39f3bf6dfd66f4339099608ece1c languageName: node linkType: hard -"@storybook/global@npm:^5.0.0, @storybook/global@npm:~5.0.0": - version: 5.0.0 - resolution: "@storybook/global@npm:5.0.0" - checksum: 10/0e7b495f4fe7f36447e793926f1c0460ec07fd66f0da68e3150da5878f6043c9eeb9b41614a45c5ec0d48d5d383c59ca8f88b6dc7882a2a784ac9b20375d8edb +"@tamagui/timer@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/timer@npm:2.0.0-rc.31" + checksum: 10/54ea9d2f9c660763c483d5cf45dd69d8903fc8541246ba6bf1e4f4322bafe602bc2ff24512b31638989bf7407bcb09944b2cbb5881f1f4ff3ba4e478d9f00677 languageName: node linkType: hard -"@storybook/icons@npm:^1.4.0": - version: 1.6.0 - resolution: "@storybook/icons@npm:1.6.0" +"@tamagui/toast@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/toast@npm:2.0.0-rc.31" + dependencies: + "@tamagui/animate-presence": "npm:2.0.0-rc.31" + "@tamagui/collection": "npm:2.0.0-rc.31" + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/dismissable": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/native": "npm:2.0.0-rc.31" + "@tamagui/polyfill-dev": "npm:2.0.0-rc.31" + "@tamagui/portal": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/start-transition": "npm:2.0.0-rc.31" + "@tamagui/text": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + "@tamagui/visually-hidden": "npm:2.0.0-rc.31" peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - checksum: 10/f9036ca3b0d2904778eb4e202305f2780b549434380f9760f0bc704fe3ee19d7332f9560a66435ebb2156346cee9a863e40fa5e4b27790bf993b0c1180a3146d + react: ">=19" + react-native: "*" + checksum: 10/055f7f59d804f01054ba7097ff75e6043be104a1951cafc8c7e80fe16463051edae3e634578ff7a3d4df6225f2bb592a201aa81bb19f907cf847bad1f0de9224 languageName: node linkType: hard -"@storybook/icons@npm:~1.2.12": - version: 1.2.12 - resolution: "@storybook/icons@npm:1.2.12" +"@tamagui/toggle-group@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/toggle-group@npm:2.0.0-rc.31" + dependencies: + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/focusable": "npm:2.0.0-rc.31" + "@tamagui/font-size": "npm:2.0.0-rc.31" + "@tamagui/get-token": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/helpers-tamagui": "npm:2.0.0-rc.31" + "@tamagui/roving-focus": "npm:2.0.0-rc.31" + "@tamagui/sizable-context": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + "@tamagui/use-direction": "npm:2.0.0-rc.31" + "@tamagui/web": "npm:2.0.0-rc.31" peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 10/5df56f0856764ed7e4bb24ef7a08a8a9c93f8eedcb16dac062f1dfd3bd1fe6cb4a0aa5a0794083d95e31c04960d126a4d2028cfb4c53681bf05513bb38eae9d2 + react: ">=19" + checksum: 10/351956bc9720a6bfa19b55b045194b96ca11f352e419fab7606111569135527cd0c6d408acb174a73e57f41cdda23a9efbf28ec96321dd09aff8593206ad414b languageName: node linkType: hard -"@storybook/preset-react-webpack@npm:9.1.17": - version: 9.1.17 - resolution: "@storybook/preset-react-webpack@npm:9.1.17" +"@tamagui/tooltip@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/tooltip@npm:2.0.0-rc.31" dependencies: - "@storybook/core-webpack": "npm:9.1.17" - "@storybook/react-docgen-typescript-plugin": "npm:1.0.6--canary.9.0c3f3b7.0" - "@types/semver": "npm:^7.3.4" - find-up: "npm:^7.0.0" - magic-string: "npm:^0.30.5" - react-docgen: "npm:^7.1.1" - resolve: "npm:^1.22.8" - semver: "npm:^7.3.7" - tsconfig-paths: "npm:^4.2.0" - webpack: "npm:5" + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/floating": "npm:2.0.0-rc.31" + "@tamagui/get-token": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/polyfill-dev": "npm:2.0.0-rc.31" + "@tamagui/popover": "npm:2.0.0-rc.31" + "@tamagui/popper": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/text": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^9.1.17 - peerDependenciesMeta: - typescript: - optional: true - checksum: 10/fc86bcc956494bba2f21511048c6326c9358b5660bed480167ff5fc2be39b6e4f367cfa4a63e340158bd2303b956439e2ccb456f18fd57d0579108695a6bec20 + react: ">=19" + react-native: "*" + checksum: 10/f58a339175e63686e42e85d27cec6c2ac34f16694fe8b5f5043457b5ff526c5699b821c21bf0aaaecf52941f13f7559fed11e66ff3bf1f80cb63264b3f29ef3d languageName: node linkType: hard -"@storybook/react-docgen-typescript-plugin@npm:1.0.6--canary.9.0c3f3b7.0": - version: 1.0.6--canary.9.0c3f3b7.0 - resolution: "@storybook/react-docgen-typescript-plugin@npm:1.0.6--canary.9.0c3f3b7.0" - dependencies: - debug: "npm:^4.1.1" - endent: "npm:^2.0.1" - find-cache-dir: "npm:^3.3.1" - flat-cache: "npm:^3.0.4" - micromatch: "npm:^4.0.2" - react-docgen-typescript: "npm:^2.2.2" - tslib: "npm:^2.0.0" +"@tamagui/types@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/types@npm:2.0.0-rc.31" + checksum: 10/2d46817f19ad67a6537c8e1954c7881b8ce6296505efb93512c26e818b2800a1fd436afa5685ce0cef32c77ae7f67f28cc32048249594d274c3f349d5df9fd0a + languageName: node + linkType: hard + +"@tamagui/use-async@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/use-async@npm:2.0.0-rc.31" peerDependencies: - typescript: ">= 4.x" - webpack: ">= 4" - checksum: 10/5d3c64b022d10d7316b600a41eec2cf38ba460e11bf9a01ae976e8d0efe6959633423d2a3546a5d3f9f04bcc946e83774e8efdf9ccfedb76a7065ec08e7ec809 + react: ">=19" + checksum: 10/d6ee8ffad7a8a0a2dbf38cadf9088eabd56714fda73ffab98002ef7f638e668a3ac09867fd0cd8cecafc8a44c51e579e8972e5b36cf70e453254a2a1bd7255df languageName: node linkType: hard -"@storybook/react-dom-shim@npm:9.1.17": - version: 9.1.17 - resolution: "@storybook/react-dom-shim@npm:9.1.17" +"@tamagui/use-callback-ref@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/use-callback-ref@npm:2.0.0-rc.31" peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^9.1.17 - checksum: 10/ef0d4eeca593db889d4e78ed1b5a353012cfdec3ee0f73ca31abbe207952cdedc0c291d4ba5bd6d7529f5ea430dc3a90e631844534d2aa972b45c4cde11ae33d + react: ">=19" + checksum: 10/aac0d16029e1399630ec94a95b5a388ffbed7cc055b5933090bd5f89207b1a7a5341b54378cbb9c4b2d60bb96270f16de0d01625a9320da416be1e603a6c9217 languageName: node linkType: hard -"@storybook/react-webpack5@npm:~9.1.17": - version: 9.1.17 - resolution: "@storybook/react-webpack5@npm:9.1.17" - dependencies: - "@storybook/builder-webpack5": "npm:9.1.17" - "@storybook/preset-react-webpack": "npm:9.1.17" - "@storybook/react": "npm:9.1.17" +"@tamagui/use-constant@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/use-constant@npm:2.0.0-rc.31" peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^9.1.17 - typescript: ">= 4.9.x" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10/b4a4a815e69ce0dc51d4e8d885597f4f0faf8b533c3f994dfd32ddffdd07c83da3f0994b3fbd5a7c448c12bb6e68673d4cdc50d6d303906a0e05801e2f0d54d5 + react: ">=19" + checksum: 10/088a85a61a95c14535895cbe6e568fe1d83fbed353c01b31543a290c292e7671f003af9a1ffc3484139ca19ba17100d51a436f20c59e04d2f6f0ffe47bf45059 languageName: node linkType: hard -"@storybook/react@npm:9.1.17": - version: 9.1.17 - resolution: "@storybook/react@npm:9.1.17" +"@tamagui/use-controllable-state@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/use-controllable-state@npm:2.0.0-rc.31" dependencies: - "@storybook/global": "npm:^5.0.0" - "@storybook/react-dom-shim": "npm:9.1.17" + "@tamagui/start-transition": "npm:2.0.0-rc.31" + "@tamagui/use-event": "npm:2.0.0-rc.31" peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^9.1.17 - typescript: ">= 4.9.x" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10/8b6be8cc93c53ce2ccc942a54ae76f4f3652060887f46649f8ea1311fe271753435d675ac3aae76ec2f41a8f8bb0c6a88389d2bbb9a2e6875f2a0b2e655df63a + react: ">=19" + checksum: 10/8bd0f83d7e251efea95559df611a3fd62a193db1cf64338cf0c8684116ad3366521ec1ffc0e078a935805c4fb651a33b01c87d0150bcf9a3d2c568de7bc2ebda languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.13.20": - version: 1.13.20 - resolution: "@swc/core-darwin-arm64@npm:1.13.20" - conditions: os=darwin & cpu=arm64 +"@tamagui/use-debounce@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/use-debounce@npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/718716ffa9a2ce747ab703aabc339958ab1e4a2eaf30677e896b3c177d541606e700797a2c999ff7d90319ef6bc4dc1bee8d6a658cf0088e0429caacad97f333 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.13.20": - version: 1.13.20 - resolution: "@swc/core-darwin-x64@npm:1.13.20" - conditions: os=darwin & cpu=x64 +"@tamagui/use-did-finish-ssr@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/use-did-finish-ssr@npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/3f381cf8d6cc501103b78e6e9ed3ca3bd8f504bf82bf711f7de05d3003046304507ab90047d1a6fb1011fdd6f3bcf06d10ce60afb59541fe0fb7ace58720151f languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.13.20": - version: 1.13.20 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.13.20" - conditions: os=linux & cpu=arm +"@tamagui/use-direction@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/use-direction@npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/7b783105a12021127953f6437e98bbb778c1de792d9c26e58cb1e752bd59d0686b6b853780c9fcb6e8dad10211b678a3798d14bdd31bf6dfd19810a04580d924 languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.13.20": - version: 1.13.20 - resolution: "@swc/core-linux-arm64-gnu@npm:1.13.20" - conditions: os=linux & cpu=arm64 & libc=glibc +"@tamagui/use-element-layout@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/use-element-layout@npm:2.0.0-rc.31" + dependencies: + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/is-equal-shallow": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/8452e0c6a2a710e442546795381f77f00e8620cdc8f8a0459a8b2614948e380c803dd6508d3fee35b8bf95d89ffcc383f8361d4ba9778b9d6989449a186d24a0 languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.13.20": - version: 1.13.20 - resolution: "@swc/core-linux-arm64-musl@npm:1.13.20" - conditions: os=linux & cpu=arm64 & libc=musl +"@tamagui/use-escape-keydown@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/use-escape-keydown@npm:2.0.0-rc.31" + dependencies: + "@tamagui/use-callback-ref": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/90ed781a78959939253c9d6491f3b44ba996bf9d7fbfafdd0d46a582899f5a56d1a4b1311e3cba9531719f079129950f9e514e4f329ae61f118e55f16ec51e52 languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.13.20": - version: 1.13.20 - resolution: "@swc/core-linux-x64-gnu@npm:1.13.20" - conditions: os=linux & cpu=x64 & libc=glibc +"@tamagui/use-event@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/use-event@npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/99771cb6ebe98b07f9b4a3f5cdb0e06acecb1a3c11e07a4c7f17f13808158aa319f2903653dcb03cf70a8ef0fb5c25cf77d5d988ed18c2739fafb4d6de193169 languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.13.20": - version: 1.13.20 - resolution: "@swc/core-linux-x64-musl@npm:1.13.20" - conditions: os=linux & cpu=x64 & libc=musl +"@tamagui/use-force-update@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/use-force-update@npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/333051d69a822bc5926dbc79e880787bb16f526891a002747c6d7e7197b261393e27b74082fa36d42d8afa7bd8c8921ec132e242eee9a2036c83cfcd11aeb6cf languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.13.20": - version: 1.13.20 - resolution: "@swc/core-win32-arm64-msvc@npm:1.13.20" - conditions: os=win32 & cpu=arm64 +"@tamagui/use-keyboard-visible@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/use-keyboard-visible@npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/bf658f62f03eec5ab0a072240bcfc42cd597ed0ddc86c0f4e81ff688b04c7f4f5edb7f04b68ab554013c6cd6553bebbdc6b72d2ac1b994a8ad752ffef02ea2d5 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.13.20": - version: 1.13.20 - resolution: "@swc/core-win32-ia32-msvc@npm:1.13.20" - conditions: os=win32 & cpu=ia32 +"@tamagui/use-presence@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/use-presence@npm:2.0.0-rc.31" + dependencies: + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/5044b9bbb416f6dffe3ba2fbe64d2ca6d20db8d8a48dc45aae9ca5a95d0f779270165ae8c305843dee89cd0d60f8997749298f1bf2739e231b36ab1c1c8beca9 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.13.20": - version: 1.13.20 - resolution: "@swc/core-win32-x64-msvc@npm:1.13.20" - conditions: os=win32 & cpu=x64 +"@tamagui/use-previous@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/use-previous@npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/e17a297f000e4c2d3de9460f6e3a30e10ce54b2e5bd49e674152c64e4d771b9c13140b3b41ec3c9e0e71a5d93ef75b39bdb11ade2e49c266e8aeb364805032f3 languageName: node linkType: hard -"@swc/core@npm:^1.13.5": - version: 1.13.20 - resolution: "@swc/core@npm:1.13.20" +"@tamagui/use-window-dimensions@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/use-window-dimensions@npm:2.0.0-rc.31" dependencies: - "@swc/core-darwin-arm64": "npm:1.13.20" - "@swc/core-darwin-x64": "npm:1.13.20" - "@swc/core-linux-arm-gnueabihf": "npm:1.13.20" - "@swc/core-linux-arm64-gnu": "npm:1.13.20" - "@swc/core-linux-arm64-musl": "npm:1.13.20" - "@swc/core-linux-x64-gnu": "npm:1.13.20" - "@swc/core-linux-x64-musl": "npm:1.13.20" - "@swc/core-win32-arm64-msvc": "npm:1.13.20" - "@swc/core-win32-ia32-msvc": "npm:1.13.20" - "@swc/core-win32-x64-msvc": "npm:1.13.20" - "@swc/counter": "npm:^0.1.3" - "@swc/types": "npm:^0.1.25" + "@tamagui/constants": "npm:2.0.0-rc.31" peerDependencies: - "@swc/helpers": ">=0.5.17" - dependenciesMeta: - "@swc/core-darwin-arm64": - optional: true - "@swc/core-darwin-x64": - optional: true - "@swc/core-linux-arm-gnueabihf": - optional: true - "@swc/core-linux-arm64-gnu": - optional: true - "@swc/core-linux-arm64-musl": - optional: true - "@swc/core-linux-x64-gnu": - optional: true - "@swc/core-linux-x64-musl": - optional: true - "@swc/core-win32-arm64-msvc": - optional: true - "@swc/core-win32-ia32-msvc": - optional: true - "@swc/core-win32-x64-msvc": - optional: true - peerDependenciesMeta: - "@swc/helpers": - optional: true - checksum: 10/fdbd34f98d894cc765e44bab27da5fa95e604660ae061f5eafeaf459418e1d32e8b8f2333966a6c32fef4770e9948f9cec3bdacc8a35d6b1b32bc48677777261 + react: ">=19" + react-native: "*" + checksum: 10/687e03a88dfa4f367f0c0add7c2d0331026af2b4e0d0777670b1cac44ed9045795e2b43592f123acefabb6ff493bd5a25de0a164222544fc49a4255d4df7aba5 languageName: node linkType: hard -"@swc/counter@npm:^0.1.3": - version: 0.1.3 - resolution: "@swc/counter@npm:0.1.3" - checksum: 10/df8f9cfba9904d3d60f511664c70d23bb323b3a0803ec9890f60133954173047ba9bdeabce28cd70ba89ccd3fd6c71c7b0bd58be85f611e1ffbe5d5c18616598 +"@tamagui/v2-toast@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/v2-toast@npm:2.0.0-rc.31" + dependencies: + "@tamagui/animate-presence": "npm:2.0.0-rc.31" + "@tamagui/collection": "npm:2.0.0-rc.31" + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/dismissable": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/native": "npm:2.0.0-rc.31" + "@tamagui/polyfill-dev": "npm:2.0.0-rc.31" + "@tamagui/portal": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/start-transition": "npm:2.0.0-rc.31" + "@tamagui/text": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + "@tamagui/visually-hidden": "npm:2.0.0-rc.31" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10/90009ca5aee4f4f763a22917422d4e4e773eecbdef81d1deb8dd82aaba65d9e08c58e7a3e7405f36e48d5a9f7548ca393550117aef206cfa142380d952408217 languageName: node linkType: hard -"@swc/helpers@npm:^0.5.0": - version: 0.5.15 - resolution: "@swc/helpers@npm:0.5.15" +"@tamagui/visually-hidden@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/visually-hidden@npm:2.0.0-rc.31" dependencies: - tslib: "npm:^2.8.0" - checksum: 10/e3f32c6deeecfb0fa3f22edff03a7b358e7ce16d27b0f1c8b5cdc3042c5c4ce4da6eac0b781ab7cc4f54696ece657467d56734fb26883439fb00017385364c4c + "@tamagui/web": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/e20086dab638a29f8e5d4db72a598c093e7ea0015a8a52830171bf49e6d6ca65aed50955fc55d0447afdaf4893d343ddf0d8daf5ee9fc0f7a66d1967ea2b3a9b languageName: node linkType: hard -"@swc/types@npm:^0.1.25": - version: 0.1.25 - resolution: "@swc/types@npm:0.1.25" +"@tamagui/web@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/web@npm:2.0.0-rc.31" dependencies: - "@swc/counter": "npm:^0.1.3" - checksum: 10/f6741450224892d12df43e5ca7f3cc0287df644dcd672626eb0cc2a3a8e3e875f4b29eb11336f37c7240cf6e010ba59eb3a79f4fb8bee5cbd168dfc1326ff369 + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/helpers": "npm:2.0.0-rc.31" + "@tamagui/is-equal-shallow": "npm:2.0.0-rc.31" + "@tamagui/native": "npm:2.0.0-rc.31" + "@tamagui/normalize-css-color": "npm:2.0.0-rc.31" + "@tamagui/timer": "npm:2.0.0-rc.31" + "@tamagui/types": "npm:2.0.0-rc.31" + "@tamagui/use-did-finish-ssr": "npm:2.0.0-rc.31" + "@tamagui/use-event": "npm:2.0.0-rc.31" + "@tamagui/use-force-update": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-dom: "*" + react-native: "*" + checksum: 10/0e15c54079e7186fa4f8e864dde02206dcc7c867535e6369d1c8e056de0dc1fcc558cff815dce57539d6a503f89d7bf55740913461db25fcf7c439b15e3372c4 + languageName: node + linkType: hard + +"@tamagui/z-index-stack@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "@tamagui/z-index-stack@npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + checksum: 10/be757f78590ccb315719436d836eee5fee982fa6edcedd7c155914760752e72270e14c6d2ac5e49ae3477223515202781c805ae31c60bc965a4d2d7bdd0e93f5 languageName: node linkType: hard @@ -17017,6 +18625,15 @@ __metadata: languageName: node linkType: hard +"react-freeze@npm:^1.0.3": + version: 1.0.4 + resolution: "react-freeze@npm:1.0.4" + peerDependencies: + react: ">=17.0.0" + checksum: 10/1dc433319341ec3dca84513c4197ef4f4c8232604d35f83546a8abfb41d9591f934b66aaaa4dc3dc8b1b65f488705a2a48ae6c1d9792660119a9cdedeab4ca8f + languageName: node + linkType: hard + "react-hook-form@npm:~7.54.2": version: 7.54.2 resolution: "react-hook-form@npm:7.54.2" @@ -19065,6 +20682,81 @@ __metadata: languageName: node linkType: hard +"tamagui@npm:2.0.0-rc.31": + version: 2.0.0-rc.31 + resolution: "tamagui@npm:2.0.0-rc.31" + dependencies: + "@tamagui/accordion": "npm:2.0.0-rc.31" + "@tamagui/adapt": "npm:2.0.0-rc.31" + "@tamagui/alert-dialog": "npm:2.0.0-rc.31" + "@tamagui/animate": "npm:2.0.0-rc.31" + "@tamagui/animate-presence": "npm:2.0.0-rc.31" + "@tamagui/avatar": "npm:2.0.0-rc.31" + "@tamagui/button": "npm:2.0.0-rc.31" + "@tamagui/card": "npm:2.0.0-rc.31" + "@tamagui/checkbox": "npm:2.0.0-rc.31" + "@tamagui/collapsible": "npm:2.0.0-rc.31" + "@tamagui/compose-refs": "npm:2.0.0-rc.31" + "@tamagui/constants": "npm:2.0.0-rc.31" + "@tamagui/context-menu": "npm:2.0.0-rc.31" + "@tamagui/core": "npm:2.0.0-rc.31" + "@tamagui/create-context": "npm:2.0.0-rc.31" + "@tamagui/create-menu": "npm:2.0.0-rc.31" + "@tamagui/dialog": "npm:2.0.0-rc.31" + "@tamagui/element": "npm:2.0.0-rc.31" + "@tamagui/elements": "npm:2.0.0-rc.31" + "@tamagui/fake-react-native": "npm:2.0.0-rc.31" + "@tamagui/focusable": "npm:2.0.0-rc.31" + "@tamagui/font-size": "npm:2.0.0-rc.31" + "@tamagui/form": "npm:2.0.0-rc.31" + "@tamagui/get-button-sized": "npm:2.0.0-rc.31" + "@tamagui/get-font-sized": "npm:2.0.0-rc.31" + "@tamagui/get-token": "npm:2.0.0-rc.31" + "@tamagui/group": "npm:2.0.0-rc.31" + "@tamagui/helpers-tamagui": "npm:2.0.0-rc.31" + "@tamagui/image": "npm:2.0.0-rc.31" + "@tamagui/input": "npm:2.0.0-rc.31" + "@tamagui/label": "npm:2.0.0-rc.31" + "@tamagui/linear-gradient": "npm:2.0.0-rc.31" + "@tamagui/list-item": "npm:2.0.0-rc.31" + "@tamagui/menu": "npm:2.0.0-rc.31" + "@tamagui/polyfill-dev": "npm:2.0.0-rc.31" + "@tamagui/popover": "npm:2.0.0-rc.31" + "@tamagui/popper": "npm:2.0.0-rc.31" + "@tamagui/portal": "npm:2.0.0-rc.31" + "@tamagui/progress": "npm:2.0.0-rc.31" + "@tamagui/radio-group": "npm:2.0.0-rc.31" + "@tamagui/react-native-media-driver": "npm:2.0.0-rc.31" + "@tamagui/scroll-view": "npm:2.0.0-rc.31" + "@tamagui/select": "npm:2.0.0-rc.31" + "@tamagui/separator": "npm:2.0.0-rc.31" + "@tamagui/shapes": "npm:2.0.0-rc.31" + "@tamagui/sheet": "npm:2.0.0-rc.31" + "@tamagui/slider": "npm:2.0.0-rc.31" + "@tamagui/spacer": "npm:2.0.0-rc.31" + "@tamagui/spinner": "npm:2.0.0-rc.31" + "@tamagui/stacks": "npm:2.0.0-rc.31" + "@tamagui/switch": "npm:2.0.0-rc.31" + "@tamagui/tabs": "npm:2.0.0-rc.31" + "@tamagui/text": "npm:2.0.0-rc.31" + "@tamagui/theme": "npm:2.0.0-rc.31" + "@tamagui/toast": "npm:2.0.0-rc.31" + "@tamagui/toggle-group": "npm:2.0.0-rc.31" + "@tamagui/tooltip": "npm:2.0.0-rc.31" + "@tamagui/use-controllable-state": "npm:2.0.0-rc.31" + "@tamagui/use-debounce": "npm:2.0.0-rc.31" + "@tamagui/use-force-update": "npm:2.0.0-rc.31" + "@tamagui/use-window-dimensions": "npm:2.0.0-rc.31" + "@tamagui/v2-toast": "npm:2.0.0-rc.31" + "@tamagui/visually-hidden": "npm:2.0.0-rc.31" + "@tamagui/z-index-stack": "npm:2.0.0-rc.31" + peerDependencies: + react: ">=19" + react-native: "*" + checksum: 10/6bbd82742316d970012ecc73d52ba4bb54957cb66ef00b4220172df147cef3cb15239ae950e3b21d59dedd0c9e203be092adad5a0fc100554db35c1a65eaf591 + languageName: node + linkType: hard + "tapable@npm:^2.0.0, tapable@npm:^2.1.1, tapable@npm:^2.2.0, tapable@npm:^2.2.1": version: 2.2.1 resolution: "tapable@npm:2.2.1" From b49e743558d10de5ec69bdc18eabd9e08beb92b7 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 01:20:30 -0300 Subject: [PATCH 02/84] refactor(fuselage): Migrate Badge component from SCSS to Tamagui v3 Uses styled(RcxText) with micro font scale, 5 color variants, small and disabled states. Public API preserved. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/Badge/Badge.styles.scss | 117 ------------------ .../fuselage/src/components/Badge/Badge.tsx | 101 ++++++++++++--- packages/fuselage/src/components/index.scss | 2 - 3 files changed, 86 insertions(+), 134 deletions(-) delete mode 100644 packages/fuselage/src/components/Badge/Badge.styles.scss diff --git a/packages/fuselage/src/components/Badge/Badge.styles.scss b/packages/fuselage/src/components/Badge/Badge.styles.scss deleted file mode 100644 index b23a72cf8b..0000000000 --- a/packages/fuselage/src/components/Badge/Badge.styles.scss +++ /dev/null @@ -1,117 +0,0 @@ -@use '../../styles/colors.scss'; -@use '../../styles/lengths.scss'; -@use '../../styles/typography.scss'; - -$badge-colors-primary-color: theme( - 'badge-colors-primary-color', - colors.font(pure-white) -); -$badge-colors-primary-background-color: theme( - 'badge-colors-primary-background-color', - colors.badge(level-2) -); - -$badge-colors-secondary-color: theme( - 'badge-colors-secondary-color', - colors.font(pure-white) -); -$badge-colors-secondary-background-color: theme( - 'badge-colors-secondary-background-color', - colors.badge(level-1) -); - -$badge-colors-warning-color: theme( - 'badge-colors-warning-color', - colors.font(pure-white) -); -$badge-colors-warning-background-color: theme( - 'badge-colors-warning-background-color', - colors.badge(level-3) -); - -$badge-colors-danger-color: theme( - 'badge-colors-danger-color', - colors.font(pure-white) -); -$badge-colors-danger-background-color: theme( - 'badge-colors-danger-background-color', - colors.badge(level-4) -); - -$badge-colors-ghost-color: theme( - 'badge-colors-ghost-color', - colors.font(pure-white) -); -$badge-colors-ghost-background-color: theme( - 'badge-colors-ghost-background-color', - colors.stroke(dark) -); - -$badge-colors-disabled-color: theme( - 'badge-colors-disabled-color', - colors.font(secondary-info) -); -$badge-colors-disabled-background-color: theme( - 'badge-colors-disabled-background-color', - colors.surface(neutral) -); - -.rcx-badge { - display: flex; - overflow: hidden; - justify-content: center; - - width: fit-content; - min-width: lengths.size(16); - min-height: lengths.size(16); - - padding: lengths.padding(2) lengths.padding(4); - - text-align: center; - - white-space: nowrap; - - text-decoration: none; - text-overflow: ellipsis; - - word-break: keep-all; - - border-radius: theme('badge-border-radius', lengths.border-radius(full)); - - @include typography.use-font-scale(micro); - - &--primary { - color: $badge-colors-primary-color; - background-color: $badge-colors-primary-background-color; - } - - &--secondary { - color: $badge-colors-secondary-color; - background-color: $badge-colors-secondary-background-color; - } - - &--warning { - color: $badge-colors-warning-color; - background-color: $badge-colors-warning-background-color; - } - - &--danger { - color: $badge-colors-danger-color; - background-color: $badge-colors-danger-background-color; - } - - &--ghost { - color: $badge-colors-ghost-color; - background-color: $badge-colors-ghost-background-color; - } - - &--disabled { - color: $badge-colors-disabled-color; - background-color: $badge-colors-disabled-background-color; - } - - &--small { - min-width: lengths.size(8); - min-height: lengths.size(8); - } -} diff --git a/packages/fuselage/src/components/Badge/Badge.tsx b/packages/fuselage/src/components/Badge/Badge.tsx index 44cefcea84..8e0e33ba13 100644 --- a/packages/fuselage/src/components/Badge/Badge.tsx +++ b/packages/fuselage/src/components/Badge/Badge.tsx @@ -1,6 +1,79 @@ import type { ElementType, HTMLAttributes, ReactNode } from 'react'; +import { styled } from 'tamagui'; -import { prependClassName } from '../../helpers/prependClassName'; +import { RcxText } from '../../primitives'; + +const StyledBadge = styled(RcxText, { + name: 'Badge', + + display: 'flex', + overflow: 'hidden', + justifyContent: 'center', + + width: 'fit-content', + minWidth: '$x16', + minHeight: '$x16', + + paddingBlock: '$x2', + paddingInline: '$x4', + + textAlign: 'center', + whiteSpace: 'nowrap', + textDecorationLine: 'none', + textOverflow: 'ellipsis', + wordBreak: 'keep-all', + + borderRadius: '$full', + + fontFamily: '$body', + fontSize: '$micro', + lineHeight: '$micro', + fontWeight: '$micro', + letterSpacing: '$micro', + + variants: { + variant: { + primary: { + color: '$fontPureWhite', + backgroundColor: '$badgeLevel2', + }, + secondary: { + color: '$fontPureWhite', + backgroundColor: '$badgeLevel1', + }, + danger: { + color: '$fontPureWhite', + backgroundColor: '$badgeLevel4', + }, + warning: { + color: '$fontPureWhite', + backgroundColor: '$badgeLevel3', + }, + ghost: { + color: '$fontPureWhite', + backgroundColor: '$strokeDark', + }, + }, + + small: { + true: { + minWidth: '$x8', + minHeight: '$x8', + }, + }, + + disabled: { + true: { + color: '$fontSecondaryInfo', + backgroundColor: '$surfaceNeutral', + }, + }, + } as const, + + defaultVariants: { + variant: 'secondary', + }, +}); export type BadgeProps = { is?: ElementType>; @@ -13,29 +86,27 @@ export type BadgeProps = { } & HTMLAttributes; /** - * Communicates notification’s amount and types. + * Communicates notification's amount and types. */ function Badge({ - is: Tag = 'span', + is, variant = 'secondary', small, - className, disabled, + children, + title, ...props }: BadgeProps) { - const modifiers = [variant, small && 'small', disabled && 'disabled'] - .filter(Boolean) - .map((modifier) => `rcx-badge--${modifier}`) - .join(' '); - return ( - + > + {children} + ); } diff --git a/packages/fuselage/src/components/index.scss b/packages/fuselage/src/components/index.scss index 1b8eee63da..7b262a9e04 100644 --- a/packages/fuselage/src/components/index.scss +++ b/packages/fuselage/src/components/index.scss @@ -2,7 +2,6 @@ @import './Banner/Banner.styles.scss'; @import './AutoComplete/AutoComplete.styles.scss'; @import './Avatar/Avatar.styles.scss'; -@import './Badge/Badge.styles.scss'; @import './Box/Box.styles.scss'; @import './Button/Button.styles.scss'; @import './Bubble/Bubble.styles.scss'; @@ -14,7 +13,6 @@ @import './Chevron/Chevron.styles.scss'; @import './Chip/Chip.styles.scss'; @import './CodeSnippet/CodeSnippet.styles.scss'; -@import './Divider/Divider.styles.scss'; @import './Dropdown/Dropdown.styles.scss'; @import './Field/Field.styles.scss'; @import './FieldGroup/FieldGroup.styles.scss'; From c103d540f838318c51f02734cbedc5d4c200d6a8 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 01:23:21 -0300 Subject: [PATCH 03/84] refactor(fuselage): Migrate Divider component from SCSS to Tamagui v3 Uses styled(RcxView) with danger and vertical variants. Divider with children uses DividerBar + DividerWrapper composition. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/Divider/Divider.styles.scss | 45 ---------- .../src/components/Divider/Divider.tsx | 86 ++++++++++++++++--- packages/fuselage/src/components/index.scss | 1 - packages/fuselage/src/themes/light.ts | 1 - 4 files changed, 74 insertions(+), 59 deletions(-) delete mode 100644 packages/fuselage/src/components/Divider/Divider.styles.scss diff --git a/packages/fuselage/src/components/Divider/Divider.styles.scss b/packages/fuselage/src/components/Divider/Divider.styles.scss deleted file mode 100644 index c2043dba4e..0000000000 --- a/packages/fuselage/src/components/Divider/Divider.styles.scss +++ /dev/null @@ -1,45 +0,0 @@ -@use '../../styles/colors.scss'; -@use '../../styles/lengths.scss'; -@use '../../styles/typography.scss'; - -$divider-size: theme('divider-size', lengths.border-width(default)); -$divider-color: theme('divider-color', colors.stroke(extra-light)); - -.rcx-divider { - margin-block: lengths.margin(8); - - border-top: $divider-size solid $divider-color; - - &--danger { - border-color: colors.stroke(error); - } - - &__bar { - display: flex; - justify-content: flex-end; - align-items: center; - flex-grow: 1; - - &::after { - flex-grow: 1; - - content: ''; - - border: $divider-size solid $divider-color; - } - } - - &__wrapper { - margin-block: lengths.margin(8); - padding-inline: lengths.padding(8); - } - - &--vertical { - width: 0; - height: lengths.size(20); - margin-block: 0; - margin-inline: lengths.margin(8); - - border-left: $divider-size solid $divider-color; - } -} diff --git a/packages/fuselage/src/components/Divider/Divider.tsx b/packages/fuselage/src/components/Divider/Divider.tsx index 433d4ebbfd..5723035b07 100644 --- a/packages/fuselage/src/components/Divider/Divider.tsx +++ b/packages/fuselage/src/components/Divider/Divider.tsx @@ -1,8 +1,70 @@ import type { ReactNode } from 'react'; +import { styled } from 'tamagui'; -import { Box, type BoxProps } from '../Box'; +import { RcxView, RcxText } from '../../primitives'; -export type DividerProps = BoxProps & { +const DividerHr = styled(RcxView, { + name: 'DividerHr', + marginBlock: '$x8', + borderTopWidth: 1, + borderTopStyle: 'solid', + borderTopColor: '$strokeExtraLight', + + variants: { + danger: { + true: { + borderTopColor: '$strokeError', + }, + }, + vertical: { + true: { + width: 0, + height: '$x20', + marginBlock: 0, + marginInline: '$x8', + borderTopWidth: 0, + borderTopStyle: 'unset', + borderTopColor: 'transparent', + borderLeftWidth: 1, + borderLeftStyle: 'solid', + borderLeftColor: '$strokeExtraLight', + }, + }, + } as const, +}); + +const DividerBar = styled(RcxView, { + name: 'DividerBar', + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-end', + alignItems: 'center', + flexGrow: 1, + borderTopWidth: 1, + borderTopStyle: 'solid', + borderTopColor: '$strokeExtraLight', +}); + +const DividerWrapper = styled(RcxText, { + name: 'DividerWrapper', + marginBlock: '$x8', + paddingInline: '$x8', + fontFamily: '$body', + fontSize: '$c2', + fontWeight: '$c2', + lineHeight: '$c2', + letterSpacing: '$c2', + color: '$fontDefault', +}); + +const DividerContainer = styled(RcxView, { + name: 'DividerContainer', + display: 'flex', + flexDirection: 'row', + alignItems: 'center', +}); + +export type DividerProps = { variation?: 'danger'; children?: ReactNode; vertical?: boolean; @@ -11,21 +73,21 @@ export type DividerProps = BoxProps & { const Divider = ({ variation, children, vertical, ...props }: DividerProps) => { if (!children) { return ( - ); } return ( - -

-
{children}
-
- + + + {children} + + ); }; diff --git a/packages/fuselage/src/components/index.scss b/packages/fuselage/src/components/index.scss index 7b262a9e04..1c509ff065 100644 --- a/packages/fuselage/src/components/index.scss +++ b/packages/fuselage/src/components/index.scss @@ -37,7 +37,6 @@ @import './States/States.styles.scss'; @import './Table/Table.styles.scss'; @import './Tabs/Tabs.styles.scss'; -@import './Tag/Tag.styles.scss'; @import './Throbber/Throbber.styles.scss'; @import './Tile/Tile.styles.scss'; @import './ToastBar/ToastBar.styles.scss'; diff --git a/packages/fuselage/src/themes/light.ts b/packages/fuselage/src/themes/light.ts index 69635f14dd..8bb233044c 100644 --- a/packages/fuselage/src/themes/light.ts +++ b/packages/fuselage/src/themes/light.ts @@ -125,7 +125,6 @@ export const lightTheme = { buttonWarningPressBg: colors.y800, buttonWarningFocusBg: colors.y600, buttonWarningDisabledBg: colors.y200, - buttonWarningColor: colors.n900, buttonWarningColor: colors.n900, // $-button-fonts on-warning = neutral(900) buttonWarningDisabledColor: colors.n700, From 6cd7ca1976df94217b451b487eb0576dd5fe5d9d Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 01:23:45 -0300 Subject: [PATCH 04/84] refactor(fuselage): Migrate Tag component from SCSS to Tamagui v3 Uses styled(RcxView/RcxText) with 8 color variants, size variants using font tokens ($micro, $c2, $p2b), disabled and clickable states. Expanded theme tokens for all tag color variants. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/Tag/Tag.styles.scss | 279 ------------------ packages/fuselage/src/components/Tag/Tag.tsx | 181 ++++++++++-- packages/fuselage/src/themes/dark.ts | 11 +- packages/fuselage/src/themes/highContrast.ts | 10 +- packages/fuselage/src/themes/light.ts | 20 +- 5 files changed, 169 insertions(+), 332 deletions(-) delete mode 100644 packages/fuselage/src/components/Tag/Tag.styles.scss diff --git a/packages/fuselage/src/components/Tag/Tag.styles.scss b/packages/fuselage/src/components/Tag/Tag.styles.scss deleted file mode 100644 index 26917fe26d..0000000000 --- a/packages/fuselage/src/components/Tag/Tag.styles.scss +++ /dev/null @@ -1,279 +0,0 @@ -@use '../../styles/colors.scss'; -@use '../../styles/lengths.scss'; -@use '../../styles/typography.scss'; -@use '../../styles/mixins/templates.scss'; - -$tag-colors-default-color: theme( - 'tag-colors-default-color', - colors.button(on-secondary) -); -$tag-colors-default-hover-color: theme( - 'tag-colors-default-hover-color', - colors.button(on-secondary) -); -$tag-colors-default-background-color: theme( - 'tag-colors-default-background-color', - colors.button(secondary-default) -); -$tag-colors-default-hover-background-color: theme( - 'tag-colors-default-hover-background-color', - colors.button(secondary-hover) -); - -$tag-colors-primary-color: theme( - 'tag-colors-primary-color', - colors.button(on-primary) -); -$tag-colors-primary-background-color: theme( - 'tag-colors-primary-background-color', - colors.button(primary-default) -); -$tag-colors-primary-hover-background-color: theme( - 'tag-colors-primary-hover-background-color', - colors.button(primary-hover) -); - -$tag-colors-secondary-color: theme( - 'tag-colors-secondary-color', - colors.button(on-secondary) -); -$tag-colors-secondary-background-color: theme( - 'tag-colors-secondary-background-color', - colors.button(secondary-default) -); -$tag-colors-secondary-hover-background-color: theme( - 'tag-colors-secondary-hover-background-color', - colors.button(secondary-hover) -); - -$tag-colors-danger-color: theme( - 'tag-colors-danger-color', - colors.button(on-danger) -); -$tag-colors-danger-background-color: theme( - 'tag-colors-danger-background-color', - colors.button(danger-default) -); -$tag-colors-danger-hover-background-color: theme( - 'tag-colors-danger-hover-background-color', - colors.button(danger-hover) -); - -$tag-colors-secondary-danger-color: theme( - 'tag-colors-secondary-danger-color', - colors.button(on-secondary-danger) -); -$tag-colors-secondary-danger-background-color: theme( - 'tag-colors-secondary-danger-background-color', - colors.button(secondary-danger-default) -); -$tag-colors-secondary-danger-hover-background-color: theme( - 'tag-colors-secondary-danger-hover-background-color', - colors.button(secondary-danger-hover) -); - -$tag-colors-secondary-warning-color: theme( - 'tag-colors-secondary-warning-color', - colors.status-font(on-warning) -); -$tag-colors-secondary-warning-background-color: theme( - 'tag-colors-secondary-warning-background-color', - colors.button(secondary-default) -); -$tag-colors-secondary-warning-hover-background-color: theme( - 'tag-colors-secondary-warning-hover-background-color', - colors.button(secondary-hover) -); - -$tag-colors-secondary-info-color: theme( - 'tag-colors-secondary-info-color', - colors.status-font(on-info) -); -$tag-colors-secondary-info-background-color: theme( - 'tag-colors-secondary-info-background-color', - colors.button(secondary-default) -); -$tag-colors-secondary-info-hover-background-color: theme( - 'tag-colors-secondary-info-hover-background-color', - colors.button(secondary-hover) -); - -$tag-colors-warning-color: theme( - 'tag-colors-warning-color', - colors.button(on-warning) -); -$tag-colors-warning-background-color: theme( - 'tag-colors-warning-background-color', - colors.button(warning-default) -); -$tag-colors-warning-hover-background-color: theme( - 'tag-colors-warning-hover-background-color', - colors.button(warning-hover) -); - -$tag-colors-featured-color: theme( - 'tag-colors-featured-color', - colors.button(on-primary) -); -$tag-colors-featured-background-color: theme( - 'tag-colors-featured-background-color', - colors.surface(featured) -); -$tag-colors-featured-hover-background-color: theme( - 'tag-colors-featured-hover-background-color', - colors.surface(featured-hover) -); - -$tag-colors-disabled-color: theme( - 'tag-colors-disabled-color', - colors.font(secondary-info) -); -$tag-colors-disabled-background-color: theme( - 'tag-colors-disabled-background-color', - colors.surface(neutral) -); - -@mixin use-clickable-colors($color, $background-color) { - &.rcx-tag--clickable { - @include use-link-colors($color); - - &:hover { - background-color: $background-color; - } - } -} - -.rcx-tag { - display: flex; - overflow: hidden; - justify-content: center; - align-items: center; - - padding: lengths.padding(2) lengths.padding(4); - - white-space: nowrap; - - text-decoration: none; - text-overflow: ellipsis; - - word-break: keep-all; - - color: $tag-colors-default-color; - border-radius: theme('tag-border-radius', lengths.border-radius(small)); - background-color: $tag-colors-default-background-color; - - @include typography.use-font-scale(micro); - @include templates.focus-state; - - &--clickable { - @include clickable; - @include use-clickable-colors( - $tag-colors-default-color, - $tag-colors-default-hover-background-color - ); - } - - &__inner { - overflow: hidden; - - min-width: 0; - - white-space: nowrap; - text-overflow: ellipsis; - } - - &--primary { - color: $tag-colors-primary-color; - background-color: $tag-colors-primary-background-color; - - @include use-clickable-colors( - $tag-colors-primary-color, - $tag-colors-primary-hover-background-color - ); - } - - &--secondary { - color: $tag-colors-secondary-color; - background-color: $tag-colors-secondary-background-color; - - @include use-clickable-colors( - $tag-colors-secondary-color, - $tag-colors-secondary-hover-background-color - ); - } - - &--danger { - color: $tag-colors-danger-color; - background-color: $tag-colors-danger-background-color; - - @include use-clickable-colors( - $tag-colors-danger-color, - $tag-colors-danger-hover-background-color - ); - } - - &--warning { - color: $tag-colors-warning-color; - background-color: $tag-colors-warning-background-color; - - @include use-clickable-colors( - $tag-colors-warning-color, - $tag-colors-warning-hover-background-color - ); - } - - &--featured { - color: $tag-colors-featured-color; - background-color: $tag-colors-featured-background-color; - - @include use-clickable-colors( - $tag-colors-featured-color, - $tag-colors-featured-hover-background-color - ); - } - - &--secondary-danger { - color: $tag-colors-secondary-danger-color; - background-color: $tag-colors-secondary-danger-background-color; - - @include use-clickable-colors( - $tag-colors-secondary-danger-color, - $tag-colors-secondary-danger-hover-background-color - ); - } - - &--secondary-warning { - color: $tag-colors-secondary-warning-color; - background-color: $tag-colors-secondary-warning-background-color; - - @include use-clickable-colors( - $tag-colors-secondary-warning-color, - $tag-colors-secondary-warning-hover-background-color - ); - } - - &--secondary-info { - color: $tag-colors-secondary-info-color; - background-color: $tag-colors-secondary-info-background-color; - - @include use-clickable-colors( - $tag-colors-secondary-info-color, - $tag-colors-secondary-info-hover-background-color - ); - } - - &--disabled { - cursor: not-allowed; - - color: $tag-colors-disabled-color; - background-color: $tag-colors-disabled-background-color; - } - - &--medium { - @include typography.use-font-scale(c2); - } - - &--large { - @include typography.use-font-scale(p2b); - } -} diff --git a/packages/fuselage/src/components/Tag/Tag.tsx b/packages/fuselage/src/components/Tag/Tag.tsx index 5e8f65afbe..28672be10c 100644 --- a/packages/fuselage/src/components/Tag/Tag.tsx +++ b/packages/fuselage/src/components/Tag/Tag.tsx @@ -1,7 +1,146 @@ import type { ReactNode } from 'react'; +import { createStyledContext, styled } from 'tamagui'; -import { prependClassName } from '../../helpers/prependClassName'; -import { Box, type BoxProps } from '../Box'; +import { RcxText, RcxView } from '../../primitives'; + +const TagContext = createStyledContext({ + size: 'default' as string, +}); + +// Outer container — View for layout. +// Known issue: ~2px text width difference vs original (parent font-size 16px vs 10px). +const TagBase = styled(RcxView, { + name: 'TagBase', + context: TagContext, + + display: 'flex', + flexDirection: 'row', + overflow: 'hidden', + justifyContent: 'center', + alignItems: 'center', + + paddingBlock: '$x2', + paddingInline: '$x4', + + borderWidth: 1, + borderStyle: 'solid', + borderColor: 'transparent', + borderRadius: '$x2', + + // default = button secondary colors + backgroundColor: '$buttonSecondaryBg', + + variants: { + variant: { + 'primary': { + backgroundColor: '$buttonPrimaryBg', + hoverStyle: { backgroundColor: '$buttonPrimaryHoverBg' }, + }, + 'secondary': { + backgroundColor: '$buttonSecondaryBg', + hoverStyle: { backgroundColor: '$buttonSecondaryHoverBg' }, + }, + 'danger': { + backgroundColor: '$buttonDangerBg', + hoverStyle: { backgroundColor: '$buttonDangerHoverBg' }, + }, + 'warning': { + backgroundColor: '$buttonWarningBg', + hoverStyle: { backgroundColor: '$buttonWarningHoverBg' }, + }, + 'featured': { + backgroundColor: '$surfaceFeatured', + hoverStyle: { backgroundColor: '$surfaceFeaturedHover' }, + }, + 'secondary-danger': { + backgroundColor: '$buttonSecondaryDangerBg', + hoverStyle: { backgroundColor: '$buttonSecondaryDangerHoverBg' }, + }, + 'secondary-warning': { + backgroundColor: '$buttonSecondaryBg', + hoverStyle: { backgroundColor: '$buttonSecondaryHoverBg' }, + }, + 'secondary-info': { + backgroundColor: '$buttonSecondaryBg', + hoverStyle: { backgroundColor: '$buttonSecondaryHoverBg' }, + }, + }, + disabled: { + true: { + cursor: 'not-allowed', + backgroundColor: '$surfaceNeutral', + }, + }, + clickable: { + true: { + cursor: 'pointer', + hoverStyle: { + backgroundColor: '$buttonSecondaryHoverBg', + }, + }, + }, + } as const, +}); + +// Inner text — use raw Text (NOT RcxText) to avoid box-sizing: border-box +// which changes width calculation vs the original content-box span +const TagInner = styled(RcxText, { + name: 'TagInner', + context: TagContext, + + overflow: 'hidden', + minWidth: 0, + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + textDecoration: 'none', + + // Font scale: micro (default) + fontFamily: '$body', + fontSize: '$micro', + fontWeight: '$micro', + lineHeight: '$micro', + letterSpacing: '$micro', + + // default text color + color: '$buttonSecondaryColor', + + variants: { + variant: { + 'primary': { color: '$buttonPrimaryColor' }, + 'secondary': { color: '$buttonSecondaryColor' }, + 'danger': { color: '$buttonDangerColor' }, + 'warning': { color: '$buttonWarningColor' }, + 'featured': { color: '$buttonPrimaryColor' }, + 'secondary-danger': { color: '$buttonSecondaryDangerColor' }, + 'secondary-warning': { color: '$statusFontOnWarning' }, + 'secondary-info': { color: '$statusFontOnInfo' }, + }, + size: { + default: {}, + medium: { + fontSize: '$c2', + fontWeight: '$c2', + lineHeight: '$c2', + letterSpacing: '$c2', + }, + large: { + fontSize: '$p2b', + fontWeight: '$p2b', + lineHeight: '$p2b', + letterSpacing: '$p2b', + }, + }, + disabled: { + true: { + color: '$fontSecondaryInfo', + }, + }, + } as const, + + defaultVariants: { + size: 'default', + }, +}); export type TagProps = { medium?: boolean; @@ -17,7 +156,11 @@ export type TagProps = { | 'featured'; disabled?: boolean; icon?: ReactNode; -} & Omit; + children?: ReactNode; + className?: string; + onClick?: () => void; + href?: string; +}; /** * Used for mentions @@ -25,7 +168,6 @@ export type TagProps = { const Tag = ({ large, medium, - className, disabled, onClick, variant, @@ -34,29 +176,26 @@ const Tag = ({ href, ...props }: TagProps) => { - const modifiers = [ - variant, - medium && 'medium', - large && 'large', - disabled && 'disabled', - onClick && 'clickable', - href && 'clickable', - ] - .filter(Boolean) - .map((modifier) => `rcx-tag--${modifier}`) - .join(' '); + const clickable = !!(onClick || href); + const size = large ? 'large' : medium ? 'medium' : undefined; return ( - {icon} - {children} - + + {children} + + ); }; diff --git a/packages/fuselage/src/themes/dark.ts b/packages/fuselage/src/themes/dark.ts index fb4a35d567..75029f92ad 100644 --- a/packages/fuselage/src/themes/dark.ts +++ b/packages/fuselage/src/themes/dark.ts @@ -146,15 +146,8 @@ export const darkTheme = { buttonSecondaryWarningColor: '#C7AA66', buttonSecondaryWarningDisabledColor: '#3C3625', - // --- Tag tokens (8) --- - tagDefaultBg: '#353B45', - tagDefaultColor: '#9EA2A8', - tagPrimaryBg: '#1A3A6B', - tagPrimaryColor: '#739EDE', - tagDangerBg: '#3D2126', - tagDangerColor: '#D88892', - tagWarningBg: '#3C3625', - tagWarningColor: '#C7AA66', + // Tag tokens removed — Tag now uses button/surface/status tokens directly + tagDisabledBg: '#2D3039', // --- Tamagui standard tokens (15) --- background: '#1F2329', diff --git a/packages/fuselage/src/themes/highContrast.ts b/packages/fuselage/src/themes/highContrast.ts index 6df2c2fdac..add94875c4 100644 --- a/packages/fuselage/src/themes/highContrast.ts +++ b/packages/fuselage/src/themes/highContrast.ts @@ -146,15 +146,7 @@ export const highContrastTheme = { buttonSecondaryWarningColor: colors.y1000, buttonSecondaryWarningDisabledColor: colors.y300, - // --- Tag tokens (8) --- - tagDefaultBg: colors.n200, - tagDefaultColor: colors.n800, - tagPrimaryBg: colors.b200, - tagPrimaryColor: colors.b800, - tagDangerBg: colors.r200, - tagDangerColor: colors.r900, - tagWarningBg: colors.y200, - tagWarningColor: colors.y1000, + // Tag tokens removed — Tag now uses button/surface/status tokens directly // --- Tamagui standard tokens (15) --- background: colors.white, diff --git a/packages/fuselage/src/themes/light.ts b/packages/fuselage/src/themes/light.ts index 8bb233044c..6da1b1c04d 100644 --- a/packages/fuselage/src/themes/light.ts +++ b/packages/fuselage/src/themes/light.ts @@ -119,11 +119,11 @@ export const lightTheme = { buttonSuccessColor: colors.white, buttonSuccessDisabledColor: colors.white, - // --- Button Warning (7) --- - buttonWarningBg: colors.y600, - buttonWarningHoverBg: colors.y700, - buttonWarningPressBg: colors.y800, - buttonWarningFocusBg: colors.y600, + // --- Button Warning (7) — uses yellow(400/500/600) not y600/y700/y800 --- + buttonWarningBg: colors.y400, // warning-default = yellow(400) + buttonWarningHoverBg: colors.y500, // warning-hover = yellow(500) + buttonWarningPressBg: colors.y600, // warning-press = yellow(600) + buttonWarningFocusBg: colors.y400, // warning-focus = yellow(400) buttonWarningDisabledBg: colors.y200, buttonWarningColor: colors.n900, // $-button-fonts on-warning = neutral(900) buttonWarningDisabledColor: colors.n700, @@ -146,15 +146,7 @@ export const lightTheme = { buttonSecondaryWarningColor: colors.y900, buttonSecondaryWarningDisabledColor: colors.y300, - // --- Tag tokens (8) --- - tagDefaultBg: colors.n200, - tagDefaultColor: colors.n700, - tagPrimaryBg: colors.b200, - tagPrimaryColor: colors.b600, - tagDangerBg: colors.r200, - tagDangerColor: colors.r700, - tagWarningBg: colors.y200, - tagWarningColor: colors.y900, + // Tag tokens removed — Tag now uses button/surface/status tokens directly // --- Tamagui standard tokens (15) --- background: colors.white, From c563fa6fa7e49ceaac5a15e043c2844403f2d9fd Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 02:43:08 -0300 Subject: [PATCH 05/84] refactor(fuselage): Migrate Chip component from SCSS to Tamagui v3 ChipBase (RcxView): layout + button secondary bg/hover/press/focus. ChipText (raw Text): p2 font scale via tokens, color $fontSecondaryInfo. borderWidth: 0 (original has border-width: 0, not 1 like Tag). disabledStyle uses $buttonSecondaryDisabledBg + $fontDisabled. Gap $x4 replaces Margins wrapper. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/Chip/Chip.styles.scss | 107 ----------------- .../fuselage/src/components/Chip/Chip.tsx | 108 ++++++++++++++---- packages/fuselage/src/components/index.scss | 1 - 3 files changed, 88 insertions(+), 128 deletions(-) delete mode 100644 packages/fuselage/src/components/Chip/Chip.styles.scss diff --git a/packages/fuselage/src/components/Chip/Chip.styles.scss b/packages/fuselage/src/components/Chip/Chip.styles.scss deleted file mode 100644 index 1b358a63a8..0000000000 --- a/packages/fuselage/src/components/Chip/Chip.styles.scss +++ /dev/null @@ -1,107 +0,0 @@ -@use '../../styles/colors.scss'; -@use '../../styles/lengths.scss'; -@use '../../styles/typography.scss'; -@use '../../styles/functions'; -@use '../../styles/primitives/button.scss'; - -// to do: replace button with stroke - -$chip-background-color: functions.theme( - 'chip-background-color', - colors.button(secondary-default) -); -$chip-border-color: functions.theme( - 'chip-border-color', - colors.button(secondary-default) -); -$chip-color: functions.theme('chip-color', colors.font(secondary-info)); -$chip-hover-background-color: functions.theme( - 'chip-hover-background-color', - colors.button(secondary-hover) -); -$chip-hover-border-color: functions.theme( - 'chip-hover-border-color', - colors.button(secondary-hover) -); -$chip-active-background-color: functions.theme( - 'chip-active-background-color', - colors.button(secondary-press) -); -$chip-active-border-color: functions.theme( - 'chip-active-border-color', - colors.button(secondary-press) -); -$chip-focus-background-color: functions.theme( - 'chip-focus-background-color', - colors.button(secondary-focus) -); -$chip-focus-border-color: functions.theme( - 'chip-focus-border-color', - colors.stroke(extra-dark) -); -$chip-focus-shadow-color: functions.theme( - 'chip-focus-shadow-color', - colors.stroke(extra-light-highlight) -); -$chip-disabled-background-color: functions.theme( - 'chip-disabled-background-color', - colors.button(secondary-disabled) -); -$chip-disabled-border-color: functions.theme( - 'chip-disabled-border-color', - colors.button(secondary-disabled) -); -$chip-disabled-color: functions.theme( - 'chip-disabled-color', - colors.font(disabled) -); - -.rcx-chip { - @extend %box--full; - @include button.kind-variant( - ( - background-color: $chip-background-color, - border-color: $chip-border-color, - color: $chip-color, - hover-background-color: $chip-hover-background-color, - hover-border-color: $chip-hover-border-color, - active-background-color: $chip-active-background-color, - active-border-color: $chip-active-border-color, - focus-background-color: $chip-focus-background-color, - focus-border-color: $chip-focus-border-color, - focus-shadow-color: $chip-focus-shadow-color, - disabled-background-color: $chip-disabled-background-color, - disabled-border-color: $chip-disabled-border-color, - disabled-color: $chip-disabled-color, - ) - ); - - @include clickable; - @include typography.use-font-scale('p2'); - - display: flex; - overflow: hidden; - align-items: center; - - min-height: lengths.size(28); - - border-width: 0; - - &.disabled, - &:disabled { - color: $button-secondary-color; - border-color: $button-secondary-border-color; - background-color: $button-secondary-background-color; - } - - &__text { - @include typography.use-text-ellipsis; - - white-space: nowrap; - letter-spacing: inherit; - - color: inherit; - - font: inherit; - } -} diff --git a/packages/fuselage/src/components/Chip/Chip.tsx b/packages/fuselage/src/components/Chip/Chip.tsx index 68edbcd213..6f5861a1cb 100644 --- a/packages/fuselage/src/components/Chip/Chip.tsx +++ b/packages/fuselage/src/components/Chip/Chip.tsx @@ -1,11 +1,84 @@ import type { ButtonHTMLAttributes, ReactNode } from 'react'; -import { prependClassName } from '../../helpers/prependClassName'; +import { styled, Text } from 'tamagui'; + +import { RcxText } from '../../primitives'; import { Avatar } from '../Avatar'; -import { Box } from '../Box'; -import { withBoxStyling } from '../Box/withBoxStyling'; import { Icon } from '../Icon'; -import { Margins } from '../Margins'; + +// Outer container — RcxText because: +// 1. Original was + {thumbUrl && renderThumb && renderThumb({ url: thumbUrl })} + {children && {children}} + {onDismiss && renderDismissSymbol && renderDismissSymbol()} + ); }; -export default withBoxStyling(Chip); +export default Chip; diff --git a/packages/fuselage/src/components/index.scss b/packages/fuselage/src/components/index.scss index 1c509ff065..c2326872c9 100644 --- a/packages/fuselage/src/components/index.scss +++ b/packages/fuselage/src/components/index.scss @@ -11,7 +11,6 @@ @import './CardGroup/CardGroup.styles.scss'; @import './CheckBox/CheckBox.styles.scss'; @import './Chevron/Chevron.styles.scss'; -@import './Chip/Chip.styles.scss'; @import './CodeSnippet/CodeSnippet.styles.scss'; @import './Dropdown/Dropdown.styles.scss'; @import './Field/Field.styles.scss'; From 46f599a13ee2705e1426cf5226b3a9925178e748 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 02:53:05 -0300 Subject: [PATCH 06/84] refactor(fuselage): Migrate Callout component from SCSS to Tamagui v3 CalloutBase (RcxView): layout + border color by type variant. CalloutTitle (RcxText): p2b font tokens. CalloutContent (RcxText): p2 font tokens. CalloutIcon: color variant matching border color per type. All colors from status-font tokens verified against SCSS. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/Callout/Callout.styles.scss | 124 --------------- .../src/components/Callout/Callout.tsx | 149 +++++++++++++++--- packages/fuselage/src/components/index.scss | 1 - 3 files changed, 125 insertions(+), 149 deletions(-) delete mode 100644 packages/fuselage/src/components/Callout/Callout.styles.scss diff --git a/packages/fuselage/src/components/Callout/Callout.styles.scss b/packages/fuselage/src/components/Callout/Callout.styles.scss deleted file mode 100644 index e842ad1870..0000000000 --- a/packages/fuselage/src/components/Callout/Callout.styles.scss +++ /dev/null @@ -1,124 +0,0 @@ -@use '../../styles/colors.scss'; -@use '../../styles/lengths.scss'; -@use '../../styles/typography.scss'; - -$callout-background-color: theme( - 'callout-background-color', - colors.surface(light) -); -$callout-default-color: theme( - 'callout-default-color', - colors.font(secondary-info) -); -$callout-info-color: theme('callout-info-color', colors.status-font(on-info)); -$callout-success-color: theme( - 'callout-success-color', - colors.status-font(on-success) -); -$callout-warning-color: theme( - 'callout-warning-color', - colors.status-font(on-warning) -); -$callout-danger-color: theme( - 'callout-danger-color', - colors.status-font(on-danger) -); -$callout-text-color: theme('callout-text-color', colors.font(default)); - -.rcx-callout { - display: flex; - - padding: lengths.padding(12); - - color: $callout-text-color; - - border-width: lengths.border-width(default); - border-style: solid; - border-color: $callout-default-color; - - border-radius: theme('callout-border-radius', lengths.border-radius(medium)); - - background-color: $callout-background-color; - - &--info { - border-color: $callout-info-color; - - .rcx-callout__icon { - color: $callout-info-color; - } - } - - &--success { - border-color: $callout-success-color; - - .rcx-callout__icon { - color: $callout-success-color; - } - } - - &--warning { - border-color: $callout-warning-color; - - .rcx-callout__icon { - color: $callout-warning-color; - } - } - - &--danger { - border-color: $callout-danger-color; - - .rcx-callout__icon { - color: $callout-danger-color; - } - } - - &__wrapper { - overflow: hidden; - - justify-content: space-between; - - flex: 1 1 0; - - margin-inline-start: lengths.margin(12); - - > :nth-child(2) { - margin-block-start: lengths.margin(12); - } - - &--large { - display: flex; - - overflow: hidden; - flex-direction: row; - align-items: center; - - > :nth-child(2) { - margin-block-start: lengths.margin(0); - } - } - } - - &__wrapper-content { - display: flex; - - overflow: hidden; - flex-flow: column nowrap; - - > :nth-child(2) { - margin-block-start: lengths.margin(4); - } - } - - &__title { - white-space: nowrap; - - @include typography.use-font-scale(p2b); - @include typography.use-text-ellipsis; - } - - &__content { - display: block; - - @include typography.use-font-scale(p2); - } -} diff --git a/packages/fuselage/src/components/Callout/Callout.tsx b/packages/fuselage/src/components/Callout/Callout.tsx index 9d776eea57..37db4c1232 100644 --- a/packages/fuselage/src/components/Callout/Callout.tsx +++ b/packages/fuselage/src/components/Callout/Callout.tsx @@ -1,15 +1,126 @@ import { useResizeObserver } from '@rocket.chat/fuselage-hooks'; import type { ReactElement, ReactNode } from 'react'; +import { styled, Text } from 'tamagui'; -import { Box, type BoxProps } from '../Box'; +import { RcxView, RcxText } from '../../primitives'; import { Icon, type IconProps } from '../Icon'; -export type CalloutProps = Omit & { +// Outer section — RcxView for layout +const CalloutBase = styled(RcxView, { + name: 'CalloutBase', + + display: 'flex', + flexDirection: 'row', + + padding: '$x12', + + borderWidth: 1, + borderStyle: 'solid', + // default border: colors.font(secondary-info) + borderColor: '$fontSecondaryInfo', + borderRadius: '$x4', + + // bg: colors.surface(light) + backgroundColor: '$surfaceLight', + + variants: { + type: { + info: { borderColor: '$statusFontOnInfo' }, + success: { borderColor: '$statusFontOnSuccess' }, + warning: { borderColor: '$statusFontOnWarning' }, + danger: { borderColor: '$statusFontOnDanger' }, + }, + } as const, +}); + +// Icon wrapper — color follows type +const CalloutIcon = styled(RcxView, { + name: 'CalloutIcon', + + variants: { + type: { + info: { color: '$statusFontOnInfo' }, + success: { color: '$statusFontOnSuccess' }, + warning: { color: '$statusFontOnWarning' }, + danger: { color: '$statusFontOnDanger' }, + }, + } as const, +}); + +// Wrapper — flex:1 1 0, overflow hidden, margin-inline-start 12 +const CalloutWrapper = styled(RcxView, { + name: 'CalloutWrapper', + + overflow: 'hidden', + justifyContent: 'space-between', + + marginInlineStart: '$x12', + gap: '$x12', + + variants: { + large: { + true: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + overflow: 'hidden', + gap: 0, + }, + }, + } as const, +}); + +// Wrapper content — flex column +const CalloutWrapperContent = styled(RcxView, { + name: 'CalloutWrapperContent', + + display: 'flex', + overflow: 'hidden', + flexDirection: 'column', + gap: '$x4', +}); + +// Title — p2b font scale, text ellipsis +const CalloutTitle = styled(RcxText, { + name: 'CalloutTitle', + + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + + fontFamily: '$body', + fontSize: '$p2b', + fontWeight: '$p2b', + lineHeight: '$p2b', + letterSpacing: '$p2b', + + // text color: colors.font(default) + color: '$fontDefault', +}); + +// Content — p2 font scale, display block +const CalloutContent = styled(RcxText, { + name: 'CalloutContent', + + display: 'block', + + fontFamily: '$body', + fontSize: '$p2', + fontWeight: '$p2', + lineHeight: '$p2', + letterSpacing: '$p2', + + // text color: colors.font(default) + color: '$fontDefault', +}); + +export type CalloutProps = { type?: 'info' | 'success' | 'warning' | 'danger'; title?: ReactNode; children?: ReactNode; icon?: IconProps['name']; actions?: ReactElement; + [key: string]: any; }; const WRAPPER_LIMIT_SIZE = 420; @@ -22,7 +133,6 @@ const Callout = ({ title, children, icon, - className, actions, ...props }: CalloutProps) => { @@ -38,27 +148,18 @@ const Callout = ({ 'info-circled'; return ( - - - - - {title && {title}} - {children && {children}} - - {actions && {actions}} - - + + + + + + + {title && {title}} + {children && {children}} + + {actions} + + ); }; diff --git a/packages/fuselage/src/components/index.scss b/packages/fuselage/src/components/index.scss index c2326872c9..fc0998dc99 100644 --- a/packages/fuselage/src/components/index.scss +++ b/packages/fuselage/src/components/index.scss @@ -6,7 +6,6 @@ @import './Button/Button.styles.scss'; @import './Bubble/Bubble.styles.scss'; @import './ButtonGroup/ButtonGroup.styles.scss'; -@import './Callout/Callout.styles.scss'; @import './Card/Card.styles.scss'; @import './CardGroup/CardGroup.styles.scss'; @import './CheckBox/CheckBox.styles.scss'; From 85658c5c82905ed6742399555d250e62a5998600 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 03:03:28 -0300 Subject: [PATCH 07/84] refactor(fuselage): Migrate FramedIcon component from SCSS to Tamagui v3 FramedIconBase (RcxText): p2m font tokens, $fontSecondaryInfo default color, $surfaceTint bg, $x4 padding/borderRadius. Boolean props mapped to single variant (info/success/warning/danger). Colors from $statusFontOn* tokens verified against SCSS. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../FramedIcon/FramedIcon.styles.scss | 29 --------- .../src/components/FramedIcon/FramedIcon.tsx | 62 ++++++++++++++----- packages/fuselage/src/components/index.scss | 1 - 3 files changed, 47 insertions(+), 45 deletions(-) delete mode 100644 packages/fuselage/src/components/FramedIcon/FramedIcon.styles.scss diff --git a/packages/fuselage/src/components/FramedIcon/FramedIcon.styles.scss b/packages/fuselage/src/components/FramedIcon/FramedIcon.styles.scss deleted file mode 100644 index 0fd539b8af..0000000000 --- a/packages/fuselage/src/components/FramedIcon/FramedIcon.styles.scss +++ /dev/null @@ -1,29 +0,0 @@ -@use '../../styles/colors.scss'; -@use '../../styles/typography.scss'; -@use '../../styles/lengths.scss'; - -.rcx-framed-icon { - @include typography.use-font-scale(p2m); - - padding: lengths.padding(4); - - color: colors.font(secondary-info); - border-radius: lengths.border-radius(medium); - background-color: colors.surface(tint); - - &--info { - color: colors.status-font(on-info); - } - - &--success { - color: colors.status-font(on-success); - } - - &--warning { - color: colors.status-font(on-warning); - } - - &--danger { - color: colors.status-font(on-danger); - } -} diff --git a/packages/fuselage/src/components/FramedIcon/FramedIcon.tsx b/packages/fuselage/src/components/FramedIcon/FramedIcon.tsx index d02af9f231..c17706f481 100644 --- a/packages/fuselage/src/components/FramedIcon/FramedIcon.tsx +++ b/packages/fuselage/src/components/FramedIcon/FramedIcon.tsx @@ -1,8 +1,41 @@ import type { Keys } from '@rocket.chat/icons'; import type { AllHTMLAttributes } from 'react'; +import { styled } from 'tamagui'; +import { RcxText } from '../../primitives'; import { Icon } from '../Icon'; +// RcxText because it needs font props (p2m scale affects icon sizing) +const FramedIconBase = styled(RcxText, { + name: 'FramedIcon', + + display: 'inline-flex', + + padding: '$x4', + borderRadius: '$x4', + + // p2m font scale (from original: @include typography.use-font-scale(p2m)) + fontFamily: '$body', + fontSize: '$p2m', + fontWeight: '$p2m', + lineHeight: '$p2m', + letterSpacing: '$p2m', + + // default: colors.font(secondary-info), bg: colors.surface(tint) + color: '$fontSecondaryInfo', + backgroundColor: '$surfaceTint', + + variants: { + variant: { + neutral: {}, + info: { color: '$statusFontOnInfo' }, + success: { color: '$statusFontOnSuccess' }, + warning: { color: '$statusFontOnWarning' }, + danger: { color: '$statusFontOnDanger' }, + }, + } as const, +}); + export type FramedIconProps = { info?: boolean; success?: boolean; @@ -20,20 +53,19 @@ const FramedIcon = ({ neutral, icon, ...props -}: FramedIconProps) => ( - -); +}: FramedIconProps) => { + const variant = + (info && 'info') || + (success && 'success') || + (warning && 'warning') || + (danger && 'danger') || + 'neutral'; + + return ( + + + + ); +}; export default FramedIcon; diff --git a/packages/fuselage/src/components/index.scss b/packages/fuselage/src/components/index.scss index fc0998dc99..60a0f4c4f3 100644 --- a/packages/fuselage/src/components/index.scss +++ b/packages/fuselage/src/components/index.scss @@ -14,7 +14,6 @@ @import './Dropdown/Dropdown.styles.scss'; @import './Field/Field.styles.scss'; @import './FieldGroup/FieldGroup.styles.scss'; -@import './FramedIcon/FramedIcon.styles.scss'; @import './Grid/Grid.styles.scss'; @import './Icon/Icon.styles.scss'; @import './InputBox/InputBox.styles.scss'; From d9c8828aa26ccdd7a9301c8c842636a22aa2740f Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 03:05:13 -0300 Subject: [PATCH 08/84] refactor(fuselage): Migrate Label component from SCSS to Tamagui v3 LabelBase (RcxText): p2m font tokens, $fontDefault color, display flex. Disabled variant: pointerEvents none, $fontSecondaryInfo color. LabelRequired (raw Text): $fontDanger color, marginInlineStart $x4. LabelContext preserved for nested label detection. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/Label/Label.styles.scss | 28 -------- .../fuselage/src/components/Label/Label.tsx | 67 +++++++++++++++---- .../src/components/Label/LabelInfo.tsx | 2 +- packages/fuselage/src/components/index.scss | 1 - 4 files changed, 55 insertions(+), 43 deletions(-) delete mode 100644 packages/fuselage/src/components/Label/Label.styles.scss diff --git a/packages/fuselage/src/components/Label/Label.styles.scss b/packages/fuselage/src/components/Label/Label.styles.scss deleted file mode 100644 index 77d373c421..0000000000 --- a/packages/fuselage/src/components/Label/Label.styles.scss +++ /dev/null @@ -1,28 +0,0 @@ -@use '../../styles/colors.scss'; -@use '../../styles/typography.scss'; -@use '../../styles/lengths.scss'; - -.rcx-label { - @include typography.use-font-scale(p2m); - display: flex; - - cursor: default; - - color: colors.font(default); - - &--disabled { - pointer-events: none; - - color: colors.font(secondary-info); - } - - &__info { - display: flex; - align-items: center; - order: 1; - } - - &__required { - color: colors.font(danger); - } -} diff --git a/packages/fuselage/src/components/Label/Label.tsx b/packages/fuselage/src/components/Label/Label.tsx index 21ab4ee306..cfab756434 100644 --- a/packages/fuselage/src/components/Label/Label.tsx +++ b/packages/fuselage/src/components/Label/Label.tsx @@ -1,14 +1,60 @@ import type { ElementType } from 'react'; import { createContext, forwardRef, useContext } from 'react'; -import { Box, type BoxProps } from '../Box'; +import { styled, Text } from 'tamagui'; + +import { RcxText } from '../../primitives'; const LabelContext = createContext(false); -export type LabelProps = Omit & { +// RcxText — needs font props, renders text content +const LabelBase = styled(RcxText, { + name: 'Label', + + display: 'flex', + + // p2m font scale (from original: @include typography.use-font-scale(p2m)) + fontFamily: '$body', + fontSize: '$p2m', + fontWeight: '$p2m', + lineHeight: '$p2m', + letterSpacing: '$p2m', + + // color: colors.font(default) + color: '$fontDefault', + + variants: { + isDisabled: { + true: { + pointerEvents: 'none', + color: '$fontSecondaryInfo', + }, + }, + } as const, +}); + +// Required asterisk +const LabelRequired = styled(Text, { + name: 'LabelRequired', + + marginInlineStart: '$x4', + color: '$fontDanger', +}); + +// Info wrapper (order: 1 in original) +const LabelInfo = styled(RcxText, { + name: 'LabelInfo', + + display: 'flex', + alignItems: 'center', +}); + +export type LabelProps = { disabled?: boolean; required?: boolean; is?: (ElementType & string) | undefined; + children?: React.ReactNode; + [key: string]: any; }; /** @@ -19,24 +65,19 @@ const Label = forwardRef(function Label( ref, ) { const isInsideLabel = useContext(LabelContext); - const component = is || (isInsideLabel && 'span') || 'label'; return ( - {children} {required && ( - + )} - + ); }); diff --git a/packages/fuselage/src/components/Label/LabelInfo.tsx b/packages/fuselage/src/components/Label/LabelInfo.tsx index daa167878c..e7de9d017c 100644 --- a/packages/fuselage/src/components/Label/LabelInfo.tsx +++ b/packages/fuselage/src/components/Label/LabelInfo.tsx @@ -7,7 +7,7 @@ export type LabelInfoProps = { } & Omit; export const LabelInfo = ({ title, id, ...props }: LabelInfoProps) => ( - + diff --git a/packages/fuselage/src/components/index.scss b/packages/fuselage/src/components/index.scss index 60a0f4c4f3..e2c7947730 100644 --- a/packages/fuselage/src/components/index.scss +++ b/packages/fuselage/src/components/index.scss @@ -17,7 +17,6 @@ @import './Grid/Grid.styles.scss'; @import './Icon/Icon.styles.scss'; @import './InputBox/InputBox.styles.scss'; -@import './Label/Label.styles.scss'; @import './Message/Messages.styles.scss'; @import './Modal/Modal.styles.scss'; @import './NavBar/NavBar.styles.scss'; From 48f53ef92c86e38af80b6cdf96a684ce36797e7f Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 10:02:59 -0300 Subject: [PATCH 09/84] refactor(fuselage): Migrate Skeleton component from SCSS to Tamagui v3 SkeletonBase (RcxView): display block, opacity 0.1, borderRadius $x4. Variants: text (height auto + scale transform via style prop), rect (base only), circle (borderRadius $full). Shimmer animation via @rocket.chat/css-in-js keyframes + className. Background gradient uses $strokeExtraDark CSS var. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/Skeleton/Skeleton.styles.scss | 51 ------------ .../src/components/Skeleton/Skeleton.tsx | 78 +++++++++++++++---- packages/fuselage/src/components/index.scss | 1 - 3 files changed, 62 insertions(+), 68 deletions(-) delete mode 100644 packages/fuselage/src/components/Skeleton/Skeleton.styles.scss diff --git a/packages/fuselage/src/components/Skeleton/Skeleton.styles.scss b/packages/fuselage/src/components/Skeleton/Skeleton.styles.scss deleted file mode 100644 index a7a83db1b7..0000000000 --- a/packages/fuselage/src/components/Skeleton/Skeleton.styles.scss +++ /dev/null @@ -1,51 +0,0 @@ -@use '../../styles/colors.scss'; -@use '../../styles/lengths.scss'; - -.rcx-skeleton { - display: block; - - height: 1.2em; - - animation: rcx-skeleton__animation 1s linear 0s infinite running; - - opacity: 10%; - - border-radius: lengths.border-radius(medium); - - background: repeat 0% 0%/100vw 100% - linear-gradient( - to right, - colors.stroke(extra-dark), - color-mix(in srgb, colors.stroke(extra-dark), transparent 50%) 50%, - colors.stroke(extra-dark) - ); - - &--text { - height: auto; - margin-block: lengths.margin(none); - - transform: scale(1, 0.6); - transform-origin: 0 60%; - - &:empty::before { - content: '\00a0'; - } - } - - &--circle { - border-radius: lengths.border-radius(full); - } - - @extend %box; - @extend %box--full; -} - -@keyframes rcx-skeleton__animation { - 0% { - background-position: 0 0; - } - - 100% { - background-position: 100vw 0; - } -} diff --git a/packages/fuselage/src/components/Skeleton/Skeleton.tsx b/packages/fuselage/src/components/Skeleton/Skeleton.tsx index 8c0c60f292..4b1ce5ac46 100644 --- a/packages/fuselage/src/components/Skeleton/Skeleton.tsx +++ b/packages/fuselage/src/components/Skeleton/Skeleton.tsx @@ -1,25 +1,71 @@ import type { AllHTMLAttributes } from 'react'; -import { cx, cxx } from '../../helpers/composeClassNames'; -import type { StylingBoxProps } from '../Box'; -import { StylingBox } from '../Box'; +import { css, keyframes } from '@rocket.chat/css-in-js'; +import { styled } from 'tamagui'; -export type SkeletonProps = Omit & { +import { RcxView } from '../../primitives'; + +const skeletonAnimation = keyframes` + 0% { background-position: 0 0; } + 100% { background-position: 100vw 0; } +`; + +const shimmerClass = css` + animation: ${skeletonAnimation} 1s linear 0s infinite running; + background: repeat 0% 0% / 100vw 100% + linear-gradient( + to right, + var(--strokeExtraDark), + color-mix(in srgb, var(--strokeExtraDark), transparent 50%) 50%, + var(--strokeExtraDark) + ); +`; + +const SkeletonBase = styled(RcxView, { + name: 'Skeleton', + + display: 'block', + height: '1.2em' as any, + opacity: 0.1, + borderRadius: '$x4', + + variants: { + variant: { + rect: {}, + text: { + height: 'auto' as any, + marginBlock: 0, + // transform not supported in styled — applied via style prop + }, + circle: { + borderRadius: '$full', + }, + }, + } as const, + + defaultVariants: { + variant: 'text', + }, +}); + +export type SkeletonProps = { variant?: 'text' | 'rect' | 'circle'; -} & AllHTMLAttributes; + [key: string]: any; +}; const Skeleton = ({ variant = 'text', ...props }: SkeletonProps) => ( - - - + + {variant === 'text' ? '\u00a0' : undefined} + ); export default Skeleton; diff --git a/packages/fuselage/src/components/index.scss b/packages/fuselage/src/components/index.scss index e2c7947730..bdaf025dd4 100644 --- a/packages/fuselage/src/components/index.scss +++ b/packages/fuselage/src/components/index.scss @@ -29,7 +29,6 @@ @import './Sidebar/Sidebar.styles.scss'; @import './Sidepanel/Sidepanel.styles.scss'; @import './SidebarV2/Sidebar.styles.scss'; -@import './Skeleton/Skeleton.styles.scss'; @import './States/States.styles.scss'; @import './Table/Table.styles.scss'; @import './Tabs/Tabs.styles.scss'; From f43f599b196cb4844619f145f6ccaafc53c21e63 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 10:12:23 -0300 Subject: [PATCH 10/84] refactor(fuselage): Migrate Tile component from SCSS to Tamagui v3 TileBase (RcxText): p2 font tokens, $fontDefault, $surfaceLight bg. Elevation variants: 0 (no shadow), 1 (12px shadow + border), 2 (2px + 12px shadow + border). Shadow uses CSS vars for theme tokens. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/Tile/Tile.styles.scss | 30 --------- .../fuselage/src/components/Tile/Tile.tsx | 67 ++++++++++++++++--- packages/fuselage/src/components/index.scss | 1 - 3 files changed, 58 insertions(+), 40 deletions(-) delete mode 100644 packages/fuselage/src/components/Tile/Tile.styles.scss diff --git a/packages/fuselage/src/components/Tile/Tile.styles.scss b/packages/fuselage/src/components/Tile/Tile.styles.scss deleted file mode 100644 index 4c7f667272..0000000000 --- a/packages/fuselage/src/components/Tile/Tile.styles.scss +++ /dev/null @@ -1,30 +0,0 @@ -@use '../../styles/colors.scss'; -@use '../../styles/lengths.scss'; -@use '../../styles/typography.scss'; -@use '../../styles/functions.scss'; -@use '../../styles/mixins/elevation.scss'; - -.rcx-tile { - display: block; - - color: colors.font(default); - border-radius: functions.theme( - 'tile-border-radius', - lengths.border-radius(medium) - ); - background-color: colors.surface(light); - - @include typography.use-font-scale(p2); - - &--elevation-0 { - box-shadow: none; - } - - &--elevation-1 { - @include elevation.elevation-level(1); - } - - &--elevation-2 { - @include elevation.elevation-level(2); - } -} diff --git a/packages/fuselage/src/components/Tile/Tile.tsx b/packages/fuselage/src/components/Tile/Tile.tsx index 30ef802cf8..cdafb21a63 100644 --- a/packages/fuselage/src/components/Tile/Tile.tsx +++ b/packages/fuselage/src/components/Tile/Tile.tsx @@ -1,20 +1,69 @@ import { forwardRef } from 'react'; -import { Box, type BoxProps } from '../Box'; +import { styled } from 'tamagui'; -export type TileProps = BoxProps; +import { RcxText } from '../../primitives'; + +// RcxText — needs font props (p2 scale), renders block content +const TileBase = styled(RcxText, { + name: 'Tile', + + display: 'block', + padding: '$x16', + + borderRadius: '$x4', + + // p2 font scale + fontFamily: '$body', + fontSize: '$p2', + fontWeight: '$p2', + lineHeight: '$p2', + letterSpacing: '$p2', + + color: '$fontDefault', + backgroundColor: '$surfaceLight', + + variants: { + elevation: { + '0': { + boxShadow: 'none', + }, + '1': { + borderWidth: 1, + borderStyle: 'solid', + borderColor: '$shadowElevationBorder', + boxShadow: '0 0 12px 0 var(--shadowElevation1)', + }, + '2': { + borderWidth: 1, + borderStyle: 'solid', + borderColor: '$shadowElevationBorder', + boxShadow: '0 0 2px 0 var(--shadowElevation2x), 0 0 12px 0 var(--shadowElevation2y)', + }, + }, + } as const, + + defaultVariants: { + elevation: '1', + }, +}); + +export type TileProps = { + elevation?: '0' | '1' | '2'; + padding?: number | string; + children?: React.ReactNode; + [key: string]: any; +}; const Tile = forwardRef(function Tile( - { elevation = '1', padding = 16, ...props }, + { elevation = '1', ...props }, ref, ) { return ( - ); }); diff --git a/packages/fuselage/src/components/index.scss b/packages/fuselage/src/components/index.scss index bdaf025dd4..c2714d29ea 100644 --- a/packages/fuselage/src/components/index.scss +++ b/packages/fuselage/src/components/index.scss @@ -33,7 +33,6 @@ @import './Table/Table.styles.scss'; @import './Tabs/Tabs.styles.scss'; @import './Throbber/Throbber.styles.scss'; -@import './Tile/Tile.styles.scss'; @import './ToastBar/ToastBar.styles.scss'; @import './ToggleSwitch/ToggleSwitch.styles.scss'; @import './Tooltip/Tooltip.styles.scss'; From ae0facfdb0de59b4900722ecf81922c23fdc984c Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 10:15:35 -0300 Subject: [PATCH 11/84] refactor(fuselage): Migrate CodeSnippet component from SCSS to Tamagui v3 CodeSnippetBase (RcxView): flex row, $surfaceNeutral bg, $x4 radius. CodeSnippetCodebox (raw Text): $mono font, $fontDefault color. whiteSpace/wordBreak via style prop (not supported in styled). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../CodeSnippet/CodeSnippet.styles.scss | 32 ---------- .../components/CodeSnippet/CodeSnippet.tsx | 63 ++++++++++++++----- packages/fuselage/src/components/index.scss | 1 - 3 files changed, 46 insertions(+), 50 deletions(-) delete mode 100644 packages/fuselage/src/components/CodeSnippet/CodeSnippet.styles.scss diff --git a/packages/fuselage/src/components/CodeSnippet/CodeSnippet.styles.scss b/packages/fuselage/src/components/CodeSnippet/CodeSnippet.styles.scss deleted file mode 100644 index 41e3932558..0000000000 --- a/packages/fuselage/src/components/CodeSnippet/CodeSnippet.styles.scss +++ /dev/null @@ -1,32 +0,0 @@ -@use '../../styles/colors.scss'; -@use '../../styles/lengths.scss'; -@use '../../styles/typography.scss'; - -.rcx-code-snippet { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - - width: 100%; - - min-height: lengths.size(60); - padding: lengths.padding(16); - - color: colors.font(default); - - border-radius: theme( - 'code-snippet-border-radius', - lengths.border-radius(medium) - ); - background-color: colors.surface(neutral); - - &__codebox { - margin-right: lengths.margin(8); - - white-space: pre-line; - word-break: break-all; - - font-family: monospace; - } -} diff --git a/packages/fuselage/src/components/CodeSnippet/CodeSnippet.tsx b/packages/fuselage/src/components/CodeSnippet/CodeSnippet.tsx index 5ae26e2086..c1a030673a 100644 --- a/packages/fuselage/src/components/CodeSnippet/CodeSnippet.tsx +++ b/packages/fuselage/src/components/CodeSnippet/CodeSnippet.tsx @@ -1,22 +1,50 @@ import type { ReactElement } from 'react'; -import { Box, type BoxProps } from '../Box'; +import { styled, Text } from 'tamagui'; + +import { RcxView } from '../../primitives'; import { Button } from '../Button'; import { Skeleton } from '../Skeleton'; -export type CodeSnippetProps = BoxProps & { +// Outer — RcxView for layout (pre element in original) +const CodeSnippetBase = styled(RcxView, { + name: 'CodeSnippet', + + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + + width: '100%' as any, + minHeight: '$x60', + padding: '$x16', + + borderRadius: '$x4', + + backgroundColor: '$surfaceNeutral', +}); + +// Codebox — Text for font props (monospace) +const CodeSnippetCodebox = styled(Text, { + name: 'CodeSnippetCodebox', + + marginRight: '$x8', + + // Unsupported in styled: whiteSpace, wordBreak applied via style + fontFamily: '$mono', + color: '$fontDefault', +}); + +export type CodeSnippetProps = { children: string; buttonText?: string; buttonDisabled?: boolean; onClick?: () => void; + [key: string]: any; }; /** * The `CodeSnippet` is used to show code or commands and make easier to copy them. - * - * The default button text is `Copy` but you can use the `buttonText` prop to handle translations in your project. - * - * Please check the `useClipBoard` hook in `fuselage-hooks` package, to handle the copy behaviour. */ const CodeSnippet = ({ children, @@ -27,25 +55,26 @@ const CodeSnippet = ({ }: CodeSnippetProps): ReactElement => { if (!children) { return ( - + - + ); } return ( - - + + {children} - + {onClick && children && ( - - - + )} - + ); }; diff --git a/packages/fuselage/src/components/index.scss b/packages/fuselage/src/components/index.scss index c2714d29ea..09a087de68 100644 --- a/packages/fuselage/src/components/index.scss +++ b/packages/fuselage/src/components/index.scss @@ -10,7 +10,6 @@ @import './CardGroup/CardGroup.styles.scss'; @import './CheckBox/CheckBox.styles.scss'; @import './Chevron/Chevron.styles.scss'; -@import './CodeSnippet/CodeSnippet.styles.scss'; @import './Dropdown/Dropdown.styles.scss'; @import './Field/Field.styles.scss'; @import './FieldGroup/FieldGroup.styles.scss'; From 537e1b5edf0e6f7d066ed3b242f02a4fbe72e76e Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 10:18:47 -0300 Subject: [PATCH 12/84] refactor(fuselage): Migrate Chevron component from SCSS to Tamagui v3 ChevronFrame (RcxView): inline-flex, alignSelf center. Direction rotation via style prop (transform not supported in styled). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/Chevron/Chevron.styles.scss | 28 ----------- .../src/components/Chevron/Chevron.tsx | 48 ++++++++++++------- 2 files changed, 32 insertions(+), 44 deletions(-) delete mode 100644 packages/fuselage/src/components/Chevron/Chevron.styles.scss diff --git a/packages/fuselage/src/components/Chevron/Chevron.styles.scss b/packages/fuselage/src/components/Chevron/Chevron.styles.scss deleted file mode 100644 index e451942593..0000000000 --- a/packages/fuselage/src/components/Chevron/Chevron.styles.scss +++ /dev/null @@ -1,28 +0,0 @@ -.rcx-chevron { - display: inline-flex; - align-self: center; - - &--up { - transform: rotate(-180deg); - } - - &--down { - transform: rotate(0deg); - } - - &--right { - transform: rotate(-90deg); - - &:dir(rtl) { - transform: rotate(-270deg); - } - } - - &--left { - transform: rotate(-270deg); - - &:dir(rtl) { - transform: rotate(-90deg); - } - } -} diff --git a/packages/fuselage/src/components/Chevron/Chevron.tsx b/packages/fuselage/src/components/Chevron/Chevron.tsx index 5f0147701f..fc5a9785df 100644 --- a/packages/fuselage/src/components/Chevron/Chevron.tsx +++ b/packages/fuselage/src/components/Chevron/Chevron.tsx @@ -1,19 +1,37 @@ -import type { ReactElement } from 'react'; +import type { ComponentPropsWithoutRef, ReactElement } from 'react'; import { useMemo } from 'react'; +import { styled } from 'tamagui'; -import { Box, type BoxProps } from '../Box'; +import { RcxView } from '../../primitives'; import { Icon } from '../Icon'; -export type ChevronProps = Omit & { - size?: BoxProps['width']; +const ChevronFrame = styled(RcxView, { + name: 'Chevron', + display: 'inline-flex', + alignSelf: 'center', +}); + +export type ChevronProps = ComponentPropsWithoutRef & { + size?: ComponentPropsWithoutRef['size']; up?: boolean; right?: boolean; left?: boolean; down?: boolean; - top?: boolean; - bottom?: boolean; }; +function getRotation({ + up, + right, + down, + left, +}: Pick): string { + if (up) return '-180deg'; + if (right) return '-90deg'; + if (left) return '-270deg'; + if (down) return '0deg'; + return '0deg'; +} + function Chevron({ up, right, @@ -21,23 +39,21 @@ function Chevron({ left, size, ...props -}: ChevronProps): ReactElement { +}: ChevronProps): ReactElement { const children = useMemo( () => , [size], ); + const rotate = getRotation({ up, right, down, left }); + return ( - + style={{ transform: `rotate(${rotate})` }} + > + {children} + ); } From 8d04afc42b382401fb9325586d13876ee63319f5 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 10:19:29 -0300 Subject: [PATCH 13/84] refactor(fuselage): Migrate ProgressBar component from SCSS to Tamagui v3 ProgressBarTrack (RcxView): bg, radius, 8px height. ProgressBarFill (RcxView): dynamic width/color via style prop. Shine animation via css-in-js keyframes + className. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ProgressBar/ProgressBar.styles.scss | 70 -------------- .../components/ProgressBar/ProgressBar.tsx | 94 +++++++++++++++---- 2 files changed, 75 insertions(+), 89 deletions(-) delete mode 100644 packages/fuselage/src/components/ProgressBar/ProgressBar.styles.scss diff --git a/packages/fuselage/src/components/ProgressBar/ProgressBar.styles.scss b/packages/fuselage/src/components/ProgressBar/ProgressBar.styles.scss deleted file mode 100644 index 87268f42fc..0000000000 --- a/packages/fuselage/src/components/ProgressBar/ProgressBar.styles.scss +++ /dev/null @@ -1,70 +0,0 @@ -@use '../../styles/colors.scss'; -@use '../../styles/lengths.scss'; -@use '../../styles/typography.scss'; - -$progress-bar-color-shine: theme( - 'progress-bar-color-shine', - colors.surface(light) -); -$progress-bar-color-background: theme( - 'progress-bar-color-background', - colors.surface(neutral) -); - -$progress-bar-border-radius: theme( - 'progress-bar-border-radius', - lengths.border-radius(large) -); - -.rcx-progress-bar { - display: block; - - overflow: hidden; - - width: 100%; - - height: 8px; - - border-radius: $progress-bar-border-radius; - background-color: $progress-bar-color-background; -} - -.rcx-progress-bar__fill { - display: block; - - height: 8px; - - border-radius: $progress-bar-border-radius; - - &--animated::before { - position: absolute; - inset: 0; - - width: inherit; - - content: ''; - animation: rcx-progress-bar__animation 2s ease-out infinite; - - opacity: 0; - border-radius: $progress-bar-border-radius; - background: $progress-bar-color-shine; - } -} - -@keyframes rcx-progress-bar__animation { - 0% { - width: 0; - - opacity: 0; - } - - 50% { - opacity: 0.5; - } - - 100% { - width: inherit; - - opacity: 0; - } -} diff --git a/packages/fuselage/src/components/ProgressBar/ProgressBar.tsx b/packages/fuselage/src/components/ProgressBar/ProgressBar.tsx index 49510c1e6d..fbaaf12cc3 100644 --- a/packages/fuselage/src/components/ProgressBar/ProgressBar.tsx +++ b/packages/fuselage/src/components/ProgressBar/ProgressBar.tsx @@ -1,22 +1,77 @@ import type { AllHTMLAttributes } from 'react'; import { forwardRef } from 'react'; -import { Box } from '../Box'; +import { css, keyframes } from '@rocket.chat/css-in-js'; +import { styled } from 'tamagui'; + +import { RcxView } from '../../primitives'; const getWidth = (percentage: number): string => `${Math.min(Math.max(0, percentage), 100).toFixed(1)}%`; +const progressBarAnimation = keyframes` + 0% { + width: 0; + opacity: 0; + } + 50% { + opacity: 0.5; + } + 100% { + width: inherit; + opacity: 0; + } +`; + +const animatedClass = css` + position: relative; + + &::before { + position: absolute; + inset: 0; + + width: inherit; + + content: ''; + animation: ${progressBarAnimation} 2s ease-out infinite; + + opacity: 0; + border-radius: 0.5rem; + background: var(--surfaceLight); + } +`; + +const ProgressBarTrack = styled(RcxView, { + name: 'ProgressBar', + + display: 'block', + overflow: 'hidden', + width: '100%' as any, + height: 8, + borderRadius: '$x8', + backgroundColor: '$surfaceNeutral', +}); + +const ProgressBarFill = styled(RcxView, { + name: 'ProgressBarFill', + + display: 'block', + height: 8, + borderRadius: '$x8', +}); + const colors = { - info: 'status-font-on-info', - success: 'status-font-on-success', - warning: 'status-font-on-warning', - danger: 'status-font-on-danger', + info: '$statusFontOnInfo', + success: '$statusFontOnSuccess', + warning: '$statusFontOnWarning', + danger: '$statusFontOnDanger', }; + const lightColors = { - info: 'status-background-info', - success: 'status-background-success', - warning: 'status-background-warning', - danger: 'status-background-danger', + info: '$statusBgInfo', + success: '$statusBgSuccess', + warning: '$statusBgWarning', + danger: '$statusBgDanger', }; const getColor = ( @@ -45,21 +100,22 @@ const ProgressBar = forwardRef(function ProgressBar( { percentage, variant = 'info', error, animated, light = false, ...props }, ref, ) { + const fillColor = getColor(light, variant, error); + return ( - - - + ); }); From 516ac2e80e12f5668cdffc351409bd84e382b41f Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 10:20:07 -0300 Subject: [PATCH 14/84] refactor(fuselage): Migrate Banner component from SCSS to Tamagui v3 6 styled components: BannerFrame (RcxText), BannerIcon, BannerContent, BannerTitle, BannerCloseButton, BannerLink. Variant colors from statusFontOn* tokens. Inline variant for compact mode. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/Banner/Banner.styles.scss | 155 ------------- .../fuselage/src/components/Banner/Banner.tsx | 212 ++++++++++++++++-- 2 files changed, 190 insertions(+), 177 deletions(-) delete mode 100644 packages/fuselage/src/components/Banner/Banner.styles.scss diff --git a/packages/fuselage/src/components/Banner/Banner.styles.scss b/packages/fuselage/src/components/Banner/Banner.styles.scss deleted file mode 100644 index 74dc17f5f8..0000000000 --- a/packages/fuselage/src/components/Banner/Banner.styles.scss +++ /dev/null @@ -1,155 +0,0 @@ -@use 'sass:map'; -@use '../../styles/functions'; -@use '../../styles/colors.scss'; -@use '../../styles/lengths.scss'; -@use '../../styles/typography.scss'; -@use '../../styles/mixins/elevation.scss'; - -// neutral -$banner-colors-neutral-color: functions.theme( - 'banner-colors-neutral-color', - colors.font(default) -); -$banner-colors-neutral-background-color: functions.theme( - 'banner-colors-neutral-background-color', - colors.surface(tint) -); - -// info -$banner-colors-info-color: functions.theme( - 'banner-colors-info-color', - colors.status-font(on-info) -); - -// success -$banner-colors-success-color: functions.theme( - 'banner-colors-success-color', - colors.status-font(on-success) -); - -// warning -$banner-colors-warning-color: functions.theme( - 'banner-colors-warning-color', - colors.status-font(on-warning) -); - -// danger -$banner-colors-danger-color: functions.theme( - 'banner-colors-danger-color', - colors.status-font(on-danger) -); - -.rcx-banner { - display: flex; - flex-flow: row nowrap; - justify-content: space-between; - align-items: flex-start; - flex: 0 1 auto; - - box-sizing: border-box; - - padding-block: 14px; - padding-inline: 16px; - - color: $banner-colors-neutral-color; - border-top-width: lengths.border-width(4); - border-top-style: solid; - border-bottom: lengths.border-width(default) solid colors.stroke(extra-light); - - background-color: $banner-colors-neutral-background-color; - - font-family: typography.font-family('sans'); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - &--inline { - padding-block: 12px; - } - - &--actionable { - cursor: pointer; - } - - &--neutral { - border-top-color: transparent; - } - - &--info { - border-top-color: $banner-colors-info-color; - } - - &--warning { - border-top-color: $banner-colors-warning-color; - } - - &--danger { - border-top-color: $banner-colors-danger-color; - } - - &--success { - border-top-color: $banner-colors-success-color; - } - - &__icon { - padding-block: 8px; - padding-inline-end: 12px; - - &--info { - color: $banner-colors-info-color; - } - - &--warning { - color: $banner-colors-warning-color; - } - - &--danger { - color: $banner-colors-danger-color; - } - - &--success { - color: $banner-colors-success-color; - } - - &--inline { - margin-block: -2px; - padding-block: 0; - } - } - - &__content { - flex-grow: 1; - align-self: center; - - @include typography.use-font-scale(p2); - - &--inline { - @include typography.use-with-truncated-text; - } - } - - &__title { - margin: 0; - padding: 0; - @include typography.use-font-scale(h5); - - &--inline { - display: inline; - - padding-inline-end: 8px; - } - } - - &__close-button { - padding-block: 6px; - padding-inline: 8px; - - &--inline { - margin-block: -4px; - padding-block: 0; - } - } - - &__link { - padding-left: 10px; - } -} diff --git a/packages/fuselage/src/components/Banner/Banner.tsx b/packages/fuselage/src/components/Banner/Banner.tsx index 8c84d6e940..836adbea86 100644 --- a/packages/fuselage/src/components/Banner/Banner.tsx +++ b/packages/fuselage/src/components/Banner/Banner.tsx @@ -9,8 +9,9 @@ import type { MouseEvent, } from 'react'; import { useRef, useCallback, useMemo } from 'react'; +import { styled } from 'tamagui'; -import { composeClassNames as cx } from '../../helpers/composeClassNames'; +import { RcxView, RcxText } from '../../primitives'; import { IconButton } from '../Button'; type VariantType = 'neutral' | 'info' | 'success' | 'warning' | 'danger'; @@ -23,6 +24,178 @@ const variants: VariantType[] = [ 'danger', ]; +const BannerFrame = styled(RcxText, { + name: 'BannerFrame', + + display: 'flex', + flexDirection: 'row', + flexWrap: 'nowrap', + justifyContent: 'space-between', + alignItems: 'flex-start', + flexShrink: 1, + + paddingBlock: 14, + paddingInline: 16, + + color: '$fontDefault', + borderTopWidth: 4, + borderTopStyle: 'solid', + borderBottomWidth: 1, + borderBottomStyle: 'solid', + borderBottomColor: '$strokeExtraLight', + + backgroundColor: '$surfaceTint', + + fontFamily: '$body', + + overflowWrap: 'normal', + + variants: { + variant: { + neutral: { + borderTopColor: 'transparent', + }, + info: { + borderTopColor: '$statusFontOnInfo', + }, + success: { + borderTopColor: '$statusFontOnSuccess', + }, + warning: { + borderTopColor: '$statusFontOnWarning', + }, + danger: { + borderTopColor: '$statusFontOnDanger', + }, + }, + + inline: { + true: { + paddingBlock: 12, + }, + }, + + actionable: { + true: { + cursor: 'pointer', + }, + }, + } as const, + + defaultVariants: { + variant: 'neutral', + }, +}); + +const BannerIcon = styled(RcxView, { + name: 'BannerIcon', + + paddingBlock: 8, + paddingInlineEnd: 12, + + variants: { + variant: { + neutral: {}, + info: { + color: '$statusFontOnInfo', + }, + success: { + color: '$statusFontOnSuccess', + }, + warning: { + color: '$statusFontOnWarning', + }, + danger: { + color: '$statusFontOnDanger', + }, + }, + + inline: { + true: { + marginBlock: -2, + paddingBlock: 0, + }, + }, + } as const, +}); + +const BannerContent = styled(RcxText, { + name: 'BannerContent', + + flexGrow: 1, + alignSelf: 'center', + + fontFamily: '$body', + fontSize: '$p2', + fontWeight: '$p2', + lineHeight: '$p2', + letterSpacing: '$p2', + + color: 'inherit', + overflowWrap: 'normal', + + variants: { + inline: { + true: { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }, + }, + } as const, +}); + +const BannerTitle = styled(RcxText, { + name: 'BannerTitle', + + margin: 0, + padding: 0, + + fontFamily: '$body', + fontSize: '$h5', + fontWeight: '$h5', + lineHeight: '$h5', + letterSpacing: '$h5', + + color: 'inherit', + overflowWrap: 'normal', + + variants: { + inline: { + true: { + display: 'inline' as any, + paddingInlineEnd: 8, + }, + }, + } as const, +}); + +const BannerCloseButton = styled(RcxView, { + name: 'BannerCloseButton', + + paddingBlock: 6, + paddingInline: 8, + + variants: { + inline: { + true: { + marginBlock: -4, + paddingBlock: 0, + }, + }, + } as const, +}); + +const BannerLink = styled(RcxText, { + name: 'BannerLink', + + tag: 'a', + paddingLeft: 10, + + color: 'inherit', + overflowWrap: 'normal', +}); + export type BannerProps = { actionable?: boolean; closeable?: boolean; @@ -82,45 +255,40 @@ const Banner = ({ const buttonProps = useButtonPattern(handleBannerClick); return ( -
{icon && isIconVisible && ( -
+ {icon} -
+ )} -
+ {title && ( -
{title}
+ {title} )} {children} {link && ( - {linkText} - + )} -
+ {closeable && ( -
+ -
+ )} -
+ ); }; From 4b148874655b0e95dc84209599f75c5a8418b192 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 10:41:27 -0300 Subject: [PATCH 15/84] refactor(fuselage): Migrate Bubble component from SCSS to Tamagui v3 BubbleFrame (RcxView), BubbleButtonFrame (RcxInteractive) with primary/secondary variants, BubbleItemFrame (RcxText). Font tokens c2/micro. Group radius handling for dismiss mode. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../fuselage/src/components/Banner/Banner.tsx | 50 ++- .../src/components/Bubble/Bubble.styles.scss | 84 ---- .../fuselage/src/components/Bubble/Bubble.tsx | 378 ++++++++++++++++-- .../src/components/Bubble/BubbleButton.tsx | 32 -- .../src/components/Bubble/BubbleItem.tsx | 27 -- .../src/components/Callout/Callout.tsx | 13 +- packages/fuselage/src/components/index.scss | 4 - 7 files changed, 375 insertions(+), 213 deletions(-) delete mode 100644 packages/fuselage/src/components/Bubble/Bubble.styles.scss delete mode 100644 packages/fuselage/src/components/Bubble/BubbleButton.tsx delete mode 100644 packages/fuselage/src/components/Bubble/BubbleItem.tsx diff --git a/packages/fuselage/src/components/Banner/Banner.tsx b/packages/fuselage/src/components/Banner/Banner.tsx index 836adbea86..0709a51664 100644 --- a/packages/fuselage/src/components/Banner/Banner.tsx +++ b/packages/fuselage/src/components/Banner/Banner.tsx @@ -119,11 +119,14 @@ const BannerIcon = styled(RcxView, { } as const, }); -const BannerContent = styled(RcxText, { - name: 'BannerContent', +const BannerContainer = styled(RcxText, { + name: 'BannerContainer', flexGrow: 1, alignSelf: 'center', + display: 'flex', + flexDirection: 'column', + gap: '$x4', fontFamily: '$body', fontSize: '$p2', @@ -170,6 +173,14 @@ const BannerTitle = styled(RcxText, { } as const, }); +const BannerContent = styled(RcxView, { + name: 'BannerContent', + + display: 'flex', + flexDirection: 'row', + gap: '$x4', +}); + const BannerCloseButton = styled(RcxView, { name: 'BannerCloseButton', @@ -186,15 +197,10 @@ const BannerCloseButton = styled(RcxView, { } as const, }); -const BannerLink = styled(RcxText, { - name: 'BannerLink', - - tag: 'a', - paddingLeft: 10, - - color: 'inherit', - overflowWrap: 'normal', -}); +// Functional component — renders real since styled(Text, { tag: 'a' }) doesn't work +const BannerLink = ({ children, ...props }: { children?: React.ReactNode; href?: string; target?: string }) => ( + {children} +); export type BannerProps = { actionable?: boolean; @@ -269,20 +275,20 @@ const Banner = ({ {icon} )} - + {title && ( {title} )} - {children} - {link && ( - - {linkText} - - )} - + + {children} + + {link && ( + + {linkText} + + )} + + {closeable && ( diff --git a/packages/fuselage/src/components/Bubble/Bubble.styles.scss b/packages/fuselage/src/components/Bubble/Bubble.styles.scss deleted file mode 100644 index ec93f4241f..0000000000 --- a/packages/fuselage/src/components/Bubble/Bubble.styles.scss +++ /dev/null @@ -1,84 +0,0 @@ -@use '../../styles/colors.scss'; -@use '../../styles/lengths.scss'; -@use '../../styles/typography.scss'; - -@use '../../styles/variables/buttons.scss' as buttonColors; -@use '../../styles/primitives/button.scss'; -@use '../../styles/mixins/interactivity.scss'; - -.rcx-bubble { - display: flex; - - overflow: hidden; - - align-items: center; - - &__button { - &--primary { - @include button.kind-variant(buttonColors.$primary); - } - - &--secondary { - @include button.kind-variant(buttonColors.$secondary); - } - - @include clickable; - @include click-animation; - } - - &__item { - &--primary { - color: buttonColors.$button-primary-color; - background-color: buttonColors.$button-primary-background-color; - } - - &--secondary { - color: buttonColors.$button-secondary-color; - background-color: buttonColors.$button-secondary-background-color; - } - } - - &__button, - &__item { - @include typography.use-font-scale(c2); - display: flex; - justify-content: center; - align-items: center; - - height: lengths.size(28); - - padding-inline: lengths.padding(12); - padding-inline-end: lengths.padding(16); - - border-radius: lengths.border-radius(extra-large); - column-gap: lengths.padding(8); - - @include typography.use-with-truncated-text; - - > span { - @include typography.use-with-truncated-text; - } - } - - &:not(.rcx-bubble__group) &__item { - padding-inline: lengths.padding(8); - } - - &--small &__button, - &--small &__item { - @include typography.use-font-scale(micro); - height: lengths.size(20); - } - - &__group { - :first-child { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - - :last-child { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } - } -} diff --git a/packages/fuselage/src/components/Bubble/Bubble.tsx b/packages/fuselage/src/components/Bubble/Bubble.tsx index dc0b88339f..b44d37affc 100644 --- a/packages/fuselage/src/components/Bubble/Bubble.tsx +++ b/packages/fuselage/src/components/Bubble/Bubble.tsx @@ -1,8 +1,310 @@ import type { Keys as IconName } from '@rocket.chat/icons'; import type { AllHTMLAttributes, ButtonHTMLAttributes, ReactNode } from 'react'; +import { createStyledContext, styled } from 'tamagui'; -import { BubbleButton } from './BubbleButton'; -import { BubbleItem } from './BubbleItem'; +import { RcxInteractive, RcxText, RcxView } from '../../primitives'; +import { Icon } from '../Icon'; + +// --- Styled Context --- + +const BubbleContext = createStyledContext({ + small: false as boolean, + variant: 'primary' as string, +}); + +// --- Styled Components --- + +const BubbleFrame = styled(RcxView, { + name: 'Bubble', + context: BubbleContext, + + display: 'flex', + flexDirection: 'row', + overflow: 'hidden', + alignItems: 'center', +}); + +const BubbleButtonFrame = styled(RcxInteractive, { + name: 'BubbleButton', + context: BubbleContext, + + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + + height: '$x28', + + paddingInline: 12, + paddingInlineEnd: 16, + + borderWidth: 1, + borderStyle: 'solid', + borderRadius: 20, + + columnGap: 8, + + variants: { + variant: { + primary: { + color: '$buttonPrimaryColor', + backgroundColor: '$buttonPrimaryBg', + borderColor: '$buttonPrimaryBorderColor', + + hoverStyle: { + backgroundColor: '$buttonPrimaryHoverBg', + borderColor: '$buttonPrimaryHoverBorderColor', + boxShadow: 'none', + }, + + pressStyle: { + backgroundColor: '$buttonPrimaryPressBg', + borderColor: '$buttonPrimaryPressBorderColor', + boxShadow: 'none', + }, + + focusVisibleStyle: { + backgroundColor: '$buttonPrimaryFocusBg', + borderColor: '$buttonPrimaryFocusBorderColor', + boxShadow: '0 0 0 2px var(--shadowHighlight)', + }, + + disabledStyle: { + backgroundColor: '$buttonPrimaryDisabledBg', + borderColor: '$buttonPrimaryDisabledBorderColor', + color: '$buttonPrimaryDisabledColor', + cursor: 'not-allowed', + }, + }, + secondary: { + color: '$buttonSecondaryColor', + backgroundColor: '$buttonSecondaryBg', + borderColor: '$buttonSecondaryBorderColor', + + hoverStyle: { + backgroundColor: '$buttonSecondaryHoverBg', + borderColor: '$buttonSecondaryHoverBorderColor', + boxShadow: 'none', + }, + + pressStyle: { + backgroundColor: '$buttonSecondaryPressBg', + borderColor: '$buttonSecondaryPressBorderColor', + boxShadow: 'none', + }, + + focusVisibleStyle: { + backgroundColor: '$buttonSecondaryFocusBg', + borderColor: '$buttonSecondaryFocusBorderColor', + boxShadow: '0 0 0 2px var(--shadowHighlight)', + }, + + disabledStyle: { + backgroundColor: '$buttonSecondaryDisabledBg', + borderColor: '$buttonSecondaryDisabledBorderColor', + color: '$buttonSecondaryDisabledColor', + cursor: 'not-allowed', + }, + }, + }, + small: { + true: { + height: '$x20', + }, + }, + } as const, + + defaultVariants: { + variant: 'primary', + }, +}); + +const BubbleButtonText = styled(RcxText, { + name: 'BubbleButtonText', + context: BubbleContext, + + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + overflowWrap: 'normal', + + color: 'inherit', + + fontFamily: '$body', + fontSize: '$c2', + fontWeight: '$c2', + lineHeight: '$c2', + letterSpacing: '$c2', + + variants: { + small: { + true: { + fontSize: '$micro', + fontWeight: '$micro', + lineHeight: '$micro', + letterSpacing: '$micro', + }, + }, + } as const, +}); + +const BubbleItemFrame = styled(RcxText, { + name: 'BubbleItem', + context: BubbleContext, + + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + + height: '$x28', + + paddingInline: 12, + paddingInlineEnd: 16, + + borderRadius: 20, + + columnGap: 8, + + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + overflowWrap: 'normal', + + fontFamily: '$body', + fontSize: '$c2', + fontWeight: '$c2', + lineHeight: '$c2', + letterSpacing: '$c2', + + variants: { + variant: { + primary: { + color: '$buttonPrimaryColor', + backgroundColor: '$buttonPrimaryBg', + }, + secondary: { + color: '$buttonSecondaryColor', + backgroundColor: '$buttonSecondaryBg', + }, + }, + small: { + true: { + height: '$x20', + fontSize: '$micro', + fontWeight: '$micro', + lineHeight: '$micro', + letterSpacing: '$micro', + }, + }, + inGroup: { + true: {}, + false: { + paddingInline: 8, + paddingInlineEnd: 8, + }, + }, + } as const, + + defaultVariants: { + variant: 'primary', + inGroup: false, + }, +}); + +const BubbleItemInnerText = styled(RcxText, { + name: 'BubbleItemInnerText', + context: BubbleContext, + + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + overflowWrap: 'normal', + + color: 'inherit', + + fontFamily: '$body', + fontSize: '$c2', + fontWeight: '$c2', + lineHeight: '$c2', + letterSpacing: '$c2', + + variants: { + small: { + true: { + fontSize: '$micro', + fontWeight: '$micro', + lineHeight: '$micro', + letterSpacing: '$micro', + }, + }, + } as const, +}); + +// --- Sub-components --- + +type BubbleButtonProps = { + onClick: () => void; + label?: ReactNode; + secondary?: boolean; + icon?: IconName; + isGroupFirst?: boolean; + isGroupLast?: boolean; +} & Omit, 'onClick'>; + +const BubbleButton = ({ + secondary, + label, + onClick, + icon, + isGroupFirst, + isGroupLast, + ...props +}: BubbleButtonProps) => ( + + {icon && } + {label && {label}} + +); + +type BubbleItemProps = { + label?: ReactNode; + secondary?: boolean; + icon?: IconName; + inGroup?: boolean; +}; + +const BubbleItem = ({ + secondary, + label, + icon, + inGroup, + ...props +}: BubbleItemProps) => ( + + {icon && } + {label && {label}} + +); + +// --- Main component --- export type BubbleProps = { secondary?: boolean; @@ -25,42 +327,40 @@ const Bubble = ({ contentProps, dismissProps, ...props -}: BubbleProps) => ( -
- {onClick ? ( - - ) : ( - - )} - {onDismiss && ( - - )} -
-); +}: BubbleProps) => { + const hasGroup = !!onDismiss; + + return ( + + {onClick ? ( + + ) : ( + + )} + {onDismiss && ( + + )} + + ); +}; export default Bubble; diff --git a/packages/fuselage/src/components/Bubble/BubbleButton.tsx b/packages/fuselage/src/components/Bubble/BubbleButton.tsx deleted file mode 100644 index f826189aa9..0000000000 --- a/packages/fuselage/src/components/Bubble/BubbleButton.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import type { Keys as IconName } from '@rocket.chat/icons'; -import type { ButtonHTMLAttributes, ReactNode } from 'react'; - -import { Icon } from '../Icon'; - -type BubbleButtonProps = { - onClick: () => void; - label?: ReactNode; - secondary?: boolean; - icon?: IconName; -} & Omit, 'onClick'>; - -export const BubbleButton = ({ - secondary, - label, - onClick, - icon, - ...props -}: BubbleButtonProps) => ( - -); diff --git a/packages/fuselage/src/components/Bubble/BubbleItem.tsx b/packages/fuselage/src/components/Bubble/BubbleItem.tsx deleted file mode 100644 index 0f58598653..0000000000 --- a/packages/fuselage/src/components/Bubble/BubbleItem.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type { Keys as IconName } from '@rocket.chat/icons'; -import type { HTMLAttributes, ReactNode } from 'react'; - -import { Icon } from '../Icon'; - -type BubbleItemProps = { - label?: ReactNode; - secondary?: boolean; - icon?: IconName; -} & HTMLAttributes; - -export const BubbleItem = ({ - secondary, - label, - icon, - ...props -}: BubbleItemProps) => ( - - {icon && } - {label && {label}} - -); diff --git a/packages/fuselage/src/components/Callout/Callout.tsx b/packages/fuselage/src/components/Callout/Callout.tsx index 37db4c1232..4bb45b7272 100644 --- a/packages/fuselage/src/components/Callout/Callout.tsx +++ b/packages/fuselage/src/components/Callout/Callout.tsx @@ -51,11 +51,13 @@ const CalloutIcon = styled(RcxView, { const CalloutWrapper = styled(RcxView, { name: 'CalloutWrapper', + display: 'block', overflow: 'hidden', - justifyContent: 'space-between', + flexGrow: 1, + flexShrink: 1, + flexBasis: 0, marginInlineStart: '$x12', - gap: '$x12', variants: { large: { @@ -64,7 +66,6 @@ const CalloutWrapper = styled(RcxView, { flexDirection: 'row', alignItems: 'center', overflow: 'hidden', - gap: 0, }, }, } as const, @@ -84,6 +85,8 @@ const CalloutWrapperContent = styled(RcxView, { const CalloutTitle = styled(RcxText, { name: 'CalloutTitle', + display: 'block', + width: '100%' as any, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', @@ -154,8 +157,8 @@ const Callout = ({ - {title && {title}} - {children && {children}} + {title && {title}} + {children && {children}} {actions} diff --git a/packages/fuselage/src/components/index.scss b/packages/fuselage/src/components/index.scss index 09a087de68..bb95599e43 100644 --- a/packages/fuselage/src/components/index.scss +++ b/packages/fuselage/src/components/index.scss @@ -1,15 +1,12 @@ @import './Accordion/Accordion.styles.scss'; -@import './Banner/Banner.styles.scss'; @import './AutoComplete/AutoComplete.styles.scss'; @import './Avatar/Avatar.styles.scss'; @import './Box/Box.styles.scss'; @import './Button/Button.styles.scss'; -@import './Bubble/Bubble.styles.scss'; @import './ButtonGroup/ButtonGroup.styles.scss'; @import './Card/Card.styles.scss'; @import './CardGroup/CardGroup.styles.scss'; @import './CheckBox/CheckBox.styles.scss'; -@import './Chevron/Chevron.styles.scss'; @import './Dropdown/Dropdown.styles.scss'; @import './Field/Field.styles.scss'; @import './FieldGroup/FieldGroup.styles.scss'; @@ -22,7 +19,6 @@ @import './Option/Option.styles.scss'; @import './Options/Options.styles.scss'; @import './Pagination/Pagination.styles.scss'; -@import './ProgressBar/ProgressBar.styles.scss'; @import './RadioButton/RadioButton.styles.scss'; @import './Select/Select.styles.scss'; @import './Sidebar/Sidebar.styles.scss'; From d5e878ea9fbc819cf443d1147d5b85c03ba7252b Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 11:38:10 -0300 Subject: [PATCH 16/84] refactor(fuselage): Migrate States component from SCSS to Tamagui v3 9 sub-components: States, StatesIcon (variant colors), StatesTitle (), StatesSubtitle (), StatesSuggestion (), StatesSuggestionList, StatesSuggestionListItem, StatesSuggestionText, StatesLink (native ). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/States/States.styles.scss | 103 ------------------ .../fuselage/src/components/States/States.tsx | 17 ++- .../src/components/States/StatesIcon.tsx | 38 ++++++- .../src/components/States/StatesLink.tsx | 29 ++++- .../src/components/States/StatesSubtitle.tsx | 28 ++++- .../components/States/StatesSuggestion.tsx | 28 ++++- .../States/StatesSuggestionList.tsx | 27 ++++- .../States/StatesSuggestionListItem.tsx | 14 ++- .../States/StatesSuggestionText.tsx | 14 ++- .../src/components/States/StatesTitle.tsx | 22 +++- 10 files changed, 186 insertions(+), 134 deletions(-) delete mode 100644 packages/fuselage/src/components/States/States.styles.scss diff --git a/packages/fuselage/src/components/States/States.styles.scss b/packages/fuselage/src/components/States/States.styles.scss deleted file mode 100644 index 4871c460d3..0000000000 --- a/packages/fuselage/src/components/States/States.styles.scss +++ /dev/null @@ -1,103 +0,0 @@ -@use '../../styles/lengths.scss'; -@use '../../styles/functions.scss'; -@use '../../styles/colors.scss'; -@use '../../styles/typography.scss'; - -$variants: ( - 'success': colors.status-font(on-success), - 'danger': colors.status-font(on-danger), - 'warning': colors.status-font(on-warning), - 'primary': colors.status-font(on-info), -); - -.rcx-states { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - padding: lengths.padding(16); - - color: colors.font(secondary-info); - - &__icon { - margin-block-end: lengths.margin(20); - padding: lengths.margin(16); - - color: colors.font(secondary-info); - - border-radius: lengths.border-radius(full); - - background-color: colors.surface(neutral); - - @each $name, $color in $variants { - &--#{$name} { - color: theme('states-icons-color-#{$name}', $color); - } - } - } - - &__title { - margin-block: lengths.margin(0) lengths.margin(8); - - text-align: center; - - color: colors.font(default); - @include typography.use-font-scale(h3); - } - - &__list, - &__suggestion { - @include typography.use-font-scale(p2m); - } - - &__subtitle { - @include typography.use-font-scale(p1); - } - - &__subtitle, - &__list, - &__suggestion { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - width: 100%; - max-width: 462px; - - margin: 0; - padding: 0; - - list-style-position: inside; - - text-align: center; - } - - &__suggestion-text-nomargin { - margin: 0; - } - - &__suggestion, - &__subtitle { - margin-block-end: lengths.margin(24); - } - - &__list { - list-style: initial; - - &-item { - &-wrapper { - margin-inline-start: lengths.margin(-4); - } - } - } - - &__link { - @include typography.use-font-scale(p2); - margin-block: lengths.margin(16); - - color: colors.font(info); - @extend %--with-inline-elements; - } -} diff --git a/packages/fuselage/src/components/States/States.tsx b/packages/fuselage/src/components/States/States.tsx index 49e4065f84..bfcfa4b748 100644 --- a/packages/fuselage/src/components/States/States.tsx +++ b/packages/fuselage/src/components/States/States.tsx @@ -1,13 +1,24 @@ import type { AllHTMLAttributes, ReactNode } from 'react'; +import { styled } from 'tamagui'; + +import { RcxView } from '../../primitives'; + +const StatesFrame = styled(RcxView, { + name: 'States', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + padding: '$x16', + color: '$fontSecondaryInfo', +}); export type StatesProps = { children?: ReactNode; } & AllHTMLAttributes; const States = ({ children, ...props }: StatesProps) => ( -
- {children} -
+ {children} ); export default States; diff --git a/packages/fuselage/src/components/States/StatesIcon.tsx b/packages/fuselage/src/components/States/StatesIcon.tsx index b3e60bb71e..50269207bd 100644 --- a/packages/fuselage/src/components/States/StatesIcon.tsx +++ b/packages/fuselage/src/components/States/StatesIcon.tsx @@ -1,16 +1,42 @@ +import { styled } from 'tamagui'; + +import { RcxView } from '../../primitives'; import { Icon, type IconProps } from '../Icon'; +const StatesIconFrame = styled(RcxView, { + name: 'StatesIcon', + marginBlockEnd: '$x20', + padding: '$x16', + color: '$fontSecondaryInfo', + borderRadius: '$full', + backgroundColor: '$surfaceNeutral', + + variants: { + variation: { + success: { + color: '$statusFontOnSuccess', + }, + danger: { + color: '$statusFontOnDanger', + }, + warning: { + color: '$statusFontOnWarning', + }, + primary: { + color: '$statusFontOnInfo', + }, + }, + } as const, +}); + export type StatesIconProps = { variation?: 'danger' | 'success' | 'warning' | 'primary'; } & IconProps; const StatesIcon = ({ variation, ...props }: StatesIconProps) => ( - + + + ); export default StatesIcon; diff --git a/packages/fuselage/src/components/States/StatesLink.tsx b/packages/fuselage/src/components/States/StatesLink.tsx index 278670eda0..d36edd03b0 100644 --- a/packages/fuselage/src/components/States/StatesLink.tsx +++ b/packages/fuselage/src/components/States/StatesLink.tsx @@ -1,11 +1,30 @@ -import type { AllHTMLAttributes } from 'react'; +import type { AllHTMLAttributes, ReactNode } from 'react'; +import { styled } from 'tamagui'; -import { Box, type BoxProps } from '../Box'; +import { RcxText } from '../../primitives'; -export type StatesLinkProps = BoxProps & AllHTMLAttributes; +const StatesLinkText = styled(RcxText, { + name: 'StatesLinkText', + fontFamily: '$body', + fontSize: '$p2', + fontWeight: '$p2', + lineHeight: '$p2', + letterSpacing: '$p2', + color: '$fontInfo', + overflowWrap: 'normal', +}); -const StatesLink = (props: StatesLinkProps) => ( - +export type StatesLinkProps = { + children?: ReactNode; +} & AllHTMLAttributes; + +const StatesLink = ({ children, ...props }: StatesLinkProps) => ( +
+ {children} + ); export default StatesLink; diff --git a/packages/fuselage/src/components/States/StatesSubtitle.tsx b/packages/fuselage/src/components/States/StatesSubtitle.tsx index 26393e986e..4dac50eec5 100644 --- a/packages/fuselage/src/components/States/StatesSubtitle.tsx +++ b/packages/fuselage/src/components/States/StatesSubtitle.tsx @@ -1,13 +1,35 @@ import type { AllHTMLAttributes, ReactNode } from 'react'; +import { styled } from 'tamagui'; + +import { RcxText } from '../../primitives'; + +const StatesSubtitleFrame = styled(RcxText, { + name: 'StatesSubtitle', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + width: '100%', + maxWidth: 462, + margin: 0, + padding: 0, + textAlign: 'center', + marginBlockEnd: '$x24', + fontFamily: '$body', + fontSize: '$p1', + fontWeight: '$p1', + lineHeight: '$p1', + letterSpacing: '$p1', + color: 'inherit', + overflowWrap: 'normal', +}); export type StatesSubtitleProps = { children?: ReactNode; } & AllHTMLAttributes; const StatesSubtitle = ({ children, ...props }: StatesSubtitleProps) => ( -
- {children} -
+ {children} ); export default StatesSubtitle; diff --git a/packages/fuselage/src/components/States/StatesSuggestion.tsx b/packages/fuselage/src/components/States/StatesSuggestion.tsx index 73da5d1d18..51c9d62ef0 100644 --- a/packages/fuselage/src/components/States/StatesSuggestion.tsx +++ b/packages/fuselage/src/components/States/StatesSuggestion.tsx @@ -1,13 +1,35 @@ import type { ReactNode, AllHTMLAttributes } from 'react'; +import { styled } from 'tamagui'; + +import { RcxText } from '../../primitives'; + +const StatesSuggestionFrame = styled(RcxText, { + name: 'StatesSuggestion', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + width: '100%', + maxWidth: 462, + margin: 0, + padding: 0, + textAlign: 'center', + marginBlockEnd: '$x24', + fontFamily: '$body', + fontSize: '$p2m', + fontWeight: '$p2m', + lineHeight: '$p2m', + letterSpacing: '$p2m', + color: 'inherit', + overflowWrap: 'normal', +}); export type StatesSuggestionProps = { children?: ReactNode; } & AllHTMLAttributes; const StatesSuggestion = ({ children, ...props }: StatesSuggestionProps) => ( -
- {children} -
+ {children} ); export default StatesSuggestion; diff --git a/packages/fuselage/src/components/States/StatesSuggestionList.tsx b/packages/fuselage/src/components/States/StatesSuggestionList.tsx index 2ef4630e97..a78be8b466 100644 --- a/packages/fuselage/src/components/States/StatesSuggestionList.tsx +++ b/packages/fuselage/src/components/States/StatesSuggestionList.tsx @@ -1,4 +1,27 @@ import type { AllHTMLAttributes, ReactNode } from 'react'; +import { styled } from 'tamagui'; + +import { RcxText } from '../../primitives'; + +const StatesSuggestionListFrame = styled(RcxText, { + name: 'StatesSuggestionList', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + width: '100%', + maxWidth: 462, + margin: 0, + padding: 0, + textAlign: 'center', + fontFamily: '$body', + fontSize: '$p2m', + fontWeight: '$p2m', + lineHeight: '$p2m', + letterSpacing: '$p2m', + color: 'inherit', + overflowWrap: 'normal', +}); export type StatesSuggestionListProps = { children?: ReactNode; @@ -8,9 +31,7 @@ const StatesSuggestionList = ({ children, ...props }: StatesSuggestionListProps) => ( -
    - {children} -
+ {children} ); export default StatesSuggestionList; diff --git a/packages/fuselage/src/components/States/StatesSuggestionListItem.tsx b/packages/fuselage/src/components/States/StatesSuggestionListItem.tsx index 8ea5b765df..528a63bacb 100644 --- a/packages/fuselage/src/components/States/StatesSuggestionListItem.tsx +++ b/packages/fuselage/src/components/States/StatesSuggestionListItem.tsx @@ -1,4 +1,14 @@ import type { AllHTMLAttributes, ReactNode } from 'react'; +import { styled } from 'tamagui'; + +import { RcxText } from '../../primitives'; + +const StatesSuggestionListItemWrapper = styled(RcxText, { + name: 'StatesSuggestionListItemWrapper', + marginInlineStart: -4, + color: 'inherit', + overflowWrap: 'normal', +}); export type StatesSuggestionListItemProps = { children?: ReactNode; @@ -8,8 +18,8 @@ const StatesSuggestionListItem = ({ children, ...props }: StatesSuggestionListItemProps) => ( -
  • - {children} +
  • + {children}
  • ); diff --git a/packages/fuselage/src/components/States/StatesSuggestionText.tsx b/packages/fuselage/src/components/States/StatesSuggestionText.tsx index 5ab812f7af..35b89aa3ce 100644 --- a/packages/fuselage/src/components/States/StatesSuggestionText.tsx +++ b/packages/fuselage/src/components/States/StatesSuggestionText.tsx @@ -1,4 +1,14 @@ import type { AllHTMLAttributes, ReactNode } from 'react'; +import { styled } from 'tamagui'; + +import { RcxText } from '../../primitives'; + +const StatesSuggestionTextFrame = styled(RcxText, { + name: 'StatesSuggestionText', + display: 'block', + color: 'inherit', + overflowWrap: 'normal', +}); export type StatesSuggestionTextProps = { children?: ReactNode; @@ -8,9 +18,7 @@ const StatesSuggestionText = ({ children, ...props }: StatesSuggestionTextProps) => ( -
    - {children} -
    + {children} ); export default StatesSuggestionText; diff --git a/packages/fuselage/src/components/States/StatesTitle.tsx b/packages/fuselage/src/components/States/StatesTitle.tsx index 1595002d07..5bc9fc4e46 100644 --- a/packages/fuselage/src/components/States/StatesTitle.tsx +++ b/packages/fuselage/src/components/States/StatesTitle.tsx @@ -1,13 +1,29 @@ import type { AllHTMLAttributes, ReactNode } from 'react'; +import { styled } from 'tamagui'; + +import { RcxText } from '../../primitives'; + +const StatesTitleFrame = styled(RcxText, { + name: 'StatesTitle', + display: 'block', + marginBlockStart: 0, + marginBlockEnd: '$x8', + textAlign: 'center', + color: '$fontDefault', + fontFamily: '$body', + fontSize: '$h3', + fontWeight: '$h3', + lineHeight: '$h3', + letterSpacing: '$h3', + overflowWrap: 'normal', +}); export type StatesTitleProps = { children?: ReactNode; } & AllHTMLAttributes; const StatesTitle = ({ children, ...props }: StatesTitleProps) => ( -

    - {children} -

    + {children} ); export default StatesTitle; From 2730fa524ea3fdfb6df2d5d40dde3adc3fb25381 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 11:38:48 -0300 Subject: [PATCH 17/84] refactor(fuselage): Migrate Field component from SCSS to Tamagui v3 7 sub-components: Field, FieldDescription (), FieldError ( danger), FieldHint (), FieldLabel (wraps Label), FieldLink (native + useTheme), FieldRow. FieldContext preserved. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/Field/Field.styles.scss | 65 ------------------- .../fuselage/src/components/Field/Field.tsx | 24 ++++++- .../src/components/Field/FieldDescription.tsx | 28 +++++++- .../src/components/Field/FieldError.tsx | 28 +++++++- .../src/components/Field/FieldHint.tsx | 28 +++++++- .../src/components/Field/FieldLabel.tsx | 15 ++++- .../src/components/Field/FieldLink.tsx | 33 +++++++++- .../src/components/Field/FieldRow.tsx | 26 +++++++- 8 files changed, 162 insertions(+), 85 deletions(-) delete mode 100644 packages/fuselage/src/components/Field/Field.styles.scss diff --git a/packages/fuselage/src/components/Field/Field.styles.scss b/packages/fuselage/src/components/Field/Field.styles.scss deleted file mode 100644 index 6b6e8051ac..0000000000 --- a/packages/fuselage/src/components/Field/Field.styles.scss +++ /dev/null @@ -1,65 +0,0 @@ -@use '../../styles/colors.scss'; -@use '../../styles/lengths.scss'; -@use '../../styles/typography.scss'; - -.rcx-field { - display: flex; - flex-flow: column nowrap; - align-items: stretch; - flex-shrink: 0; - - width: 100%; - - &__label { - @include typography.use-font-scale(p2m); - align-self: flex-start; - - margin-block: lengths.margin(2); - margin-inline-end: lengths.margin(8); - - color: colors.font(default); - } - - &__description { - @include typography.use-font-scale(p2); - margin-block: lengths.margin(2); - - color: colors.font(secondary-info); - @extend %--with-inline-elements; - } - - &__row { - display: flex; - flex-flow: row nowrap; - justify-content: space-between; - align-items: center; - - margin-block: lengths.margin(4) lengths.margin(2); - - color: colors.font(secondary-info); - } - - &__hint { - @include typography.use-font-scale(c1); - margin-block: lengths.margin(2); - - color: colors.font(secondary-info); - @extend %--with-inline-elements; - } - - &__error { - @include typography.use-font-scale(c1); - margin-block: lengths.margin(2); - - color: colors.font(danger); - @extend %--with-inline-elements; - } - - &__link { - @include typography.use-font-scale(c1); - margin-block: lengths.margin(2); - - color: colors.font(info); - @extend %--with-inline-elements; - } -} diff --git a/packages/fuselage/src/components/Field/Field.tsx b/packages/fuselage/src/components/Field/Field.tsx index 954751bd71..96fc46b430 100644 --- a/packages/fuselage/src/components/Field/Field.tsx +++ b/packages/fuselage/src/components/Field/Field.tsx @@ -1,10 +1,28 @@ +import type { ReactNode } from 'react'; import { createContext } from 'react'; -import { Box, type BoxProps } from '../Box'; +import { styled } from 'tamagui'; + +import { RcxView } from '../../primitives'; export const FieldContext = createContext(false); -export type FieldProps = BoxProps; +const FieldFrame = styled(RcxView, { + name: 'Field', + + display: 'flex', + flexDirection: 'column', + flexWrap: 'nowrap', + alignItems: 'stretch', + flexShrink: 0, + + width: '100%', +}); + +export type FieldProps = { + children?: ReactNode; + [key: string]: any; +}; /** * A `Field` is a wrapper representing an entry in a form. @@ -12,7 +30,7 @@ export type FieldProps = BoxProps; function Field(props: FieldProps) { return ( - + ); } diff --git a/packages/fuselage/src/components/Field/FieldDescription.tsx b/packages/fuselage/src/components/Field/FieldDescription.tsx index cf73cb3f38..d6f9861886 100644 --- a/packages/fuselage/src/components/Field/FieldDescription.tsx +++ b/packages/fuselage/src/components/Field/FieldDescription.tsx @@ -1,12 +1,34 @@ +import { styled } from 'tamagui'; + +import { RcxText } from '../../primitives'; import WithErrorWrapper from '../../helpers/WithErrorWrapper'; -import { Box, type BoxProps } from '../Box'; import { FieldContext } from './Field'; -export type FieldDescriptionProps = BoxProps; +const FieldDescriptionBase = styled(RcxText, { + name: 'FieldDescription', + + display: 'block', + + // p2 font scale + fontFamily: '$body', + fontSize: '$p2', + fontWeight: '$p2', + lineHeight: '$p2', + letterSpacing: '$p2', + + marginBlock: '$x2', + + color: '$fontSecondaryInfo', +}); + +export type FieldDescriptionProps = { + children?: React.ReactNode; + [key: string]: any; +}; const FieldDescription = (props: FieldDescriptionProps) => { - const component = ; + const component = ; if (process.env['NODE_ENV'] === 'development') { return ( diff --git a/packages/fuselage/src/components/Field/FieldError.tsx b/packages/fuselage/src/components/Field/FieldError.tsx index 4981f9437c..9ee183bd0d 100644 --- a/packages/fuselage/src/components/Field/FieldError.tsx +++ b/packages/fuselage/src/components/Field/FieldError.tsx @@ -1,12 +1,34 @@ +import { styled } from 'tamagui'; + +import { RcxText } from '../../primitives'; import WithErrorWrapper from '../../helpers/WithErrorWrapper'; -import { Box, type BoxProps } from '../Box'; import { FieldContext } from './Field'; -export type FieldErrorProps = BoxProps; +const FieldErrorBase = styled(RcxText, { + name: 'FieldError', + + display: 'block', + + // c1 font scale + fontFamily: '$body', + fontSize: '$c1', + fontWeight: '$c1', + lineHeight: '$c1', + letterSpacing: '$c1', + + marginBlock: '$x2', + + color: '$fontDanger', +}); + +export type FieldErrorProps = { + children?: React.ReactNode; + [key: string]: any; +}; const FieldError = (props: FieldErrorProps) => { - const component = ; + const component = ; if (process.env['NODE_ENV'] === 'development') { return ( diff --git a/packages/fuselage/src/components/Field/FieldHint.tsx b/packages/fuselage/src/components/Field/FieldHint.tsx index 9eb10a9a6d..fad28bc83f 100644 --- a/packages/fuselage/src/components/Field/FieldHint.tsx +++ b/packages/fuselage/src/components/Field/FieldHint.tsx @@ -1,12 +1,34 @@ +import { styled } from 'tamagui'; + +import { RcxText } from '../../primitives'; import WithErrorWrapper from '../../helpers/WithErrorWrapper'; -import { Box, type BoxProps } from '../Box'; import { FieldContext } from './Field'; -export type FieldHintProps = BoxProps; +const FieldHintBase = styled(RcxText, { + name: 'FieldHint', + + display: 'block', + + // c1 font scale + fontFamily: '$body', + fontSize: '$c1', + fontWeight: '$c1', + lineHeight: '$c1', + letterSpacing: '$c1', + + marginBlock: '$x2', + + color: '$fontSecondaryInfo', +}); + +export type FieldHintProps = { + children?: React.ReactNode; + [key: string]: any; +}; const FieldHint = (props: FieldHintProps) => { - const component = ; + const component = ; if (process.env['NODE_ENV'] === 'development') { return ( diff --git a/packages/fuselage/src/components/Field/FieldLabel.tsx b/packages/fuselage/src/components/Field/FieldLabel.tsx index fd8caee091..94dc4d7630 100644 --- a/packages/fuselage/src/components/Field/FieldLabel.tsx +++ b/packages/fuselage/src/components/Field/FieldLabel.tsx @@ -6,11 +6,22 @@ import { Label } from '../Label'; import { FieldContext } from './Field'; -export type FieldLabelProps = LabelProps; +export type FieldLabelProps = LabelProps & { + [key: string]: any; +}; const FieldLabel = forwardRef( function FieldLabel(props, ref) { - const component = + ); +}; const FieldLink = (props: FieldLinkProps) => { - const component = ; + const component = ; if (process.env['NODE_ENV'] === 'development') { return ( diff --git a/packages/fuselage/src/components/Field/FieldRow.tsx b/packages/fuselage/src/components/Field/FieldRow.tsx index 1e031055a4..a10ff3a6c3 100644 --- a/packages/fuselage/src/components/Field/FieldRow.tsx +++ b/packages/fuselage/src/components/Field/FieldRow.tsx @@ -1,12 +1,32 @@ +import { styled } from 'tamagui'; + +import { RcxView } from '../../primitives'; import WithErrorWrapper from '../../helpers/WithErrorWrapper'; -import { Box, type BoxProps } from '../Box'; import { FieldContext } from './Field'; -export type FieldRowProps = BoxProps; +const FieldRowBase = styled(RcxView, { + name: 'FieldRow', + + display: 'flex', + flexDirection: 'row', + flexWrap: 'nowrap', + justifyContent: 'space-between', + alignItems: 'center', + + marginBlockStart: '$x4', + marginBlockEnd: '$x2', + + color: '$fontSecondaryInfo', +}); + +export type FieldRowProps = { + children?: React.ReactNode; + [key: string]: any; +}; const FieldRow = (props: FieldRowProps) => { - const component = ; + const component = ; if (process.env['NODE_ENV'] === 'development') { return ( From 9c53e44d3516efa783d201d53f92776000fdced7 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 11:38:49 -0300 Subject: [PATCH 18/84] refactor(fuselage): Migrate FieldGroup component from SCSS to Tamagui v3 FieldGroupFrame (RcxView) with role=group. Children wrapped in FieldGroupItem with marginBlockStart for spacing. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../FieldGroup/FieldGroup.styles.scss | 22 ------- .../src/components/FieldGroup/FieldGroup.tsx | 59 ++++++++++++++----- 2 files changed, 44 insertions(+), 37 deletions(-) delete mode 100644 packages/fuselage/src/components/FieldGroup/FieldGroup.styles.scss diff --git a/packages/fuselage/src/components/FieldGroup/FieldGroup.styles.scss b/packages/fuselage/src/components/FieldGroup/FieldGroup.styles.scss deleted file mode 100644 index 7fe556f557..0000000000 --- a/packages/fuselage/src/components/FieldGroup/FieldGroup.styles.scss +++ /dev/null @@ -1,22 +0,0 @@ -@use '../../styles/lengths.scss'; - -.rcx-field-group { - display: flex; - flex-flow: column nowrap; - justify-content: center; - align-items: stretch; - - min-width: lengths.size(none); -} - -.rcx-field-group__item { - .rcx-field-group > & { - flex: 0 0 auto; - - width: lengths.size(full); - } - - & + & { - margin-block-start: lengths.margin(24); - } -} diff --git a/packages/fuselage/src/components/FieldGroup/FieldGroup.tsx b/packages/fuselage/src/components/FieldGroup/FieldGroup.tsx index ab3116fe51..08f8c58921 100644 --- a/packages/fuselage/src/components/FieldGroup/FieldGroup.tsx +++ b/packages/fuselage/src/components/FieldGroup/FieldGroup.tsx @@ -1,24 +1,53 @@ -import { appendClassName } from '../../helpers/appendClassName'; -import { patchChildren } from '../../helpers/patchChildren'; -import { Box, type BoxProps } from '../Box'; +import type { ReactNode } from 'react'; +import { Children, isValidElement } from 'react'; -export type FieldGroupProps = BoxProps; +import { styled } from 'tamagui'; + +import { RcxView } from '../../primitives'; + +const FieldGroupFrame = styled(RcxView, { + name: 'FieldGroup', + + display: 'flex', + flexDirection: 'column', + flexWrap: 'nowrap', + justifyContent: 'center', + alignItems: 'stretch', + + minWidth: 0, +}); + +const FieldGroupItem = styled(RcxView, { + name: 'FieldGroupItem', + + flexGrow: 0, + flexShrink: 0, + flexBasis: 'auto', + + width: '100%', +}); + +export type FieldGroupProps = { + children?: ReactNode; + [key: string]: any; +}; /** * A container for grouping fields that semantically share a common data context. */ const FieldGroup = ({ children, ...props }: FieldGroupProps) => ( - - {patchChildren( - children, - (childProps: { className: string | string[] }) => ({ - className: appendClassName( - childProps.className, - 'rcx-field-group__item', - ), - }), - )} - + + {Children.map(children, (child, index) => { + if (!isValidElement(child)) return child; + return ( + 0 && { marginBlockStart: '$x24' })} + > + {child} + + ); + })} + ); export default FieldGroup; From f6e297241a8313d9bbe8cebf5f86f4595f00eede Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 11:39:03 -0300 Subject: [PATCH 19/84] refactor(fuselage): Migrate StatusBullet component from SCSS to Tamagui v3 SVG icons use css-in-js for styles. Size 0.75rem default, 0.625rem small. Colors via CSS custom properties. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../StatusBullet/StatusBullet.styles.scss | 46 ------------------- .../components/StatusBullet/StatusBullet.tsx | 40 ++++++++++++++++ .../components/StatusBullet/icons/Away.tsx | 16 +++++-- .../components/StatusBullet/icons/Busy.tsx | 16 +++++-- .../StatusBullet/icons/Disabled.tsx | 16 +++++-- .../components/StatusBullet/icons/Loading.tsx | 18 ++++++-- .../components/StatusBullet/icons/Offline.tsx | 18 ++++++-- .../components/StatusBullet/icons/Online.tsx | 16 +++++-- 8 files changed, 120 insertions(+), 66 deletions(-) delete mode 100644 packages/fuselage/src/components/StatusBullet/StatusBullet.styles.scss diff --git a/packages/fuselage/src/components/StatusBullet/StatusBullet.styles.scss b/packages/fuselage/src/components/StatusBullet/StatusBullet.styles.scss deleted file mode 100644 index 61f50ce8aa..0000000000 --- a/packages/fuselage/src/components/StatusBullet/StatusBullet.styles.scss +++ /dev/null @@ -1,46 +0,0 @@ -@use 'sass:list'; -@use '../../styles/colors.scss'; -@use '../../styles/lengths.scss'; -@use '../../styles/functions.scss'; -@use '../../styles/mixins/size.scss'; - -.rcx-status-bullet { - display: inline-block; - - flex-grow: 0; - flex-shrink: 0; - - border-radius: lengths.border-radius(full); - - background-size: contain; - - @include size.square(lengths.size(12)); - - &--small { - @include size.square(functions.to-rem(10)); - } - - &--online { - fill: colors.status-bullet(online); - } - - &--away { - fill: colors.status-bullet(away); - } - - &--busy { - fill: colors.status-bullet(busy); - } - - &--disabled { - fill: colors.status-bullet(disabled); - } - - &--offline { - stroke: colors.status-bullet(offline); - } - - &--loading { - stroke: colors.status-bullet(loading); - } -} diff --git a/packages/fuselage/src/components/StatusBullet/StatusBullet.tsx b/packages/fuselage/src/components/StatusBullet/StatusBullet.tsx index af20eb4aa7..9fc9bf5508 100644 --- a/packages/fuselage/src/components/StatusBullet/StatusBullet.tsx +++ b/packages/fuselage/src/components/StatusBullet/StatusBullet.tsx @@ -1,3 +1,4 @@ +import { css } from '@rocket.chat/css-in-js'; import type { AllHTMLAttributes } from 'react'; import Away from './icons/Away'; @@ -7,6 +8,45 @@ import Loading from './icons/Loading'; import Offline from './icons/Offline'; import Online from './icons/Online'; +export const statusBulletBaseStyle = css` + display: inline-block; + flex-grow: 0; + flex-shrink: 0; + border-radius: 9999px; + background-size: contain; + width: 0.75rem; + height: 0.75rem; +`; + +export const statusBulletSmallStyle = css` + width: 0.625rem; + height: 0.625rem; +`; + +export const statusBulletOnlineStyle = css` + fill: var(--rcx-color-status-bullet-online, var(--statusBulletOnline)); +`; + +export const statusBulletAwayStyle = css` + fill: var(--rcx-color-status-bullet-away, var(--statusBulletAway)); +`; + +export const statusBulletBusyStyle = css` + fill: var(--rcx-color-status-bullet-busy, var(--statusBulletBusy)); +`; + +export const statusBulletDisabledStyle = css` + fill: var(--rcx-color-status-bullet-disabled, var(--statusBulletDisabled)); +`; + +export const statusBulletOfflineStyle = css` + stroke: var(--rcx-color-status-bullet-offline, var(--statusBulletOffline)); +`; + +export const statusBulletLoadingStyle = css` + stroke: var(--rcx-color-status-bullet-loading, var(--statusBulletLoading)); +`; + export type StatusBulletProps = { status?: 'loading' | 'online' | 'busy' | 'away' | 'offline' | 'disabled'; size?: 'small' | 'large'; diff --git a/packages/fuselage/src/components/StatusBullet/icons/Away.tsx b/packages/fuselage/src/components/StatusBullet/icons/Away.tsx index 58c3f100a6..7e1dbd20e4 100644 --- a/packages/fuselage/src/components/StatusBullet/icons/Away.tsx +++ b/packages/fuselage/src/components/StatusBullet/icons/Away.tsx @@ -1,4 +1,9 @@ import type { StatusBulletProps } from '../StatusBullet'; +import { + statusBulletBaseStyle, + statusBulletAwayStyle, + statusBulletSmallStyle, +} from '../StatusBullet'; const Away = ({ size, className, ...props }: StatusBulletProps) => ( ( width='10' height='10' viewBox='0 0 10 10' - className={`rcx-status-bullet rcx-status-bullet--away ${className} ${ - size === 'small' ? 'rcx-status-bullet--small' : '' - }`} + className={[ + statusBulletBaseStyle, + statusBulletAwayStyle, + size === 'small' && statusBulletSmallStyle, + className, + ] + .filter(Boolean) + .join(' ')} xmlns='http://www.w3.org/2000/svg' > ( ( width='10' height='10' viewBox='0 0 10 10' - className={`rcx-status-bullet rcx-status-bullet--busy ${className} ${ - size === 'small' ? 'rcx-status-bullet--small' : '' - }`} + className={[ + statusBulletBaseStyle, + statusBulletBusyStyle, + size === 'small' && statusBulletSmallStyle, + className, + ] + .filter(Boolean) + .join(' ')} xmlns='http://www.w3.org/2000/svg' > ( ( width='24' height='24' viewBox='0 0 24 24' - className={`rcx-status-bullet rcx-status-bullet--disabled ${className} ${ - size === 'small' ? 'rcx-status-bullet--small' : '' - }`} + className={[ + statusBulletBaseStyle, + statusBulletDisabledStyle, + size === 'small' && statusBulletSmallStyle, + className, + ] + .filter(Boolean) + .join(' ')} xmlns='http://www.w3.org/2000/svg' > ( ( viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg' - className={`rcx-status-bullet rcx-status-bullet--loading ${className} ${ - size === 'small' ? 'rcx-status-bullet--small' : '' - }`} + className={[ + statusBulletBaseStyle, + statusBulletLoadingStyle, + size === 'small' && statusBulletSmallStyle, + className, + ] + .filter(Boolean) + .join(' ')} > diff --git a/packages/fuselage/src/components/StatusBullet/icons/Offline.tsx b/packages/fuselage/src/components/StatusBullet/icons/Offline.tsx index 6f587babd1..69bd6ed647 100644 --- a/packages/fuselage/src/components/StatusBullet/icons/Offline.tsx +++ b/packages/fuselage/src/components/StatusBullet/icons/Offline.tsx @@ -1,4 +1,9 @@ import type { StatusBulletProps } from '../StatusBullet'; +import { + statusBulletBaseStyle, + statusBulletOfflineStyle, + statusBulletSmallStyle, +} from '../StatusBullet'; const Offline = ({ size, className, ...props }: StatusBulletProps) => ( ( viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg' - className={`rcx-status-bullet rcx-status-bullet--offline ${className} ${ - size === 'small' ? 'rcx-status-bullet--small' : '' - }`} + className={[ + statusBulletBaseStyle, + statusBulletOfflineStyle, + size === 'small' && statusBulletSmallStyle, + className, + ] + .filter(Boolean) + .join(' ')} > diff --git a/packages/fuselage/src/components/StatusBullet/icons/Online.tsx b/packages/fuselage/src/components/StatusBullet/icons/Online.tsx index d078973c89..679a09f1e2 100644 --- a/packages/fuselage/src/components/StatusBullet/icons/Online.tsx +++ b/packages/fuselage/src/components/StatusBullet/icons/Online.tsx @@ -1,4 +1,9 @@ import type { StatusBulletProps } from '../StatusBullet'; +import { + statusBulletBaseStyle, + statusBulletOnlineStyle, + statusBulletSmallStyle, +} from '../StatusBullet'; const Online = ({ size, className, ...props }: StatusBulletProps) => ( ( width='24' height='24' viewBox='0 0 24 24' - className={`rcx-status-bullet rcx-status-bullet--online ${className} ${ - size === 'small' ? 'rcx-status-bullet--small' : '' - }`} + className={[ + statusBulletBaseStyle, + statusBulletOnlineStyle, + size === 'small' && statusBulletSmallStyle, + className, + ] + .filter(Boolean) + .join(' ')} xmlns='http://www.w3.org/2000/svg' > From c89f589f441e317ccb7fc7c7120d1f5ce151de96 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 11:39:04 -0300 Subject: [PATCH 20/84] refactor(fuselage): Migrate Throbber component from SCSS to Tamagui v3 ThrobberFrame (RcxView) + ThrobberCircle. Bounce animation via css-in-js keyframes. Colors /. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/Throbber/Throbber.styles.scss | 39 --------- .../src/components/Throbber/Throbber.tsx | 79 +++++++++++++++---- 2 files changed, 64 insertions(+), 54 deletions(-) delete mode 100644 packages/fuselage/src/components/Throbber/Throbber.styles.scss diff --git a/packages/fuselage/src/components/Throbber/Throbber.styles.scss b/packages/fuselage/src/components/Throbber/Throbber.styles.scss deleted file mode 100644 index b1dfce1370..0000000000 --- a/packages/fuselage/src/components/Throbber/Throbber.styles.scss +++ /dev/null @@ -1,39 +0,0 @@ -@use '../../styles/colors.scss'; -@use '../../styles/lengths.scss'; - -.rcx-throbber { - display: flex; - justify-content: center; - - margin-block: lengths.margin(-1); - - &__circle { - margin-inline: lengths.margin(1); - - animation: bounce 1.4s infinite ease-in-out both; - - border-radius: 100%; - - background-color: colors.button(primary-default); - - &--disabled { - background-color: colors.button(secondary-default); - } - - &--inherit-color { - background-color: currentColor; - } - } -} - -@keyframes bounce { - 0%, - 80%, - 100% { - transform: scale(0); - } - - 40% { - transform: scale(1); - } -} diff --git a/packages/fuselage/src/components/Throbber/Throbber.tsx b/packages/fuselage/src/components/Throbber/Throbber.tsx index 0f40c91696..d28f223169 100644 --- a/packages/fuselage/src/components/Throbber/Throbber.tsx +++ b/packages/fuselage/src/components/Throbber/Throbber.tsx @@ -1,41 +1,90 @@ -import { css } from '@rocket.chat/css-in-js'; +import { css, keyframes } from '@rocket.chat/css-in-js'; import { forwardRef } from 'react'; +import { styled } from 'tamagui'; -import { Box, type BoxProps } from '../Box'; +import { RcxView } from '../../primitives'; + +const bounceAnimation = keyframes` + 0%, 80%, 100% { + transform: scale(0); + } + 40% { + transform: scale(1); + } +`; + +const ThrobberFrame = styled(RcxView, { + name: 'Throbber', + + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + marginBlock: -1, +}); + +const ThrobberCircle = styled(RcxView, { + name: 'ThrobberCircle', + + marginInline: 1, + borderRadius: '$full', + backgroundColor: '$buttonPrimaryBg', + + variants: { + disabled: { + true: { + backgroundColor: '$buttonSecondaryBg', + }, + }, + } as const, +}); type CircleProps = { circleCount: number; iteration: number; inheritColor?: boolean; disabled?: boolean; -} & Pick; + size?: string; +}; function Circle({ disabled, circleCount, iteration, inheritColor, + size = 'x16', ...props }: CircleProps) { + const animationClass = css` + animation: ${bounceAnimation} ${circleCount * 0.466}s infinite ease-in-out + both; + animation-delay: ${iteration * 0.16}s; + `; + + const inheritColorClass = inheritColor + ? css` + background-color: currentColor; + ` + : undefined; + + const sizeNum = parseInt(size.replace('x', ''), 10) || 16; + return ( - ); } -export type ThrobberProps = Omit & { +export type ThrobberProps = { circleCount?: number; disabled?: boolean; inheritColor?: boolean; + size?: string; + [key: string]: any; }; /** @@ -46,7 +95,7 @@ const Throbber = forwardRef(function Throbber( ref, ) { return ( - + {Array.from({ length: circleCount || 3 }, (_, iteration) => ( (function Throbber( inheritColor={!!inheritColor} /> ))} - + ); }); From 8c6045d3120319ff5bfda4787848daa0fe481bfd Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 11:39:05 -0300 Subject: [PATCH 21/84] refactor(fuselage): Migrate Tooltip component from SCSS to Tamagui v3 TooltipBase (RcxText) with dark/light variation, font tokens. Arrow pseudo-elements via css-in-js. Direction/position via className. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/Tooltip/Tooltip.styles.scss | 146 --------------- .../src/components/Tooltip/Tooltip.tsx | 176 ++++++++++++++++-- packages/fuselage/src/components/index.scss | 9 - 3 files changed, 165 insertions(+), 166 deletions(-) delete mode 100644 packages/fuselage/src/components/Tooltip/Tooltip.styles.scss diff --git a/packages/fuselage/src/components/Tooltip/Tooltip.styles.scss b/packages/fuselage/src/components/Tooltip/Tooltip.styles.scss deleted file mode 100644 index 2c7700ae2a..0000000000 --- a/packages/fuselage/src/components/Tooltip/Tooltip.styles.scss +++ /dev/null @@ -1,146 +0,0 @@ -@use '../../styles/colors.scss'; -@use '../../styles/lengths.scss'; -@use '../../styles/typography.scss'; -@use '../../styles/functions.scss'; - -$tooltip-dark-background-color: functions.theme( - 'tooltip-dark-background-color', - colors.surface(dark) -); -$tooltip-dark-text-color: functions.theme( - 'tooltip-dark-text-color', - colors.font(white) -); -$tooltip-light-background-color: functions.theme( - 'tooltip-light-background-color', - colors.surface(neutral) -); -$tooltip-light-text-color: functions.theme( - 'tooltip-light-text-color', - colors.font(default) -); - -@mixin triangle-direction($direction) { - &::after { - position: absolute; - - box-sizing: border-box; - - content: ' '; - - border-width: 4px; - border-color: transparent transparent $tooltip-dark-background-color - $tooltip-dark-background-color; - border-radius: 0 0 0 (2px); - - block-size: 0; - inline-size: 0; - - @if $direction == 'bottom' { - inset-block-start: -4px; - - transform: rotate(135deg); - } - @if $direction == 'top' { - inset-block-end: -4px; - - transform: rotate(-45deg); - } - @if $direction == 'right' { - inset-block-start: 50%; - inset-inline-start: -4px; - - margin-block-start: -4px; - - transform: rotate(45deg); - } - @if $direction == 'left' { - inset-block-start: 50%; - inset-inline-end: -4px; - - margin-block-start: -4px; - - transform: rotate(-135deg); - } - } -} - -.rcx-tooltip { - position: relative; - - display: inline-block; - - max-width: 240px; - - padding: 8px 12px; - - user-select: none; - - word-break: break-word; - - pointer-events: none; - - color: $tooltip-dark-text-color; - - border-radius: functions.theme( - 'tooltip-border-radius', - lengths.border-radius(medium) - ); - - background-color: $tooltip-dark-background-color; - - @include typography.use-font-scale(p2m); - - &--dir-top { - @include triangle-direction('top'); - } - - &--dir-bottom { - @include triangle-direction('bottom'); - } - - &--dir-left { - @include triangle-direction('left'); - } - - &--dir-right { - @include triangle-direction('right'); - } - - &--dark { - color: $tooltip-dark-text-color; - background-color: $tooltip-dark-background-color; - } - - &--light { - color: $tooltip-light-text-color; - background-color: $tooltip-light-background-color; - } - - &--pos { - &-middle { - &::after { - inset-inline-start: 50%; - - margin-inline-start: -4px; - } - } - - &-start { - &::after { - inset-inline-start: 8px; - - margin: 0; - } - } - - &-end { - &::after { - inset-inline-start: initial; - inset-inline-end: 8px; - - margin: 0; - } - } - } -} diff --git a/packages/fuselage/src/components/Tooltip/Tooltip.tsx b/packages/fuselage/src/components/Tooltip/Tooltip.tsx index 14916cb356..2274e5c472 100644 --- a/packages/fuselage/src/components/Tooltip/Tooltip.tsx +++ b/packages/fuselage/src/components/Tooltip/Tooltip.tsx @@ -1,6 +1,151 @@ +import { css } from '@rocket.chat/css-in-js'; import { forwardRef } from 'react'; +import { styled } from 'tamagui'; -import { Box, type BoxProps } from '../Box'; +import { RcxText } from '../../primitives'; + +const TooltipBase = styled(RcxText, { + name: 'Tooltip', + + position: 'relative', + display: 'inline-block', + maxWidth: 240, + paddingBlock: 8, + paddingInline: 12, + userSelect: 'none', + pointerEvents: 'none', + borderRadius: '$x4', + fontFamily: '$body', + fontSize: '$p2m', + fontWeight: '$p2m', + lineHeight: '$p2m', + letterSpacing: '$p2m', + overflowWrap: 'normal', + + variants: { + variation: { + dark: { + color: '$fontWhite', + backgroundColor: '$surfaceDark', + }, + light: { + color: '$fontDefault', + backgroundColor: '$surfaceNeutral', + }, + }, + } as const, + + defaultVariants: { + variation: 'dark', + }, +}); + +const dirTopArrow = css` + &::after { + position: absolute; + box-sizing: border-box; + content: ' '; + border-width: 4px; + border-color: transparent transparent var(--surfaceDark) var(--surfaceDark); + border-style: solid; + border-radius: 0 0 0 2px; + block-size: 0; + inline-size: 0; + inset-block-end: -4px; + transform: rotate(-45deg); + } +`; + +const dirBottomArrow = css` + &::after { + position: absolute; + box-sizing: border-box; + content: ' '; + border-width: 4px; + border-color: transparent transparent var(--surfaceDark) var(--surfaceDark); + border-style: solid; + border-radius: 0 0 0 2px; + block-size: 0; + inline-size: 0; + inset-block-start: -4px; + transform: rotate(135deg); + } +`; + +const dirLeftArrow = css` + &::after { + position: absolute; + box-sizing: border-box; + content: ' '; + border-width: 4px; + border-color: transparent transparent var(--surfaceDark) var(--surfaceDark); + border-style: solid; + border-radius: 0 0 0 2px; + block-size: 0; + inline-size: 0; + inset-block-start: 50%; + inset-inline-end: -4px; + margin-block-start: -4px; + transform: rotate(-135deg); + } +`; + +const dirRightArrow = css` + &::after { + position: absolute; + box-sizing: border-box; + content: ' '; + border-width: 4px; + border-color: transparent transparent var(--surfaceDark) var(--surfaceDark); + border-style: solid; + border-radius: 0 0 0 2px; + block-size: 0; + inline-size: 0; + inset-block-start: 50%; + inset-inline-start: -4px; + margin-block-start: -4px; + transform: rotate(45deg); + } +`; + +const posMiddle = css` + &::after { + inset-inline-start: 50%; + margin-inline-start: -4px; + } +`; + +const posStart = css` + &::after { + inset-inline-start: 8px; + margin: 0; + } +`; + +const posEnd = css` + &::after { + inset-inline-start: initial; + inset-inline-end: 8px; + margin: 0; + } +`; + +const wordBreakStyle = css` + word-break: break-word; +`; + +const directionClassMap: Record = { + top: dirTopArrow, + bottom: dirBottomArrow, + left: dirLeftArrow, + right: dirRightArrow, +}; + +const positionClassMap: Record = { + middle: posMiddle, + start: posStart, + end: posEnd, +}; const parsePlacement = (placement: string | null | undefined) => { const [direction, position] = placement @@ -14,7 +159,7 @@ const parsePlacement = (placement: string | null | undefined) => { return [direction, position]; }; -export type TooltipProps = BoxProps & { +export type TooltipProps = { variation?: 'dark' | 'light'; placement?: | 'top-start' @@ -28,25 +173,34 @@ export type TooltipProps = BoxProps & { | 'bottom' | 'right' | null; + children?: React.ReactNode; + [key: string]: any; }; const Tooltip = forwardRef(function Tooltip( - { variation = 'dark', placement, ...props }, + { variation = 'dark', placement, children, ...props }, ref, ) { const [direction, position] = parsePlacement(placement); + const classNames = [wordBreakStyle]; + + if (direction && directionClassMap[direction]) { + classNames.push(directionClassMap[direction]); + } + if (position && positionClassMap[position as string]) { + classNames.push(positionClassMap[position as string]); + } + return ( - + > + {children} + ); }); diff --git a/packages/fuselage/src/components/index.scss b/packages/fuselage/src/components/index.scss index bb95599e43..7d4d68f800 100644 --- a/packages/fuselage/src/components/index.scss +++ b/packages/fuselage/src/components/index.scss @@ -7,10 +7,6 @@ @import './Card/Card.styles.scss'; @import './CardGroup/CardGroup.styles.scss'; @import './CheckBox/CheckBox.styles.scss'; -@import './Dropdown/Dropdown.styles.scss'; -@import './Field/Field.styles.scss'; -@import './FieldGroup/FieldGroup.styles.scss'; -@import './Grid/Grid.styles.scss'; @import './Icon/Icon.styles.scss'; @import './InputBox/InputBox.styles.scss'; @import './Message/Messages.styles.scss'; @@ -18,17 +14,12 @@ @import './NavBar/NavBar.styles.scss'; @import './Option/Option.styles.scss'; @import './Options/Options.styles.scss'; -@import './Pagination/Pagination.styles.scss'; @import './RadioButton/RadioButton.styles.scss'; @import './Select/Select.styles.scss'; @import './Sidebar/Sidebar.styles.scss'; @import './Sidepanel/Sidepanel.styles.scss'; @import './SidebarV2/Sidebar.styles.scss'; -@import './States/States.styles.scss'; @import './Table/Table.styles.scss'; @import './Tabs/Tabs.styles.scss'; -@import './Throbber/Throbber.styles.scss'; @import './ToastBar/ToastBar.styles.scss'; @import './ToggleSwitch/ToggleSwitch.styles.scss'; -@import './Tooltip/Tooltip.styles.scss'; -@import './StatusBullet/StatusBullet.styles.scss'; From 2aeb35f5de71e15aedaf13cc51b9773cbb0c6c79 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 11:40:08 -0300 Subject: [PATCH 22/84] refactor(fuselage): Migrate Grid component from SCSS to Tamagui v3 GridWrapper/GridInner (RcxView) with GridContext for breakpoint/gutter. GridItem computes flexBasis/maxWidth from column count. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/Grid/Grid.styles.scss | 86 ------------ .../fuselage/src/components/Grid/Grid.tsx | 123 +++++++++++++++--- .../fuselage/src/components/Grid/GridItem.tsx | 62 +++++++-- 3 files changed, 157 insertions(+), 114 deletions(-) delete mode 100644 packages/fuselage/src/components/Grid/Grid.styles.scss diff --git a/packages/fuselage/src/components/Grid/Grid.styles.scss b/packages/fuselage/src/components/Grid/Grid.styles.scss deleted file mode 100644 index 19813690a2..0000000000 --- a/packages/fuselage/src/components/Grid/Grid.styles.scss +++ /dev/null @@ -1,86 +0,0 @@ -@use 'sass:math'; -@use '~@rocket.chat/fuselage-tokens/breakpoints.scss'; - -$breakpoint-names: map-keys(breakpoints.$breakpoints); - -@mixin when-item-is-on-breakpoint($breakpoint-name) { - @include on-breakpoint($breakpoint-name) { - @content; - } - - .rcx-grid--#{$breakpoint-name} > & { - @content; - } -} - -.rcx-grid { - display: flex; - flex-flow: row wrap; - - &__wrapper { - overflow: hidden; - } - - @each $breakpoint-name in $breakpoint-names { - @include when-item-is-on-breakpoint($breakpoint-name) { - margin-block: calc( - #{to-rem( - map-get( - map-get(breakpoints.$breakpoints, $breakpoint-name), - gutter-width - ) - )} / - -2 - ); - margin-inline: calc( - #{to-rem( - map-get( - map-get(breakpoints.$breakpoints, $breakpoint-name), - gutter-width - ) - )} / - -2 - ); - } - } -} - -.rcx-grid__item { - flex: 1 1 0; - - @each $breakpoint-name in $breakpoint-names { - @include when-item-is-on-breakpoint($breakpoint-name) { - padding-block: calc( - #{to-rem( - map-get( - map-get(breakpoints.$breakpoints, $breakpoint-name), - gutter-width - ) - )} / - 2 - ); - padding-inline: calc( - #{to-rem( - map-get( - map-get(breakpoints.$breakpoints, $breakpoint-name), - gutter-width - ) - )} / - 2 - ); - - $total-columns: map-get( - map-get(breakpoints.$breakpoints, $breakpoint-name), - columns - ); - @for $columns from 1 through $total-columns { - &--#{$breakpoint-name}-#{$columns} { - flex-grow: 0; - flex-basis: (math.div($columns, $total-columns) * 100%); - - max-width: (math.div($columns, $total-columns) * 100%); - } - } - } - } -} diff --git a/packages/fuselage/src/components/Grid/Grid.tsx b/packages/fuselage/src/components/Grid/Grid.tsx index 2e4a560714..860f62fd3c 100644 --- a/packages/fuselage/src/components/Grid/Grid.tsx +++ b/packages/fuselage/src/components/Grid/Grid.tsx @@ -1,28 +1,121 @@ -import { Box, type BoxProps } from '../Box'; +import { useMediaQuery } from '@rocket.chat/fuselage-hooks'; +import type { ComponentPropsWithoutRef, ReactNode } from 'react'; +import { createContext, useContext } from 'react'; +import { styled } from 'tamagui'; + +import { RcxView } from '../../primitives'; import GridItem from './GridItem'; -export type GridProps = BoxProps & { +const GridWrapper = styled(RcxView, { + name: 'GridWrapper', + overflow: 'hidden', +}); + +const GridInner = styled(RcxView, { + name: 'Grid', + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap', +}); + +type BreakpointName = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + +export type GridProps = ComponentPropsWithoutRef & { xs?: boolean; sm?: boolean; md?: boolean; lg?: boolean; xl?: boolean; + children?: ReactNode; +}; + +const GUTTER_MAP: Record = { + xs: 16, + sm: 16, + md: 24, + lg: 24, + xl: 24, +}; + +const COLUMNS_MAP: Record = { + xs: 4, + sm: 8, + md: 8, + lg: 12, + xl: 12, +}; + +const BREAKPOINT_QUERIES: Record = { + xs: '(min-width: 0px)', + sm: '(min-width: 600px)', + md: '(min-width: 768px)', + lg: '(min-width: 1024px)', + xl: '(min-width: 1280px)', }; -const Grid = ({ xs, sm, md, lg, xl, ...props }: GridProps) => ( - - - -); +export type GridContextValue = { + activeBreakpoint: BreakpointName; + gutter: number; + totalColumns: number; +}; + +export const GridContext = createContext({ + activeBreakpoint: 'xs', + gutter: 16, + totalColumns: 4, +}); + +export const useGridContext = () => useContext(GridContext); + +const useActiveBreakpoint = ( + forced?: BreakpointName, +): BreakpointName => { + const isXl = useMediaQuery(BREAKPOINT_QUERIES.xl); + const isLg = useMediaQuery(BREAKPOINT_QUERIES.lg); + const isMd = useMediaQuery(BREAKPOINT_QUERIES.md); + const isSm = useMediaQuery(BREAKPOINT_QUERIES.sm); + + if (forced) return forced; + if (isXl) return 'xl'; + if (isLg) return 'lg'; + if (isMd) return 'md'; + if (isSm) return 'sm'; + return 'xs'; +}; + +const Grid = ({ xs, sm, md, lg, xl, children, ...props }: GridProps) => { + const forcedBreakpoint = xs + ? 'xs' + : sm + ? 'sm' + : md + ? 'md' + : lg + ? 'lg' + : xl + ? 'xl' + : undefined; + + const activeBreakpoint = useActiveBreakpoint(forcedBreakpoint); + const gutter = GUTTER_MAP[activeBreakpoint]; + const totalColumns = COLUMNS_MAP[activeBreakpoint]; + const halfGutter = gutter / 2; + + return ( + + + + {children} + + + + ); +}; Grid.Item = GridItem; diff --git a/packages/fuselage/src/components/Grid/GridItem.tsx b/packages/fuselage/src/components/Grid/GridItem.tsx index 82b957a7fe..f280bf956f 100644 --- a/packages/fuselage/src/components/Grid/GridItem.tsx +++ b/packages/fuselage/src/components/Grid/GridItem.tsx @@ -1,23 +1,59 @@ -import { Box, type BoxProps } from '../Box'; +import type { ComponentPropsWithoutRef, ReactNode } from 'react'; +import { styled } from 'tamagui'; -export type GridItemProps = BoxProps & { +import { RcxView } from '../../primitives'; + +import { useGridContext } from './Grid'; + +const GridItemBase = styled(RcxView, { + name: 'GridItem', + flexGrow: 1, + flexShrink: 1, + flexBasis: 0, +}); + +export type GridItemProps = ComponentPropsWithoutRef & { xs?: 1 | 2 | 3 | 4; sm?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8; md?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8; lg?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; xl?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; + children?: ReactNode; }; -const GridItem = ({ xs, sm, md, lg, xl, ...props }: GridItemProps) => ( - -); +const GridItem = ({ xs, sm, md, lg, xl, children, ...props }: GridItemProps) => { + const { activeBreakpoint, gutter, totalColumns } = useGridContext(); + const halfGutter = gutter / 2; + + // Determine the column span for the active breakpoint + const breakpointColumns: Record = { + xs, + sm, + md, + lg, + xl, + }; + + const columns = breakpointColumns[activeBreakpoint]; + + const columnStyles = columns + ? { + flexGrow: 0 as const, + flexBasis: `${(columns / totalColumns) * 100}%`, + maxWidth: `${(columns / totalColumns) * 100}%`, + } + : {}; + + return ( + + {children} + + ); +}; export default GridItem; From 25691631e43c1ec2866bc6b91a1f3a0a6960341f Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 11:40:09 -0300 Subject: [PATCH 23/84] refactor(fuselage): Migrate Dropdown component from SCSS to Tamagui v3 DropdownContent (RcxView) replacing Box in Desktop/Mobile variants. Original SCSS only had CSSTransition classes (unused). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/Dropdown/Dropdown.styles.scss | 29 ------------------- .../components/Dropdown/DropdownDesktop.tsx | 18 ++++++++---- .../components/Dropdown/DropdownMobile.tsx | 20 +++++++++---- 3 files changed, 27 insertions(+), 40 deletions(-) delete mode 100644 packages/fuselage/src/components/Dropdown/Dropdown.styles.scss diff --git a/packages/fuselage/src/components/Dropdown/Dropdown.styles.scss b/packages/fuselage/src/components/Dropdown/Dropdown.styles.scss deleted file mode 100644 index 401ce8e87e..0000000000 --- a/packages/fuselage/src/components/Dropdown/Dropdown.styles.scss +++ /dev/null @@ -1,29 +0,0 @@ -.rcx-dropdown-enter { - transform: translate3d(0, -1rem, 0); - - opacity: 0; -} - -.rcx-dropdown-enter-active { - transition: - opacity 300ms, - transform 300ms; - transform: translate3d(0, 0, 0); - - opacity: 1; -} - -.rcx-dropdown-exit { - transform: translate3d(0, 0, 0); - - opacity: 1; -} - -.rcx-dropdown-exit-active { - transition: - transform 300ms, - opacity 300ms; - transform: translate3d(0, -1rem, 0); - - opacity: 0 !important; -} diff --git a/packages/fuselage/src/components/Dropdown/DropdownDesktop.tsx b/packages/fuselage/src/components/Dropdown/DropdownDesktop.tsx index 6c5bc0a12d..5edbca9e77 100644 --- a/packages/fuselage/src/components/Dropdown/DropdownDesktop.tsx +++ b/packages/fuselage/src/components/Dropdown/DropdownDesktop.tsx @@ -1,7 +1,15 @@ import type { CSSProperties, ReactNode } from 'react'; import { forwardRef } from 'react'; +import { styled } from 'tamagui'; -import { Box, Tile } from '..'; +import { RcxView } from '../../primitives'; +import Tile from '../Tile/Tile'; + +const DropdownContent = styled(RcxView, { + name: 'DropdownContent', + flexShrink: 1, + paddingBottom: 12, +}); export type DropdownDesktopProps = { children: ReactNode; @@ -18,17 +26,17 @@ export const DropdownDesktop = forwardRef( style={style} ref={ref} elevation='2' - pi='0' - pb='0' + paddingInline={0} + paddingBottom={0} display='flex' flexDirection='column' overflow='auto' data-testid='dropdown' {...props} > - + {style?.visibility === 'hidden' ? null : children} - + ); }, diff --git a/packages/fuselage/src/components/Dropdown/DropdownMobile.tsx b/packages/fuselage/src/components/Dropdown/DropdownMobile.tsx index bebcce5ccf..5735ce9b37 100644 --- a/packages/fuselage/src/components/Dropdown/DropdownMobile.tsx +++ b/packages/fuselage/src/components/Dropdown/DropdownMobile.tsx @@ -1,7 +1,15 @@ import type { ReactNode } from 'react'; import { forwardRef } from 'react'; +import { styled } from 'tamagui'; -import { Box, Tile } from '..'; +import { RcxView } from '../../primitives'; +import Tile from '../Tile/Tile'; + +const DropdownContent = styled(RcxView, { + name: 'DropdownMobileContent', + flexShrink: 1, + paddingBottom: 16, +}); export type DropdownMobileProps = { children: ReactNode; @@ -13,9 +21,9 @@ export const DropdownMobile = forwardRef( ( data-testid='dropdown' {...props} > - + {children} - + ); }, From 97e94f7e1982e84465fdb305a8b5503926748e3b Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 11:40:09 -0300 Subject: [PATCH 24/84] refactor(fuselage): Migrate Pagination component from SCSS to Tamagui v3 Full rewrite: PaginationNav, PaginationLabel (), PaginationLink (RcxInteractiveText), PaginationNavButton. Responsive via useMediaQuery. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Pagination/Pagination.styles.scss | 139 ---------- .../src/components/Pagination/Pagination.tsx | 250 ++++++++++++++---- .../src/components/States/StatesSubtitle.tsx | 1 - 3 files changed, 201 insertions(+), 189 deletions(-) delete mode 100644 packages/fuselage/src/components/Pagination/Pagination.styles.scss diff --git a/packages/fuselage/src/components/Pagination/Pagination.styles.scss b/packages/fuselage/src/components/Pagination/Pagination.styles.scss deleted file mode 100644 index 119b202566..0000000000 --- a/packages/fuselage/src/components/Pagination/Pagination.styles.scss +++ /dev/null @@ -1,139 +0,0 @@ -@use '../../styles/colors.scss'; -@use '../../styles/lengths.scss'; -@use '../../styles/typography.scss'; - -.rcx-pagination { - display: flex; - flex-flow: column-reverse nowrap; - - align-items: center; - - padding: lengths.padding(12) lengths.padding(24); - - @include on-breakpoint(sm) { - flex-direction: column; - } - - @include on-breakpoint(md) { - flex-direction: row; - } - - &--divider { - position: relative; - - &::before { - position: absolute; - top: 0; - left: 0; - right: 0; - - height: 1px; - - content: ''; - - border-radius: theme( - 'pagination-border-radius', - lengths.border-radius(small) - ); - background-color: colors.stroke(extra-light); - } - } -} - -.rcx-pagination__left, -.rcx-pagination__right { - display: flex; - flex-flow: row nowrap; - align-items: center; - flex: 0 1 auto; -} - -.rcx-pagination__left { - justify-content: center; - - margin-inline-start: lengths.margin(none); - - @include on-breakpoint(sm) { - margin-inline-start: auto; - } - - @include on-breakpoint(md) { - margin-inline: lengths.margin(none) auto; - } -} - -.rcx-pagination__right { - flex-flow: column nowrap; - - align-items: center; - - margin-inline-start: lengths.margin(none); - - @include on-breakpoint(sm) { - flex-flow: row nowrap; - align-items: center; - - margin-inline-start: auto; - } -} - -.rcx-pagination__label { - @include typography.use-font-scale(c1); - - color: colors.font(secondary-info); -} - -.rcx-pagination__list { - display: flex; - flex-flow: row nowrap; - align-items: center; - - margin-inline: lengths.margin(4); -} - -.rcx-pagination__list-item { - @include typography.use-font-scale(c1); - display: flex; - - margin-inline: lengths.margin(2); - padding: lengths.padding(4); - - color: colors.font(secondary-info); -} - -.rcx-pagination__link { - @include typography.use-font-scale(c1); - @include clickable; - display: inline-flex; - - color: colors.font(info); - background: transparent; - - &:hover:not(.disabled):not(:disabled), - &:focus:not(.disabled):not(:disabled) { - text-decoration: underline; - } - - &.disabled, - &:disabled { - @include typography.use-font-scale(c2); - cursor: default; - - color: colors.font(default); - } -} - -.rcx-pagination__back, -.rcx-pagination__forward { - @include typography.use-font-scale(c1); - @include clickable; - display: inline-flex; - - color: colors.font(secondary-info); - background: transparent; - - &.disabled, - &:disabled { - color: colors.font(secondary-info); - } -} diff --git a/packages/fuselage/src/components/Pagination/Pagination.tsx b/packages/fuselage/src/components/Pagination/Pagination.tsx index d63f49347d..f7734c7500 100644 --- a/packages/fuselage/src/components/Pagination/Pagination.tsx +++ b/packages/fuselage/src/components/Pagination/Pagination.tsx @@ -1,12 +1,150 @@ -import type { Dispatch, SetStateAction } from 'react'; +import { useMediaQuery } from '@rocket.chat/fuselage-hooks'; +import type { ComponentPropsWithoutRef, Dispatch, ReactNode, SetStateAction } from 'react'; import { useMemo } from 'react'; +import { styled } from 'tamagui'; -import { Box, type BoxProps } from '../Box'; +import { RcxInteractiveText, RcxText, RcxView } from '../../primitives'; import { Chevron } from '../Chevron'; type ItemsPerPage = 25 | 50 | 100; -export type PaginationProps = BoxProps & { +// --- Styled components --- + +const PaginationNav = styled(RcxView, { + name: 'Pagination', + role: 'navigation', + display: 'flex', + flexDirection: 'column-reverse', + flexWrap: 'nowrap', + alignItems: 'center', + paddingBlock: 12, + paddingInline: 24, +}); + +const PaginationDividerLine = styled(RcxView, { + name: 'PaginationDividerLine', + position: 'absolute', + top: 0, + left: 0, + right: 0, + height: 1, + borderRadius: '$x2', + backgroundColor: '$strokeExtraLight', +}); + +const PaginationLeft = styled(RcxView, { + name: 'PaginationLeft', + display: 'flex', + flexDirection: 'row', + flexWrap: 'nowrap', + alignItems: 'center', + flexGrow: 0, + flexShrink: 1, + justifyContent: 'center', + marginInlineStart: 0, +}); + +const PaginationRight = styled(RcxView, { + name: 'PaginationRight', + display: 'flex', + flexDirection: 'column', + flexWrap: 'nowrap', + alignItems: 'center', + flexGrow: 0, + flexShrink: 1, + marginInlineStart: 0, +}); + +const PaginationLabel = styled(RcxText, { + name: 'PaginationLabel', + display: 'block', + fontFamily: '$body', + fontSize: '$c1', + fontWeight: '$c1', + lineHeight: '$c1', + letterSpacing: '$c1', + color: '$fontSecondaryInfo', + overflowWrap: 'normal', +}); + +const PaginationList = styled(RcxView, { + name: 'PaginationList', + role: 'list', + display: 'flex', + flexDirection: 'row', + flexWrap: 'nowrap', + alignItems: 'center', + marginInline: 4, +}); + +const PaginationListItem = styled(RcxText, { + name: 'PaginationListItem', + role: 'listitem', + display: 'flex', + marginInline: 2, + padding: 4, + fontFamily: '$body', + fontSize: '$c1', + fontWeight: '$c1', + lineHeight: '$c1', + letterSpacing: '$c1', + color: '$fontSecondaryInfo', + overflowWrap: 'normal', +}); + +const PaginationLink = styled(RcxInteractiveText, { + name: 'PaginationLink', + role: 'button', + display: 'inline-flex', + fontFamily: '$body', + fontSize: '$c1', + fontWeight: '$c1', + lineHeight: '$c1', + letterSpacing: '$c1', + color: '$fontInfo', + backgroundColor: 'transparent', + borderWidth: 0, + overflowWrap: 'normal', + + hoverStyle: { + textDecorationLine: 'underline', + }, + + focusVisibleStyle: { + textDecorationLine: 'underline', + }, + + disabledStyle: { + fontWeight: '$c2', + color: '$fontDefault', + cursor: 'default', + textDecorationLine: 'none', + }, +}); + +const PaginationNavButton = styled(RcxInteractiveText, { + name: 'PaginationNavButton', + role: 'button', + display: 'inline-flex', + fontFamily: '$body', + fontSize: '$c1', + fontWeight: '$c1', + lineHeight: '$c1', + letterSpacing: '$c1', + color: '$fontSecondaryInfo', + backgroundColor: 'transparent', + borderWidth: 0, + overflowWrap: 'normal', + + disabledStyle: { + color: '$fontSecondaryInfo', + cursor: 'not-allowed', + }, +}); + +// --- Component --- + +export type PaginationProps = ComponentPropsWithoutRef & { count: number; current?: number; divider?: boolean; @@ -23,6 +161,7 @@ export type PaginationProps = BoxProps & { }) => string; onSetCurrent?: Dispatch>; onSetItemsPerPage?: Dispatch>; + children?: ReactNode; }; const defaultItemsPerPageLabel = () => 'Items per page:'; @@ -54,26 +193,25 @@ const Pagination = ({ divider, ...props }: PaginationProps) => { + const isSm = useMediaQuery('(min-width: 600px)'); + const isMd = useMediaQuery('(min-width: 768px)'); + const hasItemsPerPageSelection = itemsPerPageOptions.length > 1; const currentPage = Math.floor(current / itemsPerPage); const pages = Math.ceil(count / itemsPerPage); const displayedPages = useMemo(() => { if (pages <= 7) { - // 0 1 2 3 4 5 6 return Array.from({ length: pages }, (_, i) => i); } if (currentPage < 5) { - // 0 1 2 3 4 ... N return [0, 1, 2, 3, 4, '⋯', pages - 1]; } if (currentPage > pages - 5) { - // 0 ... N-4 N-3 N-2 N-1 N return [0, '⋯', pages - 5, pages - 4, pages - 3, pages - 2, pages - 1]; } - // 0 ... x-1 x x-2 ... N return [ 0, '⋯', @@ -96,74 +234,88 @@ const Pagination = ({ onSetCurrent?.(page * itemsPerPage); }; + // Responsive direction: column-reverse default, column at sm, row at md + const flexDirection = isMd ? 'row' : isSm ? 'column' : 'column-reverse'; + + // Responsive left section margin + const leftMarginInlineStart = isSm ? 'auto' : 0; + const leftMarginInlineEnd = isMd ? 'auto' : undefined; + + // Responsive right section + const rightFlexDirection = isSm ? 'row' : 'column'; + const rightMarginInlineStart = isSm ? 'auto' : 0; + return ( - + + {divider && } {hasItemsPerPageSelection && ( - - + + {itemsPerPageLabel(renderingContext)} - - + + {itemsPerPageOptions.map((itemsPerPageOption) => ( - - + {itemsPerPageOption} - - + + ))} - - + + )} - - + + {showingResultsLabel(renderingContext)} - - - - + + + - - + + {displayedPages.map((page, i) => ( - + {page === '⋯' ? ( '⋯' ) : ( - {(page as number) + 1} - + )} - + ))} - - + - - - - - + + + + + ); }; diff --git a/packages/fuselage/src/components/States/StatesSubtitle.tsx b/packages/fuselage/src/components/States/StatesSubtitle.tsx index 4dac50eec5..176d81fcdb 100644 --- a/packages/fuselage/src/components/States/StatesSubtitle.tsx +++ b/packages/fuselage/src/components/States/StatesSubtitle.tsx @@ -11,7 +11,6 @@ const StatesSubtitleFrame = styled(RcxText, { alignItems: 'center', width: '100%', maxWidth: 462, - margin: 0, padding: 0, textAlign: 'center', marginBlockEnd: '$x24', From d1d8addfbc931c42fc29fd0f5c30174b002a10f8 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 11:52:10 -0300 Subject: [PATCH 25/84] refactor(fuselage): Migrate Card component from SCSS to Tamagui v3 CardFrame/CardClickableFrame with createStyledContext for horizontal. CardBody (), CardTitle (// variants), CardCol, CardRow, CardHeader, CardControls. Clickable gets hover/focus states. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/Card/Card.styles.scss | 86 -------------- .../fuselage/src/components/Card/Card.tsx | 110 ++++++++++++++++-- .../fuselage/src/components/Card/CardBody.tsx | 39 +++++-- .../fuselage/src/components/Card/CardCol.tsx | 25 +++- .../src/components/Card/CardControls.tsx | 12 +- .../src/components/Card/CardHeader.tsx | 14 ++- .../fuselage/src/components/Card/CardRow.tsx | 17 ++- .../src/components/Card/CardTitle.tsx | 49 +++++++- 8 files changed, 236 insertions(+), 116 deletions(-) delete mode 100644 packages/fuselage/src/components/Card/Card.styles.scss diff --git a/packages/fuselage/src/components/Card/Card.styles.scss b/packages/fuselage/src/components/Card/Card.styles.scss deleted file mode 100644 index a8916301be..0000000000 --- a/packages/fuselage/src/components/Card/Card.styles.scss +++ /dev/null @@ -1,86 +0,0 @@ -@use '../../styles/colors.scss'; -@use '../../styles/functions'; -@use '../../styles/lengths.scss'; - -$card-spacing: lengths.margin(8); -$card-vertical-padding: lengths.padding(20); -$card-vertical-gap: lengths.margin(24); -$card-horizontal-padding: lengths.padding(12); -$card-horizontal-gap: lengths.padding(16); -$card-horizontal-row-gap: lengths.padding(4); -$card-hero-padding: lengths.padding(28); - -.rcx-card { - display: flex; - - color: functions.theme('card-color', colors.font(default)); - border-radius: lengths.border-radius(large); - - background-color: functions.theme( - 'card-background-color', - colors.surface(light) - ); - - &__clickable { - &:hover, - &:focus { - cursor: pointer; - - outline: 0; - background-color: colors.surface(hover); - } - } - - &__header, - &__title, - &__controls, - &__body, - &__row, - &__col { - gap: $card-spacing; - } - - &__col { - display: flex; - flex-direction: column; - } - - &__row { - flex-grow: 1; - flex-shrink: 1; - } - - &__horizontal { - align-items: center; - - padding: $card-horizontal-padding; - gap: $card-horizontal-gap; - - &--wrap { - flex-wrap: wrap; - } - } - - &__horizontal &__col { - row-gap: $card-horizontal-row-gap; - } - - &__vertical { - flex-direction: column; - - padding: $card-vertical-padding; - gap: $card-vertical-gap; - } - - &__hero { - padding: $card-hero-padding; - } - - &__title, - &__row, - &__header, - &__controls { - display: flex; - align-items: center; - } -} diff --git a/packages/fuselage/src/components/Card/Card.tsx b/packages/fuselage/src/components/Card/Card.tsx index f0bf412224..e617989fe1 100644 --- a/packages/fuselage/src/components/Card/Card.tsx +++ b/packages/fuselage/src/components/Card/Card.tsx @@ -1,7 +1,102 @@ import { useBreakpoints } from '@rocket.chat/fuselage-hooks'; import type { AllHTMLAttributes } from 'react'; +import { styled, createStyledContext } from 'tamagui'; -import { Box } from '../Box'; +import { RcxInteractive, RcxView } from '../../primitives'; + +export const CardContext = createStyledContext({ + horizontal: false as boolean, +}); + +const CardFrame = styled(RcxView, { + name: 'Card', + context: CardContext, + display: 'flex', + color: '$fontDefault', + borderRadius: '$x8', + backgroundColor: '$surfaceLight', + + variants: { + horizontal: { + true: { + alignItems: 'center', + padding: '$x12', + gap: '$x16', + }, + false: { + flexDirection: 'column', + padding: '$x20', + gap: '$x24', + }, + }, + + hero: { + true: { + padding: '$x28', + }, + }, + + wrap: { + true: { + flexWrap: 'wrap', + }, + }, + } as const, + + defaultVariants: { + horizontal: false, + }, +}); + +const CardClickableFrame = styled(RcxInteractive, { + name: 'CardClickable', + context: CardContext, + display: 'flex', + color: '$fontDefault', + borderRadius: '$x8', + backgroundColor: '$surfaceLight', + + hoverStyle: { + backgroundColor: '$surfaceHover', + }, + + focusStyle: { + backgroundColor: '$surfaceHover', + outlineWidth: 0, + }, + + variants: { + horizontal: { + true: { + alignItems: 'center', + flexDirection: 'row', + padding: '$x12', + gap: '$x16', + }, + false: { + flexDirection: 'column', + padding: '$x20', + gap: '$x24', + }, + }, + + hero: { + true: { + padding: '$x28', + }, + }, + + wrap: { + true: { + flexWrap: 'wrap', + }, + }, + } as const, + + defaultVariants: { + horizontal: false, + }, +}); export type CardProps = { horizontal?: boolean; @@ -13,14 +108,13 @@ const Card = ({ horizontal, hero, clickable, ...props }: CardProps) => { const breakpoints = useBreakpoints(); const isMobile = !breakpoints.includes('sm'); + const Frame = clickable ? CardClickableFrame : CardFrame; + return ( - ); diff --git a/packages/fuselage/src/components/Card/CardBody.tsx b/packages/fuselage/src/components/Card/CardBody.tsx index 3ff14a6132..cea54d8152 100644 --- a/packages/fuselage/src/components/Card/CardBody.tsx +++ b/packages/fuselage/src/components/Card/CardBody.tsx @@ -1,10 +1,35 @@ import type { AllHTMLAttributes, CSSProperties, ReactNode } from 'react'; +import { styled } from 'tamagui'; -import { Box, type BoxProps } from '../Box'; +import { RcxText, RcxView } from '../../primitives'; + +const CardBodyFrameContent = styled(RcxView, { + name: 'CardBodyContent', + display: 'flex', + flexGrow: 1, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + flexShrink: 1, +}); + +const CardBodyFrameText = styled(RcxText, { + name: 'CardBodyText', + display: 'flex', + wordBreak: 'break-word', + gap: '$x8', + fontFamily: '$body', + fontSize: '$p2m', + fontWeight: '$p2m', + lineHeight: '$p2m', + letterSpacing: '$p2m', + color: 'inherit', + overflowWrap: 'normal', +}); export type CardBodyProps = { flexDirection?: CSSProperties['flexDirection']; - height?: BoxProps['height']; + height?: string | number; children: ReactNode; } & Omit, 'is'>; @@ -14,17 +39,13 @@ const CardBody = ({ height, ...props }: CardBodyProps) => ( - - {children} - + {children} + ); export default CardBody; diff --git a/packages/fuselage/src/components/Card/CardCol.tsx b/packages/fuselage/src/components/Card/CardCol.tsx index 7e064ad441..d3e56f49be 100644 --- a/packages/fuselage/src/components/Card/CardCol.tsx +++ b/packages/fuselage/src/components/Card/CardCol.tsx @@ -1,13 +1,32 @@ import type { AllHTMLAttributes, ReactNode } from 'react'; +import { styled } from 'tamagui'; + +import { RcxView } from '../../primitives'; +import { CardContext } from './Card'; + +const CardColFrame = styled(RcxView, { + name: 'CardCol', + context: CardContext, + display: 'flex', + flexDirection: 'column', + gap: '$x8', + + variants: { + horizontal: { + true: { + rowGap: '$x4', + }, + false: {}, + }, + } as const, +}); export type CardColProps = { children: ReactNode; } & AllHTMLAttributes; const CardCol = ({ children, ...props }: CardColProps) => ( -
    - {children} -
    + {children} ); export default CardCol; diff --git a/packages/fuselage/src/components/Card/CardControls.tsx b/packages/fuselage/src/components/Card/CardControls.tsx index a5de212f5b..c036b8a838 100644 --- a/packages/fuselage/src/components/Card/CardControls.tsx +++ b/packages/fuselage/src/components/Card/CardControls.tsx @@ -1,9 +1,19 @@ import type { HTMLAttributes } from 'react'; +import { styled } from 'tamagui'; + +import { RcxView } from '../../primitives'; + +const CardControlsFrame = styled(RcxView, { + name: 'CardControls', + display: 'flex', + alignItems: 'center', + gap: '$x8', +}); export type CardControlsProps = HTMLAttributes; const CardControls = ({ ...props }: CardControlsProps) => ( -
    + ); export default CardControls; diff --git a/packages/fuselage/src/components/Card/CardHeader.tsx b/packages/fuselage/src/components/Card/CardHeader.tsx index 432b102e52..48127b17df 100644 --- a/packages/fuselage/src/components/Card/CardHeader.tsx +++ b/packages/fuselage/src/components/Card/CardHeader.tsx @@ -1,13 +1,21 @@ import type { AllHTMLAttributes, ReactNode } from 'react'; +import { styled } from 'tamagui'; + +import { RcxView } from '../../primitives'; + +const CardHeaderFrame = styled(RcxView, { + name: 'CardHeader', + display: 'flex', + alignItems: 'center', + gap: '$x8', +}); export type CardHeaderProps = { children: ReactNode; } & AllHTMLAttributes; const CardHeader = ({ children, ...props }: CardHeaderProps) => ( -
    - {children} -
    + {children} ); export default CardHeader; diff --git a/packages/fuselage/src/components/Card/CardRow.tsx b/packages/fuselage/src/components/Card/CardRow.tsx index bf9b8424d9..ce0a19dcd4 100644 --- a/packages/fuselage/src/components/Card/CardRow.tsx +++ b/packages/fuselage/src/components/Card/CardRow.tsx @@ -1,13 +1,24 @@ import type { AllHTMLAttributes, ReactNode } from 'react'; +import { styled } from 'tamagui'; + +import { RcxView } from '../../primitives'; + +const CardRowFrame = styled(RcxView, { + name: 'CardRow', + display: 'flex', + alignItems: 'center', + flexDirection: 'row', + gap: '$x8', + flexGrow: 1, + flexShrink: 1, +}); export type CardRowProps = { children: ReactNode; } & AllHTMLAttributes; const CardRow = ({ children, ...props }: CardRowProps) => ( -
    - {children} -
    + {children} ); export default CardRow; diff --git a/packages/fuselage/src/components/Card/CardTitle.tsx b/packages/fuselage/src/components/Card/CardTitle.tsx index aeb8c1756d..4693d0bcb4 100644 --- a/packages/fuselage/src/components/Card/CardTitle.tsx +++ b/packages/fuselage/src/components/Card/CardTitle.tsx @@ -1,8 +1,46 @@ import type { AllHTMLAttributes, ReactNode } from 'react'; +import { styled } from 'tamagui'; -import { Box } from '../Box'; +import { RcxText } from '../../primitives'; import { LabelInfo } from '../Label/LabelInfo'; +const CardTitleFrame = styled(RcxText, { + name: 'CardTitle', + display: 'flex', + alignItems: 'center', + gap: '$x8', + fontFamily: '$body', + color: 'inherit', + overflowWrap: 'normal', + + variants: { + variant: { + h3: { + fontSize: '$h3', + fontWeight: '$h3', + lineHeight: '$h3', + letterSpacing: '$h3', + }, + h4: { + fontSize: '$h4', + fontWeight: '$h4', + lineHeight: '$h4', + letterSpacing: '$h4', + }, + h5: { + fontSize: '$h5', + fontWeight: '$h5', + lineHeight: '$h5', + letterSpacing: '$h5', + }, + }, + } as const, + + defaultVariants: { + variant: 'h4', + }, +}); + export type CardTitleProps = { children: ReactNode; info?: string; @@ -15,10 +53,15 @@ const CardTitle = ({ variant = 'h4', ...props }: CardTitleProps) => ( - + {children} {info && } - + ); export default CardTitle; From dc49692a15f9308965269baed38db0a526acfe6e Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 11:52:11 -0300 Subject: [PATCH 26/84] refactor(fuselage): Migrate CardGroup component from SCSS to Tamagui v3 CardGroupFrame with createStyledContext for stretch. Gap-based spacing replacing patchChildren margins. Variants: wrap, stretch, vertical, align. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../CardGroup/CardGroup.styles.scss | 71 ------------ .../src/components/CardGroup/CardGroup.tsx | 106 ++++++++++++++---- 2 files changed, 85 insertions(+), 92 deletions(-) delete mode 100644 packages/fuselage/src/components/CardGroup/CardGroup.styles.scss diff --git a/packages/fuselage/src/components/CardGroup/CardGroup.styles.scss b/packages/fuselage/src/components/CardGroup/CardGroup.styles.scss deleted file mode 100644 index 25aa3c7c27..0000000000 --- a/packages/fuselage/src/components/CardGroup/CardGroup.styles.scss +++ /dev/null @@ -1,71 +0,0 @@ -@use '../../styles/lengths.scss'; - -.rcx-card-group { - display: flex; - - flex-flow: row nowrap; - justify-content: flex-start; - - align-items: center; - - &--wrap { - flex-wrap: wrap; - - margin-block-end: lengths.margin(-16); - } - - &--stretch { - justify-content: stretch; - align-items: stretch; - } - - &--vertical { - flex-direction: column; - } - - &--align-start { - justify-content: flex-start; - } - - &--align-center { - justify-content: center; - } - - &--align-end { - justify-content: flex-end; - } -} - -.rcx-card-group__item { - margin-inline: lengths.margin(8); - - &:first-of-type { - margin-inline-start: lengths.margin(none); - } - - &:last-of-type { - margin-inline-end: lengths.margin(none); - } - - .rcx-card-group--wrap > & { - margin-block-end: lengths.margin(16); - margin-inline: lengths.margin(8) lengths.margin(8); - } - - .rcx-card-group--stretch > & { - flex-grow: 1; - } - - .rcx-card-group--vertical & { - margin-block: lengths.margin(4); - margin-inline: lengths.margin(none); - - &:first-child { - margin-block-start: lengths.margin(none); - } - - &:last-child { - margin-block-end: lengths.margin(none); - } - } -} diff --git a/packages/fuselage/src/components/CardGroup/CardGroup.tsx b/packages/fuselage/src/components/CardGroup/CardGroup.tsx index a09252b073..536437e4d1 100644 --- a/packages/fuselage/src/components/CardGroup/CardGroup.tsx +++ b/packages/fuselage/src/components/CardGroup/CardGroup.tsx @@ -1,8 +1,76 @@ -import type { AllHTMLAttributes, ReactNode } from 'react'; +import type { ReactNode } from 'react'; +import { Children, isValidElement } from 'react'; +import { styled, createStyledContext } from 'tamagui'; -import { appendClassName } from '../../helpers/appendClassName'; -import { patchChildren } from '../../helpers/patchChildren'; -import { Box } from '../Box'; +import { RcxView } from '../../primitives'; + +const CardGroupContext = createStyledContext({ + stretch: false as boolean, +}); + +const CardGroupFrame = styled(RcxView, { + name: 'CardGroup', + context: CardGroupContext, + display: 'flex', + flexDirection: 'row', + flexWrap: 'nowrap', + justifyContent: 'flex-start', + alignItems: 'center', + gap: '$x16', + + variants: { + wrap: { + true: { + flexWrap: 'wrap', + }, + }, + + stretch: { + true: { + justifyContent: 'stretch', + alignItems: 'stretch', + }, + false: {}, + }, + + vertical: { + true: { + flexDirection: 'column', + gap: '$x8', + }, + }, + + align: { + start: { + justifyContent: 'flex-start', + }, + center: { + justifyContent: 'center', + }, + end: { + justifyContent: 'flex-end', + }, + }, + } as const, + + defaultVariants: { + stretch: false, + }, +}); + +const CardGroupItem = styled(RcxView, { + name: 'CardGroupItem', + context: CardGroupContext, + + variants: { + stretch: { + true: { + flexGrow: 1, + }, + false: {}, + }, + } as const, +}); export type CardGroupProps = { align?: 'start' | 'center' | 'end'; @@ -12,7 +80,7 @@ export type CardGroupProps = { small?: boolean; large?: boolean; children?: ReactNode; -} & Omit, 'is' | 'wrap'>; +} & Omit, 'is' | 'wrap'>; const CardGroup = ({ align = 'start', @@ -22,25 +90,21 @@ const CardGroup = ({ wrap, ...props }: CardGroupProps) => ( - - {patchChildren( - children, - (childProps: { className: string | string[] }) => ({ - className: appendClassName( - childProps.className, - 'rcx-card-group__item', - ), - }), - )} - + {Children.map(children, (child) => { + if (!isValidElement(child)) { + return child; + } + return {child}; + })} + ); export default CardGroup; From 647d74726ff67d08521949446a8ca9d55034b679 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 11:52:29 -0300 Subject: [PATCH 27/84] refactor(fuselage): Migrate Avatar component from SCSS to Tamagui v3 AvatarContainer (RcxView) with size variants x16-x332. Avatar img with computed borderRadius. AvatarStack with row-reverse. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/Avatar/Avatar.styles.scss | 76 ------------------- .../fuselage/src/components/Avatar/Avatar.tsx | 49 +++++++++--- .../src/components/Avatar/AvatarContainer.tsx | 42 ++++++++-- .../src/components/Avatar/AvatarStack.tsx | 31 +++++++- 4 files changed, 101 insertions(+), 97 deletions(-) delete mode 100644 packages/fuselage/src/components/Avatar/Avatar.styles.scss diff --git a/packages/fuselage/src/components/Avatar/Avatar.styles.scss b/packages/fuselage/src/components/Avatar/Avatar.styles.scss deleted file mode 100644 index a96a8feb31..0000000000 --- a/packages/fuselage/src/components/Avatar/Avatar.styles.scss +++ /dev/null @@ -1,76 +0,0 @@ -@use '../../styles/colors.scss'; -@use '../../styles/lengths.scss'; -@use '../../styles/functions.scss'; - -$avatar-stack-background-color: theme( - 'avatar-background-color', - colors.surface(light) -); - -$sizes: 16, 18, 20, 24, 28, 32, 36, 40, 48, 124, 200, 332; - -.rcx-avatar { - display: inline-flex; - - vertical-align: middle; - - @each $size in $sizes { - &--x#{$size} { - @include square(functions.to-rem($size)); - } - } - - &__element { - position: relative; - - width: 100%; - height: 100%; - @each $size in $sizes { - &--x#{$size} { - @if $size <= 18 { - border-radius: theme( - 'avatar-border-radius-#{$size}', - lengths.border-radius(small) - ); - } @else if $size == 332 { - border-radius: theme( - 'avatar-border-radius-#{$size}', - lengths.border-radius(large) - ); - } @else { - border-radius: theme( - 'avatar-border-radius-#{$size}', - lengths.border-radius(medium) - ); - } - } - } - - &--object-fit { - object-fit: contain; - } - - &--rounded { - border-radius: theme( - 'avatar-border-radius-rounded', - lengths.border-radius(full) - ); - } - } - - &-stack { - display: flex; - flex-direction: row-reverse; - justify-content: center; - - background-color: #{$avatar-stack-background-color}; - - & > .rcx-avatar { - margin: auto lengths.margin(-2); - - & > .rcx-avatar__element { - border: lengths.border-width(default) solid transparent; - } - } - } -} diff --git a/packages/fuselage/src/components/Avatar/Avatar.tsx b/packages/fuselage/src/components/Avatar/Avatar.tsx index 82187abc02..6a4f77b47a 100644 --- a/packages/fuselage/src/components/Avatar/Avatar.tsx +++ b/packages/fuselage/src/components/Avatar/Avatar.tsx @@ -1,4 +1,5 @@ -import type { AllHTMLAttributes } from 'react'; +import type { AllHTMLAttributes, CSSProperties } from 'react'; +import { useMemo } from 'react'; import AvatarContainer, { type AvatarContainerProps } from './AvatarContainer'; @@ -8,6 +9,21 @@ export type AvatarProps = AvatarContainerProps & { url: string; } & Omit, 'size'>; +const borderRadiusMap: Record = { + x16: '2px', // small + x18: '2px', // small + x20: '4px', // medium + x24: '4px', + x28: '4px', + x32: '4px', + x36: '4px', + x40: '4px', + x48: '4px', + x124: '4px', + x200: '4px', + x332: '8px', // large +}; + const Avatar = ({ size = 'x36', rounded = false, @@ -15,20 +31,33 @@ const Avatar = ({ url, className, alt, + style: styleProp, ...props }: AvatarProps) => { - const innerClass = [ - 'rcx-avatar__element', - objectFit && 'rcx-avatar__element--object-fit', - size && `rcx-avatar__element--${size}`, - rounded && 'rcx-avatar__element--rounded', - ] - .filter(Boolean) - .join(' '); + const imgStyle = useMemo(() => { + const base: CSSProperties = { + position: 'relative', + width: '100%', + height: '100%', + borderRadius: rounded ? '9999px' : borderRadiusMap[size] || '4px', + }; + + if (objectFit) { + base.objectFit = 'contain'; + } + + return { ...base, ...styleProp }; + }, [size, rounded, objectFit, styleProp]); return ( - {alt + {alt ); }; diff --git a/packages/fuselage/src/components/Avatar/AvatarContainer.tsx b/packages/fuselage/src/components/Avatar/AvatarContainer.tsx index 76db544940..730ecfc2ef 100644 --- a/packages/fuselage/src/components/Avatar/AvatarContainer.tsx +++ b/packages/fuselage/src/components/Avatar/AvatarContainer.tsx @@ -1,6 +1,35 @@ import type { HTMLAttributes } from 'react'; +import { styled } from 'tamagui'; -import { prependClassName } from '../../helpers/prependClassName'; +import { RcxView } from '../../primitives'; + +const StyledAvatarContainer = styled(RcxView, { + name: 'AvatarContainer', + + display: 'inline-flex', + verticalAlign: 'middle', + + variants: { + size: { + x16: { width: 16, height: 16 }, + x18: { width: 18, height: 18 }, + x20: { width: 20, height: 20 }, + x24: { width: 24, height: 24 }, + x28: { width: 28, height: 28 }, + x32: { width: 32, height: 32 }, + x36: { width: 36, height: 36 }, + x40: { width: 40, height: 40 }, + x48: { width: 48, height: 48 }, + x124: { width: 124, height: 124 }, + x200: { width: 200, height: 200 }, + x332: { width: 332, height: 332 }, + }, + } as const, + + defaultVariants: { + size: 'x36', + }, +}); export type AvatarContainerProps = { size?: @@ -23,14 +52,11 @@ const AvatarContainer = ({ children, ...props }: AvatarContainerProps) => { - props.className = prependClassName( - props.className, - ['rcx-box rcx-box--full rcx-avatar', size && `rcx-avatar--${size}`] - .filter(Boolean) - .join(' '), + return ( + + {children} + ); - - return
    {children}
    ; }; export default AvatarContainer; diff --git a/packages/fuselage/src/components/Avatar/AvatarStack.tsx b/packages/fuselage/src/components/Avatar/AvatarStack.tsx index 4d3fa8d0f4..024de2c239 100644 --- a/packages/fuselage/src/components/Avatar/AvatarStack.tsx +++ b/packages/fuselage/src/components/Avatar/AvatarStack.tsx @@ -1,7 +1,25 @@ import type { DetailedHTMLProps, HTMLAttributes } from 'react'; import flattenChildren from 'react-keyed-flatten-children'; +import { styled } from 'tamagui'; -import { prependClassName } from '../../helpers/prependClassName'; +import { RcxView } from '../../primitives'; + +const StyledAvatarStack = styled(RcxView, { + name: 'AvatarStack', + + display: 'flex', + flexDirection: 'row-reverse', + justifyContent: 'center', + + backgroundColor: '$surfaceLight', +}); + +const AvatarStackItem = styled(RcxView, { + name: 'AvatarStackItem', + + marginBlock: 'auto', + marginInline: -2, +}); export type AvatarStackProps = DetailedHTMLProps< HTMLAttributes, @@ -9,8 +27,15 @@ export type AvatarStackProps = DetailedHTMLProps< >; const AvatarStack = ({ children, ...props }: AvatarStackProps) => { - props.className = prependClassName(props.className, 'rcx-avatar-stack'); - return
    {flattenChildren(children).reverse()}
    ; + const reversed = flattenChildren(children).reverse(); + + return ( + + {reversed.map((child, index) => ( + {child} + ))} + + ); }; export default AvatarStack; From 11b256f58de798cadfdbf7a0db2947c3ffbd8b06 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 11:52:30 -0300 Subject: [PATCH 28/84] refactor(fuselage): Migrate Icon component from SCSS to Tamagui v3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StyledIcon (RcxText) with RocketChat font, inline-block, size→fontSize. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/Icon/Icon.styles.scss | 18 ---------- .../fuselage/src/components/Icon/Icon.tsx | 36 ++++++++++++++----- 2 files changed, 27 insertions(+), 27 deletions(-) delete mode 100644 packages/fuselage/src/components/Icon/Icon.styles.scss diff --git a/packages/fuselage/src/components/Icon/Icon.styles.scss b/packages/fuselage/src/components/Icon/Icon.styles.scss deleted file mode 100644 index 1c0738508d..0000000000 --- a/packages/fuselage/src/components/Icon/Icon.styles.scss +++ /dev/null @@ -1,18 +0,0 @@ -.rcx-icon { - display: inline-block; - - user-select: none; - - vertical-align: text-bottom; - - letter-spacing: 0; - - font-family: 'RocketChat'; - font-size: inherit; - font-weight: 400; - - font-style: normal; - font-variant: normal; - line-height: 1; - text-rendering: auto; -} diff --git a/packages/fuselage/src/components/Icon/Icon.tsx b/packages/fuselage/src/components/Icon/Icon.tsx index 4c02c9e2f2..5cfac5a33e 100644 --- a/packages/fuselage/src/components/Icon/Icon.tsx +++ b/packages/fuselage/src/components/Icon/Icon.tsx @@ -1,8 +1,28 @@ import type { Keys as IconName } from '@rocket.chat/icons'; import nameToCharacterMapping from '@rocket.chat/icons'; import { forwardRef } from 'react'; +import { styled } from 'tamagui'; -import { Box, type BoxProps } from '../Box'; +import type { BoxProps } from '../Box'; +import { RcxText } from '../../primitives'; + +const StyledIcon = styled(RcxText, { + name: 'Icon', + + display: 'inline-block', + userSelect: 'none', + verticalAlign: 'text-bottom', + color: 'inherit', + + letterSpacing: 0, + fontFamily: 'RocketChat', + fontWeight: '400', + fontStyle: 'normal', + // fontVariant: 'normal' — not supported as Tamagui prop, handled by rcx-box reset + lineHeight: 1, + textRendering: 'auto', + overflowWrap: 'normal', +}); export type IconProps = Omit & { name: IconName; @@ -14,16 +34,14 @@ const Icon = forwardRef(function Icon( ref, ) { return ( -
    + ); export default OptionAvatar; diff --git a/packages/fuselage/src/components/Option/OptionColumn.tsx b/packages/fuselage/src/components/Option/OptionColumn.tsx index 77df1f7753..f31bcb469f 100644 --- a/packages/fuselage/src/components/Option/OptionColumn.tsx +++ b/packages/fuselage/src/components/Option/OptionColumn.tsx @@ -1,11 +1,31 @@ import type { ReactNode } from 'react'; +import { styled } from 'tamagui'; + +import { RcxView } from '../../primitives'; + +// .rcx-option__column — extends %column + flex display +const StyledOptionColumn = styled(RcxView, { + name: 'OptionColumn', + + display: 'flex', + flexGrow: 0, + flexShrink: 0, + flexBasis: 'auto', + + justifyContent: 'center', + alignItems: 'center', + + minWidth: '$x20', + minHeight: '$x20', + marginInline: '$x4', +}); export type OptionColumnProps = { children?: ReactNode; }; const OptionColumn = (props: OptionColumnProps) => ( -
    + ); export default OptionColumn; diff --git a/packages/fuselage/src/components/Option/OptionContent.tsx b/packages/fuselage/src/components/Option/OptionContent.tsx index 5d1023a311..fb1aacf11c 100644 --- a/packages/fuselage/src/components/Option/OptionContent.tsx +++ b/packages/fuselage/src/components/Option/OptionContent.tsx @@ -1,9 +1,38 @@ import type { HTMLAttributes } from 'react'; +import { styled } from 'tamagui'; + +import { RcxText } from '../../primitives'; + +// .rcx-option__content — extends %column + text-ellipsis +const StyledOptionContent = styled(RcxText, { + name: 'OptionContent', + + display: 'block', + + flexGrow: 1, + flexShrink: 1, + flexBasis: '100%', + + marginInline: '$x4', + + textAlign: 'start', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + + color: 'inherit', + + fontFamily: '$body', + fontSize: '$p2', + fontWeight: '$p2', + lineHeight: '$p2', + letterSpacing: '$p2', +}); export type OptionContentProps = HTMLAttributes; const OptionContent = (props: OptionContentProps) => ( -
    + ); export default OptionContent; diff --git a/packages/fuselage/src/components/Option/OptionDescription.tsx b/packages/fuselage/src/components/Option/OptionDescription.tsx index 1b83348a4a..1f0f6191df 100644 --- a/packages/fuselage/src/components/Option/OptionDescription.tsx +++ b/packages/fuselage/src/components/Option/OptionDescription.tsx @@ -1,11 +1,35 @@ import type { ReactNode } from 'react'; +import { styled } from 'tamagui'; + +import { RcxText } from '../../primitives'; + +// .rcx-option__description — extends %column + inline display +const StyledOptionDescription = styled(RcxText, { + name: 'OptionDescription', + + display: 'inline', + + flexGrow: 0, + flexShrink: 0, + flexBasis: 'auto', + + marginInline: '$x4', + + color: '$fontSecondaryInfo', + + fontFamily: '$body', + fontSize: '$p2', + fontWeight: '$p2', + lineHeight: '$p2', + letterSpacing: '$p2', +}); export type OptionDescriptionProps = { children?: ReactNode; }; const OptionDescription = (props: OptionDescriptionProps) => ( -
    + ); export default OptionDescription; diff --git a/packages/fuselage/src/components/Option/OptionDescriptionBlock.tsx b/packages/fuselage/src/components/Option/OptionDescriptionBlock.tsx index 16b69d0594..0f5f9b9f27 100644 --- a/packages/fuselage/src/components/Option/OptionDescriptionBlock.tsx +++ b/packages/fuselage/src/components/Option/OptionDescriptionBlock.tsx @@ -1,11 +1,33 @@ import type { ReactNode } from 'react'; +import { styled } from 'tamagui'; + +import { RcxText } from '../../primitives'; + +// .rcx-option__description-block +const StyledOptionDescriptionBlock = styled(RcxText, { + name: 'OptionDescriptionBlock', + + display: 'block', + + padding: '$x4', + + whiteSpace: 'normal', + + color: '$fontSecondaryInfo', + + fontFamily: '$body', + fontSize: '$p2', + fontWeight: '$p2', + lineHeight: '$p2', + letterSpacing: '$p2', +}); export type OptionDescriptionBlockProps = { children?: ReactNode; }; const OptionDescriptionBlock = (props: OptionDescriptionBlockProps) => ( -
    + ); export default OptionDescriptionBlock; diff --git a/packages/fuselage/src/components/Option/OptionHeader.tsx b/packages/fuselage/src/components/Option/OptionHeader.tsx index 93b2809987..db13f4aaf5 100644 --- a/packages/fuselage/src/components/Option/OptionHeader.tsx +++ b/packages/fuselage/src/components/Option/OptionHeader.tsx @@ -1,11 +1,35 @@ import type { ReactNode } from 'react'; +import { styled } from 'tamagui'; + +import { RcxText } from '../../primitives'; + +// .rcx-option__header +const StyledOptionHeader = styled(RcxText, { + name: 'OptionHeader', + + display: 'block', + + paddingBlock: '$x8', + paddingInline: '$x16', + + textTransform: 'uppercase', + + color: '$fontDefault', + + fontFamily: '$body', + fontSize: '$micro', + lineHeight: '$micro', + letterSpacing: '$micro', + // SCSS: font-weight: 400 (overrides micro's 700) + fontWeight: '400', +}); export type OptionHeaderProps = { children: ReactNode; }; const OptionHeader = ({ children }: OptionHeaderProps) => ( -
    {children}
    + {children} ); export default OptionHeader; diff --git a/packages/fuselage/src/components/Option/OptionIcon.tsx b/packages/fuselage/src/components/Option/OptionIcon.tsx index 2540f5312a..64b40d7d37 100644 --- a/packages/fuselage/src/components/Option/OptionIcon.tsx +++ b/packages/fuselage/src/components/Option/OptionIcon.tsx @@ -4,9 +4,10 @@ import OptionColumn from './OptionColumn'; export type OptionIconProps = IconProps; +// .rcx-option__icon — color: inherit const OptionIcon = (props: OptionIconProps) => ( - + ); diff --git a/packages/fuselage/src/components/Option/OptionInput.tsx b/packages/fuselage/src/components/Option/OptionInput.tsx index 694bc5ca60..6c6be5db94 100644 --- a/packages/fuselage/src/components/Option/OptionInput.tsx +++ b/packages/fuselage/src/components/Option/OptionInput.tsx @@ -1,11 +1,29 @@ import type { ReactNode } from 'react'; +import { styled } from 'tamagui'; + +import { RcxView } from '../../primitives'; + +// .rcx-option__input +const StyledOptionInput = styled(RcxView, { + name: 'OptionInput', + + display: 'flex', + + justifyContent: 'flex-end', + alignItems: 'center', + + minWidth: '$x20', + minHeight: '$x20', + marginInlineStart: '$x16', + marginInlineEnd: -12, +}); export type OptionInputProps = { children?: ReactNode; }; const OptionInput = (props: OptionInputProps) => ( -
    + ); export default OptionInput; diff --git a/packages/fuselage/src/components/Option/OptionMenu.tsx b/packages/fuselage/src/components/Option/OptionMenu.tsx index 4d31308c73..e8f70d0e82 100644 --- a/packages/fuselage/src/components/Option/OptionMenu.tsx +++ b/packages/fuselage/src/components/Option/OptionMenu.tsx @@ -1,9 +1,26 @@ import type { HTMLAttributes } from 'react'; +import { styled } from 'tamagui'; + +import { RcxView } from '../../primitives'; + +// .rcx-option__menu-wrapper +const StyledOptionMenu = styled(RcxView, { + name: 'OptionMenu', + + className: 'rcx-box rcx-box--animated', + + flexShrink: 0, + + width: 0, + height: '100%', + + opacity: 0, +}); export type OptionMenuProps = HTMLAttributes; const OptionMenu = (props: OptionMenuProps) => ( -
    + ); export default OptionMenu; diff --git a/packages/fuselage/src/components/Option/OptionTitle.tsx b/packages/fuselage/src/components/Option/OptionTitle.tsx index 8cd74b0ae5..1f2cbb49db 100644 --- a/packages/fuselage/src/components/Option/OptionTitle.tsx +++ b/packages/fuselage/src/components/Option/OptionTitle.tsx @@ -1,11 +1,34 @@ import type { ReactNode } from 'react'; +import { styled } from 'tamagui'; + +import { RcxText } from '../../primitives'; + +// .rcx-option__title +const StyledOptionTitle = styled(RcxText, { + name: 'OptionTitle', + + display: 'block', + + paddingBlockStart: '$x8', + paddingBlockEnd: '$x4', + paddingInlineStart: '$x12', + paddingInlineEnd: '$x24', + + color: '$fontDefault', + + fontFamily: '$body', + fontSize: '$c2', + fontWeight: '$c2', + lineHeight: '$c2', + letterSpacing: '$c2', +}); export type OptionTitleProps = { children?: ReactNode; }; const OptionTitle = (props: OptionTitleProps) => ( -
    + ); export default OptionTitle; From 0b029e6db71bb7ab38ba3e5c9d9036f30c144220 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 11:53:51 -0300 Subject: [PATCH 30/84] refactor(fuselage): Migrate Options component from SCSS to Tamagui v3 OptionsFrame (RcxView) replacing Box. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/Options/Options.styles.scss | 7 ------- .../fuselage/src/components/Options/Options.tsx | 14 +++++++++++--- 2 files changed, 11 insertions(+), 10 deletions(-) delete mode 100644 packages/fuselage/src/components/Options/Options.styles.scss diff --git a/packages/fuselage/src/components/Options/Options.styles.scss b/packages/fuselage/src/components/Options/Options.styles.scss deleted file mode 100644 index 7b3568c872..0000000000 --- a/packages/fuselage/src/components/Options/Options.styles.scss +++ /dev/null @@ -1,7 +0,0 @@ -.rcx-options { - &:hover { - .rcx-option--focus:not(.rcx-option--selected):not(:hover) { - background: initial; - } - } -} diff --git a/packages/fuselage/src/components/Options/Options.tsx b/packages/fuselage/src/components/Options/Options.tsx index f912f9f4ef..09b5b9a3a7 100644 --- a/packages/fuselage/src/components/Options/Options.tsx +++ b/packages/fuselage/src/components/Options/Options.tsx @@ -7,9 +7,11 @@ import type { SyntheticEvent, } from 'react'; import { forwardRef, useLayoutEffect, useMemo, useRef } from 'react'; +import { styled } from 'tamagui'; import { prevent } from '../../helpers/prevent'; -import { Box, type BoxProps } from '../Box'; +import { RcxView } from '../../primitives'; +import { type BoxProps } from '../Box'; import { Option, OptionHeader, OptionDivider } from '../Option'; import { Scrollable } from '../Scrollable'; import { Tile } from '../Tile'; @@ -17,6 +19,12 @@ import { Tile } from '../Tile'; import type { OptionType } from './OptionType'; import OptionsEmpty from './OptionsEmpty'; +// .rcx-options — on hover, clears focus bg from non-hovered non-selected items +// This behavior is handled via CSS class still applied to children. +const OptionsFrame = styled(RcxView, { + name: 'OptionsFrame', +}); + export type OptionsProps = Omit< BoxProps, 'onSelect' @@ -109,7 +117,7 @@ const Options = forwardRef(function Options( ); return ( - + (function Options( - + ); }) as ForwardRefExoticComponent< PropsWithoutRef & RefAttributes From 22c50a6062b867165596431253a2522c43bbde1c Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 11:53:52 -0300 Subject: [PATCH 31/84] refactor(fuselage): Migrate AutoComplete component from SCSS to Tamagui v3 AutoCompleteFrame (RcxView) with input-like styling: border, focus ring, invalid/disabled states. AutoCompleteAddon for icon section. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../AutoComplete/AutoComplete.styles.scss | 3 - .../components/AutoComplete/AutoComplete.tsx | 108 ++++++++++++++++-- packages/fuselage/src/components/index.scss | 10 -- 3 files changed, 97 insertions(+), 24 deletions(-) delete mode 100644 packages/fuselage/src/components/AutoComplete/AutoComplete.styles.scss diff --git a/packages/fuselage/src/components/AutoComplete/AutoComplete.styles.scss b/packages/fuselage/src/components/AutoComplete/AutoComplete.styles.scss deleted file mode 100644 index 5629b50719..0000000000 --- a/packages/fuselage/src/components/AutoComplete/AutoComplete.styles.scss +++ /dev/null @@ -1,3 +0,0 @@ -.rcx-autocomplete { - @extend .rcx-select; -} diff --git a/packages/fuselage/src/components/AutoComplete/AutoComplete.tsx b/packages/fuselage/src/components/AutoComplete/AutoComplete.tsx index 324e33a7a6..da821a69fc 100644 --- a/packages/fuselage/src/components/AutoComplete/AutoComplete.tsx +++ b/packages/fuselage/src/components/AutoComplete/AutoComplete.tsx @@ -11,7 +11,9 @@ import type { ReactNode, } from 'react'; import { useEffect, useRef, useMemo, useState } from 'react'; +import { styled } from 'tamagui'; +import { RcxView } from '../../primitives'; import { AnimatedVisibility } from '../AnimatedVisibility'; import { Box } from '../Box'; import { Chip } from '../Chip'; @@ -55,6 +57,94 @@ export type AutoCompleteProps = Omit< value?: string | string[]; }; +// .rcx-autocomplete — extends .rcx-select which extends %rcx-input-box / %input +const AutoCompleteFrame = styled(RcxView, { + name: 'AutoCompleteFrame', + + position: 'relative', + + display: 'inline-flex', + flexDirection: 'row', + flexWrap: 'nowrap', + flexGrow: 1, + + alignItems: 'center', + + minWidth: 144, + minHeight: '$x40', + + paddingBlock: '$x8', + paddingInline: 15, // 16px - 1px border + + borderWidth: 1, + borderStyle: 'solid', + borderColor: '$strokeLight', + borderRadius: '$x4', + backgroundColor: '$surfaceLight', + boxShadow: 'none', + + hoverStyle: { + borderColor: '$strokeLight', + }, + + focusVisibleStyle: { + borderColor: '$strokeHighlight', + boxShadow: '0 0 0 2px var(--shadowHighlight)', + }, + + pressStyle: { + borderColor: '$strokeMedium', + boxShadow: 'none', + }, + + variants: { + isInvalid: { + true: { + borderColor: '$strokeError', + + hoverStyle: { + borderColor: '$strokeError', + }, + + focusVisibleStyle: { + borderColor: '$strokeError', + boxShadow: '0 0 0 2px var(--shadowDanger)', + }, + + pressStyle: { + borderColor: '$strokeMedium', + boxShadow: 'none', + }, + }, + }, + + isDisabled: { + true: { + cursor: 'not-allowed', + pointerEvents: 'none', + borderColor: '$strokeLight', + backgroundColor: '$surfaceDisabled', + }, + }, + } as const, +}); + +// .rcx-autocomplete__addon — extends .rcx-input-box__addon +const AutoCompleteAddon = styled(RcxView, { + name: 'AutoCompleteAddon', + + cursor: 'pointer', + + display: 'flex', + flexDirection: 'row', + flexWrap: 'nowrap', + flexGrow: 0, + flexShrink: 0, + flexBasis: 'auto', + + alignItems: 'flex-start', +}); + const getSelected = ( value: string | string[] | undefined, options: AutoCompleteOption[], @@ -180,15 +270,11 @@ function AutoComplete({ useEffect(reset, [filter, reset]); return ( - ref.current?.focus())} - flexGrow={1} - className={useMemo( - () => [error && 'invalid', disabled && 'disabled'], - [error, disabled], - )} + isInvalid={error || undefined} + isDisabled={disabled || undefined} > ({ )} - + ({ size='x20' color='default' /> - + ({ options={memoizedOptions} /> - + ); } diff --git a/packages/fuselage/src/components/index.scss b/packages/fuselage/src/components/index.scss index 7d4d68f800..4090cdf77a 100644 --- a/packages/fuselage/src/components/index.scss +++ b/packages/fuselage/src/components/index.scss @@ -1,25 +1,15 @@ -@import './Accordion/Accordion.styles.scss'; -@import './AutoComplete/AutoComplete.styles.scss'; -@import './Avatar/Avatar.styles.scss'; @import './Box/Box.styles.scss'; @import './Button/Button.styles.scss'; @import './ButtonGroup/ButtonGroup.styles.scss'; -@import './Card/Card.styles.scss'; -@import './CardGroup/CardGroup.styles.scss'; @import './CheckBox/CheckBox.styles.scss'; -@import './Icon/Icon.styles.scss'; @import './InputBox/InputBox.styles.scss'; @import './Message/Messages.styles.scss'; @import './Modal/Modal.styles.scss'; @import './NavBar/NavBar.styles.scss'; -@import './Option/Option.styles.scss'; -@import './Options/Options.styles.scss'; @import './RadioButton/RadioButton.styles.scss'; @import './Select/Select.styles.scss'; @import './Sidebar/Sidebar.styles.scss'; @import './Sidepanel/Sidepanel.styles.scss'; @import './SidebarV2/Sidebar.styles.scss'; -@import './Table/Table.styles.scss'; -@import './Tabs/Tabs.styles.scss'; @import './ToastBar/ToastBar.styles.scss'; @import './ToggleSwitch/ToggleSwitch.styles.scss'; From 90075097ec9e2570e5e60b47472447c16e2dda43 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 11:55:18 -0300 Subject: [PATCH 32/84] refactor(fuselage): Migrate Accordion component from SCSS to Tamagui v3 AccordionFrame (RcxView), AccordionItemBar (RcxInteractive) with hover/focus/disabled states, AccordionItemTitle (), AccordionItemPanel with expanded/collapsed variants. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Accordion/Accordion.styles.scss | 80 ---------- .../src/components/Accordion/Accordion.tsx | 22 +-- .../components/Accordion/AccordionItem.tsx | 146 +++++++++++++----- 3 files changed, 124 insertions(+), 124 deletions(-) delete mode 100644 packages/fuselage/src/components/Accordion/Accordion.styles.scss diff --git a/packages/fuselage/src/components/Accordion/Accordion.styles.scss b/packages/fuselage/src/components/Accordion/Accordion.styles.scss deleted file mode 100644 index 962eedaeff..0000000000 --- a/packages/fuselage/src/components/Accordion/Accordion.styles.scss +++ /dev/null @@ -1,80 +0,0 @@ -@use '../../styles/colors.scss'; -@use '../../styles/lengths.scss'; -@use '../../styles/typography.scss'; - -.rcx-accordion { - display: flex; - flex-flow: column nowrap; - border-block-end-color: colors.stroke(extra-light); - border-block-end-width: lengths.border-width(default); -} - -.rcx-accordion-item { - display: flex; - flex-flow: column nowrap; -} - -.rcx-accordion-item__bar { - display: flex; - flex-flow: row nowrap; - - min-height: lengths.size(2 * 32 + 24); - padding: (lengths.padding(32) - lengths.border-width(default, rem)) - (lengths.padding(8) - lengths.border-width(default, rem)); - - text-align: start; - - color: colors.font(titles-labels); - - border-width: lengths.border-width(default); - border-color: colors.stroke(extra-light) transparent transparent; - - &[tabindex] { - @include clickable; - - &.hover, - &:hover { - background-color: colors.surface(tint); - } - - &.focus, - &:focus { - border-color: colors.stroke(highlight); - @include use-focus-shadow( - $outer-color: colors.stroke(extra-light-highlight) - ); - } - } - - &--disabled { - cursor: not-allowed; - - color: colors.font(disabled); - background-color: colors.surface(disabled); - } -} - -.rcx-accordion-item__title { - flex: 1 1 lengths.size(none); - - @include typography.use-text-ellipsis; - white-space: nowrap; - - @include typography.use-font-scale(h4); -} - -.rcx-accordion-item__panel { - visibility: hidden; - - overflow: hidden; - - height: lengths.size(none); - padding: lengths.padding(none) lengths.padding(8); - - &--expanded { - visibility: visible; - - height: auto; - padding: lengths.padding(32) lengths.padding(8); - } -} diff --git a/packages/fuselage/src/components/Accordion/Accordion.tsx b/packages/fuselage/src/components/Accordion/Accordion.tsx index cd0402279a..d7686423a1 100644 --- a/packages/fuselage/src/components/Accordion/Accordion.tsx +++ b/packages/fuselage/src/components/Accordion/Accordion.tsx @@ -1,22 +1,26 @@ import type { ReactNode } from 'react'; +import { styled } from 'tamagui'; -import { cx, cxx } from '../../helpers/composeClassNames'; -import { StylingBox } from '../Box'; -import type { StylingProps } from '../Box/stylingProps'; +import { RcxView } from '../../primitives'; + +const AccordionFrame = styled(RcxView, { + name: 'Accordion', + display: 'flex', + flexDirection: 'column', + flexWrap: 'nowrap', + borderBlockEndColor: '$strokeExtraLight', + borderBlockEndWidth: 1, +}); export type AccordionProps = { children: ReactNode; -} & Partial; +}; /** * An `Accordion` allows users to toggle the display of sections of content. */ const Accordion = ({ children, ...props }: AccordionProps) => ( - -
    - {children} -
    -
    + {children} ); export default Accordion; diff --git a/packages/fuselage/src/components/Accordion/AccordionItem.tsx b/packages/fuselage/src/components/Accordion/AccordionItem.tsx index 0723df1a79..61cda9f4f2 100644 --- a/packages/fuselage/src/components/Accordion/AccordionItem.tsx +++ b/packages/fuselage/src/components/Accordion/AccordionItem.tsx @@ -5,11 +5,102 @@ import { type MouseEvent, type ReactNode, } from 'react'; +import { styled } from 'tamagui'; -import { cx, cxx } from '../../helpers/composeClassNames'; -import { StylingBox } from '../Box'; +import { RcxInteractive, RcxText, RcxView } from '../../primitives'; import { Chevron } from '../Chevron'; +const AccordionItemFrame = styled(RcxView, { + name: 'AccordionItem', + display: 'flex', + flexDirection: 'column', + flexWrap: 'nowrap', +}); + +const AccordionItemBar = styled(RcxInteractive, { + name: 'AccordionItemBar', + display: 'flex', + flexDirection: 'row', + flexWrap: 'nowrap', + role: 'button', + + // min-height: 2 * 32 + 24 = 88px + minHeight: 88, + // padding: (32 - 1px border) (8 - 1px border) => 31px 7px + paddingBlock: 31, + paddingInline: 7, + + textAlign: 'left', + + color: '$fontTitlesLabels', + + borderWidth: 1, + borderStyle: 'solid', + borderBlockStartColor: '$strokeExtraLight', + borderBlockEndColor: 'transparent', + borderInlineColor: 'transparent', + + hoverStyle: { + backgroundColor: '$surfaceTint', + }, + + focusVisibleStyle: { + borderColor: '$strokeHighlight', + boxShadow: '0 0 0 2px var(--strokeExtraLightHighlight)', + }, + + variants: { + isDisabled: { + true: { + cursor: 'not-allowed', + color: '$fontDisabled', + backgroundColor: '$surfaceDisabled', + hoverStyle: { + backgroundColor: '$surfaceDisabled', + }, + }, + }, + } as const, +}); + +const AccordionItemTitle = styled(RcxText, { + name: 'AccordionItemTitle', + display: 'block', + flexGrow: 1, + + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + + fontFamily: '$body', + fontSize: '$h4', + fontWeight: '$h4', + lineHeight: '$h4', + letterSpacing: '$h4', + + color: 'inherit', +}); + +const AccordionItemPanel = styled(RcxView, { + name: 'AccordionItemPanel', + visibility: 'hidden', + overflow: 'hidden', + height: 0, + paddingBlock: 0, + paddingInline: '$x8', + + variants: { + expanded: { + true: { + visibility: 'visible', + height: 'auto', + paddingBlock: '$x32', + paddingInline: '$x8', + }, + }, + } as const, +}); + export type AccordionItemProps = { children?: ReactNode; className?: string; @@ -82,40 +173,25 @@ const AccordionItem = ({ const barProps = noncollapsible ? nonCollapsibleProps : collapsibleProps; return ( - -
    - {title && ( -
    -

    - {title} -

    - {!noncollapsible && } -
    - )} -
    + {title && ( + - {children} -
    -
    -
    + + {title} + + {!noncollapsible && } + + )} + + {children} + + ); }; From c865e6ecd1deb83595ad979ed591aa727416ef8d Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 11:55:19 -0300 Subject: [PATCH 33/84] refactor(fuselage): Migrate Tabs component from SCSS to Tamagui v3 TabsFrame with divider variant, TabsScrollBox, TabsWrapper. TabsItem (RcxInteractiveText ) with selected/disabled/hover/press states and 4px bottom border indicator. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/Tabs/Tabs.styles.scss | 177 ------------------ .../fuselage/src/components/Tabs/Tabs.tsx | 53 +++++- .../fuselage/src/components/Tabs/TabsItem.tsx | 136 +++++++++++--- 3 files changed, 160 insertions(+), 206 deletions(-) delete mode 100644 packages/fuselage/src/components/Tabs/Tabs.styles.scss diff --git a/packages/fuselage/src/components/Tabs/Tabs.styles.scss b/packages/fuselage/src/components/Tabs/Tabs.styles.scss deleted file mode 100644 index 0f1816ea83..0000000000 --- a/packages/fuselage/src/components/Tabs/Tabs.styles.scss +++ /dev/null @@ -1,177 +0,0 @@ -@use '../../styles/lengths.scss'; -@use '../../styles/colors.scss'; -@use '../../styles/typography.scss'; - -$tabs-background-color: theme('tabs-background-color', transparent); -$tabs-border-color: theme('tabs-border-color', transparent); -$tabs-color: theme('tabs-color', colors.font(hint)); - -$tabs-hover-color: theme('tabs-hover-border-color', colors.font(default)); - -$tabs-selected-color: theme('tabs-selected-color', colors.font(info)); -$tabs-selected-border-color: theme( - 'tabs-selected-border-color', - colors.font(info) -); -$tabs-hover-selected-color: theme( - 'tabs-hover-selected-color', - colors.button(primary-hover) -); -$tabs-hover-selected-border-color: theme( - 'tabs-hover-selected-border-color', - colors.button(primary-hover) -); -$tabs-active-color: theme('tabs-active-color', colors.font(titles-labels)); - -$tabs-active-selected-color: theme( - 'tabs-active-selected-color', - colors.button(primary-press) -); -$tabs-active-selected-border-color: theme( - 'tabs-active-selected-border-color', - colors.button(primary-press) -); -$tabs-focus-border-color: theme( - 'tabs-focus-border-color', - colors.button(primary-default) -); -$tabs-focus-shadow-color: theme( - 'tabs-focus-shadow-color', - colors.stroke(extra-light-highlight) -); -$tabs-disabled-color: theme('tabs-disabled-color', colors.font(disabled)); -$tabs-disabled-selected-color: theme( - 'tabs-disabled-selected-color', - colors.status-background(info) -); -$tabs-disabled-selected-border-color: theme( - 'tabs-disabled-selected-border-color', - colors.status-background(info) -); - -.rcx-tabs__scroll-box { - position: relative; - - overflow: auto; - - margin-block: lengths.margin(-4); - -ms-overflow-style: none; - - &::-webkit-scrollbar { - display: none; - } -} - -.rcx-tabs__wrapper { - display: flex; - - flex-wrap: nowrap; - - margin: lengths.margin(none) lengths.margin(12); - - padding: lengths.padding(4) lengths.padding(none); -} - -.rcx-tabs__item { - position: relative; - - align-items: center; - - flex: 0 0 auto; - - min-height: lengths.size(40); - - margin: lengths.margin(none) lengths.margin(12); - padding: (lengths.padding(4) + lengths.border-width(default, rem)) - lengths.padding(none); - - color: $tabs-color; - border-width: lengths.border-width(default); - - border-style: solid; - border-color: transparent; - background-color: transparent; - - @include clickable; - @include typography.use-font-scale(h4); - - &.hover, - &:hover { - color: $tabs-hover-color; - border-block-end-color: $tabs-hover-color; - border-block-end-width: lengths.border-width(4); - } - - &.active, - &:active { - color: $tabs-active-color; - border-block-end-color: $tabs-active-color; - border-block-end-width: lengths.border-width(4); - } - - @include on-focus-visible { - border-color: $tabs-focus-border-color; - border-radius: theme('tabs-border-radius', lengths.border-radius(medium)); - - @include use-button-focus-shadow($tabs-focus-shadow-color); - } - - &--disabled { - cursor: not-allowed; - - color: $tabs-disabled-color; - - &:hover { - color: $tabs-disabled-color; - } - } - - &--selected.rcx-tabs__item { - padding: lengths.padding(4) lengths.padding(none); - - color: $tabs-selected-color; - border-block-end-color: $tabs-selected-border-color; - border-block-end-width: lengths.border-width(default); - border-inline-width: lengths.border-width(default); - - &:hover:not(.rcx-tabs__item--selected--disabled), - &.hover:not(.rcx-tabs__item--selected--disabled) { - color: $tabs-hover-selected-color; - border-block-end-color: $tabs-hover-selected-border-color; - } - - &.active:not(.rcx-tabs__item--selected--disabled), - &:active:not(.rcx-tabs__item--selected--disabled) { - color: $tabs-active-selected-color; - border-block-end-color: $tabs-active-selected-border-color; - } - - &--disabled { - cursor: not-allowed; - - color: $tabs-disabled-selected-color; - border-block-end-color: $tabs-disabled-selected-border-color; - - &:hover { - color: $tabs-disabled-selected-color; - border-block-end-color: $tabs-disabled-selected-border-color; - } - } - } -} - -// TODO: Indication that there are more tab__items left to scroll (some sort of shadow maybe) -.rcx-tabs { - position: relative; - - display: flex; - - &--with-divider { - border-block-end: lengths.border-width(default) solid colors.stroke(light); - - .rcx-tabs__item { - margin-block-end: lengths.margin(-1); - border-block-width: lengths.border-width(default); - } - } -} diff --git a/packages/fuselage/src/components/Tabs/Tabs.tsx b/packages/fuselage/src/components/Tabs/Tabs.tsx index ea67003fc8..7608138fad 100644 --- a/packages/fuselage/src/components/Tabs/Tabs.tsx +++ b/packages/fuselage/src/components/Tabs/Tabs.tsx @@ -1,19 +1,58 @@ -import { Box, type BoxProps } from '../Box'; +import type { AllHTMLAttributes, ReactNode } from 'react'; +import { styled } from 'tamagui'; + +import { RcxView } from '../../primitives'; import TabsItem from './TabsItem'; -export type TabsProps = BoxProps & { divider?: boolean }; +const TabsFrame = styled(RcxView, { + name: 'Tabs', + position: 'relative', + display: 'flex', + + variants: { + divider: { + true: { + borderBlockEndWidth: 1, + borderBlockEndStyle: 'solid', + borderBlockEndColor: '$strokeLight', + }, + }, + } as const, +}); + +const TabsScrollBox = styled(RcxView, { + name: 'TabsScrollBox', + position: 'relative', + overflow: 'auto', + marginBlock: -4, +}); + +const TabsWrapper = styled(RcxView, { + name: 'TabsWrapper', + display: 'flex', + flexWrap: 'nowrap', + marginBlock: 0, + marginInline: '$x12', + paddingBlock: '$x4', + paddingInline: 0, +}); + +export type TabsProps = { + children?: ReactNode; + divider?: boolean; +} & Omit, 'is'>; /** * Tabs is a component to navigate around the UI using buttons arranged together with the selected tab highlighted. */ function Tabs({ children, divider = true, ...props }: TabsProps) { return ( - - - - - + + + {children} + + ); } diff --git a/packages/fuselage/src/components/Tabs/TabsItem.tsx b/packages/fuselage/src/components/Tabs/TabsItem.tsx index a92b66bb0d..1003222720 100644 --- a/packages/fuselage/src/components/Tabs/TabsItem.tsx +++ b/packages/fuselage/src/components/Tabs/TabsItem.tsx @@ -1,29 +1,121 @@ import { forwardRef } from 'react'; +import type { AllHTMLAttributes } from 'react'; +import { styled } from 'tamagui'; -import { Box, type BoxProps } from '../Box'; +import { RcxInteractiveText } from '../../primitives'; -export type TabsItemProps = BoxProps & { +const TabsItemBase = styled(RcxInteractiveText, { + name: 'TabsItem', + tag: 'button', + + position: 'relative', + alignItems: 'center', + flexGrow: 0, + flexShrink: 0, + + minHeight: '$x40', + marginBlock: 0, + marginInline: '$x12', + // padding: (4px + 1px border) 0 — the SCSS adds border-width to padding + paddingBlock: 5, + paddingInline: 0, + + fontFamily: '$body', + fontSize: '$h4', + fontWeight: '$h4', + lineHeight: '$h4', + letterSpacing: '$h4', + + borderWidth: 1, + borderStyle: 'solid', + borderColor: 'transparent', + backgroundColor: 'transparent', + + overflowWrap: 'normal', + + variants: { + selected: { + true: { + // Selected: padding without extra border compensation + paddingBlock: '$x4', + paddingInline: 0, + color: '$fontInfo', + borderBlockEndColor: '$fontInfo', + borderBlockEndWidth: 1, + borderInlineWidth: 1, + + hoverStyle: { + color: '$buttonPrimaryHoverBg', + borderBlockEndColor: '$buttonPrimaryHoverBg', + }, + + pressStyle: { + color: '$buttonPrimaryPressBg', + borderBlockEndColor: '$buttonPrimaryPressBg', + }, + }, + false: { + color: '$fontHint', + + hoverStyle: { + color: '$fontDefault', + borderBlockEndColor: '$fontDefault', + borderBlockEndWidth: 4, + }, + + pressStyle: { + color: '$fontTitlesLabels', + borderBlockEndColor: '$fontTitlesLabels', + borderBlockEndWidth: 4, + }, + }, + }, + + isDisabled: { + true: { + cursor: 'not-allowed', + }, + }, + } as const, + + defaultVariants: { + selected: false, + }, +}); + +export type TabsItemProps = { selected?: boolean; disabled?: boolean; -}; - -const TabsItem = forwardRef(function TabsItem( - { selected, disabled, ...props }, - ref, -) { - return ( - - ); -}); + children?: any; +} & Omit, 'is'>; + +const TabsItem = forwardRef( + function TabsItem({ selected, disabled, ...props }, ref) { + // Determine disabled color overrides + const disabledColorStyle = disabled + ? selected + ? { color: '$statusBgInfo', borderBlockEndColor: '$statusBgInfo' } + : { color: '$fontDisabled' } + : undefined; + + return ( + + ); + }, +); export default TabsItem; From d087f30cafc6f35f18702c7f35a8a4b37bbb0c03 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 11:55:20 -0300 Subject: [PATCH 34/84] refactor(fuselage): Migrate Table component from SCSS to Tamagui v3 Uses native HTML table elements (table/thead/tbody/tr/td/th) with inline styles since Tamagui cannot render table elements. TableContext for striped/sticky. TableSelection uses Tamagui styled components. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../fuselage/src/components/Card/Card.tsx | 1 + .../fuselage/src/components/Card/CardCol.tsx | 2 + .../src/components/Card/CardControls.tsx | 1 + .../src/components/Card/CardHeader.tsx | 1 + .../components/Dropdown/DropdownDesktop.tsx | 6 +- .../components/Dropdown/DropdownMobile.tsx | 6 +- .../src/components/FramedIcon/FramedIcon.tsx | 25 ++-- .../fuselage/src/components/Option/Option.tsx | 7 +- .../src/components/Table/Table.styles.scss | 123 ------------------ .../fuselage/src/components/Table/Table.tsx | 66 ++++++++-- .../src/components/Table/TableBody.tsx | 26 +++- .../src/components/Table/TableCell.tsx | 120 ++++++++++++----- .../src/components/Table/TableFoot.tsx | 26 +++- .../src/components/Table/TableHead.tsx | 34 +++-- .../src/components/Table/TableRow.tsx | 45 +++++-- .../Table/TableSelection/TableSelection.tsx | 65 ++++++--- 16 files changed, 316 insertions(+), 238 deletions(-) delete mode 100644 packages/fuselage/src/components/Table/Table.styles.scss diff --git a/packages/fuselage/src/components/Card/Card.tsx b/packages/fuselage/src/components/Card/Card.tsx index e617989fe1..d714c723bb 100644 --- a/packages/fuselage/src/components/Card/Card.tsx +++ b/packages/fuselage/src/components/Card/Card.tsx @@ -19,6 +19,7 @@ const CardFrame = styled(RcxView, { variants: { horizontal: { true: { + flexDirection: 'row', alignItems: 'center', padding: '$x12', gap: '$x16', diff --git a/packages/fuselage/src/components/Card/CardCol.tsx b/packages/fuselage/src/components/Card/CardCol.tsx index d3e56f49be..298fe9bc5b 100644 --- a/packages/fuselage/src/components/Card/CardCol.tsx +++ b/packages/fuselage/src/components/Card/CardCol.tsx @@ -2,6 +2,7 @@ import type { AllHTMLAttributes, ReactNode } from 'react'; import { styled } from 'tamagui'; import { RcxView } from '../../primitives'; + import { CardContext } from './Card'; const CardColFrame = styled(RcxView, { @@ -9,6 +10,7 @@ const CardColFrame = styled(RcxView, { context: CardContext, display: 'flex', flexDirection: 'column', + flexShrink: 1, gap: '$x8', variants: { diff --git a/packages/fuselage/src/components/Card/CardControls.tsx b/packages/fuselage/src/components/Card/CardControls.tsx index c036b8a838..7d95540bba 100644 --- a/packages/fuselage/src/components/Card/CardControls.tsx +++ b/packages/fuselage/src/components/Card/CardControls.tsx @@ -6,6 +6,7 @@ import { RcxView } from '../../primitives'; const CardControlsFrame = styled(RcxView, { name: 'CardControls', display: 'flex', + flexDirection: 'row', alignItems: 'center', gap: '$x8', }); diff --git a/packages/fuselage/src/components/Card/CardHeader.tsx b/packages/fuselage/src/components/Card/CardHeader.tsx index 48127b17df..f4d3e90b0d 100644 --- a/packages/fuselage/src/components/Card/CardHeader.tsx +++ b/packages/fuselage/src/components/Card/CardHeader.tsx @@ -6,6 +6,7 @@ import { RcxView } from '../../primitives'; const CardHeaderFrame = styled(RcxView, { name: 'CardHeader', display: 'flex', + flexDirection: 'row', alignItems: 'center', gap: '$x8', }); diff --git a/packages/fuselage/src/components/Dropdown/DropdownDesktop.tsx b/packages/fuselage/src/components/Dropdown/DropdownDesktop.tsx index 5edbca9e77..d21c570df5 100644 --- a/packages/fuselage/src/components/Dropdown/DropdownDesktop.tsx +++ b/packages/fuselage/src/components/Dropdown/DropdownDesktop.tsx @@ -7,8 +7,9 @@ import Tile from '../Tile/Tile'; const DropdownContent = styled(RcxView, { name: 'DropdownContent', + display: 'block', flexShrink: 1, - paddingBottom: 12, + paddingBlock: '$x12', }); export type DropdownDesktopProps = { @@ -26,8 +27,7 @@ export const DropdownDesktop = forwardRef( style={style} ref={ref} elevation='2' - paddingInline={0} - paddingBottom={0} + padding={0} display='flex' flexDirection='column' overflow='auto' diff --git a/packages/fuselage/src/components/Dropdown/DropdownMobile.tsx b/packages/fuselage/src/components/Dropdown/DropdownMobile.tsx index 5735ce9b37..f8e62a308c 100644 --- a/packages/fuselage/src/components/Dropdown/DropdownMobile.tsx +++ b/packages/fuselage/src/components/Dropdown/DropdownMobile.tsx @@ -7,8 +7,9 @@ import Tile from '../Tile/Tile'; const DropdownContent = styled(RcxView, { name: 'DropdownMobileContent', + display: 'block', flexShrink: 1, - paddingBottom: 16, + paddingBlock: '$x16', }); export type DropdownMobileProps = { @@ -21,8 +22,7 @@ export const DropdownMobile = forwardRef( with both framed-icon + icon classes +// Renders the icon glyph directly (no nested Icon component) const FramedIconBase = styled(RcxText, { name: 'FramedIcon', - display: 'inline-flex', + display: 'inline-block', padding: '$x4', borderRadius: '$x4', - // p2m font scale (from original: @include typography.use-font-scale(p2m)) - fontFamily: '$body', - fontSize: '$p2m', - fontWeight: '$p2m', - lineHeight: '$p2m', - letterSpacing: '$p2m', + // Icon font (RocketChat glyph font) + fontFamily: 'RocketChat', + fontWeight: '400', + fontStyle: 'normal', + fontSize: 20, + lineHeight: 20, + letterSpacing: 0, + userSelect: 'none', // default: colors.font(secondary-info), bg: colors.surface(tint) color: '$fontSecondaryInfo', @@ -62,8 +65,8 @@ const FramedIcon = ({ 'neutral'; return ( - - + ); }; diff --git a/packages/fuselage/src/components/Option/Option.tsx b/packages/fuselage/src/components/Option/Option.tsx index 90095d41bd..18f8f8ef63 100644 --- a/packages/fuselage/src/components/Option/Option.tsx +++ b/packages/fuselage/src/components/Option/Option.tsx @@ -13,10 +13,11 @@ import OptionContent from './OptionContent'; import OptionIcon from './OptionIcon'; // .rcx-option -const OptionFrame = styled(RcxText, { +const OptionFrame = styled(RcxView, { name: 'OptionFrame', display: 'list-item', + margin: 0, cursor: 'pointer', paddingBlock: '$x4', @@ -75,13 +76,14 @@ const OptionFrame = styled(RcxText, { }); // .rcx-option__wrapper -const OptionWrapper = styled(RcxView, { +const OptionWrapper = styled(RcxText, { name: 'OptionWrapper', display: 'flex', alignItems: 'center', marginInline: -2, + paddingInline: '$x4', variants: { alignTop: { @@ -158,6 +160,7 @@ const Option = forwardRef(function Option( ; + +export const TableContext = createContext<{ + striped?: boolean; + sticky?: boolean; +}>({}); + +const wrapperStyle: CSSProperties = { + position: 'relative', }; -const Table = ({ striped, sticky, fixed = false, ...props }: TableProps) => ( - - - +const tableBaseStyle: CSSProperties = { + display: 'table', + width: '100%', + borderSpacing: '0', + borderCollapse: 'collapse', +}; + +const Table = forwardRef( + ({ striped, sticky, fixed = false, children, className, style, ...props }, ref) => { + const mergedStyle = useMemo( + () => ({ + ...tableBaseStyle, + ...(fixed ? { tableLayout: 'fixed' as const } : undefined), + ...style, + }), + [fixed, style], + ); + + const contextValue = useMemo( + () => ({ striped, sticky }), + [striped, sticky], + ); + + return ( + +
    + + {children} +
    +
    +
    + ); + }, ); +Table.displayName = 'Table'; + export default Table; diff --git a/packages/fuselage/src/components/Table/TableBody.tsx b/packages/fuselage/src/components/Table/TableBody.tsx index d10fff4d80..241ff7398e 100644 --- a/packages/fuselage/src/components/Table/TableBody.tsx +++ b/packages/fuselage/src/components/Table/TableBody.tsx @@ -1,11 +1,27 @@ -import { Box } from '../Box'; +import type { HTMLAttributes, ReactNode } from 'react'; +import { forwardRef } from 'react'; -import type { TableProps } from './Table'; +export type TableBodyProps = { + children?: ReactNode; +} & HTMLAttributes; -export type TableBodyProps = TableProps; +const tbodyStyle: React.CSSProperties = { + display: 'table-row-group', +}; -const TableBody = (props: TableBodyProps) => ( - +const TableBody = forwardRef( + ({ children, className, style, ...props }, ref) => ( + + {children} + + ), ); +TableBody.displayName = 'TableBody'; + export default TableBody; diff --git a/packages/fuselage/src/components/Table/TableCell.tsx b/packages/fuselage/src/components/Table/TableCell.tsx index f9513c9695..9729e97b7a 100644 --- a/packages/fuselage/src/components/Table/TableCell.tsx +++ b/packages/fuselage/src/components/Table/TableCell.tsx @@ -1,40 +1,98 @@ -import { useContext } from 'react'; +import type { HTMLAttributes, ReactNode } from 'react'; +import { forwardRef, useContext, useMemo } from 'react'; -import { Box } from '../Box'; - -import type { TableProps } from './Table'; +import { TableContext } from './Table'; import { TableHeadContext } from './TableHead'; -export type TableCellProps = TableProps & { - align?: 'start' | 'center' | 'end' | 'justify' | object; +export type TableCellProps = { + align?: 'start' | 'center' | 'end' | 'justify'; clickable?: boolean; + children?: ReactNode; +} & HTMLAttributes; + +const cellBaseStyle: React.CSSProperties = { + display: 'table-cell', + padding: '8px', + userSelect: 'text', + textAlign: 'unset', + verticalAlign: 'middle', + color: 'var(--fontSecondaryInfo)', + fontFamily: 'var(--font-family_body)', + fontSize: 'var(--fontSize_p2)', + fontWeight: 'var(--fontWeight_p2)' as any, + lineHeight: 'var(--lineHeight_p2)', + letterSpacing: '0', +}; + +const headerExtraStyle: React.CSSProperties = { + position: 'relative', + fontSize: 'var(--fontSize_c2)', + fontWeight: 'var(--fontWeight_c2)' as any, + lineHeight: 'var(--lineHeight_c2)', +}; + +const stickyHeaderStyle: React.CSSProperties = { + position: 'sticky', + zIndex: 10, + top: 0, + backgroundColor: 'var(--surfaceLight)', }; -const TableCell = ({ - align, - clickable, - children, - ...props -}: TableCellProps) => { - const isInsideHead = useContext(TableHeadContext); - - const innerElement = - children ?? - (!isInsideHead ? ( - - ) : undefined); - - return ( - - ); +const headerAfterStyle: React.CSSProperties = { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + display: 'flex', + borderBlockEnd: '1px solid var(--strokeLight)', }; +const TableCell = forwardRef( + ({ align, clickable, children, className, style, ...props }, ref) => { + const isInsideHead = useContext(TableHeadContext); + const { sticky } = useContext(TableContext); + + const innerElement = + children ?? + (!isInsideHead ? ( +
    + ) : undefined); + + const mergedStyle = useMemo( + () => ({ + ...cellBaseStyle, + ...(isInsideHead ? headerExtraStyle : undefined), + ...(isInsideHead && sticky ? stickyHeaderStyle : undefined), + ...(align ? { textAlign: align } : undefined), + ...(clickable ? { cursor: 'pointer' } : undefined), + ...style, + }), + [isInsideHead, sticky, align, clickable, style], + ); + + const Element = isInsideHead ? 'th' : 'td'; + + return ( + + {innerElement} + {isInsideHead && + ); + }, +); + +TableCell.displayName = 'TableCell'; + export default TableCell; diff --git a/packages/fuselage/src/components/Table/TableFoot.tsx b/packages/fuselage/src/components/Table/TableFoot.tsx index c79a6d0853..828bb9ed7e 100644 --- a/packages/fuselage/src/components/Table/TableFoot.tsx +++ b/packages/fuselage/src/components/Table/TableFoot.tsx @@ -1,11 +1,27 @@ -import { Box } from '../Box'; +import type { HTMLAttributes, ReactNode } from 'react'; +import { forwardRef } from 'react'; -import type { TableProps } from './Table'; +export type TableFootProps = { + children?: ReactNode; +} & HTMLAttributes; -export type TableFootProps = TableProps; +const tfootStyle: React.CSSProperties = { + display: 'table-footer-group', +}; -const TableFoot = (props: TableFootProps) => ( - +const TableFoot = forwardRef( + ({ children, className, style, ...props }, ref) => ( + + {children} + + ), ); +TableFoot.displayName = 'TableFoot'; + export default TableFoot; diff --git a/packages/fuselage/src/components/Table/TableHead.tsx b/packages/fuselage/src/components/Table/TableHead.tsx index 481497901e..190dad776a 100644 --- a/packages/fuselage/src/components/Table/TableHead.tsx +++ b/packages/fuselage/src/components/Table/TableHead.tsx @@ -1,17 +1,31 @@ -import { createContext } from 'react'; - -import { Box } from '../Box'; - -import type { TableProps } from './Table'; +import type { HTMLAttributes, ReactNode } from 'react'; +import { createContext, forwardRef } from 'react'; export const TableHeadContext = createContext(false); -export type TableHeadProps = TableProps; +export type TableHeadProps = { + children?: ReactNode; +} & HTMLAttributes; -const TableHead = (props: TableHeadProps) => ( - - - +const theadStyle: React.CSSProperties = { + display: 'table-header-group', +}; + +const TableHead = forwardRef( + ({ children, className, style, ...props }, ref) => ( + + + {children} + + + ), ); +TableHead.displayName = 'TableHead'; + export default TableHead; diff --git a/packages/fuselage/src/components/Table/TableRow.tsx b/packages/fuselage/src/components/Table/TableRow.tsx index 06e4cf9454..7849fdf7a5 100644 --- a/packages/fuselage/src/components/Table/TableRow.tsx +++ b/packages/fuselage/src/components/Table/TableRow.tsx @@ -1,18 +1,41 @@ -import { Box, type BoxProps } from '../Box'; +import type { HTMLAttributes, ReactNode } from 'react'; +import { forwardRef, useMemo } from 'react'; -export type TableRowProps = Omit & { +export type TableRowProps = { action?: boolean; - hasAction?: boolean; + selected?: boolean; + children?: ReactNode; +} & HTMLAttributes; + +const rowBaseStyle: React.CSSProperties = { + display: 'table-row', }; -const TableRow = ({ action, selected, ...props }: TableRowProps) => ( - +const TableRow = forwardRef( + ({ action, selected, children, className, style, ...props }, ref) => { + const mergedStyle = useMemo( + () => ({ + ...rowBaseStyle, + ...(selected ? { backgroundColor: 'var(--surfaceTint)' } : undefined), + ...(action ? { cursor: 'pointer' } : undefined), + ...style, + }), + [action, selected, style], + ); + + return ( + + {children} + + ); + }, ); +TableRow.displayName = 'TableRow'; + export default TableRow; diff --git a/packages/fuselage/src/components/Table/TableSelection/TableSelection.tsx b/packages/fuselage/src/components/Table/TableSelection/TableSelection.tsx index fdd9234149..e2cf94fc01 100644 --- a/packages/fuselage/src/components/Table/TableSelection/TableSelection.tsx +++ b/packages/fuselage/src/components/Table/TableSelection/TableSelection.tsx @@ -1,28 +1,51 @@ -import { Box, type BoxProps } from '../../Box'; +import type { HTMLAttributes, ReactNode } from 'react'; +import { styled } from 'tamagui'; -export type TableSelectionProps = BoxProps & { +import { RcxText, RcxView } from '../../../primitives'; + +const SelectionFrame = styled(RcxView, { + name: 'TableSelection', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + paddingInline: '$x24', + color: '$fontTitlesLabels', + borderRadius: '$x4', + backgroundColor: '$surfaceNeutral', +}); + +const SelectionText = styled(RcxText, { + name: 'TableSelectionText', + display: 'block', + flexShrink: 1, + marginBottom: '$x16', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + fontFamily: '$body', + fontSize: '$p2b', + fontWeight: '$p2b', + lineHeight: '$p2b', + letterSpacing: '$p2b', + color: 'inherit', +}); + +const SelectionActions = styled(RcxView, { + name: 'TableSelectionActions', + flexShrink: 0, + marginInline: -8, +}); + +export type TableSelectionProps = { text?: string; -}; + children?: ReactNode; +} & Omit, 'is'>; const TableSelection = ({ children, text, ...props }: TableSelectionProps) => ( - - - {text} - - {children && ( - - {children} - - )} - + + {text} + {children && {children}} + ); export default TableSelection; From 475bc11f9d324482feb566bce10735000d8754f6 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 13:48:51 -0300 Subject: [PATCH 35/84] refactor(fuselage): Migrate ButtonGroup from SCSS to Tamagui v3 Uses gap ( default, small, large) instead of patchChildren. Variants: wrap, stretch, vertical, align. createStyledContext for stretch. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ButtonGroup/ButtonGroup.styles.scss | 50 --------- .../components/ButtonGroup/ButtonGroup.tsx | 101 ++++++++++++++---- 2 files changed, 82 insertions(+), 69 deletions(-) delete mode 100644 packages/fuselage/src/components/ButtonGroup/ButtonGroup.styles.scss diff --git a/packages/fuselage/src/components/ButtonGroup/ButtonGroup.styles.scss b/packages/fuselage/src/components/ButtonGroup/ButtonGroup.styles.scss deleted file mode 100644 index dba2a830fb..0000000000 --- a/packages/fuselage/src/components/ButtonGroup/ButtonGroup.styles.scss +++ /dev/null @@ -1,50 +0,0 @@ -@use '../../styles/lengths.scss'; - -.rcx-button-group { - display: flex; - - flex-flow: row nowrap; - justify-content: flex-start; - - align-items: center; - - gap: lengths.margin(8); - - &--small { - gap: lengths.margin(4); - } - - &--large { - gap: lengths.margin(16); - } - - &--wrap { - flex-wrap: wrap; - } - - &--stretch { - align-items: stretch; - - flex-grow: 1; - - > * { - flex-grow: 1; - } - } - - &--vertical { - flex-direction: column; - } - - &--align-start { - justify-content: flex-start; - } - - &--align-center { - justify-content: center; - } - - &--align-end { - justify-content: flex-end; - } -} diff --git a/packages/fuselage/src/components/ButtonGroup/ButtonGroup.tsx b/packages/fuselage/src/components/ButtonGroup/ButtonGroup.tsx index 30ae4bddf3..e7eb95d438 100644 --- a/packages/fuselage/src/components/ButtonGroup/ButtonGroup.tsx +++ b/packages/fuselage/src/components/ButtonGroup/ButtonGroup.tsx @@ -1,5 +1,71 @@ -import type { HTMLAttributes } from 'react'; -import { forwardRef } from 'react'; +import type { ReactNode } from 'react'; +import { Children, forwardRef } from 'react'; +import { styled, createStyledContext, type GetProps } from 'tamagui'; + +import { RcxView } from '../../primitives'; + +const ButtonGroupContext = createStyledContext({ + stretch: false as boolean, +}); + +const ButtonGroupFrame = styled(RcxView, { + name: 'ButtonGroup', + context: ButtonGroupContext, + role: 'group', + + display: 'flex', + flexDirection: 'row', + flexWrap: 'nowrap', + justifyContent: 'flex-start', + alignItems: 'center', + gap: '$x8', + + variants: { + wrap: { + true: { + flexWrap: 'wrap', + }, + }, + + stretch: { + true: { + justifyContent: 'stretch', + alignItems: 'stretch', + flexGrow: 1, + }, + }, + + vertical: { + true: { + flexDirection: 'column', + }, + }, + + align: { + start: { + justifyContent: 'flex-start', + }, + center: { + justifyContent: 'center', + }, + end: { + justifyContent: 'flex-end', + }, + }, + + small: { + true: { + gap: '$x4', + }, + }, + + large: { + true: { + gap: '$x16', + }, + }, + } as const, +}); export type ButtonGroupProps = { align?: 'start' | 'center' | 'end'; @@ -8,7 +74,12 @@ export type ButtonGroupProps = { vertical?: boolean; small?: boolean; large?: boolean; -} & HTMLAttributes; + children?: ReactNode; + className?: string; +} & Omit< + GetProps, + 'align' | 'stretch' | 'wrap' | 'vertical' | 'small' | 'large' +>; /** * A container for grouping buttons that semantically share a common action context. @@ -23,31 +94,23 @@ const ButtonGroup = forwardRef( wrap, small, large, - className, ...props }, ref, ) { return ( -
    {children} -
    + ); }, ); From 746d2ea8513a4756f21eeeb1aa46cb3674976249 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 13:48:53 -0300 Subject: [PATCH 36/84] refactor(fuselage): Migrate CheckBox from SCSS to Tamagui v3 Hidden input + CheckBoxFake with compoundVariants for all state combos. Checkmark as View elements with CSS transforms. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/CheckBox/CheckBox.styles.scss | 92 ----- .../src/components/CheckBox/CheckBox.tsx | 371 ++++++++++++++++-- 2 files changed, 331 insertions(+), 132 deletions(-) delete mode 100644 packages/fuselage/src/components/CheckBox/CheckBox.styles.scss diff --git a/packages/fuselage/src/components/CheckBox/CheckBox.styles.scss b/packages/fuselage/src/components/CheckBox/CheckBox.styles.scss deleted file mode 100644 index 3e2811183a..0000000000 --- a/packages/fuselage/src/components/CheckBox/CheckBox.styles.scss +++ /dev/null @@ -1,92 +0,0 @@ -@use 'sass:math'; -@use '../../styles/lengths.scss'; - -.rcx-check-box { - @include is-selection-button( - $checked: 'primary', - $unchecked: 'empty', - $indeterminate: 'primary' - ); - - &__input { - @extend %selection-button__input; - } - - $icon-smoothness: to-rem(1); - $icon-thickness: to-rem(2); - $icon-size: 0.6; - - &__fake { - @extend %selection-button__fake; - display: flex; - justify-content: center; - align-items: center; - - border-radius: theme( - 'check-box-border-radius', - lengths.border-radius(small) - ); - inline-size: lengths.size(20); - - &::before, - &::after { - position: absolute; - - display: block; - visibility: hidden; - - content: ''; - - opacity: 0; - - background-color: currentColor; - } - } - - &__input:indeterminate + &__fake::before { - visibility: visible; - - width: $icon-size * lengths.size(20); - height: $icon-thickness; - - opacity: 1; - - border-radius: $icon-smoothness; - } - - &__input:checked + &__fake { - &::before, - &::after { - visibility: visible; - - opacity: 1; - border-radius: $icon-smoothness; - } - - &::before { - width: $icon-size * lengths.size(20); - height: $icon-thickness; - - transform: translate( - $icon-size * math.div(lengths.size(20), -3), - $icon-size * math.div(lengths.size(20), 6) - ) - rotate(-45deg) - translate( - $icon-size * math.div(lengths.size(20), 2), - $icon-size * math.div(lengths.size(20), 6) - ); - } - - &::after { - width: $icon-thickness; - height: 0.5 * $icon-size * lengths.size(20); - - transform: translate( - $icon-size * math.div(lengths.size(20), -3), - $icon-size * math.div(lengths.size(20), 6) - ) - rotate(-45deg); - } - } -} diff --git a/packages/fuselage/src/components/CheckBox/CheckBox.tsx b/packages/fuselage/src/components/CheckBox/CheckBox.tsx index 30e2916a75..a20f6a9b09 100644 --- a/packages/fuselage/src/components/CheckBox/CheckBox.tsx +++ b/packages/fuselage/src/components/CheckBox/CheckBox.tsx @@ -1,51 +1,342 @@ import { useMergedRefs } from '@rocket.chat/fuselage-hooks'; import type { FormEvent, AllHTMLAttributes, ReactNode } from 'react'; -import { forwardRef, useLayoutEffect, useRef, useCallback } from 'react'; +import { + forwardRef, + useLayoutEffect, + useRef, + useCallback, + useState, +} from 'react'; +import { styled } from 'tamagui'; -import { Box, type BoxProps } from '../Box'; +import { RcxView } from '../../primitives'; -export type CheckBoxProps = BoxProps & { +// --- Styled components --- + +const CheckBoxWrapper = styled(RcxView, { + name: 'CheckBox', + tag: 'label', + + position: 'relative', + display: 'inline-flex', + flexDirection: 'row', + verticalAlign: 'middle', + cursor: 'pointer', + + variants: { + isDisabled: { + true: { + cursor: 'not-allowed', + }, + }, + } as const, +}); + +const CheckBoxFake = styled(RcxView, { + name: 'CheckBoxFake', + + position: 'relative', + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + + width: 20, + height: 20, + + borderWidth: 1, + borderStyle: 'solid', + borderRadius: '$x2', + + // Default unchecked (empty) state + backgroundColor: '$surfaceLight', + borderColor: '$strokeDark', + color: '$fontWhite', + + variants: { + state: { + unchecked: { + backgroundColor: '$surfaceLight', + borderColor: '$strokeDark', + color: '$fontWhite', + }, + checked: { + backgroundColor: '$buttonPrimaryBg', + borderColor: '$buttonPrimaryBg', + color: '$buttonPrimaryColor', + }, + indeterminate: { + backgroundColor: '$buttonPrimaryBg', + borderColor: '$buttonPrimaryBg', + color: '$buttonPrimaryColor', + }, + }, + + isHovered: { + true: {}, + }, + + isActive: { + true: {}, + }, + + isFocused: { + true: {}, + }, + + isDisabled: { + true: {}, + }, + } as const, + + compoundVariants: [ + // Unchecked + hovered + { + state: 'unchecked', + isHovered: true, + backgroundColor: '$surfaceLight', + borderColor: '$strokeExtraDark', + boxShadow: 'none', + }, + // Unchecked + active + { + state: 'unchecked', + isActive: true, + backgroundColor: '$surfaceLight', + borderColor: '$strokeExtraDark', + boxShadow: 'none', + }, + // Unchecked + focused + { + state: 'unchecked', + isFocused: true, + backgroundColor: '$surfaceLight', + borderColor: '$strokeExtraDark', + boxShadow: '0 0 0 2px var(--shadowHighlight)', + }, + // Unchecked + disabled + { + state: 'unchecked', + isDisabled: true, + backgroundColor: '$buttonSecondaryDisabledBg', + borderColor: '$strokeLight', + color: '$fontWhite', + cursor: 'not-allowed', + }, + + // Checked + hovered + { + state: 'checked', + isHovered: true, + backgroundColor: '$buttonPrimaryHoverBg', + borderColor: '$buttonPrimaryHoverBg', + boxShadow: 'none', + }, + // Checked + active + { + state: 'checked', + isActive: true, + backgroundColor: '$buttonPrimaryPressBg', + borderColor: '$buttonPrimaryPressBg', + boxShadow: 'none', + }, + // Checked + focused + { + state: 'checked', + isFocused: true, + backgroundColor: '$buttonPrimaryFocusBg', + borderColor: '$strokeExtraDark', + boxShadow: '0 0 0 2px var(--shadowHighlight)', + }, + // Checked + disabled + { + state: 'checked', + isDisabled: true, + backgroundColor: '$buttonPrimaryDisabledBg', + borderColor: '$buttonPrimaryDisabledBg', + color: '$buttonPrimaryDisabledColor', + cursor: 'not-allowed', + }, + + // Indeterminate + hovered + { + state: 'indeterminate', + isHovered: true, + backgroundColor: '$buttonPrimaryHoverBg', + borderColor: '$buttonPrimaryHoverBg', + boxShadow: 'none', + }, + // Indeterminate + active + { + state: 'indeterminate', + isActive: true, + backgroundColor: '$buttonPrimaryPressBg', + borderColor: '$buttonPrimaryPressBg', + boxShadow: 'none', + }, + // Indeterminate + focused + { + state: 'indeterminate', + isFocused: true, + backgroundColor: '$buttonPrimaryFocusBg', + borderColor: '$strokeExtraDark', + boxShadow: '0 0 0 2px var(--shadowHighlight)', + }, + // Indeterminate + disabled + { + state: 'indeterminate', + isDisabled: true, + backgroundColor: '$buttonPrimaryDisabledBg', + borderColor: '$buttonPrimaryDisabledBg', + color: '$buttonPrimaryDisabledColor', + cursor: 'not-allowed', + }, + ], +}); + +// Indeterminate indicator: horizontal bar +const IndeterminateIndicator = styled(RcxView, { + name: 'CheckBoxIndeterminate', + + position: 'absolute', + width: 12, // 0.6 * 20 + height: 2, + borderRadius: 1, + backgroundColor: 'currentColor', +}); + +// Check mark: two bars forming an L shape rotated -45deg +// Using a wrapper div with transform, containing the two bars +const CheckMarkWrapper = styled(RcxView, { + name: 'CheckMarkWrapper', + + position: 'absolute', + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + // translate(-4px, 2px) rotate(-45deg) + transform: 'translate(-4px, 2px) rotate(-45deg)', +}); + +// Long bar of the checkmark (horizontal) +const CheckMarkLong = styled(RcxView, { + name: 'CheckMarkLong', + + width: 12, // 0.6 * 20 + height: 2, + borderRadius: 1, + backgroundColor: 'currentColor', + // translate(6px, 2px) relative to the wrapper + transform: 'translate(6px, 2px)', +}); + +// Short bar of the checkmark (vertical) +const CheckMarkShort = styled(RcxView, { + name: 'CheckMarkShort', + + width: 2, + height: 6, // 0.5 * 0.6 * 20 + borderRadius: 1, + backgroundColor: 'currentColor', +}); + +// --- Types --- + +export type CheckBoxProps = { indeterminate?: boolean; labelChildren?: ReactNode; -} & AllHTMLAttributes; - -const CheckBox = forwardRef(function CheckBox( - { indeterminate, onChange, className, labelChildren, ...props }, - ref, -) { - const innerRef = useRef(null); - const mergedRef = useMergedRefs(ref, innerRef); - - useLayoutEffect(() => { - if (innerRef && innerRef.current && indeterminate !== undefined) { - innerRef.current.indeterminate = indeterminate; - } - }, [innerRef, indeterminate]); - - const handleChange = useCallback( - (event: FormEvent) => { + className?: string; +} & Omit, 'is'>; + +// --- Component --- + +const CheckBox = forwardRef( + function CheckBox( + { indeterminate, onChange, className, labelChildren, checked, disabled, ...props }, + ref, + ) { + const innerRef = useRef(null); + const mergedRef = useMergedRefs(ref, innerRef); + + const [isFocused, setIsFocused] = useState(false); + const [isHovered, setIsHovered] = useState(false); + const [isActive, setIsActive] = useState(false); + + useLayoutEffect(() => { if (innerRef && innerRef.current && indeterminate !== undefined) { innerRef.current.indeterminate = indeterminate; } - onChange?.call(innerRef.current, event); - }, - [innerRef, indeterminate, onChange], - ); - - return ( - - {labelChildren} - - - ); -}); + }, [innerRef, indeterminate]); + + const handleChange = useCallback( + (event: FormEvent) => { + if (innerRef && innerRef.current && indeterminate !== undefined) { + innerRef.current.indeterminate = indeterminate; + } + onChange?.call(innerRef.current, event); + }, + [innerRef, indeterminate, onChange], + ); + + const state = indeterminate + ? 'indeterminate' + : checked + ? 'checked' + : 'unchecked'; + + return ( + setIsHovered(true)} + onMouseLeave={() => { + setIsHovered(false); + setIsActive(false); + }} + onMouseDown={() => setIsActive(true)} + onMouseUp={() => setIsActive(false)} + > + {labelChildren} + setIsFocused(true)} + onBlur={() => setIsFocused(false)} + style={{ + position: 'absolute', + width: 1, + height: 1, + padding: 0, + margin: -1, + overflow: 'hidden', + clip: 'rect(0, 0, 0, 0)', + whiteSpace: 'nowrap', + borderWidth: 0, + }} + {...props} + /> + + + ); + }, +); export default CheckBox; From b9ba7ba3ec66d7c117414cf70527a84ea9bb3725 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 13:48:53 -0300 Subject: [PATCH 37/84] refactor(fuselage): Migrate RadioButton from SCSS to Tamagui v3 Hidden input + circular fake element. RadioDot for checked state. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../RadioButton/RadioButton.styles.scss | 32 --- .../components/RadioButton/RadioButton.tsx | 223 +++++++++++++++++- 2 files changed, 210 insertions(+), 45 deletions(-) delete mode 100644 packages/fuselage/src/components/RadioButton/RadioButton.styles.scss diff --git a/packages/fuselage/src/components/RadioButton/RadioButton.styles.scss b/packages/fuselage/src/components/RadioButton/RadioButton.styles.scss deleted file mode 100644 index 91075848e9..0000000000 --- a/packages/fuselage/src/components/RadioButton/RadioButton.styles.scss +++ /dev/null @@ -1,32 +0,0 @@ -@use '../../styles/lengths.scss'; - -.rcx-radio-button { - @include is-selection-button($checked: 'primary', $unchecked: 'empty'); - - &__input { - @extend %selection-button__input; - } - - &__fake { - @extend %selection-button__fake; - display: flex; - justify-content: center; - align-items: center; - - border-radius: lengths.border-radius(full); - inline-size: lengths.size(20); - } - - &__input:checked + &__fake::before { - display: block; - - width: 0.3 * lengths.size(20); - height: 0.3 * lengths.size(20); - - content: ''; - - border-radius: lengths.border-radius(full); - - background-color: currentColor; - } -} diff --git a/packages/fuselage/src/components/RadioButton/RadioButton.tsx b/packages/fuselage/src/components/RadioButton/RadioButton.tsx index 93ebedcb64..50e2966b72 100644 --- a/packages/fuselage/src/components/RadioButton/RadioButton.tsx +++ b/packages/fuselage/src/components/RadioButton/RadioButton.tsx @@ -1,27 +1,224 @@ import type { AllHTMLAttributes, ReactNode } from 'react'; -import { forwardRef } from 'react'; +import { forwardRef, useState } from 'react'; +import { styled } from 'tamagui'; -import { Box, type BoxProps } from '../Box'; +import { RcxView } from '../../primitives'; -export type RadioButtonProps = BoxProps & - AllHTMLAttributes & { - labelChildren?: ReactNode; - }; +// --- Styled components --- + +const RadioButtonWrapper = styled(RcxView, { + name: 'RadioButton', + tag: 'label', + + position: 'relative', + display: 'inline-flex', + flexDirection: 'row', + verticalAlign: 'middle', + cursor: 'pointer', + + variants: { + isDisabled: { + true: { + cursor: 'not-allowed', + }, + }, + } as const, +}); + +const RadioButtonFake = styled(RcxView, { + name: 'RadioButtonFake', + + position: 'relative', + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + + width: 20, + height: 20, + + borderWidth: 1, + borderStyle: 'solid', + borderRadius: '$full', + + // Default unchecked (empty) state + backgroundColor: '$surfaceLight', + borderColor: '$strokeDark', + color: '$fontWhite', + + variants: { + state: { + unchecked: { + backgroundColor: '$surfaceLight', + borderColor: '$strokeDark', + color: '$fontWhite', + }, + checked: { + backgroundColor: '$buttonPrimaryBg', + borderColor: '$buttonPrimaryBg', + color: '$buttonPrimaryColor', + }, + }, + + isHovered: { + true: {}, + }, + + isActive: { + true: {}, + }, + + isFocused: { + true: {}, + }, + + isDisabled: { + true: {}, + }, + } as const, + + compoundVariants: [ + // Unchecked + hovered + { + state: 'unchecked', + isHovered: true, + backgroundColor: '$surfaceLight', + borderColor: '$strokeExtraDark', + boxShadow: 'none', + }, + // Unchecked + active + { + state: 'unchecked', + isActive: true, + backgroundColor: '$surfaceLight', + borderColor: '$strokeExtraDark', + boxShadow: 'none', + }, + // Unchecked + focused + { + state: 'unchecked', + isFocused: true, + backgroundColor: '$surfaceLight', + borderColor: '$strokeExtraDark', + boxShadow: '0 0 0 2px var(--shadowHighlight)', + }, + // Unchecked + disabled + { + state: 'unchecked', + isDisabled: true, + backgroundColor: '$buttonSecondaryDisabledBg', + borderColor: '$strokeLight', + color: '$fontWhite', + cursor: 'not-allowed', + }, + + // Checked + hovered + { + state: 'checked', + isHovered: true, + backgroundColor: '$buttonPrimaryHoverBg', + borderColor: '$buttonPrimaryHoverBg', + boxShadow: 'none', + }, + // Checked + active + { + state: 'checked', + isActive: true, + backgroundColor: '$buttonPrimaryPressBg', + borderColor: '$buttonPrimaryPressBg', + boxShadow: 'none', + }, + // Checked + focused + { + state: 'checked', + isFocused: true, + backgroundColor: '$buttonPrimaryFocusBg', + borderColor: '$strokeExtraDark', + boxShadow: '0 0 0 2px var(--shadowHighlight)', + }, + // Checked + disabled + { + state: 'checked', + isDisabled: true, + backgroundColor: '$buttonPrimaryDisabledBg', + borderColor: '$buttonPrimaryDisabledBg', + color: '$buttonPrimaryDisabledColor', + cursor: 'not-allowed', + }, + ], +}); + +// Radio dot: 0.3 * 20 = 6px circle +const RadioDot = styled(RcxView, { + name: 'RadioDot', + + width: 6, + height: 6, + borderRadius: '$full', + backgroundColor: 'currentColor', +}); + +// --- Types --- + +export type RadioButtonProps = { + labelChildren?: ReactNode; + className?: string; +} & Omit, 'is'>; + +// --- Component --- const RadioButton = forwardRef( - function RadioButton({ className, labelChildren, ...props }, ref) { + function RadioButton({ className, labelChildren, checked, disabled, ...props }, ref) { + const [isFocused, setIsFocused] = useState(false); + const [isHovered, setIsHovered] = useState(false); + const [isActive, setIsActive] = useState(false); + + const state = checked ? 'checked' : 'unchecked'; + return ( - + setIsHovered(true)} + onMouseLeave={() => { + setIsHovered(false); + setIsActive(false); + }} + onMouseDown={() => setIsActive(true)} + onMouseUp={() => setIsActive(false)} + > {labelChildren} - setIsFocused(true)} + onBlur={() => setIsFocused(false)} + style={{ + position: 'absolute', + width: 1, + height: 1, + padding: 0, + margin: -1, + overflow: 'hidden', + clip: 'rect(0, 0, 0, 0)', + whiteSpace: 'nowrap', + borderWidth: 0, + }} {...props} /> - + + ); }, ); From 757628ebb2d4c7cccd0309fcdb86ca90469755b4 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 13:48:55 -0300 Subject: [PATCH 38/84] refactor(fuselage): Migrate ToggleSwitch from SCSS to Tamagui v3 Hidden input + track (36x18) with sliding knob (14x14). Unchecked/checked state positioning and colors. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ToggleSwitch/ToggleSwitch.styles.scss | 53 ---- .../components/ToggleSwitch/ToggleSwitch.tsx | 262 +++++++++++++++++- 2 files changed, 247 insertions(+), 68 deletions(-) delete mode 100644 packages/fuselage/src/components/ToggleSwitch/ToggleSwitch.styles.scss diff --git a/packages/fuselage/src/components/ToggleSwitch/ToggleSwitch.styles.scss b/packages/fuselage/src/components/ToggleSwitch/ToggleSwitch.styles.scss deleted file mode 100644 index 448472c280..0000000000 --- a/packages/fuselage/src/components/ToggleSwitch/ToggleSwitch.styles.scss +++ /dev/null @@ -1,53 +0,0 @@ -@use '../../styles/colors.scss'; -@use '../../styles/lengths.scss'; - -$toggle-switch-border-radius: theme( - 'toggle-switch-border-radius', - lengths.border-radius(full) -); - -.rcx-toggle-switch { - @include is-selection-button($checked: 'primary', $unchecked: 'off'); - - &__input { - @extend %selection-button__input; - } - - &__fake { - @extend %selection-button__fake; - - width: lengths.size(2 * 18); - height: lengths.size(20) - 2 * lengths.border-width(default, rem); - - border-radius: $toggle-switch-border-radius; - } - - &__input + &__fake::before { - position: absolute; - - top: 50%; - inset-inline-start: lengths.inset(1); - - width: lengths.size(16) - 2 * lengths.border-width(default, rem); - height: lengths.size(16) - 2 * lengths.border-width(default, rem); - - content: ''; - transform: translateY(-50%); - - border-radius: $toggle-switch-border-radius; - - background-color: colors.button(on-primary); - } - - &__input:disabled + &__fake::before { - background-color: colors.button(on-secondary-disabled); - } - - &__input:checked + &__fake::before { - inset-inline-start: calc( - 100% - #{lengths.inset(16) - lengths.border-width(default, rem)} - ); - - background-color: colors.button(on-primary); - } -} diff --git a/packages/fuselage/src/components/ToggleSwitch/ToggleSwitch.tsx b/packages/fuselage/src/components/ToggleSwitch/ToggleSwitch.tsx index 24babfe1a8..b0e15fac55 100644 --- a/packages/fuselage/src/components/ToggleSwitch/ToggleSwitch.tsx +++ b/packages/fuselage/src/components/ToggleSwitch/ToggleSwitch.tsx @@ -1,28 +1,260 @@ import type { AllHTMLAttributes, ReactNode } from 'react'; -import { forwardRef } from 'react'; +import { forwardRef, useState } from 'react'; +import { styled } from 'tamagui'; -import { Box, type BoxProps } from '../Box'; +import { RcxView } from '../../primitives'; -export type ToggleSwitchProps = BoxProps & - AllHTMLAttributes & { - labelChildren?: ReactNode; - }; +// --- Styled components --- + +const ToggleSwitchWrapper = styled(RcxView, { + name: 'ToggleSwitch', + tag: 'label', + + position: 'relative', + display: 'inline-flex', + flexDirection: 'row', + verticalAlign: 'middle', + cursor: 'pointer', + + variants: { + isDisabled: { + true: { + cursor: 'not-allowed', + }, + }, + } as const, +}); + +const ToggleSwitchFake = styled(RcxView, { + name: 'ToggleSwitchFake', + + position: 'relative', + + width: 36, // 2 * 18 + height: 18, // 20 - 2*1 (border) + + borderWidth: 1, + borderStyle: 'solid', + borderRadius: '$full', + + // Default unchecked (off) state + backgroundColor: '$strokeMedium', + borderColor: '$strokeMedium', + color: '$fontWhite', + + variants: { + state: { + unchecked: { + backgroundColor: '$strokeMedium', + borderColor: '$strokeMedium', + color: '$fontWhite', + }, + checked: { + backgroundColor: '$buttonPrimaryBg', + borderColor: '$buttonPrimaryBg', + color: '$buttonPrimaryColor', + }, + }, + + isHovered: { + true: {}, + }, + + isActive: { + true: {}, + }, + + isFocused: { + true: {}, + }, + + isDisabled: { + true: {}, + }, + } as const, + + compoundVariants: [ + // Unchecked (off) + hovered + { + state: 'unchecked', + isHovered: true, + backgroundColor: '$fontHint', + borderColor: '$fontHint', + boxShadow: 'none', + }, + // Unchecked + active + { + state: 'unchecked', + isActive: true, + backgroundColor: '$strokeDark', + borderColor: '$strokeDark', + boxShadow: 'none', + }, + // Unchecked + focused + { + state: 'unchecked', + isFocused: true, + backgroundColor: '$strokeMedium', + borderColor: '$strokeExtraDark', + boxShadow: '0 0 0 2px var(--shadowHighlight)', + }, + // Unchecked + disabled + { + state: 'unchecked', + isDisabled: true, + backgroundColor: '$buttonSecondaryDisabledBg', + borderColor: '$buttonSecondaryDisabledBg', + color: '$buttonSecondaryDisabledColor', + cursor: 'not-allowed', + }, + + // Checked + hovered + { + state: 'checked', + isHovered: true, + backgroundColor: '$buttonPrimaryHoverBg', + borderColor: '$buttonPrimaryHoverBg', + boxShadow: 'none', + }, + // Checked + active + { + state: 'checked', + isActive: true, + backgroundColor: '$buttonPrimaryPressBg', + borderColor: '$buttonPrimaryPressBg', + boxShadow: 'none', + }, + // Checked + focused + { + state: 'checked', + isFocused: true, + backgroundColor: '$buttonPrimaryFocusBg', + borderColor: '$strokeExtraDark', + boxShadow: '0 0 0 2px var(--shadowHighlight)', + }, + // Checked + disabled + { + state: 'checked', + isDisabled: true, + backgroundColor: '$buttonPrimaryDisabledBg', + borderColor: '$buttonPrimaryDisabledBg', + color: '$buttonPrimaryDisabledColor', + cursor: 'not-allowed', + }, + ], +}); + +// Toggle knob: 14px circle +const ToggleKnob = styled(RcxView, { + name: 'ToggleKnob', + + position: 'absolute', + top: '50%', + transform: 'translateY(-50%)', + + width: 14, // 16 - 2*1 (border) + height: 14, + borderRadius: '$full', + + // Default: on-primary = white + backgroundColor: '$fontPureWhite', + + variants: { + state: { + unchecked: { + left: 1, + }, + checked: { + // calc(100% - 15px) = toggle width(36) - border(2) - knob(14) - offset(1) from right = inset 19px from left + left: 'calc(100% - 15px)', + }, + }, + + isDisabled: { + true: {}, + }, + } as const, + + compoundVariants: [ + // Disabled unchecked: knob uses on-secondary-disabled color + { + state: 'unchecked', + isDisabled: true, + backgroundColor: '$buttonSecondaryDisabledColor', + }, + // Disabled checked: knob stays white (on-primary) + { + state: 'checked', + isDisabled: true, + backgroundColor: '$fontPureWhite', + }, + ], +}); + +// --- Types --- + +export type ToggleSwitchProps = { + labelChildren?: ReactNode; + className?: string; +} & Omit, 'is'>; + +// --- Component --- const ToggleSwitch = forwardRef( - function ToggleSwitch({ className, labelChildren, ...props }, ref) { + function ToggleSwitch({ className, labelChildren, checked, disabled, ...props }, ref) { + const [isFocused, setIsFocused] = useState(false); + const [isHovered, setIsHovered] = useState(false); + const [isActive, setIsActive] = useState(false); + + const state = checked ? 'checked' : 'unchecked'; + return ( - + setIsHovered(true)} + onMouseLeave={() => { + setIsHovered(false); + setIsActive(false); + }} + onMouseDown={() => setIsActive(true)} + onMouseUp={() => setIsActive(false)} + > {labelChildren} - - setIsFocused(true)} + onBlur={() => setIsFocused(false)} + style={{ + position: 'absolute', + width: 1, + height: 1, + padding: 0, + margin: -1, + overflow: 'hidden', + clip: 'rect(0, 0, 0, 0)', + whiteSpace: 'nowrap', + borderWidth: 0, + }} {...props} /> - + + ); }, ); From bf803bb6376ad14f964072882bf6a3e5d917ce99 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 15:02:39 -0300 Subject: [PATCH 39/84] refactor(fuselage): Migrate Modal component from SCSS to Tamagui v3 8 sub-components: ModalFrame, ModalInnerFrame (elevation-2 shadow), ModalBackdrop, ModalHeader, ModalHeaderText, ModalTitle (), ModalTagline (), ModalHeroImage, ModalFooter, ModalFooterAnnotation. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/Modal/Modal.styles.scss | 114 ------------------ .../fuselage/src/components/Modal/Modal.tsx | 61 ++++++++-- .../src/components/Modal/ModalBackdrop.tsx | 20 ++- .../src/components/Modal/ModalFooter.tsx | 22 +++- .../Modal/ModalFooterAnnotation.tsx | 21 +++- .../src/components/Modal/ModalHeader.tsx | 29 ++++- .../src/components/Modal/ModalHeaderText.tsx | 21 +++- .../src/components/Modal/ModalHeroImage.tsx | 31 ++++- .../src/components/Modal/ModalTagline.tsx | 23 +++- .../src/components/Modal/ModalTitle.tsx | 29 ++++- 10 files changed, 214 insertions(+), 157 deletions(-) delete mode 100644 packages/fuselage/src/components/Modal/Modal.styles.scss diff --git a/packages/fuselage/src/components/Modal/Modal.styles.scss b/packages/fuselage/src/components/Modal/Modal.styles.scss deleted file mode 100644 index 8a6aa4b86b..0000000000 --- a/packages/fuselage/src/components/Modal/Modal.styles.scss +++ /dev/null @@ -1,114 +0,0 @@ -@use '../../styles/colors.scss'; -@use '../../styles/lengths.scss'; -@use '../../styles/typography.scss'; - -$modal-container-margin: theme('modal-container-margin', lengths.size(24)); - -$modal-margin: theme('modal-margin', auto); - -.rcx-modal { - position: static; - - display: flex; - - width: 100%; - max-height: 100%; - margin: $modal-margin; - - background: none; - - &__inner { - display: flex; - flex-direction: column; - flex-grow: 1; - - width: 100%; - - min-width: 0; - padding: 0; - - color: colors.font(default); - border-radius: theme('modal-border-radius', lengths.border-radius(large)); - background-color: colors.surface(light); - @include typography.use-font-scale(p2); - } - - &__header { - margin: $modal-container-margin; - - &-text { - display: flex; - flex-direction: column; - flex-grow: 1; - flex-shrink: 1; - @include typography.use-text-ellipsis; - } - } - - &__header-inner { - display: flex; - flex-wrap: nowrap; - - margin: -4px; - } - - &__title { - @include typography.use-text-ellipsis; - flex-grow: 1; - flex-shrink: 1; - - white-space: nowrap; - - color: colors.font(default); - @include typography.use-font-scale(h2); - } - - &__tagline { - color: colors.font(default); - - @include typography.use-font-scale(c2); - } - - &__hero-image { - display: block; - - width: 100%; - height: auto; - object-fit: contain; - - &-wrapper { - margin: 0; - margin-bottom: lengths.size(24); - margin-inline: -(lengths.size(24)); - } - } - - &__backdrop { - position: fixed; - - z-index: 100; - inset: 0; - - display: flex; - flex-direction: column; - - background-color: colors.surface(overlay); - } - - &__footer { - display: flex; - align-items: center; - - margin: $modal-container-margin; - - &-annotation { - color: colors.font(secondary-info); - @include typography.use-font-scale(c1); - } - } - - @include on-breakpoint(sm) { - max-width: lengths.size(640); - padding: lengths.padding(16); - } -} diff --git a/packages/fuselage/src/components/Modal/Modal.tsx b/packages/fuselage/src/components/Modal/Modal.tsx index 97137fa9e1..31070c0c49 100644 --- a/packages/fuselage/src/components/Modal/Modal.tsx +++ b/packages/fuselage/src/components/Modal/Modal.tsx @@ -1,8 +1,40 @@ import type { ElementType, ReactNode } from 'react'; import { createElement, forwardRef } from 'react'; +import { styled } from 'tamagui'; +import { RcxView } from '../../primitives'; import { Box, type BoxProps } from '../Box'; +const ModalFrame = styled(RcxView, { + name: 'Modal', + role: 'dialog', + position: 'static', + display: 'flex', + flexDirection: 'row', + width: '100%', + maxHeight: '100%', + margin: 'auto', + backgroundColor: 'transparent', +}); + +const ModalInnerFrame = styled(RcxView, { + name: 'ModalInner', + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + width: '100%', + minWidth: 0, + padding: 0, + color: '$fontDefault', + borderRadius: '$x8', + backgroundColor: '$surfaceLight', + borderWidth: 1, + borderStyle: 'solid', + borderColor: '$shadowElevationBorder', + boxShadow: + '0px 0px 2px 0px var(--shadowElevation2x), 0px 0px 12px 0px var(--shadowElevation2y)', +}); + export type ModalProps = { wrapperFunction?: ( props: Pick, @@ -12,18 +44,27 @@ export type ModalProps = { const Modal = forwardRef( ({ children, wrapper = Box, wrapperFunction, ...props }, ref) => { - const wrapperProps = { - children, - className: 'rcx-modal__inner', - elevation: '2', - } as const; + if (wrapperFunction || wrapper !== Box) { + // Legacy path: use SCSS-based wrapper + const wrapperProps = { + children, + className: 'rcx-modal__inner', + elevation: '2', + } as const; + + return ( + + {wrapperFunction + ? wrapperFunction(wrapperProps) + : createElement(wrapper, wrapperProps)} + + ); + } return ( - - {wrapperFunction - ? wrapperFunction(wrapperProps) - : createElement(wrapper, wrapperProps)} - + + {children} + ); }, ); diff --git a/packages/fuselage/src/components/Modal/ModalBackdrop.tsx b/packages/fuselage/src/components/Modal/ModalBackdrop.tsx index cb9ab42737..0d01dfa69e 100644 --- a/packages/fuselage/src/components/Modal/ModalBackdrop.tsx +++ b/packages/fuselage/src/components/Modal/ModalBackdrop.tsx @@ -1,9 +1,23 @@ -import { Box, type BoxProps } from '../Box'; +import type { AllHTMLAttributes } from 'react'; +import { styled } from 'tamagui'; -export type ModalBackdropProps = BoxProps; +import { RcxView } from '../../primitives'; + +const ModalBackdropFrame = styled(RcxView, { + name: 'ModalBackdrop', + position: 'fixed', + zIndex: '$x100', + // @ts-ignore - inset shorthand + inset: 0, + display: 'flex', + flexDirection: 'column', + backgroundColor: '$surfaceOverlay', +}); + +export type ModalBackdropProps = AllHTMLAttributes; const ModalBackdrop = (props: ModalBackdropProps) => ( - + ); export default ModalBackdrop; diff --git a/packages/fuselage/src/components/Modal/ModalFooter.tsx b/packages/fuselage/src/components/Modal/ModalFooter.tsx index c1741a8be0..f3306fe44c 100644 --- a/packages/fuselage/src/components/Modal/ModalFooter.tsx +++ b/packages/fuselage/src/components/Modal/ModalFooter.tsx @@ -1,14 +1,28 @@ -import { Box, type BoxProps } from '../Box'; +import type { AllHTMLAttributes } from 'react'; +import { styled } from 'tamagui'; -export type ModalFooterProps = BoxProps; +import { RcxView } from '../../primitives'; + +const ModalFooterFrame = styled(RcxView, { + name: 'ModalFooter', + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + margin: '$x24', +}); + +export type ModalFooterProps = { + justifyContent?: 'start' | 'center' | 'end' | 'space-between'; +} & AllHTMLAttributes; const ModalFooter = ({ children, justifyContent = 'end', + ...props }: ModalFooterProps) => ( - + {children} - + ); export default ModalFooter; diff --git a/packages/fuselage/src/components/Modal/ModalFooterAnnotation.tsx b/packages/fuselage/src/components/Modal/ModalFooterAnnotation.tsx index 2cdae544ba..f20d6e9719 100644 --- a/packages/fuselage/src/components/Modal/ModalFooterAnnotation.tsx +++ b/packages/fuselage/src/components/Modal/ModalFooterAnnotation.tsx @@ -1,9 +1,24 @@ -import { Box, type BoxProps } from '../Box'; +import type { AllHTMLAttributes } from 'react'; +import { styled } from 'tamagui'; -export type ModalFooterAnnotationProps = BoxProps; +import { RcxText } from '../../primitives'; + +const ModalFooterAnnotationFrame = styled(RcxText, { + name: 'ModalFooterAnnotation', + display: 'block', + color: '$fontSecondaryInfo', + fontFamily: '$body', + fontSize: '$c1', + fontWeight: '$c1', + lineHeight: '$c1', + letterSpacing: '$c1', + overflowWrap: 'normal', +}); + +export type ModalFooterAnnotationProps = AllHTMLAttributes; const ModalFooterAnnotation = ({ children }: ModalFooterAnnotationProps) => ( - {children} + {children} ); export default ModalFooterAnnotation; diff --git a/packages/fuselage/src/components/Modal/ModalHeader.tsx b/packages/fuselage/src/components/Modal/ModalHeader.tsx index 9eded5fe40..e5479bd1c1 100644 --- a/packages/fuselage/src/components/Modal/ModalHeader.tsx +++ b/packages/fuselage/src/components/Modal/ModalHeader.tsx @@ -1,14 +1,31 @@ -import { Box, type BoxProps } from '../Box'; +import type { AllHTMLAttributes } from 'react'; +import { styled } from 'tamagui'; + +import { RcxView } from '../../primitives'; import { Margins } from '../Margins'; -export type ModalHeaderProps = BoxProps; +const ModalHeaderFrame = styled(RcxView, { + name: 'ModalHeader', + tag: 'header', + margin: '$x24', +}); + +const ModalHeaderInner = styled(RcxView, { + name: 'ModalHeaderInner', + display: 'flex', + flexDirection: 'row', + flexWrap: 'nowrap', + margin: -4, +}); + +export type ModalHeaderProps = AllHTMLAttributes; const ModalHeader = ({ children, ...props }: ModalHeaderProps) => ( - - + + {children} - - + + ); export default ModalHeader; diff --git a/packages/fuselage/src/components/Modal/ModalHeaderText.tsx b/packages/fuselage/src/components/Modal/ModalHeaderText.tsx index 6f7541d0ff..4c1f7259e3 100644 --- a/packages/fuselage/src/components/Modal/ModalHeaderText.tsx +++ b/packages/fuselage/src/components/Modal/ModalHeaderText.tsx @@ -1,11 +1,24 @@ -import { Box, type BoxProps } from '../Box'; +import type { AllHTMLAttributes } from 'react'; +import { styled } from 'tamagui'; -export type ModalHeaderTextProps = BoxProps; +import { RcxView } from '../../primitives'; + +const ModalHeaderTextFrame = styled(RcxView, { + name: 'ModalHeaderText', + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + flexShrink: 1, + overflow: 'hidden', + textOverflow: 'ellipsis', +}); + +export type ModalHeaderTextProps = AllHTMLAttributes; const ModalHeaderText = ({ children, ...props }: ModalHeaderTextProps) => ( - + {children} - + ); export default ModalHeaderText; diff --git a/packages/fuselage/src/components/Modal/ModalHeroImage.tsx b/packages/fuselage/src/components/Modal/ModalHeroImage.tsx index 31242f4477..5130600a4d 100644 --- a/packages/fuselage/src/components/Modal/ModalHeroImage.tsx +++ b/packages/fuselage/src/components/Modal/ModalHeroImage.tsx @@ -1,11 +1,32 @@ -import { Box, type BoxProps } from '../Box'; +import type { AllHTMLAttributes } from 'react'; +import { styled } from 'tamagui'; -export type ModalHeroImageProps = BoxProps; +import { RcxView } from '../../primitives'; + +const ModalHeroImageWrapper = styled(RcxView, { + name: 'ModalHeroImageWrapper', + tag: 'figure', + margin: 0, + marginBottom: '$x24', + marginInline: -24, +}); + +const ModalHeroImageElement = styled(RcxView, { + name: 'ModalHeroImage', + tag: 'img', + display: 'block', + width: '100%', + height: 'auto', + // @ts-ignore - objectFit for img + objectFit: 'contain', +}); + +export type ModalHeroImageProps = AllHTMLAttributes; const ModalHeroImage = ({ ...props }: ModalHeroImageProps) => ( -
    - -
    + + + ); export default ModalHeroImage; diff --git a/packages/fuselage/src/components/Modal/ModalTagline.tsx b/packages/fuselage/src/components/Modal/ModalTagline.tsx index c7078178b0..837edd103b 100644 --- a/packages/fuselage/src/components/Modal/ModalTagline.tsx +++ b/packages/fuselage/src/components/Modal/ModalTagline.tsx @@ -1,11 +1,26 @@ -import { Box, type BoxProps } from '../Box'; +import type { AllHTMLAttributes } from 'react'; +import { styled } from 'tamagui'; -export type ModalTaglineProps = BoxProps; +import { RcxText } from '../../primitives'; + +const ModalTaglineFrame = styled(RcxText, { + name: 'ModalTagline', + display: 'block', + color: '$fontDefault', + fontFamily: '$body', + fontSize: '$c2', + fontWeight: '$c2', + lineHeight: '$c2', + letterSpacing: '$c2', + overflowWrap: 'normal', +}); + +export type ModalTaglineProps = AllHTMLAttributes; const ModalTagline = ({ children, ...props }: ModalTaglineProps) => ( - + {children} - + ); export default ModalTagline; diff --git a/packages/fuselage/src/components/Modal/ModalTitle.tsx b/packages/fuselage/src/components/Modal/ModalTitle.tsx index 29f9b474ed..f277f72a39 100644 --- a/packages/fuselage/src/components/Modal/ModalTitle.tsx +++ b/packages/fuselage/src/components/Modal/ModalTitle.tsx @@ -1,11 +1,32 @@ -import { Box, type BoxProps } from '../Box'; +import type { AllHTMLAttributes } from 'react'; +import { styled } from 'tamagui'; -export type ModalTitleProps = BoxProps; +import { RcxText } from '../../primitives'; + +const ModalTitleFrame = styled(RcxText, { + name: 'ModalTitle', + tag: 'h2', + display: 'block', + flexGrow: 1, + flexShrink: 1, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + color: '$fontDefault', + fontFamily: '$body', + fontSize: '$h2', + fontWeight: '$h2', + lineHeight: '$h2', + letterSpacing: '$h2', + overflowWrap: 'normal', +}); + +export type ModalTitleProps = AllHTMLAttributes; const ModalTitle = ({ children, ...props }: ModalTitleProps) => ( - + {children} - + ); export default ModalTitle; From 84f9c550bf6626aabf1548e9b2a2ce9133d9b319 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Mar 2026 15:02:40 -0300 Subject: [PATCH 40/84] refactor(fuselage): Migrate NavBar component from SCSS to Tamagui v3 NavBarFrame (flex row, surfaceSidebar bg, strokeLight border). NavBarSection (flex row, auto-divider). NavBarDivider. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/NavBar/NavBar.styles.scss | 24 ------------------- .../fuselage/src/components/NavBar/NavBar.tsx | 23 +++++++++++++++--- .../src/components/NavBar/NavBarDivider.tsx | 2 +- .../src/components/NavBar/NavBarSection.tsx | 14 +++++++++-- 4 files changed, 33 insertions(+), 30 deletions(-) delete mode 100644 packages/fuselage/src/components/NavBar/NavBar.styles.scss diff --git a/packages/fuselage/src/components/NavBar/NavBar.styles.scss b/packages/fuselage/src/components/NavBar/NavBar.styles.scss deleted file mode 100644 index 13e74ffa5a..0000000000 --- a/packages/fuselage/src/components/NavBar/NavBar.styles.scss +++ /dev/null @@ -1,24 +0,0 @@ -@use '../../styles/colors.scss'; -@use '../../styles/lengths.scss'; - -.rcx-navbar { - display: flex; - justify-content: space-between; - align-items: center; - - width: 100%; - padding: lengths.padding(8) lengths.padding(16); - - border-bottom: lengths.border-width(default) solid colors.stroke(light); - - background-color: colors.surface(sidebar); - - &-section { - display: flex; - align-items: center; - } - - &-divider { - border-color: colors.stroke(medium); - } -} diff --git a/packages/fuselage/src/components/NavBar/NavBar.tsx b/packages/fuselage/src/components/NavBar/NavBar.tsx index ee7d8fcad9..905006a6e8 100644 --- a/packages/fuselage/src/components/NavBar/NavBar.tsx +++ b/packages/fuselage/src/components/NavBar/NavBar.tsx @@ -1,9 +1,26 @@ import type { HTMLAttributes } from 'react'; +import { styled } from 'tamagui'; + +import { RcxView } from '../../primitives'; + +const NavBarFrame = styled(RcxView, { + name: 'NavBar', + tag: 'nav', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + width: '100%', + paddingBlock: '$x8', + paddingInline: '$x16', + borderBottomWidth: 1, + borderBottomStyle: 'solid', + borderBottomColor: '$strokeLight', + backgroundColor: '$surfaceSidebar', +}); export type NavBarProps = HTMLAttributes; -const NavBar = (props: NavBarProps) => ( -