Files
twenty/packages/twenty-ui-deprecated/src/input/components/Checkbox.tsx
T
Félix Malfait 02aa086866 fix(twenty-front): new layout fast-follows (#21360)
Fast-follows for the new layout / flat redesign (master:
twentyhq/core-team-issues#2478).

## Changes
- **Main navbar 48px** (twentyhq/core-team-issues#2479) —
`SIDE_PANEL_TOP_BAR_HEIGHT` 40 → 48, so `PageCardHeader` matches the
Figma target. The side panel top bar shares this constant and stays
aligned.
- **Content panel 12px radius** (twentyhq/core-team-issues#2480) —
`PageCardLayout` card gets a full border + 12px radius and an 8px inset
(`spacing[2]`) so it floats on the shell instead of square full-bleed.
- **Square three-dots button** (twentyhq/core-team-issues#2481) — added
a `square` option to `AnimatedButton`; the page-header side-panel toggle
now renders a 24×24 square icon button instead of a 32×24 pill.
- **Table checkbox sizing** (twentyhq/core-team-issues#2482) — restored
`box-sizing: content-box` on the checkbox box. Its border is declared
outside the label size, so the global `border-box` reset (#21349) was
shrinking it (14px → 12px). Same fix pattern as #21349.
- **Tertiary navbar background** (twentyhq/core-team-issues#2483) — left
navbar / app shell use `background/tertiary` instead of the noisy
surface (`DefaultLayout`, `UserOrMetadataLoader`).
- **Skeleton loading** (twentyhq/core-team-issues#2484) — metadata +
content loading now match the new layout: tertiary shell, 12px rounded
content panel, 48px navbar, sparse bars, empty body (removed the dense
full-width rows). Left-panel skeleton bars use `quaternary` so they stay
visible on the tertiary shell.

## Verification
- typecheck (tsgo) + oxlint + oxfmt pass for all changed files.
- Verified live against a running workspace: measured navbar = 48px,
three-dots = 24×24, checkbox box-sizing = content-box (14px), card
radius = 12px, shell background = tertiary (no noisy image).
Content-loading skeleton matches the target.
2026-06-09 16:10:36 +02:00

252 lines
6.9 KiB
TypeScript

import { styled } from '@linaria/react';
import { IconCheck, IconMinus } from '@ui/display/icon/components/TablerIcons';
import { themeCssVariables } from '@ui/theme-constants';
import * as React from 'react';
export enum CheckboxVariant {
Primary = 'primary',
Secondary = 'secondary',
Tertiary = 'tertiary',
}
export enum CheckboxShape {
Squared = 'squared',
Rounded = 'rounded',
}
export enum CheckboxSize {
Large = 'large',
Small = 'small',
}
export enum CheckboxAccent {
Blue = 'blue',
Orange = 'orange',
}
type CheckboxProps = {
checked: boolean;
indeterminate?: boolean;
hoverable?: boolean;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
onCheckedChange?: (value: boolean) => void;
variant?: CheckboxVariant;
size?: CheckboxSize;
shape?: CheckboxShape;
className?: string;
disabled?: boolean;
accent?: CheckboxAccent;
};
type InputProps = {
checkboxSize: CheckboxSize;
variant: CheckboxVariant;
accent?: CheckboxAccent;
indeterminate?: boolean;
hoverable?: boolean;
shape?: CheckboxShape;
isChecked?: boolean;
disabled?: boolean;
};
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';
}
}};
--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
? 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};
align-items: center;
border-radius: ${({ shape }) =>
shape === CheckboxShape.Rounded
? themeCssVariables.border.radius.rounded
: themeCssVariables.border.radius.md};
cursor: var(--checkbox-cursor);
display: flex;
padding: ${({ checkboxSize, hoverable }) => {
if (hoverable === true) {
return checkboxSize === CheckboxSize.Large ? '6px' : '5px';
} else {
return '0';
}
}};
position: relative;
&: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 {
box-sizing: content-box;
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);
// The border sits outside the declared label size, so size against the content box.
box-sizing: content-box;
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 StyledCheckboxInput = styled.input`
cursor: ${({ disabled }) => (disabled === true ? 'not-allowed' : 'pointer')};
margin: 0;
opacity: 0;
position: absolute;
z-index: 10;
`;
export const Checkbox = ({
checked,
onChange,
onCheckedChange,
indeterminate,
variant = CheckboxVariant.Primary,
size = CheckboxSize.Small,
shape = CheckboxShape.Squared,
hoverable = true,
className,
disabled = false,
accent = CheckboxAccent.Blue,
}: CheckboxProps) => {
const [isInternalChecked, setIsInternalChecked] =
React.useState<boolean>(false);
React.useEffect(() => {
setIsInternalChecked(checked ?? false);
}, [checked]);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
onChange?.(event);
onCheckedChange?.(event.target.checked);
setIsInternalChecked(event.target.checked ?? false);
};
const checkboxId = React.useId();
return (
<StyledCheckboxContainer
checkboxSize={size}
variant={variant}
shape={shape}
isChecked={isInternalChecked}
hoverable={hoverable}
indeterminate={indeterminate}
className={className}
disabled={disabled}
accent={accent}
>
<StyledCheckboxInput
autoComplete="off"
type="checkbox"
id={checkboxId}
name="styled-checkbox"
data-testid="input-checkbox"
checked={isInternalChecked}
onChange={handleChange}
disabled={disabled}
/>
<label htmlFor={checkboxId}>
{indeterminate ? (
<IconMinus />
) : isInternalChecked ? (
<IconCheck />
) : (
<></>
)}
</label>
</StyledCheckboxContainer>
);
};