Migrate twenty ui to linaria (#18307)
## Migrate twenty-ui from Emotion to Linaria
Completes the migration of all `twenty-ui` components from Emotion
(runtime CSS-in-JS) to Linaria (zero-runtime, CSS extracted at build
time).
- Replaced `@emotion/styled` with `@linaria/react` across ~170 files
- Removed all Emotion dependencies from `twenty-ui`
- Introduced a CSS custom properties-based theme system:
`themeCssVariables` where every leaf is a `var(--t-xxx)` reference,
injected onto `document.documentElement` by
`ThemeCssVariableInjectorEffect`
- No more `theme` prop threading — styled components reference
`themeCssVariables.x.y` directly at build time
- Updated `twenty-front` consumers to remove `theme={theme}` prop
passing
**Before / After:**
```tsx
// Emotion
color: ${({ theme }) => theme.font.color.primary};
padding: ${({ theme }) => theme.spacing(4)};
// Linaria
color: ${themeCssVariables.font.color.primary};
padding: ${themeCssVariables.spacing[4]};
```
### Theme architecture
Two build-time utilities produce the theme system:
- **`buildThemeReferencingRootCssVariables`** — walks the theme object
and builds a nested mirror where every leaf is a `var(--t-xxx)` string
(evaluated at build time by wyw-in-js)
- **`prepareThemeForRootCssVariableInjection`** — walks the runtime
theme and collects flat `[--css-variable-name, value]` pairs, injected
onto `document.documentElement` by `ThemeCssVariableInjectorEffect`
Both share naming conventions (`camelToKebab`, `SPACING_VALUES`,
`formatSpacingKey`) and are unit tested.
### Spacing cleanup
Spacing scale now uses integers 0–32 (generated via loop), with `0.5`
and `1.5` as the only fractional exceptions. All other fractional
spacing usages (`0.25`, `0.75`, `1.25`, `2.5`, `3.5`) were replaced with
literal pixel values across ~20 twenty-front files.
### Framer Motion integration
Linaria doesn't support `styled(motion.div)` — wrapping a motion element
with `styled()` causes the component body to be stripped at build time.
Instead, we define the styled component first, then wrap it with
`motion.create()`:
```tsx
const StyledBarBase = styled.div`
background-color: ${themeCssVariables.font.color.primary};
height: 100%;
`;
const StyledBar = motion.create(StyledBarBase);
```
### Block interpolations
Linaria doesn't support interpolations that return multiple CSS
declarations (Linaria wraps the entire block in a single `var()`,
producing invalid CSS). These were split into individual property
interpolations:
```tsx
// Emotion — single interpolation returning multiple declarations
border-left: ${({ divider, theme }) => {
const border = `1px solid ${theme.border.color.light}`;
return divider ? `border-${divider}: ${border}` : '';
}}
// Linaria — one interpolation per property
border-left: ${({ divider }) =>
divider === 'left' ? `1px solid ${themeCssVariables.border.color.light}` : 'none'};
border-right: ${({ divider }) =>
divider === 'right' ? `1px solid ${themeCssVariables.border.color.light}` : 'none'};
```
### Dynamic styles via CSS variables
When a component needs to compute styles from multiple props with
complex branching logic (e.g. `Button` combining `variant`, `accent`,
`inverted`, `disabled`, `focus`, `position`), Linaria's prop
interpolations become unwieldy. In those cases we use a
`computeDynamicStyles` function that returns a `CSSProperties` object
injected via `style={}`, referenced from the static CSS with `var()`:
```tsx
const StyledButton = styled.button`
background: var(--btn-bg);
border-color: var(--btn-border-color);
&:hover { background: var(--btn-hover-bg); }
`;
const dynamicStyles = useMemo(() => {
const s = computeButtonDynamicStyles(variant, accent, ...);
return { '--btn-bg': s.background, '--btn-hover-bg': s.hoverBackground } as CSSProperties;
}, [variant, accent, ...]);
return <StyledButton style={dynamicStyles} />;
```
### CSS var + unit concatenation
CSS custom properties can't be concatenated with unit suffixes directly
(`var(--x)px` is invalid). Values that need units use `calc()`:
```tsx
// Broken
transition: background ${themeCssVariables.animation.duration.instant}s ease;
// Fixed
transition: background calc(${themeCssVariables.animation.duration.instant} * 1s) ease;
```
This commit is contained in:
@@ -1,26 +1,27 @@
|
||||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import { themeCssVariables } from '@ui/theme';
|
||||
import { Radio } from './Radio';
|
||||
|
||||
const StyledSubscriptionCardContainer = styled.button`
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
background-color: ${themeCssVariables.background.secondary};
|
||||
border: 1px solid ${themeCssVariables.border.color.medium};
|
||||
border-radius: ${themeCssVariables.border.radius.md};
|
||||
display: flex;
|
||||
padding: ${({ theme }) => theme.spacing(4)} ${({ theme }) => theme.spacing(3)};
|
||||
padding: ${themeCssVariables.spacing[4]} ${themeCssVariables.spacing[3]};
|
||||
position: relative;
|
||||
width: 100%;
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
background: ${({ theme }) => theme.background.tertiary};
|
||||
background: ${themeCssVariables.background.tertiary};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledRadioContainer = styled.div`
|
||||
position: absolute;
|
||||
right: ${({ theme }) => theme.spacing(2)};
|
||||
top: ${({ theme }) => theme.spacing(2)};
|
||||
right: ${themeCssVariables.spacing[2]};
|
||||
top: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
type CardPickerProps = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import { IconCheck, IconMinus } from '@ui/display/icon/components/TablerIcons';
|
||||
import { themeCssVariables } from '@ui/theme';
|
||||
import * as React from 'react';
|
||||
|
||||
export enum CheckboxVariant {
|
||||
@@ -49,131 +50,138 @@ type InputProps = {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const StyledInputContainer = styled.div<InputProps>`
|
||||
--size: ${({ checkboxSize, hoverable }) => {
|
||||
const StyledCheckboxContainer = styled.div<InputProps>`
|
||||
--checkbox-outer-size: ${({ checkboxSize, hoverable }) => {
|
||||
if (hoverable === true) {
|
||||
return checkboxSize === CheckboxSize.Large ? '32px' : '24px';
|
||||
} else {
|
||||
return checkboxSize === CheckboxSize.Large ? '20px' : '14px';
|
||||
}
|
||||
}};
|
||||
align-items: center;
|
||||
border-radius: ${({ theme, shape }) =>
|
||||
--checkbox-label-size: ${({ checkboxSize }) =>
|
||||
checkboxSize === CheckboxSize.Large ? '18px' : '12px'};
|
||||
--checkbox-icon-size: ${({ checkboxSize }) =>
|
||||
checkboxSize === CheckboxSize.Large ? '20px' : '14px'};
|
||||
--checkbox-bg: ${({ indeterminate, isChecked, disabled, accent }) => {
|
||||
if (!(indeterminate || isChecked)) return 'transparent';
|
||||
return disabled === true
|
||||
? accent === CheckboxAccent.Blue
|
||||
? themeCssVariables.color.blue7
|
||||
: themeCssVariables.color.orange7
|
||||
: accent === CheckboxAccent.Blue
|
||||
? themeCssVariables.color.blue
|
||||
: themeCssVariables.color.orange;
|
||||
}};
|
||||
--checkbox-border-color: ${({
|
||||
indeterminate,
|
||||
isChecked,
|
||||
variant,
|
||||
disabled,
|
||||
accent,
|
||||
}) => {
|
||||
switch (true) {
|
||||
case indeterminate || isChecked:
|
||||
return disabled === true
|
||||
? accent === CheckboxAccent.Blue
|
||||
? themeCssVariables.color.blue7
|
||||
: themeCssVariables.color.orange7
|
||||
: accent === CheckboxAccent.Blue
|
||||
? themeCssVariables.color.blue
|
||||
: themeCssVariables.color.orange;
|
||||
case disabled === true:
|
||||
return themeCssVariables.border.color.strong;
|
||||
case variant === CheckboxVariant.Primary:
|
||||
return themeCssVariables.border.color.inverted;
|
||||
case variant === CheckboxVariant.Tertiary:
|
||||
return themeCssVariables.border.color.medium;
|
||||
default:
|
||||
return themeCssVariables.border.color.secondaryInverted;
|
||||
}
|
||||
}};
|
||||
--checkbox-border-radius: ${({ shape }) =>
|
||||
shape === CheckboxShape.Rounded
|
||||
? theme.border.radius.rounded
|
||||
: theme.border.radius.md};
|
||||
? themeCssVariables.border.radius.rounded
|
||||
: themeCssVariables.border.radius.sm};
|
||||
--checkbox-border-width: ${({ variant, checkboxSize }) =>
|
||||
checkboxSize === CheckboxSize.Large || variant === CheckboxVariant.Tertiary
|
||||
? '1.43px'
|
||||
: '1px'};
|
||||
--checkbox-cursor: ${({ disabled }) =>
|
||||
disabled === true ? 'not-allowed' : 'pointer'};
|
||||
--checkbox-stroke: ${themeCssVariables.font.color.inverted};
|
||||
|
||||
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
|
||||
align-items: center;
|
||||
border-radius: ${({ shape }) =>
|
||||
shape === CheckboxShape.Rounded
|
||||
? themeCssVariables.border.radius.rounded
|
||||
: themeCssVariables.border.radius.md};
|
||||
cursor: var(--checkbox-cursor);
|
||||
display: flex;
|
||||
padding: ${({ theme, checkboxSize, hoverable }) => {
|
||||
padding: ${({ checkboxSize, hoverable }) => {
|
||||
if (hoverable === true) {
|
||||
return checkboxSize === CheckboxSize.Large
|
||||
? theme.spacing(1.5)
|
||||
: theme.spacing(1.25);
|
||||
return checkboxSize === CheckboxSize.Large ? '6px' : '5px';
|
||||
} else {
|
||||
return 0;
|
||||
return '0';
|
||||
}
|
||||
}};
|
||||
position: relative;
|
||||
${({ hoverable, isChecked, theme, indeterminate, disabled, accent }) => {
|
||||
if (!hoverable || disabled === true) return '';
|
||||
return `&:hover{
|
||||
background-color: ${
|
||||
indeterminate || isChecked
|
||||
? accent === CheckboxAccent.Blue
|
||||
? theme.background.transparent.blue
|
||||
: theme.background.transparent.orange
|
||||
: theme.background.transparent.light
|
||||
};
|
||||
}}
|
||||
}`;
|
||||
}}
|
||||
|
||||
&:hover {
|
||||
background-color: ${({
|
||||
hoverable,
|
||||
isChecked,
|
||||
indeterminate,
|
||||
disabled,
|
||||
accent,
|
||||
}) => {
|
||||
if (hoverable !== true || disabled === true) return 'transparent';
|
||||
if (indeterminate === true || isChecked === true) {
|
||||
return accent === CheckboxAccent.Blue
|
||||
? themeCssVariables.background.transparent.blue
|
||||
: themeCssVariables.background.transparent.orange;
|
||||
}
|
||||
return themeCssVariables.background.transparent.light;
|
||||
}};
|
||||
}
|
||||
|
||||
input + label {
|
||||
cursor: var(--checkbox-cursor);
|
||||
height: calc(var(--checkbox-label-size) + 2px);
|
||||
padding: 0;
|
||||
position: relative;
|
||||
width: calc(var(--checkbox-label-size) + 2px);
|
||||
}
|
||||
|
||||
input + label:before {
|
||||
background: var(--checkbox-bg);
|
||||
border-color: var(--checkbox-border-color);
|
||||
border-radius: var(--checkbox-border-radius);
|
||||
border-style: solid;
|
||||
border-width: var(--checkbox-border-width);
|
||||
content: '';
|
||||
cursor: var(--checkbox-cursor);
|
||||
display: inline-block;
|
||||
height: var(--checkbox-label-size);
|
||||
width: var(--checkbox-label-size);
|
||||
}
|
||||
|
||||
input + label > svg {
|
||||
--padding: 0px;
|
||||
height: var(--checkbox-icon-size);
|
||||
left: var(--padding);
|
||||
position: absolute;
|
||||
stroke: var(--checkbox-stroke);
|
||||
top: var(--padding);
|
||||
width: var(--checkbox-icon-size);
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input<InputProps>`
|
||||
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
|
||||
const StyledCheckboxInput = styled.input`
|
||||
cursor: ${({ disabled }) => (disabled === true ? 'not-allowed' : 'pointer')};
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
& + label {
|
||||
--size: ${({ checkboxSize }) =>
|
||||
checkboxSize === CheckboxSize.Large ? '18px' : '12px'};
|
||||
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
|
||||
height: calc(var(--size) + 2px);
|
||||
padding: 0;
|
||||
position: relative;
|
||||
width: calc(var(--size) + 2px);
|
||||
}
|
||||
|
||||
& + label:before {
|
||||
--size: ${({ checkboxSize }) =>
|
||||
checkboxSize === CheckboxSize.Large ? '18px' : '12px'};
|
||||
background: ${({ theme, indeterminate, isChecked, disabled, accent }) => {
|
||||
if (!(indeterminate || isChecked)) return 'transparent';
|
||||
return disabled
|
||||
? accent === CheckboxAccent.Blue
|
||||
? theme.color.blue7
|
||||
: theme.color.orange7
|
||||
: accent === CheckboxAccent.Blue
|
||||
? theme.color.blue
|
||||
: theme.color.orange;
|
||||
}};
|
||||
border-color: ${({
|
||||
theme,
|
||||
indeterminate,
|
||||
isChecked,
|
||||
variant,
|
||||
disabled,
|
||||
accent,
|
||||
}) => {
|
||||
switch (true) {
|
||||
case indeterminate || isChecked:
|
||||
return disabled
|
||||
? accent === CheckboxAccent.Blue
|
||||
? theme.color.blue7
|
||||
: theme.color.orange7
|
||||
: accent === CheckboxAccent.Blue
|
||||
? theme.color.blue
|
||||
: theme.color.orange;
|
||||
case disabled:
|
||||
return theme.border.color.strong;
|
||||
case variant === CheckboxVariant.Primary:
|
||||
return theme.border.color.inverted;
|
||||
case variant === CheckboxVariant.Tertiary:
|
||||
return theme.border.color.medium;
|
||||
default:
|
||||
return theme.border.color.secondaryInverted;
|
||||
}
|
||||
}};
|
||||
border-radius: ${({ theme, shape }) =>
|
||||
shape === CheckboxShape.Rounded
|
||||
? theme.border.radius.rounded
|
||||
: theme.border.radius.sm};
|
||||
border-style: solid;
|
||||
border-width: ${({ variant, checkboxSize }) =>
|
||||
checkboxSize === CheckboxSize.Large ||
|
||||
variant === CheckboxVariant.Tertiary
|
||||
? '1.43px'
|
||||
: '1px'};
|
||||
content: '';
|
||||
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
|
||||
display: inline-block;
|
||||
height: var(--size);
|
||||
width: var(--size);
|
||||
}
|
||||
|
||||
& + label > svg {
|
||||
--padding: 0px;
|
||||
--size: ${({ checkboxSize }) =>
|
||||
checkboxSize === CheckboxSize.Large ? '20px' : '14px'};
|
||||
height: var(--size);
|
||||
left: var(--padding);
|
||||
position: absolute;
|
||||
stroke: ${({ theme }) => theme.font.color.inverted};
|
||||
top: var(--padding);
|
||||
width: var(--size);
|
||||
}
|
||||
`;
|
||||
|
||||
export const Checkbox = ({
|
||||
@@ -205,7 +213,7 @@ export const Checkbox = ({
|
||||
const checkboxId = React.useId();
|
||||
|
||||
return (
|
||||
<StyledInputContainer
|
||||
<StyledCheckboxContainer
|
||||
checkboxSize={size}
|
||||
variant={variant}
|
||||
shape={shape}
|
||||
@@ -216,21 +224,15 @@ export const Checkbox = ({
|
||||
disabled={disabled}
|
||||
accent={accent}
|
||||
>
|
||||
<StyledInput
|
||||
<StyledCheckboxInput
|
||||
autoComplete="off"
|
||||
type="checkbox"
|
||||
id={checkboxId}
|
||||
name="styled-checkbox"
|
||||
data-testid="input-checkbox"
|
||||
checked={isInternalChecked}
|
||||
indeterminate={indeterminate}
|
||||
variant={variant}
|
||||
checkboxSize={size}
|
||||
shape={shape}
|
||||
isChecked={isInternalChecked}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
accent={accent}
|
||||
/>
|
||||
<label htmlFor={checkboxId}>
|
||||
{indeterminate ? (
|
||||
@@ -241,6 +243,6 @@ export const Checkbox = ({
|
||||
<></>
|
||||
)}
|
||||
</label>
|
||||
</StyledInputContainer>
|
||||
</StyledCheckboxContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { motion } from 'framer-motion';
|
||||
import * as React from 'react';
|
||||
|
||||
import { themeCssVariables } from '@ui/theme';
|
||||
import { RadioGroup } from './RadioGroup';
|
||||
|
||||
export enum RadioSize {
|
||||
@@ -30,12 +32,12 @@ type RadioInputProps = {
|
||||
'radio-size'?: RadioSize;
|
||||
};
|
||||
|
||||
const StyledRadioInput = styled(motion.input)<RadioInputProps>`
|
||||
const StyledRadioInputBase = styled.input<RadioInputProps>`
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
border: 1px solid ${({ theme }) => theme.font.color.secondary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.rounded};
|
||||
border: 1px solid ${themeCssVariables.font.color.secondary};
|
||||
border-radius: ${themeCssVariables.border.radius.rounded};
|
||||
height: ${({ 'radio-size': radioSize }) =>
|
||||
radioSize === RadioSize.Large ? '18px' : '16px'};
|
||||
margin: 0;
|
||||
@@ -45,25 +47,26 @@ const StyledRadioInput = styled(motion.input)<RadioInputProps>`
|
||||
radioSize === RadioSize.Large ? '18px' : '16px'};
|
||||
|
||||
:hover {
|
||||
background-color: ${({ theme, checked }) => {
|
||||
background-color: ${({ checked }) => {
|
||||
if (!checked) {
|
||||
return theme.background.tertiary;
|
||||
return themeCssVariables.background.tertiary;
|
||||
}
|
||||
return '';
|
||||
}};
|
||||
outline: 4px solid
|
||||
${({ theme, checked }) => {
|
||||
${({ checked }) => {
|
||||
if (!checked) {
|
||||
return theme.background.tertiary;
|
||||
return themeCssVariables.background.tertiary;
|
||||
}
|
||||
return theme.color.transparent.blue2;
|
||||
return themeCssVariables.color.transparent.blue2;
|
||||
}};
|
||||
}
|
||||
|
||||
&:checked {
|
||||
background-color: ${({ theme }) => theme.color.blue};
|
||||
background-color: ${themeCssVariables.color.blue};
|
||||
border: none;
|
||||
&::after {
|
||||
background-color: ${({ theme }) => theme.grayScale.gray1};
|
||||
background-color: ${themeCssVariables.grayScale.gray1};
|
||||
border-radius: 50%;
|
||||
content: '';
|
||||
height: ${({ 'radio-size': radioSize }) =>
|
||||
@@ -83,20 +86,26 @@ const StyledRadioInput = styled(motion.input)<RadioInputProps>`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledRadioInput = motion.create(StyledRadioInputBase);
|
||||
|
||||
type LabelProps = {
|
||||
disabled?: boolean;
|
||||
labelPosition?: LabelPosition;
|
||||
};
|
||||
|
||||
const StyledLabel = styled.label<LabelProps>`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
cursor: pointer;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
margin-left: ${({ theme, labelPosition }) =>
|
||||
labelPosition === LabelPosition.Right ? theme.spacing(2) : '0px'};
|
||||
margin-right: ${({ theme, labelPosition }) =>
|
||||
labelPosition === LabelPosition.Left ? theme.spacing(2) : '0px'};
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
font-weight: ${themeCssVariables.font.weight.regular};
|
||||
margin-left: ${({ labelPosition }) =>
|
||||
labelPosition === LabelPosition.Right
|
||||
? themeCssVariables.spacing[2]
|
||||
: '0px'};
|
||||
margin-right: ${({ labelPosition }) =>
|
||||
labelPosition === LabelPosition.Left
|
||||
? themeCssVariables.spacing[2]
|
||||
: '0px'};
|
||||
opacity: ${({ disabled }) => (disabled ? 0.32 : 1)};
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
import { type RadioProps } from './Radio';
|
||||
import { themeCssVariables } from '@ui/theme';
|
||||
|
||||
type RadioGroupProps = React.PropsWithChildren & {
|
||||
value?: string;
|
||||
@@ -15,8 +15,6 @@ export const RadioGroup = ({
|
||||
onValueChange,
|
||||
children,
|
||||
}: RadioGroupProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange?.(event);
|
||||
onValueChange?.(event.target.value);
|
||||
@@ -27,7 +25,7 @@ export const RadioGroup = ({
|
||||
{React.Children.map(children, (child) => {
|
||||
if (React.isValidElement<RadioProps>(child)) {
|
||||
return React.cloneElement(child, {
|
||||
style: { marginBottom: theme.spacing(2) },
|
||||
style: { marginBottom: themeCssVariables.spacing[2] },
|
||||
checked: child.props.value === value,
|
||||
onChange: handleChange,
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { IconFilter, IconSearch } from '@ui/display';
|
||||
import { IconButton } from '@ui/input/button/components/IconButton';
|
||||
import { type ChangeEvent, type ReactNode, useState } from 'react';
|
||||
import { ThemeContext, themeCssVariables } from '@ui/theme';
|
||||
import { type ChangeEvent, type ReactNode, useContext, useState } from 'react';
|
||||
|
||||
export type SearchInputProps = {
|
||||
value: string;
|
||||
@@ -17,31 +17,35 @@ export type SearchInputProps = {
|
||||
const StyledWrapper = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
background-color: ${themeCssVariables.background.transparent.lighter};
|
||||
border: 1px solid ${themeCssVariables.border.color.medium};
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
height: 32px;
|
||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||
padding: 0 ${themeCssVariables.spacing[2]};
|
||||
|
||||
&:focus-within {
|
||||
border-color: ${({ theme }) => theme.color.blue};
|
||||
border-color: ${themeCssVariables.color.blue};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledIconContainer = styled.div<{ isFocused: boolean }>`
|
||||
const StyledIconContainer = styled.div<{
|
||||
isFocused: boolean;
|
||||
}>`
|
||||
align-items: center;
|
||||
color: ${({ theme, isFocused }) =>
|
||||
isFocused ? theme.font.color.secondary : theme.font.color.light};
|
||||
color: ${({ isFocused }) =>
|
||||
isFocused
|
||||
? themeCssVariables.font.color.secondary
|
||||
: themeCssVariables.font.color.light};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
@@ -49,21 +53,21 @@ const StyledIconContainer = styled.div<{ isFocused: boolean }>`
|
||||
const StyledInput = styled.input`
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
flex: 1;
|
||||
font-family: ${({ theme }) => theme.font.family};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
font-family: ${themeCssVariables.font.family};
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
font-weight: ${themeCssVariables.font.weight.regular};
|
||||
outline: none;
|
||||
width: 100%;
|
||||
|
||||
&::placeholder {
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
color: ${themeCssVariables.font.color.light};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -76,7 +80,7 @@ export const SearchInput = ({
|
||||
disabled,
|
||||
className,
|
||||
}: SearchInputProps) => {
|
||||
const theme = useTheme();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
const filterButton = <IconButton Icon={IconFilter} variant="secondary" />;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { VisibilityHiddenInput } from '@ui/accessibility';
|
||||
import { themeCssVariables } from '@ui/theme';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export type ToggleSize = 'small' | 'medium';
|
||||
@@ -13,8 +14,10 @@ type ContainerProps = {
|
||||
|
||||
const StyledContainer = styled.label<ContainerProps>`
|
||||
align-items: center;
|
||||
background-color: ${({ theme, isOn, color }) =>
|
||||
isOn ? (color ?? theme.color.blue) : theme.background.transparent.medium};
|
||||
background-color: ${({ isOn, color }) =>
|
||||
isOn
|
||||
? (color ?? themeCssVariables.color.blue)
|
||||
: themeCssVariables.background.transparent.medium};
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
@@ -28,10 +31,10 @@ const StyledContainer = styled.label<ContainerProps>`
|
||||
width: ${({ toggleSize }) => (toggleSize === 'small' ? 24 : 32)}px;
|
||||
`;
|
||||
|
||||
const StyledCircle = styled(motion.span)<{
|
||||
const StyledCircleBase = styled.span<{
|
||||
size: ToggleSize;
|
||||
}>`
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
background-color: ${themeCssVariables.background.primary};
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
height: ${({ size }) => (size === 'small' ? 12 : 16)}px;
|
||||
@@ -40,6 +43,8 @@ const StyledCircle = styled(motion.span)<{
|
||||
width: ${({ size }) => (size === 'small' ? 12 : 16)}px;
|
||||
`;
|
||||
|
||||
const StyledCircle = motion.create(StyledCircleBase);
|
||||
|
||||
export type ToggleProps = {
|
||||
id?: string;
|
||||
value?: boolean;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { ComponentDecorator } from '@ui/testing';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { SearchInput } from '../SearchInput';
|
||||
|
||||
type InteractiveSearchInputProps = {
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
autoFocus?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const InteractiveSearchInput = ({
|
||||
placeholder,
|
||||
disabled,
|
||||
autoFocus,
|
||||
className,
|
||||
}: InteractiveSearchInputProps) => {
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
return (
|
||||
<SearchInput
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
autoFocus={autoFocus}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const meta: Meta<typeof InteractiveSearchInput> = {
|
||||
title: 'UI/Input/SearchInput',
|
||||
component: InteractiveSearchInput,
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof InteractiveSearchInput>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
placeholder: 'Search...',
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
placeholder: 'Search...',
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user