From f5b0926b6350982cd5e901d3343c1b8e03593e0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?= Date: Thu, 16 Jan 2025 10:03:05 +0100 Subject: [PATCH] feat: record group chevron button (#9645) This ticket is related to this Discord post https://discord.com/channels/1130383047699738754/1328756649657110559 --- package.json | 2 +- .../RecordTableRecordGroupSection.tsx | 20 +-- .../src/display/icon/types/IconComponent.ts | 2 +- .../components/AnimatedLightIconButton.tsx | 129 ++++++++++++++++++ .../button/components/LightIconButton.tsx | 1 + .../AnimatedLightIconButton.stories.tsx | 108 +++++++++++++++ packages/twenty-ui/src/input/index.ts | 1 + yarn.lock | 37 +++-- 8 files changed, 279 insertions(+), 21 deletions(-) create mode 100644 packages/twenty-ui/src/input/button/components/AnimatedLightIconButton.tsx create mode 100644 packages/twenty-ui/src/input/button/components/__stories__/AnimatedLightIconButton.stories.tsx diff --git a/package.json b/package.json index f9b0f86ca..fd0b704b6 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "esbuild-plugin-svgr": "^2.1.0", "facepaint": "^1.2.1", "file-type": "16.5.4", - "framer-motion": "^10.12.17", + "framer-motion": "^11.18.0", "googleapis": "105", "graphiql": "^3.1.1", "graphql": "16.8.0", diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx index ea1da6afd..1c3bc7c76 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx @@ -1,8 +1,12 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { motion } from 'framer-motion'; import { useCallback } from 'react'; -import { IconChevronDown, isDefined, Tag } from 'twenty-ui'; +import { + AnimatedLightIconButton, + IconChevronDown, + isDefined, + Tag, +} from 'twenty-ui'; import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId'; import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; @@ -84,15 +88,13 @@ export const RecordTableRecordGroupSection = () => { - - - + /> ; diff --git a/packages/twenty-ui/src/input/button/components/AnimatedLightIconButton.tsx b/packages/twenty-ui/src/input/button/components/AnimatedLightIconButton.tsx new file mode 100644 index 000000000..077141859 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/AnimatedLightIconButton.tsx @@ -0,0 +1,129 @@ +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { IconComponent } from '@ui/display'; +import { + LightIconButtonAccent, + LightIconButtonSize, +} from '@ui/input/button/components/LightIconButton'; +import { motion, MotionProps } from 'framer-motion'; +import { ComponentProps, MouseEvent } from 'react'; + +export type AnimatedLightIconButtonProps = { + className?: string; + testId?: string; + Icon?: IconComponent; + title?: string; + size?: LightIconButtonSize; + accent?: LightIconButtonAccent; + active?: boolean; + disabled?: boolean; + focus?: boolean; + onClick?: (event: MouseEvent) => void; +} & Pick, 'aria-label' | 'title'> & + Pick; + +const StyledButton = styled.button< + Pick +>` + align-items: center; + background: transparent; + border: none; + + border: ${({ disabled, theme, focus }) => + !disabled && focus ? `1px solid ${theme.color.blue}` : 'none'}; + border-radius: ${({ theme }) => theme.border.radius.sm}; + box-shadow: ${({ disabled, theme, focus }) => + !disabled && focus ? `0 0 0 3px ${theme.color.blue10}` : 'none'}; + color: ${({ theme, accent, active, disabled, focus }) => { + switch (accent) { + case 'secondary': + return active || focus + ? theme.color.blue + : !disabled + ? theme.font.color.secondary + : theme.font.color.extraLight; + case 'tertiary': + return active || focus + ? theme.color.blue + : !disabled + ? theme.font.color.tertiary + : theme.font.color.extraLight; + } + }}; + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; + display: flex; + flex-direction: row; + + font-family: ${({ theme }) => theme.font.family}; + font-weight: ${({ theme }) => theme.font.weight.regular}; + gap: ${({ theme }) => theme.spacing(1)}; + height: ${({ size }) => (size === 'small' ? '24px' : '32px')}; + justify-content: center; + padding: ${({ theme }) => theme.spacing(1)}; + transition: background 0.1s ease; + + white-space: nowrap; + width: ${({ size }) => (size === 'small' ? '24px' : '32px')}; + min-width: ${({ size }) => (size === 'small' ? '24px' : '32px')}; + + &:hover { + background: ${({ theme, disabled }) => + !disabled ? theme.background.transparent.light : 'transparent'}; + } + + &:focus { + outline: none; + } + + &:active { + background: ${({ theme, disabled }) => + !disabled ? theme.background.transparent.medium : 'transparent'}; + } +`; + +const StyledIconContainer = styled(motion.div)` + display: flex; + align-items: center; + justify-content: center; +`; + +export const AnimatedLightIconButton = ({ + 'aria-label': ariaLabel, + className, + testId, + animate, + transition, + Icon, + active = false, + size = 'small', + accent = 'secondary', + disabled = false, + focus = false, + onClick, + title, +}: AnimatedLightIconButtonProps) => { + const theme = useTheme(); + + return ( + + + {Icon && ( + + )} + + + ); +}; diff --git a/packages/twenty-ui/src/input/button/components/LightIconButton.tsx b/packages/twenty-ui/src/input/button/components/LightIconButton.tsx index 915ae2b2d..fedf7fcd9 100644 --- a/packages/twenty-ui/src/input/button/components/LightIconButton.tsx +++ b/packages/twenty-ui/src/input/button/components/LightIconButton.tsx @@ -92,6 +92,7 @@ export const LightIconButton = ({ title, }: LightIconButtonProps) => { const theme = useTheme(); + return ( = { + title: 'UI/Input/Button/AnimatedLightIconButton', + component: AnimatedLightIconButton, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + title: 'Filter', + accent: 'secondary', + disabled: false, + active: false, + focus: false, + Icon: IconSearch, + animate: { scale: 1.2 }, + transition: { duration: 0.3 }, + }, + argTypes: { + Icon: { control: false }, + animate: { control: 'object' }, + transition: { control: 'object' }, + }, + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory = { + args: { + title: 'Filter', + Icon: IconSearch, + animate: { scale: 1.2 }, + transition: { duration: 0.3 }, + }, + argTypes: { + accent: { control: false }, + disabled: { control: false }, + active: { control: false }, + focus: { control: false }, + animate: { control: 'object' }, + transition: { control: 'object' }, + }, + parameters: { + pseudo: { hover: ['.hover'], active: ['.pressed'] }, + catalog: { + dimensions: [ + { + name: 'states', + values: [ + 'default', + 'hover', + 'pressed', + 'disabled', + 'active', + 'focus', + 'disabled+focus', + 'disabled+active', + ], + props: (state: string) => { + switch (state) { + case 'default': + return {}; + case 'hover': + case 'pressed': + return { className: state }; + case 'focus': + return { focus: true }; + case 'disabled': + return { disabled: true }; + case 'active': + return { active: true }; + case 'disabled+focus': + return { disabled: true, focus: true }; + case 'disabled+active': + return { disabled: true, active: true }; + default: + return {}; + } + }, + }, + { + name: 'accents', + values: ['secondary', 'tertiary'] satisfies LightIconButtonAccent[], + props: (accent: LightIconButtonAccent) => ({ accent }), + }, + { + name: 'sizes', + values: ['small', 'medium'] satisfies LightIconButtonSize[], + props: (size: LightIconButtonSize) => ({ size }), + }, + ], + }, + }, + decorators: [CatalogDecorator], +}; diff --git a/packages/twenty-ui/src/input/index.ts b/packages/twenty-ui/src/input/index.ts index 581bcb2e7..1c9282835 100644 --- a/packages/twenty-ui/src/input/index.ts +++ b/packages/twenty-ui/src/input/index.ts @@ -1,3 +1,4 @@ +export * from './button/components/AnimatedLightIconButton'; export * from './button/components/Button'; export * from './button/components/ButtonGroup'; export * from './button/components/ColorPickerButton'; diff --git a/yarn.lock b/yarn.lock index 352de163a..2b068a3c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27645,24 +27645,25 @@ __metadata: languageName: node linkType: hard -"framer-motion@npm:^10.12.17": - version: 10.18.0 - resolution: "framer-motion@npm:10.18.0" +"framer-motion@npm:^11.18.0": + version: 11.18.0 + resolution: "framer-motion@npm:11.18.0" dependencies: - "@emotion/is-prop-valid": "npm:^0.8.2" + motion-dom: "npm:^11.16.4" + motion-utils: "npm:^11.16.0" tslib: "npm:^2.4.0" peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - dependenciesMeta: + "@emotion/is-prop-valid": "*" + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: "@emotion/is-prop-valid": optional: true - peerDependenciesMeta: react: optional: true react-dom: optional: true - checksum: 10c0/0aea1b3dc5cf06687e31f3b6c0b6b1a2cd070afdd4a9d38ebf15715c662ca1d6d1c25e6778695e5ebff37a6ce92b031d036c02570370e6057e66aa9de9f9370f + checksum: 10c0/7f3c1e420bca2d920b7f48dfb54b072938771f9237feed02d576884398f4a68ccb2d1ae36b28cf2410dbfe2db6edb4c03429a1d896e789d08a972360c6ad82b1 languageName: node linkType: hard @@ -35868,6 +35869,22 @@ __metadata: languageName: node linkType: hard +"motion-dom@npm:^11.16.4": + version: 11.16.4 + resolution: "motion-dom@npm:11.16.4" + dependencies: + motion-utils: "npm:^11.16.0" + checksum: 10c0/66de6d40aef59d3004aaf17e39c7c2d2c679306207fdb28c73917d6a05e8c962747fea53d2e516c25d994109d38e127943a759fed18a0fadfc2572a3335fc0d2 + languageName: node + linkType: hard + +"motion-utils@npm:^11.16.0": + version: 11.16.0 + resolution: "motion-utils@npm:11.16.0" + checksum: 10c0/e68efa08b9546a2fb065537cedcbab1a416d43cdc5773e02ea01408c276e56bbea9ef76d330e80d8536a6ac585b0bbb6f4f2b9d97637d8d36418483e4492ddff + languageName: node + linkType: hard + "mri@npm:^1.1.0": version: 1.2.0 resolution: "mri@npm:1.2.0" @@ -45270,7 +45287,7 @@ __metadata: eslint-plugin-unused-imports: "npm:^3.0.0" facepaint: "npm:^1.2.1" file-type: "npm:16.5.4" - framer-motion: "npm:^10.12.17" + framer-motion: "npm:^11.18.0" googleapis: "npm:105" graphiql: "npm:^3.1.1" graphql: "npm:16.8.0"