02aa086866
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.
252 lines
6.9 KiB
TypeScript
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>
|
|
);
|
|
};
|