From 0e4f33168483e86bb52366787b4c69c2c76b7660 Mon Sep 17 00:00:00 2001 From: ThanishaDewangan Date: Mon, 7 Jul 2025 14:18:39 +0000 Subject: [PATCH 1/8] feat: add tamagui autocomplete support --- README.md | 2 + packages/fuselage-tamagui-tokens/README.md | 1 + packages/fuselage-tamagui/.storybook/main.ts | 35 +++ .../fuselage-tamagui/.storybook/preview.tsx | 31 ++ packages/fuselage-tamagui/README.md | 1 + packages/fuselage-tamagui/package.json | 31 ++ .../src/components/Button/Button.stories.tsx | 216 ++++++++++++++ .../src/components/Button/Button.tsx | 282 ++++++++++++++++++ .../src/components/Button/button.css | 0 packages/fuselage-tamagui/tamagui.config.ts | 193 ++++++++++++ packages/fuselage-tamagui/tsconfig.json | 25 ++ 11 files changed, 817 insertions(+) create mode 100644 packages/fuselage-tamagui-tokens/README.md create mode 100644 packages/fuselage-tamagui/.storybook/main.ts create mode 100644 packages/fuselage-tamagui/.storybook/preview.tsx create mode 100644 packages/fuselage-tamagui/README.md create mode 100644 packages/fuselage-tamagui/package.json create mode 100644 packages/fuselage-tamagui/src/components/Button/Button.stories.tsx create mode 100644 packages/fuselage-tamagui/src/components/Button/Button.tsx create mode 100644 packages/fuselage-tamagui/src/components/Button/button.css create mode 100644 packages/fuselage-tamagui/tamagui.config.ts create mode 100644 packages/fuselage-tamagui/tsconfig.json diff --git a/README.md b/README.md index 4dca772c17..b5e47d2599 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ | 📦 [`@rocket.chat/fuselage`](/packages/fuselage) | Rocket.Chat's React Components Library | [![npm](https://img.shields.io/npm/v/@rocket.chat/fuselage?style=flat-square)](https://www.npmjs.com/package/@rocket.chat/fuselage) | ![deps](https://img.shields.io/librariesio/release/npm/@rocket.chat/fuselage?style=flat-square) | | 📦 [`@rocket.chat/fuselage-hooks`](/packages/fuselage-hooks) | React hooks for Fuselage, Rocket.Chat's design system and UI toolkit | [![npm](https://img.shields.io/npm/v/@rocket.chat/fuselage-hooks?style=flat-square)](https://www.npmjs.com/package/@rocket.chat/fuselage-hooks) | ![deps](https://img.shields.io/librariesio/release/npm/@rocket.chat/fuselage-hooks?style=flat-square) | | 📦 [`@rocket.chat/fuselage-polyfills`](/packages/fuselage-polyfills) | A bundle of useful poly/ponyfills used by fuselage | [![npm](https://img.shields.io/npm/v/@rocket.chat/fuselage-polyfills?style=flat-square)](https://www.npmjs.com/package/@rocket.chat/fuselage-polyfills) | ![deps](https://img.shields.io/librariesio/release/npm/@rocket.chat/fuselage-polyfills?style=flat-square) | +| 📦 [`fuselage-tamagui`](/packages/fuselage-tamagui) | | [![npm](https://img.shields.io/npm/v/fuselage-tamagui?style=flat-square)](https://www.npmjs.com/package/fuselage-tamagui) | ![deps](https://img.shields.io/librariesio/release/npm/fuselage-tamagui?style=flat-square) | +| 📦 [`@rocket.chat/fuselage-tamagui-tokens`](/packages/fuselage-tamagui-tokens) | Tamagui tokens for Fuselage, Rocket.Chat's | [![npm](https://img.shields.io/npm/v/@rocket.chat/fuselage-tamagui-tokens?style=flat-square)](https://www.npmjs.com/package/@rocket.chat/fuselage-tamagui-tokens) | ![deps](https://img.shields.io/librariesio/release/npm/@rocket.chat/fuselage-tamagui-tokens?style=flat-square) | | 📦 [`@rocket.chat/fuselage-toastbar`](/packages/fuselage-toastbar) | Fuselage ToastBar component | [![npm](https://img.shields.io/npm/v/@rocket.chat/fuselage-toastbar?style=flat-square)](https://www.npmjs.com/package/@rocket.chat/fuselage-toastbar) | ![deps](https://img.shields.io/librariesio/release/npm/@rocket.chat/fuselage-toastbar?style=flat-square) | | 📦 [`@rocket.chat/fuselage-tokens`](/packages/fuselage-tokens) | Design tokens for Fuselage, Rocket.Chat's design system | [![npm](https://img.shields.io/npm/v/@rocket.chat/fuselage-tokens?style=flat-square)](https://www.npmjs.com/package/@rocket.chat/fuselage-tokens) | ![deps](https://img.shields.io/librariesio/release/npm/@rocket.chat/fuselage-tokens?style=flat-square) | | 📦 [`@rocket.chat/icons`](/packages/icons) | Rocket.Chat's Icons | [![npm](https://img.shields.io/npm/v/@rocket.chat/icons?style=flat-square)](https://www.npmjs.com/package/@rocket.chat/icons) | ![deps](https://img.shields.io/librariesio/release/npm/@rocket.chat/icons?style=flat-square) | diff --git a/packages/fuselage-tamagui-tokens/README.md b/packages/fuselage-tamagui-tokens/README.md new file mode 100644 index 0000000000..e890bf4de8 --- /dev/null +++ b/packages/fuselage-tamagui-tokens/README.md @@ -0,0 +1 @@ +# fuselage-tamagui-tokens diff --git a/packages/fuselage-tamagui/.storybook/main.ts b/packages/fuselage-tamagui/.storybook/main.ts new file mode 100644 index 0000000000..d90d09afce --- /dev/null +++ b/packages/fuselage-tamagui/.storybook/main.ts @@ -0,0 +1,35 @@ +import type { StorybookConfig } from '@storybook/react-webpack5'; + +import { join, dirname } from "path" + +/** +* This function is used to resolve the absolute path of a package. +* It is needed in projects that use Yarn PnP or are set up within a monorepo. +*/ +function getAbsolutePath(value: string): any { + return dirname(require.resolve(join(value, 'package.json'))) +} +const config: StorybookConfig = { + "stories": [ + "../src/**/*.mdx", + "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)" + ], + "addons": [ + getAbsolutePath('@storybook/addon-webpack5-compiler-swc'), + getAbsolutePath('@storybook/addon-essentials'), + getAbsolutePath('@storybook/addon-onboarding'), + getAbsolutePath('@storybook/addon-interactions'), + ], + "framework": { + "name": getAbsolutePath('@storybook/react-webpack5'), + "options": {} + }, + docs: { + autodocs: true, + }, + typescript: { + reactDocgen: 'react-docgen', + } +}; + +export default config; \ No newline at end of file diff --git a/packages/fuselage-tamagui/.storybook/preview.tsx b/packages/fuselage-tamagui/.storybook/preview.tsx new file mode 100644 index 0000000000..8c35a31e92 --- /dev/null +++ b/packages/fuselage-tamagui/.storybook/preview.tsx @@ -0,0 +1,31 @@ +import type { Preview } from '@storybook/react' +import { TamaguiProvider } from 'tamagui' +import { config } from '../tamagui.config' +import React from 'react' + +const preview: Preview = { + parameters: { + actions: { argTypesRegex: '^on[A-Z].*' }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + docs: { + toc: true, + source: { + excludeDecorators: true, + }, + }, + }, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +export default preview; \ No newline at end of file diff --git a/packages/fuselage-tamagui/README.md b/packages/fuselage-tamagui/README.md new file mode 100644 index 0000000000..cd122346fb --- /dev/null +++ b/packages/fuselage-tamagui/README.md @@ -0,0 +1 @@ +# fuselage-tamagui diff --git a/packages/fuselage-tamagui/package.json b/packages/fuselage-tamagui/package.json new file mode 100644 index 0000000000..c5ab0ff350 --- /dev/null +++ b/packages/fuselage-tamagui/package.json @@ -0,0 +1,31 @@ +{ + "name": "fuselage-tamagui", + "packageManager": "yarn@4.6.0", + "devDependencies": { + "@storybook/addon-essentials": "^8.6.12", + "@storybook/addon-interactions": "^8.6.12", + "@storybook/addon-onboarding": "^8.6.12", + "@storybook/blocks": "^8.6.12", + "@storybook/react": "^8.6.12", + "@storybook/react-webpack5": "^8.6.12", + "@storybook/test": "^8.6.12", + "@types/node": "~24.0.10", + "@types/react": "~19.1.8", + "@types/react-native-web": "^0", + "storybook": "^8.6.12", + "ts-loader": "~9.5.2", + "typescript": "~5.8.3" + }, + "scripts": { + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" + }, + "dependencies": { + "@tamagui/core": "~1.126.5", + "@tamagui/get-button-sized": "~1.130.8", + "@tamagui/get-font-sized": "~1.130.8", + "@tamagui/get-size": "~1.27.3", + "react-native-web": "~0.20.0", + "tamagui": "~1.126.5" + } +} diff --git a/packages/fuselage-tamagui/src/components/Button/Button.stories.tsx b/packages/fuselage-tamagui/src/components/Button/Button.stories.tsx new file mode 100644 index 0000000000..a2f7d0173f --- /dev/null +++ b/packages/fuselage-tamagui/src/components/Button/Button.stories.tsx @@ -0,0 +1,216 @@ +import type { Meta, StoryFn } from "@storybook/react" +import {Button} from "./Button" +import { Spinner , XStack, YStack, Anchor} from 'tamagui' +import { useState } from "react" +const meta: Meta = { + title: "INPUTS/Button", + component: Button, + parameters:{ + layout:"centered", + }, + tags: ["autodocs"], +}; + +export default meta; + +export const Default: StoryFn = () => { + return ( + + ) +}; + +export const Loading: StoryFn = () => { + return ( + ); +}; + + + +export const LoadingInteraction: StoryFn = () => { + const [loading, setLoading] = useState(false); + + const handlePress = () => { + setLoading(true); + // after 2 seconds, set loading to false + // setTimeout(() => { + // setLoading(false); + // }, 2000); + }; + if(loading){ + return ( + + ) + } + return ( + + ); +}; + +export const Variants: StoryFn = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + ) +}; + + + +export const Sizes: StoryFn = () => { + return <> + + + + + + +}; + +export const AsLink: StoryFn = () => { + return ( + + + +) +}; + + +export const States: StoryFn = () => { + return ( + + + + + + + + + + + + + + + + {/* Button Variations (Primary, Secondary, etc.) */} + + + + + + + + + + + + + + + + + + + + + {/* Small Button Variations */} + + + + + + + + + + + + + + + + + + + + ); +}; + +export const AsIconButton: StoryFn = () => { + return ( + +) +}; \ No newline at end of file diff --git a/packages/fuselage-tamagui/src/components/Button/Button.tsx b/packages/fuselage-tamagui/src/components/Button/Button.tsx new file mode 100644 index 0000000000..78471bd374 --- /dev/null +++ b/packages/fuselage-tamagui/src/components/Button/Button.tsx @@ -0,0 +1,282 @@ +import { getSize, getSpace } from '@tamagui/get-token' +import { + GetProps, + SizeTokens, + View, + Text, + createStyledContext, + styled, + useTheme, + withStaticProperties, +} from '@tamagui/web' +import { cloneElement, isValidElement, useContext } from 'react' +import { getTokens } from '@tamagui/core' + +export const ButtonContext = createStyledContext({ + size: '$md' as SizeTokens, +}) + +export const ButtonFrame = styled(View, { + name: 'Button', + context: ButtonContext, + backgroundColor: '$background', + alignItems: 'center', + flexDirection: 'row', + // background: '#353B45', + // backgroundPress: '#4C5362', // darker background on press + // backgroundHover: '#404754', // lighter background on hover + // color: '#353B45', + borderRadius: '$1', + + hoverStyle: { + backgroundColor: '#404754', + borderColor:'none', + cursor: 'pointer', + }, + pressStyle: { + backgroundColor: '#4C5362', + borderColor:'none' + }, + + focusVisibleStyle: { + backgroundColor: '#404754', + // borderColor: '$borderColorFocus', + }, + + variants: { + size: { + '...size': (name, { tokens }) => { + return { + height: tokens.size[name], + // borderRadius: tokens.radius[name], + // note the getSpace and getSize helpers will let you shift down/up token sizes + // whereas with gap we just multiply by 0.2 + // this is a stylistic choice, and depends on your design system values + gap: tokens.space[name].val * 0.2, + paddingHorizontal: getSpace(name, { + shift: -1, + }), + } + }, + }, + Primary:{ + true:{ + backgroundColor:'$primary_button.background', + hoverStyle: { + backgroundColor: '$primary_button.backgroundHover', + borderColor:'none', + cursor: 'pointer', + }, + + pressStyle: { + backgroundColor: '$primary_button.backgroundPress', + borderColor:'none' + }, + + focusVisibleStyle: { + backgroundColor: '$primary_button.backgroundFocus', + }, + }, + }, + Danger:{ + true:{ + backgroundColor:'$danger_button.background', + hoverStyle: { + backgroundColor: '$danger_button.backgroundHover', + borderColor:'none', + cursor: 'pointer', + }, + + pressStyle: { + backgroundColor: '$danger_button.backgroundPress', + borderColor:'none' + }, + + focusVisibleStyle: { + backgroundColor: '$danger_button.backgroundFocus', + }, + + }, + + }, + Warning:{ + true:{ + //font color:'FFFFFF', + backgroundColor:'$warning_button.background', + hoverStyle: { + backgroundColor: '$warning_button.backgroundHover', + borderColor:'none', + cursor: 'pointer', + }, + + pressStyle: { + backgroundColor: '$warning_button.backgroundPress', + borderColor:'none' + }, + + focusVisibleStyle: { + backgroundColor: '$warning_button.backgroundFocus', + }, + + }, + + }, + Success:{ + true:{ + backgroundColor:'$success_button.background', + hoverStyle: { + backgroundColor: '$success_button.backgroundHover', + borderColor:'none', + cursor: 'pointer', + }, + + pressStyle: { + backgroundColor: '$success_button.backgroundPress', + borderColor:'none' + }, + + focusVisibleStyle: { + backgroundColor: '$success_button.backgroundFocus', + }, + + }, + + }, + Secondary:{ + true:{ + backgroundColor:'$secondary_button.background', + hoverStyle: { + backgroundColor: '$secondary_button.backgroundHover', + borderColor:'none', + cursor: 'pointer', + }, + + pressStyle: { + backgroundColor: '$secondary_button.backgroundPress', + borderColor:'none' + }, + + focusVisibleStyle: { + backgroundColor: '$secondary_button.backgroundFocus', + }, + }, + }, + SecondaryDanger:{ + true:{ + backgroundColor:'$secondaryDanger_button.background', + hoverStyle: { + backgroundColor: '$secondaryDanger_button.backgroundHover', + borderColor:'none', + cursor: 'pointer', + }, + + pressStyle: { + backgroundColor: '$secondaryDanger_button.backgroundPress', + borderColor:'none' + }, + + focusVisibleStyle: { + backgroundColor: '$secondaryDanger_button.backgroundFocus', + }, + }, + }, + SecondaryWarning:{ + true:{ + // font color:'FEEFBE', + + backgroundColor:'$secondaryWarning_button.background', + hoverStyle: { + backgroundColor: '$secondaryWarning_button.backgroundHover', + borderColor:'none', + cursor: 'pointer', + }, + + pressStyle: { + backgroundColor: '$secondaryWarning_button.backgroundPress', + borderColor:'none' + }, + + focusVisibleStyle: { + backgroundColor: '$secondaryWarning_button.backgroundFocus', + }, + }, + }, + // 404754,4C5362,404754 + SecondarySuccess:{ + true:{ + backgroundColor:'$secondarySuccess_button.background', + hoverStyle: { + backgroundColor: '$secondarySuccess_button.backgroundHover', + borderColor:'none', + cursor: 'pointer', + }, + + pressStyle: { + backgroundColor: '$secondarySuccess_button.backgroundPress', + borderColor:'none' + }, + + focusVisibleStyle: { + backgroundColor: '$secondarySuccess_button.backgroundFocus', + }, + }, + }, + disabled: { + true: { + cursor: 'not-allowed', + pointerEvents: 'none', + backgroundColor: '$disabled_button.background', + borderRadius: '$2', + hoverStyle: { + cursor: 'not-allowed', + backgroundColor: '$disabled_button.backgroundHover', + borderColor:'none' + }, + + focusVisibleStyle: { + backgroundColor: '$disabled_button.backgroundFocus', + // borderColor: '$borderColorFocus', + }, + }, + }, + } as const, + + defaultVariants: { + size: '$md', + }, +}) + +type ButtonProps = GetProps + +export const ButtonText = styled(Text, { + name: 'ButtonText', + context: ButtonContext, + color: '$color', + userSelect: 'none', + + variants: { + size: { + '...fontSize': (name, { font }) => ({ + fontSize: font?.size[name], + }), + }, + } as const, +}) + +const ButtonIcon = (props: { children: any }) => { + const { size } = useContext(ButtonContext.context) + const smaller = getSize(size, { + shift: -2, + }) + const theme = useTheme() + return isValidElement(props.children) ? cloneElement(props.children, { + size: smaller.val * 0.5, + color: theme.color.get(), + }) : null +} + +export const Button = withStaticProperties(ButtonFrame, { + Props: ButtonContext.Provider, + Text: ButtonText, + Icon: ButtonIcon, +}) \ No newline at end of file diff --git a/packages/fuselage-tamagui/src/components/Button/button.css b/packages/fuselage-tamagui/src/components/Button/button.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/fuselage-tamagui/tamagui.config.ts b/packages/fuselage-tamagui/tamagui.config.ts new file mode 100644 index 0000000000..531d82df4d --- /dev/null +++ b/packages/fuselage-tamagui/tamagui.config.ts @@ -0,0 +1,193 @@ +import { createFont, createTamagui, createTokens, isWeb } from 'tamagui'; + +// To work with the tamagui UI kit styled components (which is optional) +// you'd want the keys used for `size`, `lineHeight`, `weight` and +// `letterSpacing` to be consistent. The `createFont` function +// will fill-in any missing values if `lineHeight`, `weight` or +// `letterSpacing` are subsets of `size`. +const systemFont = createFont({ + family: isWeb ? 'Helvetica, Arial, sans-serif' : 'System', + size: { + 1: 12, + 2: 14, + 3: 15, + }, + lineHeight: { + // 1 will be 22 + 2: 22, + }, + weight: { + 1: '300', + // 2 will be 300 + 3: '600', + }, + letterSpacing: { + 1: 0, + 2: -1, + // 3 will be -1 + }, + // (native only) swaps out fonts by face/style + face: { + 300: { normal: 'InterLight', italic: 'InterItalic' }, + 600: { normal: 'InterBold' }, + }, +}); + +// Set up tokens + +// The keys can be whatever you want, but if using `tamagui` you'll want 1-10: + +const size = { + small: 20, + medium: 30, + true: 30, // note true = 30 just like medium, your default size token + large: 40, +}; + +export const tokens = createTokens({ + primary_button: { + background: '#095AD2', + backgroundHover: '#10529E', + backgroundPress: '#01336B', + backgroundFocus: '#095AD2', + }, + danger_button: { + background: '#BB3E4E', + backgroundHover: '#95323F', + backgroundPress: '#822C37', + backgroundFocus: '#BB3E4E', + }, + warning_button: { + background: '#B08C30', + backgroundHover: '#C7AA66', + backgroundPress: '#B08C30', + backgroundFocus: '#095AD2', + }, + success_button: { + background: '#1D7256', + backgroundHover: '#175943', + backgroundPress: '#134937', + backgroundFocus: '#1D7256', + }, + secondary_button: { + background: '#353B45', + backgroundHover: '#404754', + backgroundPress: '#4C5362', + backgroundFocus: '#353B45', + }, + secondaryDanger_button: { + background: '#353B45', + backgroundHover: '#404754', + backgroundPress: '#4C5362', + backgroundFocus: '#353B45', + }, + secondaryWarning_button: { + background: '#e1e1e1', + backgroundHover: '#404754', + backgroundPress: '#e1e1e1', + backgroundFocus: '#404754', + }, + secondarySuccess_button: { + background: '#e1e1e1', + backgroundHover: '#404754', + backgroundPress: '#4C5362', + backgroundFocus: '#404754', + }, + disabled_button: { + background: '#353B45', + backgroundHover: '#404754', + backgroundPress: '#4C5362', + backgroundFocus: '#353B45', + }, + + size: { + sm: 38, + md: 46, + true: 46, + lg: 60, + }, + space: { + sm: 15, + md: 20, + true: 20, + lg: 25, + }, + radius: { + sm: 4, + md: 8, + true: 8, + lg: 12, + }, + + zIndex: { small: 0, medium: 100, true: 3, large: 1000 }, + color: { + white: '#fff', + black: '#000', + }, +}); + +export const config = createTamagui({ + fonts: { + heading: systemFont, + body: systemFont, + }, + tokens, + themes: { + light: { + bg: '#fff', + color: '#000', + }, + dark: { + bg: '#111', + color: tokens.color.white, + }, + light_Button: { + background: '#353B45', + backgroundPress: '#4C5362', // darker background on press + backgroundHover: '#404754', // lighter background on hover + color: '#353B45', + }, + primary_Button: { + background: '#095AD2', + backgroundHover: '#10529E', + backgroundPress: '#01336B', + backgroundFocus: '#095AD2', + color: '#fff', + }, + }, + media: { + sm: { maxWidth: 860 }, + gtSm: { minWidth: 860 + 1 }, + short: { maxHeight: 820 }, + hoverNone: { hover: 'none' }, + pointerCoarse: { pointer: 'coarse' }, + }, + + // Shorthands + // Adds to + // See Settings section on this page to only allow shorthands + // Be sure to have `as const` at the end + shorthands: { + px: 'paddingHorizontal', + f: 'flex', + m: 'margin', + w: 'width', + } as const, + + // Change the default props for any styled() component with a name. + // We are discouraging the use of this and have deprecated it, prefer to use + // styled() on any component to change it's styles. + defaultProps: { + Text: { + color: 'green', + }, + }, +}); + +type AppConfig = typeof config; + +// this will give you types for your components +// note - if using your own design system, put the package name here instead of tamagui +declare module 'tamagui' { + interface TamaguiCustomConfig extends AppConfig {} +} diff --git a/packages/fuselage-tamagui/tsconfig.json b/packages/fuselage-tamagui/tsconfig.json new file mode 100644 index 0000000000..449a6cc64f --- /dev/null +++ b/packages/fuselage-tamagui/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDirs": ["./src", "./.storybook"], + "target": "ES5", + "module": "CommonJS", + "lib": ["ES2020", "DOM"], + "downlevelIteration": true, + "outDir": "./dist", + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "allowJs": true, + "jsx": "react-jsx" + }, + "include": [ + "./src", + "./.storybook/**/*", + "./jest.config.ts", + "./jest-setup.ts" + ], + "exclude": ["./dist", "./storybook-static/", "./*.js"] + } + \ No newline at end of file From 3ca39030b145b7ee709c93b7c658b3aa9a2bc289 Mon Sep 17 00:00:00 2001 From: ThanishaDewangan Date: Mon, 7 Jul 2025 15:06:52 +0000 Subject: [PATCH 2/8] feat: add new components --- package.json | 7 + packages/fuselage-tamagui-tokens/package.json | 52 + packages/fuselage-tamagui-tokens/src/index.ts | 168 + .../AnimatedVisibility.stories.tsx | 40 + .../AnimatedVisibility/AnimatedVisibility.tsx | 57 + .../components/AnimatedVisibility/index.ts | 2 + .../AutoComplete/AutoComplete.stories.tsx | 187 + .../components/AutoComplete/AutoComplete.tsx | 217 + .../src/components/AutoComplete/index.ts | 1 + .../src/components/Box/Box.spec.tsx | 27 + .../src/components/Box/Box.stories.tsx | 117 + .../src/components/Box/Box.styles.scss | 28 + .../src/components/Box/Box.tsx | 41 + .../src/components/Box/BoxTransforms.tsx | 7 + .../src/components/Box/StylingBox.tsx | 12 + .../src/components/Box/index.ts | 4 + .../components/Box/stories/Colors.stories.tsx | 47 + .../src/components/Box/stories/Is.stories.tsx | 39 + .../components/Box/stories/Layout.stories.tsx | 149 + .../Box/stories/RichContent.stories.tsx | 29 + .../src/components/CheckBox/CheckBox.spec.tsx | 41 + .../components/CheckBox/CheckBox.stories.tsx | 127 + .../src/components/CheckBox/CheckBox.tsx | 167 + .../src/components/CheckBox/index.ts | 1 + .../components/EmailInput/EmailInput.spec.tsx | 50 + .../EmailInput/EmailInput.stories.tsx | 84 + .../src/components/EmailInput/EmailInput.tsx | 81 + .../src/components/EmailInput/index.ts | 1 + .../src/components/Icon/IconButton.spec.tsx | 17 + .../components/Icon/IconButton.stroies.tsx | 100 + .../src/components/Icon/IconButton.tsx | 59 + .../src/components/Icon/index.ts | 1 + .../src/components/Label/Label.stories.tsx | 91 + .../src/components/Label/Label.tsx | 42 + .../src/components/Label/index.ts | 1 + .../src/hooks/useArrayLikeClassNameProp.ts | 20 + .../src/hooks/useBoxOnlyProps.ts | 13 + packages/fuselage-tamagui/src/types/Falsy.ts | 1 + yarn.lock | 5615 ++++++++++++++++- 39 files changed, 7586 insertions(+), 157 deletions(-) create mode 100644 packages/fuselage-tamagui-tokens/package.json create mode 100644 packages/fuselage-tamagui-tokens/src/index.ts create mode 100644 packages/fuselage-tamagui/src/components/AnimatedVisibility/AnimatedVisibility.stories.tsx create mode 100644 packages/fuselage-tamagui/src/components/AnimatedVisibility/AnimatedVisibility.tsx create mode 100644 packages/fuselage-tamagui/src/components/AnimatedVisibility/index.ts create mode 100644 packages/fuselage-tamagui/src/components/AutoComplete/AutoComplete.stories.tsx create mode 100644 packages/fuselage-tamagui/src/components/AutoComplete/AutoComplete.tsx create mode 100644 packages/fuselage-tamagui/src/components/AutoComplete/index.ts create mode 100644 packages/fuselage-tamagui/src/components/Box/Box.spec.tsx create mode 100644 packages/fuselage-tamagui/src/components/Box/Box.stories.tsx create mode 100644 packages/fuselage-tamagui/src/components/Box/Box.styles.scss create mode 100644 packages/fuselage-tamagui/src/components/Box/Box.tsx create mode 100644 packages/fuselage-tamagui/src/components/Box/BoxTransforms.tsx create mode 100644 packages/fuselage-tamagui/src/components/Box/StylingBox.tsx create mode 100644 packages/fuselage-tamagui/src/components/Box/index.ts create mode 100644 packages/fuselage-tamagui/src/components/Box/stories/Colors.stories.tsx create mode 100644 packages/fuselage-tamagui/src/components/Box/stories/Is.stories.tsx create mode 100644 packages/fuselage-tamagui/src/components/Box/stories/Layout.stories.tsx create mode 100644 packages/fuselage-tamagui/src/components/Box/stories/RichContent.stories.tsx create mode 100644 packages/fuselage-tamagui/src/components/CheckBox/CheckBox.spec.tsx create mode 100644 packages/fuselage-tamagui/src/components/CheckBox/CheckBox.stories.tsx create mode 100644 packages/fuselage-tamagui/src/components/CheckBox/CheckBox.tsx create mode 100644 packages/fuselage-tamagui/src/components/CheckBox/index.ts create mode 100644 packages/fuselage-tamagui/src/components/EmailInput/EmailInput.spec.tsx create mode 100644 packages/fuselage-tamagui/src/components/EmailInput/EmailInput.stories.tsx create mode 100644 packages/fuselage-tamagui/src/components/EmailInput/EmailInput.tsx create mode 100644 packages/fuselage-tamagui/src/components/EmailInput/index.ts create mode 100644 packages/fuselage-tamagui/src/components/Icon/IconButton.spec.tsx create mode 100644 packages/fuselage-tamagui/src/components/Icon/IconButton.stroies.tsx create mode 100644 packages/fuselage-tamagui/src/components/Icon/IconButton.tsx create mode 100644 packages/fuselage-tamagui/src/components/Icon/index.ts create mode 100644 packages/fuselage-tamagui/src/components/Label/Label.stories.tsx create mode 100644 packages/fuselage-tamagui/src/components/Label/Label.tsx create mode 100644 packages/fuselage-tamagui/src/components/Label/index.ts create mode 100644 packages/fuselage-tamagui/src/hooks/useArrayLikeClassNameProp.ts create mode 100644 packages/fuselage-tamagui/src/hooks/useBoxOnlyProps.ts create mode 100644 packages/fuselage-tamagui/src/types/Falsy.ts diff --git a/package.json b/package.json index 6188e48f32..1045706756 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@changesets/cli": "~2.29.4", "@eslint/js": "~9.29.0", "@rocket.chat/prettier-config": "workspace:~", + "@tamagui/get-button-sized": "~1.130.8", "eslint": "~9.29.0", "eslint-import-resolver-typescript": "~4.4.3", "eslint-plugin-import": "~2.31.0", @@ -57,5 +58,11 @@ "volta": { "node": "22.16.0", "yarn": "4.9.2" + }, + "dependencies": { + "@tamagui/core": "~1.130.8", + "@tamagui/portal": "~1.130.8", + "@tamagui/stacks": "~1.130.8", + "tamagui": "~1.130.8" } } diff --git a/packages/fuselage-tamagui-tokens/package.json b/packages/fuselage-tamagui-tokens/package.json new file mode 100644 index 0000000000..957b3792b7 --- /dev/null +++ b/packages/fuselage-tamagui-tokens/package.json @@ -0,0 +1,52 @@ +{ + "name": "@rocket.chat/fuselage-tamagui-tokens", + "version": "0.33.2", + "description": "Tamagui tokens for Fuselage, Rocket.Chat's", + "homepage": "https://rocketchat.github.io/Rocket.Chat.Fuselage/", + "author": { + "name": "Rocket.Chat", + "url": "https://rocket.chat/" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/RocketChat/fuselage.git", + "directory": "packages/fuselage-tamagui-tokens" + }, + "bugs": { + "url": "https://github.com/RocketChat/fuselage/issues" + }, + "keywords": [ + "design", + "tokens", + "fuselage", + "tamagui", + "rocket.chat" + ], + "main": "./src/index.ts", + "publishConfig": { + "access": "public" + }, + "type": "module", + "devDependencies": { + "build-design-tokens": "workspace:~", + "eslint": "~9.21.0", + "eslint-config-prettier": "~8.8.0", + "lint-all": "workspace:~", + "npm-run-all": "^4.1.5", + "postcss-scss": "~4.0.9", + "prettier": "~3.5.2", + "rimraf": "^3.0.2", + "style-dictionary": "~4.3.3", + "stylelint": "~16.14.1", + "stylelint-order": "~6.0.4", + "stylelint-prettier": "~5.0.3", + "stylelint-scss": "~6.11.1" + }, + "dependencies": { + "@tamagui/core": "^1.125.22" + }, + "peerDependencies": { + "@tamagui/core": "*" + } +} diff --git a/packages/fuselage-tamagui-tokens/src/index.ts b/packages/fuselage-tamagui-tokens/src/index.ts new file mode 100644 index 0000000000..55052f3443 --- /dev/null +++ b/packages/fuselage-tamagui-tokens/src/index.ts @@ -0,0 +1,168 @@ +import buttons from '@rocket.chat/fuselage-tokens/dist/button.json'; +import fonts from '@rocket.chat/fuselage-tokens/dist/font.json'; +import shadows from '@rocket.chat/fuselage-tokens/dist/shadow.json'; +import strokes from '@rocket.chat/fuselage-tokens/dist/stroke.json'; +import surfaces from '@rocket.chat/fuselage-tokens/dist/surface.json'; +import { createTamagui } from '@tamagui/core'; + +const { surface } = surfaces; +const { button } = buttons; +const { font } = fonts; +const { shadow } = shadows; +const { stroke } = strokes; + +// Keep your reMapSurface function as is +const reMapSurface = ( + prefix: K, + surface: S, +): { + [I in keyof S as `${K}-${I extends string ? I : never}`]: S[I]; +} => { + return Object.keys(surface).reduce((acc, key) => { + acc[`${prefix}-${key}`] = surface[key]; + return acc; + }, {} as any); +}; + +// Add these essential Tamagui tokens +const tokens = { + size: { + 0: 0, + 1: 4, + 2: 8, + 3: 16, + 4: 24, + 5: 32, + 6: 44, + 7: 56, + 8: 68, + 9: 80, + 10: 96, + true: 1, + }, + space: { + 0: 0, + 1: 4, + 2: 8, + 3: 16, + 4: 24, + 5: 32, + 6: 44, + 7: 56, + 8: 68, + 9: 80, + 10: 96, + true: 1, + }, + radius: { + 0: 0, + 1: 4, + 2: 8, + 3: 12, + 4: 16, + 5: 20, + 6: 24, + 7: 28, + 8: 32, + 9: 36, + 10: 40, + true: 1, + }, + zIndex: { + 0: 0, + 1: 100, + 2: 200, + 3: 300, + 4: 400, + 5: 500, + dropdown: 1000, + modal: 1300, + tooltip: 1500, + }, +}; + +export const config = createTamagui({ + tokens, + themes: { + light: { + ...reMapSurface('surface', surface.light), + ...reMapSurface('shadow', shadow.light), + ...reMapSurface('stroke', stroke.light), + ...reMapSurface('font', font.light), + ...reMapSurface('button', button.light), + backgroundColor: surface.light.neutral, + background: surface.light.neutral, + borderColor: stroke.light.medium, + // Add essential theme colors + color: font.light.default, + color1: font.light.info, + color2: font.light.hint, + color3: font.light.disabled, + background1: surface.light.tint, + background2: surface.light.shade, + }, + dark: { + ...reMapSurface('surface', surface.dark), + ...reMapSurface('shadow', shadow.dark), + ...reMapSurface('stroke', stroke.dark), + ...reMapSurface('font', font.dark), + ...reMapSurface('button', button.dark), + backgroundColor: surface.dark.neutral, + background: surface.dark.neutral, + borderColor: stroke.dark.medium, + // Add essential theme colors + color: font.dark.default, + color1: font.dark.info, + color2: font.dark.hint, + color3: font.dark.disabled, + background1: surface.dark.tint, + background2: surface.dark.shade, + }, + }, + // Add essential Tamagui configurations + defaultFont: 'body', + animations: { + fast: { + type: 'spring', + damping: 20, + mass: 1.2, + stiffness: 250, + }, + medium: { + type: 'spring', + damping: 10, + mass: 0.9, + stiffness: 100, + }, + slow: { + type: 'spring', + damping: 20, + stiffness: 60, + }, + }, + media: { + xs: { maxWidth: 660 }, + sm: { maxWidth: 800 }, + md: { maxWidth: 1020 }, + lg: { maxWidth: 1280 }, + xl: { maxWidth: 1420 }, + xxl: { maxWidth: 1600 }, + gtXs: { minWidth: 660 + 1 }, + gtSm: { minWidth: 800 + 1 }, + gtMd: { minWidth: 1020 + 1 }, + gtLg: { minWidth: 1280 + 1 }, + short: { maxHeight: 820 }, + tall: { minHeight: 820 }, + hoverNone: { hover: 'none' }, + pointerCoarse: { pointer: 'coarse' }, + }, +}); + +export default config; + +export type AppConfig = typeof config; + +declare module '@tamagui/core' { + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + interface TamaguiCustomConfig extends AppConfig {} +} diff --git a/packages/fuselage-tamagui/src/components/AnimatedVisibility/AnimatedVisibility.stories.tsx b/packages/fuselage-tamagui/src/components/AnimatedVisibility/AnimatedVisibility.stories.tsx new file mode 100644 index 0000000000..0eb0748ca3 --- /dev/null +++ b/packages/fuselage-tamagui/src/components/AnimatedVisibility/AnimatedVisibility.stories.tsx @@ -0,0 +1,40 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { YStack } from 'tamagui'; + +import { AnimatedVisibility } from './AnimatedVisibility'; + +const meta = { + title: 'Layout/AnimatedVisibility', + component: AnimatedVisibility, + parameters: { + docs: { + description: { + component: 'AnimatedVisibility', + }, + }, + }, + argTypes: { + visibility: { + description: '"hidden" "visible" "hiding" "unhiding"', + control: 'radio', + options: ['visible', 'hidden', 'hiding', 'unhiding'], + table: { + defaultValue: { summary: '-' }, + }, + }, + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Example: Story = { + args: { + children: ( + + Visible + + ), + }, +}; diff --git a/packages/fuselage-tamagui/src/components/AnimatedVisibility/AnimatedVisibility.tsx b/packages/fuselage-tamagui/src/components/AnimatedVisibility/AnimatedVisibility.tsx new file mode 100644 index 0000000000..67d47b957e --- /dev/null +++ b/packages/fuselage-tamagui/src/components/AnimatedVisibility/AnimatedVisibility.tsx @@ -0,0 +1,57 @@ +import type { ReactNode } from 'react'; +import { useEffect, useState } from 'react'; +import { styled, Stack, Theme } from 'tamagui'; + +export type VisibilityType = 'hidden' | 'visible' | 'hiding' | 'unhiding'; + +type AnimatedVisibilityProps = { + children: ReactNode; + visibility?: VisibilityType; +}; + +const AnimatedStack = styled(Stack, { + animation: '230ms', + opacity: 1, + transform: [{ translateY: 0 }], + + variants: { + visibility: { + hiding: { + animation: 'quick', + opacity: 0, + transform: [{ translateY: 16 }], + }, + unhiding: { + animation: 'quick', + opacity: 1, + transform: [{ translateY: 0 }], + }, + }, + } as const, +}); + +export const AnimatedVisibility = ({ + children, + visibility = 'visible', +}: AnimatedVisibilityProps) => { + const [currentVisibility, setVisibility] = + useState(visibility); + + useEffect(() => { + setVisibility(visibility); + }, [visibility]); + + if (currentVisibility === 'hidden') { + return null; + } + + return ( + + + {children} + + + ); +}; diff --git a/packages/fuselage-tamagui/src/components/AnimatedVisibility/index.ts b/packages/fuselage-tamagui/src/components/AnimatedVisibility/index.ts new file mode 100644 index 0000000000..70128dc91e --- /dev/null +++ b/packages/fuselage-tamagui/src/components/AnimatedVisibility/index.ts @@ -0,0 +1,2 @@ +export { AnimatedVisibility } from './AnimatedVisibility'; +export type { VisibilityType } from './AnimatedVisibility'; diff --git a/packages/fuselage-tamagui/src/components/AutoComplete/AutoComplete.stories.tsx b/packages/fuselage-tamagui/src/components/AutoComplete/AutoComplete.stories.tsx new file mode 100644 index 0000000000..87431f64f7 --- /dev/null +++ b/packages/fuselage-tamagui/src/components/AutoComplete/AutoComplete.stories.tsx @@ -0,0 +1,187 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { useState } from 'react'; +import { XStack, Avatar, Button, Text } from 'tamagui'; + +import { AutoComplete } from './AutoComplete'; + +const meta = { + title: 'INPUTS/AutoComplete', + component: AutoComplete, + tags: ['autodocs'], // This is important for documentation generation + parameters: { + layout: 'centered', + }, + argTypes: { + value: { + description: 'The selected value(s)', + control: 'text', + table: { + type: { summary: 'string | string[]' }, + }, + }, + filter: { + description: 'The current filter text', + control: 'text', + }, + setFilter: { + description: 'Callback to update the filter value', + table: { + type: { summary: '(filter: string) => void' }, + }, + }, + options: { + description: 'Array of options to select from', + control: 'object', + }, + multiple: { + description: 'Allow multiple selections', + control: 'boolean', + table: { + defaultValue: { summary: false }, + }, + }, + error: { + description: 'Show error state', + control: 'boolean', + }, + disabled: { + description: 'Disable the input', + control: 'boolean', + }, + placeholder: { + description: 'Placeholder text', + control: 'text', + }, + }, +} satisfies Meta; + +export default meta; + +// Rest of your stories code remains the same... +type Story = StoryObj; + +const options = [ + { value: '1', label: 'test1' }, + { value: '2', label: 'test2' }, + { value: '3', label: 'test3' }, + { value: '4', label: 'test4' }, +]; + +// Example Avatar URL +const exampleAvatar = 'https://via.placeholder.com/40'; + +const Template: Story = { + render: ({ value: defaultValue, ...args }) => { + const [filter, setFilter] = useState(''); + const [value, setValue] = useState(defaultValue || []); + + return ( + + ); + }, +}; + +/** + * Basic autocomplete with default styling + */ +export const Default: Story = { + ...Template, +}; + +/** + * Autocomplete with custom selected item rendering including an avatar + */ +export const CustomSelected: Story = { + ...Template, + args: { + value: '1', + renderSelected: ({ selected, onRemove }) => ( + + ), + }, +}; + +/** + * Multiple selection support + */ +export const Multiple: Story = { + ...Template, + args: { + value: ['1', '3'], + multiple: true, + }, +}; + +/** + * Multiple selection with custom item rendering + */ +export const MultipleCustomSelected: Story = { + ...Template, + args: { + value: ['1', '3'], + multiple: true, + renderSelected: ({ selected, onRemove }) => ( + + ), + }, +}; + +const Option = ({ value, label, onSelect, avatar }) => ( + +); + +/** + * Custom option rendering in the dropdown + */ +export const CustomItem: Story = { + ...Template, + args: { + renderItem: (props) =>