3bfdc2c83f
## Summary
Continues the Emotion → Linaria migration (PR 4-6 from the [migration
plan](docs/emotion-to-linaria-migration-plan.md)). Migrates **311
files** across four module groups:
| Module | Files |
|---|---|
| command-menu | 53 |
| workflow | 84 |
| page-layout | 84 |
| UI (partial - first ~80 files) | ~80 |
| twenty-ui (TEXT_INPUT_STYLE) | 1 |
| misc (hooks, keyboard-shortcut-menu, file-upload) | ~9 |
### Migration patterns applied
- `import styled from '@emotion/styled'` → `import { styled } from
'@linaria/react'`
- `import { useTheme } from '@emotion/react'` → `import { useContext }
from 'react'` + `import { ThemeContext } from 'twenty-ui/theme'`
- `${({ theme }) => theme.X.Y.Z}` → `${themeCssVariables.X.Y.Z}` (static
CSS variables)
- `theme.spacing(N)` → `themeCssVariables.spacing[N]`
- `styled(motion.div)` → `motion.create(StyledBase)` (11 components)
- `styled(Component)<TypeParams>` → wrapper div approach for non-HTML
elements
- Multi-declaration interpolations split into one CSS property per
interpolation
- Interpolation return types fixed (`&&` → ternary `? : ''`)
- `TEXT_INPUT_STYLE` converted from function to static string constant
(backward compatible)
- Emotion `<Global>` replaced with `useEffect` style injection
- Complex runtime-dependent styles use CSS custom properties via
`style={}` prop
### After this PR
- **Remaining files**: ~400 (object-record: ~160, settings: ~200, UI:
~44)
- **No breaking changes**: CSS variables resolve identically to the
previous Emotion theme values
111 lines
4.8 KiB
TypeScript
111 lines
4.8 KiB
TypeScript
import { COMMAND_MENU_ANIMATION_VARIANTS } from '@/command-menu/constants/CommandMenuAnimationVariants';
|
|
import { COMMAND_MENU_CLICK_OUTSIDE_ID } from '@/command-menu/constants/CommandMenuClickOutsideId';
|
|
import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
|
|
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
|
import { isSidePanelAnimatingState } from '@/command-menu/states/isSidePanelAnimatingState';
|
|
import { type CommandMenuAnimationVariant } from '@/command-menu/types/CommandMenuAnimationVariant';
|
|
import { RECORD_CHIP_CLICK_OUTSIDE_ID } from '@/object-record/record-table/constants/RecordChipClickOutsideId';
|
|
import { MENTION_MENU_DROPDOWN_CLICK_OUTSIDE_ID } from '@/ui/input/constants/MentionMenuDropdownClickOutsideId';
|
|
import { SLASH_MENU_DROPDOWN_CLICK_OUTSIDE_ID } from '@/ui/input/constants/SlashMenuDropdownClickOutsideId';
|
|
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
|
|
import { NAVIGATION_DRAWER_CLICK_OUTSIDE_ID } from '@/ui/navigation/navigation-drawer/constants/NavigationDrawerClickOutsideId';
|
|
import { PAGE_HEADER_COMMAND_MENU_BUTTON_CLICK_OUTSIDE_ID } from '@/ui/layout/page-header/constants/PageHeaderCommandMenuButtonClickOutsideId';
|
|
import { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector';
|
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
|
import { useStore } from 'jotai';
|
|
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
|
import { WORKFLOW_DIAGRAM_CREATE_STEP_NODE_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramCreateStepNodeClickOutsideId';
|
|
import { WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramStepNodeClickOutsideId';
|
|
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
|
|
|
|
import { styled } from '@linaria/react';
|
|
import { motion } from 'framer-motion';
|
|
import { useCallback, useContext, useRef } from 'react';
|
|
import { LINK_CHIP_CLICK_OUTSIDE_ID } from 'twenty-ui/components';
|
|
import { useIsMobile } from 'twenty-ui/utilities';
|
|
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
|
import { ThemeContext } from 'twenty-ui/theme';
|
|
|
|
const StyledCommandMenuBase = styled.div`
|
|
background: ${themeCssVariables.background.primary};
|
|
border-left: 1px solid ${themeCssVariables.border.color.medium};
|
|
box-shadow: ${themeCssVariables.boxShadow.strong};
|
|
font-family: ${themeCssVariables.font.family};
|
|
height: 100%;
|
|
overflow: hidden;
|
|
padding: 0;
|
|
position: fixed;
|
|
right: 0%;
|
|
top: 0%;
|
|
z-index: ${RootStackingContextZIndices.CommandMenu};
|
|
display: flex;
|
|
flex-direction: column;
|
|
`;
|
|
const StyledCommandMenu = motion.create(StyledCommandMenuBase);
|
|
|
|
export const CommandMenuOpenContainer = ({
|
|
children,
|
|
}: React.PropsWithChildren) => {
|
|
const isMobile = useIsMobile();
|
|
|
|
const targetVariantForAnimation: CommandMenuAnimationVariant = isMobile
|
|
? 'fullScreen'
|
|
: 'normal';
|
|
|
|
const { theme } = useContext(ThemeContext);
|
|
|
|
const { closeCommandMenu } = useCommandMenu();
|
|
|
|
const commandMenuRef = useRef<HTMLDivElement>(null);
|
|
const setIsSidePanelAnimating = useSetAtomState(isSidePanelAnimatingState);
|
|
|
|
const store = useStore();
|
|
|
|
const handleClickOutside = useCallback(
|
|
(event: MouseEvent | TouchEvent) => {
|
|
const currentFocusId = store.get(currentFocusIdSelector.atom);
|
|
|
|
if (currentFocusId === SIDE_PANEL_FOCUS_ID) {
|
|
event.stopImmediatePropagation();
|
|
event.preventDefault();
|
|
closeCommandMenu();
|
|
}
|
|
},
|
|
[closeCommandMenu, store],
|
|
);
|
|
|
|
useListenClickOutside({
|
|
refs: [commandMenuRef],
|
|
callback: handleClickOutside,
|
|
listenerId: 'COMMAND_MENU_LISTENER_ID',
|
|
excludedClickOutsideIds: [
|
|
NAVIGATION_DRAWER_CLICK_OUTSIDE_ID,
|
|
PAGE_HEADER_COMMAND_MENU_BUTTON_CLICK_OUTSIDE_ID,
|
|
LINK_CHIP_CLICK_OUTSIDE_ID,
|
|
RECORD_CHIP_CLICK_OUTSIDE_ID,
|
|
SLASH_MENU_DROPDOWN_CLICK_OUTSIDE_ID,
|
|
MENTION_MENU_DROPDOWN_CLICK_OUTSIDE_ID,
|
|
WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID,
|
|
WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID,
|
|
WORKFLOW_DIAGRAM_CREATE_STEP_NODE_CLICK_OUTSIDE_ID,
|
|
],
|
|
});
|
|
|
|
return (
|
|
<StyledCommandMenu
|
|
data-testid="command-menu"
|
|
data-click-outside-id={COMMAND_MENU_CLICK_OUTSIDE_ID}
|
|
ref={commandMenuRef}
|
|
animate={targetVariantForAnimation}
|
|
initial="closed"
|
|
exit="closed"
|
|
variants={COMMAND_MENU_ANIMATION_VARIANTS}
|
|
transition={{ duration: theme.animation.duration.normal }}
|
|
onAnimationStart={() => setIsSidePanelAnimating(true)}
|
|
onAnimationComplete={() => setIsSidePanelAnimating(false)}
|
|
>
|
|
{children}
|
|
</StyledCommandMenu>
|
|
);
|
|
};
|