mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-02 14:33:54 +03:00
refactor(component): new Radio component (#6910)
# New `RadioGroup` component to replace `RadioButton` ![CleanShot 2024-06-25 at 17.18.02.gif](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/LakojjjzZNf6ogjOVwKE/77241b07-a2dd-4d27-a322-3056f483f75a.gif) ### what's new - [x] Change the API - [x] More customizable options - [x] Indicator animation - [x] Dynamic width support(responsive) - [x] Storybook - [x] JSDoc
This commit is contained in:
parent
f15d1911ee
commit
827c952e9f
@ -102,7 +102,7 @@
|
||||
"@vanilla-extract/css": "^1.14.2",
|
||||
"fake-indexeddb": "^6.0.0",
|
||||
"storybook": "^7.6.17",
|
||||
"storybook-dark-mode": "^4.0.0",
|
||||
"storybook-dark-mode": "4.0.1",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.8",
|
||||
"vitest": "1.6.0",
|
||||
|
@ -1,4 +1,3 @@
|
||||
// TODO(@catsjuice): Check `input` , `loading`, not migrated from `design`
|
||||
export * from './lit-react';
|
||||
export * from './styles';
|
||||
export * from './ui/avatar';
|
||||
@ -18,6 +17,7 @@ export * from './ui/menu';
|
||||
export * from './ui/modal';
|
||||
export * from './ui/notification';
|
||||
export * from './ui/popover';
|
||||
export * from './ui/radio';
|
||||
export * from './ui/scrollbar';
|
||||
export * from './ui/skeleton';
|
||||
export * from './ui/switch';
|
||||
|
@ -2,19 +2,27 @@ import type {
|
||||
RadioGroupItemProps,
|
||||
RadioGroupProps,
|
||||
} from '@radix-ui/react-radio-group';
|
||||
import * as RadioGroup from '@radix-ui/react-radio-group';
|
||||
import * as RadixRadioGroup from '@radix-ui/react-radio-group';
|
||||
import clsx from 'clsx';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { RadioGroup } from '../radio';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
// for reference
|
||||
RadioGroup;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* use {@link RadioGroup } instead
|
||||
*/
|
||||
export const RadioButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
RadioGroupItemProps & { spanStyle?: string }
|
||||
>(({ children, className, spanStyle, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroup.Item
|
||||
<RadixRadioGroup.Item
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={clsx(styles.radioButton, className)}
|
||||
@ -22,27 +30,31 @@ export const RadioButton = forwardRef<
|
||||
<span className={clsx(styles.radioUncheckedButton, spanStyle)}>
|
||||
{children}
|
||||
</span>
|
||||
<RadioGroup.Indicator
|
||||
<RadixRadioGroup.Indicator
|
||||
className={clsx(styles.radioButtonContent, spanStyle)}
|
||||
>
|
||||
{children}
|
||||
</RadioGroup.Indicator>
|
||||
</RadioGroup.Item>
|
||||
</RadixRadioGroup.Indicator>
|
||||
</RadixRadioGroup.Item>
|
||||
);
|
||||
});
|
||||
RadioButton.displayName = 'RadioButton';
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* use {@link RadioGroup} instead
|
||||
*/
|
||||
export const RadioButtonGroup = forwardRef<
|
||||
HTMLDivElement,
|
||||
RadioGroupProps & { width?: CSSProperties['width'] }
|
||||
>(({ className, style, width, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroup.Root
|
||||
<RadixRadioGroup.Root
|
||||
ref={ref}
|
||||
className={clsx(styles.radioButtonGroup, className)}
|
||||
style={{ width, ...style }}
|
||||
{...props}
|
||||
></RadioGroup.Root>
|
||||
></RadixRadioGroup.Root>
|
||||
);
|
||||
});
|
||||
RadioButtonGroup.displayName = 'RadioButtonGroup';
|
||||
|
2
packages/frontend/component/src/ui/radio/index.ts
Normal file
2
packages/frontend/component/src/ui/radio/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './radio';
|
||||
export type { RadioItem, RadioProps } from './types';
|
149
packages/frontend/component/src/ui/radio/radio.stories.tsx
Normal file
149
packages/frontend/component/src/ui/radio/radio.stories.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
import { AiIcon, FrameIcon, TocIcon, TodayIcon } from '@blocksuite/icons/rc';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { ResizePanel } from '../resize-panel/resize-panel';
|
||||
import { RadioGroup } from './radio';
|
||||
import type { RadioItem } from './types';
|
||||
|
||||
export default {
|
||||
title: 'UI/RadioGroup',
|
||||
};
|
||||
|
||||
export const FixedWidth = () => {
|
||||
const [value, setValue] = useState('Radio 1');
|
||||
return (
|
||||
<>
|
||||
<p style={{ marginBottom: 10, fontSize: cssVar('fontXs') }}>
|
||||
width:
|
||||
<code
|
||||
style={{
|
||||
padding: '2px 4px',
|
||||
borderRadius: 3,
|
||||
background: cssVar('hoverColorFilled'),
|
||||
}}
|
||||
>
|
||||
300px
|
||||
</code>
|
||||
</p>
|
||||
<RadioGroup
|
||||
width={300}
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
items={['Radio 1', 'Radio 2, Longer', 'S3']}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const AutoWidth = () => {
|
||||
const [value, setValue] = useState('Radio 1');
|
||||
return (
|
||||
<RadioGroup
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
items={['Radio 1', 'Radio 2, Longer', 'S3']}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DynamicWidth = () => {
|
||||
const [value, setValue] = useState('Radio 1');
|
||||
return (
|
||||
<ResizePanel
|
||||
horizontal
|
||||
vertical={false}
|
||||
maxWidth={1080}
|
||||
minWidth={235}
|
||||
width={250}
|
||||
>
|
||||
<RadioGroup
|
||||
width="100%"
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
items={['Radio 1', 'Radio 2, Longer', 'S3']}
|
||||
/>
|
||||
</ResizePanel>
|
||||
);
|
||||
};
|
||||
|
||||
export const IconTabs = () => {
|
||||
const [value, setValue] = useState('ai');
|
||||
const items: RadioItem[] = [
|
||||
{
|
||||
value: 'ai',
|
||||
label: <AiIcon width={20} height={20} />,
|
||||
style: { width: 28 },
|
||||
testId: 'ai-radio',
|
||||
},
|
||||
{
|
||||
value: 'calendar',
|
||||
label: <TodayIcon width={20} height={20} />,
|
||||
style: { width: 28 },
|
||||
testId: 'calendar-radio',
|
||||
},
|
||||
{
|
||||
value: 'outline',
|
||||
label: <TocIcon width={20} height={20} />,
|
||||
style: { width: 28 },
|
||||
testId: 'outline-radio',
|
||||
},
|
||||
{
|
||||
value: 'frame',
|
||||
label: <FrameIcon width={20} height={20} />,
|
||||
style: { width: 28 },
|
||||
testId: 'frame-radio',
|
||||
},
|
||||
];
|
||||
return (
|
||||
<RadioGroup
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
items={items}
|
||||
padding={4}
|
||||
borderRadius={12}
|
||||
gap={8}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const CustomizeActiveStyle = () => {
|
||||
const [value, setValue] = useState('ai');
|
||||
const items: RadioItem[] = [
|
||||
{
|
||||
value: 'ai',
|
||||
label: <AiIcon width={20} height={20} />,
|
||||
style: { width: 28 },
|
||||
testId: 'ai-radio',
|
||||
},
|
||||
{
|
||||
value: 'calendar',
|
||||
label: <TodayIcon width={20} height={20} />,
|
||||
style: { width: 28 },
|
||||
testId: 'calendar-radio',
|
||||
},
|
||||
{
|
||||
value: 'outline',
|
||||
label: <TocIcon width={20} height={20} />,
|
||||
style: { width: 28 },
|
||||
testId: 'outline-radio',
|
||||
},
|
||||
{
|
||||
value: 'frame',
|
||||
label: <FrameIcon width={20} height={20} />,
|
||||
style: { width: 28 },
|
||||
testId: 'frame-radio',
|
||||
},
|
||||
];
|
||||
return (
|
||||
<RadioGroup
|
||||
activeItemStyle={{ color: cssVar('primaryColor') }}
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
items={items}
|
||||
padding={4}
|
||||
borderRadius={12}
|
||||
gap={8}
|
||||
/>
|
||||
);
|
||||
};
|
185
packages/frontend/component/src/ui/radio/radio.tsx
Normal file
185
packages/frontend/component/src/ui/radio/radio.tsx
Normal file
@ -0,0 +1,185 @@
|
||||
import * as RadixRadioGroup from '@radix-ui/react-radio-group';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import clsx from 'clsx';
|
||||
import { createRef, memo, useCallback, useMemo, useRef } from 'react';
|
||||
|
||||
import { withUnit } from '../../utils/with-unit';
|
||||
import * as styles from './styles.css';
|
||||
import type { RadioItem, RadioProps } from './types';
|
||||
|
||||
/**
|
||||
* ### Radio button group (Tabs)
|
||||
* A tab-like radio button group
|
||||
*
|
||||
* #### 1. Basic usage with fixed width
|
||||
* ```tsx
|
||||
* <RadioGroup
|
||||
* width={300}
|
||||
* value={value}
|
||||
* onChange={setValue}
|
||||
* items={['Radio 1', 'Radio 2, Longer', 'S3']}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* #### 2. Dynamic width
|
||||
* ```tsx
|
||||
* <RadioGroup
|
||||
* width="100%"
|
||||
* value={value}
|
||||
* onChange={setValue}
|
||||
* items={['Radio 1', 'Radio 2, Longer', 'S3']}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* #### 3. `ReactNode` as label
|
||||
* ```tsx
|
||||
* const [value, setValue] = useState('ai');
|
||||
* const items: RadioItem[] = [
|
||||
* {
|
||||
* value: 'ai',
|
||||
* label: <AiIcon width={20} height={20} />,
|
||||
* style: { width: 28 },
|
||||
* },
|
||||
* {
|
||||
* value: 'calendar',
|
||||
* label: <TodayIcon width={20} height={20} />,
|
||||
* style: { width: 28 },
|
||||
* },
|
||||
* ];
|
||||
* return (
|
||||
* <RadioGroup
|
||||
* value={value}
|
||||
* onChange={setValue}
|
||||
* items={items}
|
||||
* padding={4}
|
||||
* borderRadius={12}
|
||||
* gap={8}
|
||||
* />
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export const RadioGroup = memo(function RadioGroup({
|
||||
items,
|
||||
value,
|
||||
width,
|
||||
style,
|
||||
padding = 2,
|
||||
gap = 4,
|
||||
borderRadius = 10,
|
||||
itemHeight = 28,
|
||||
animationDuration = 250,
|
||||
animationEasing = 'cubic-bezier(.18,.22,0,1)',
|
||||
activeItemClassName,
|
||||
activeItemStyle,
|
||||
onChange,
|
||||
}: RadioProps) {
|
||||
const animationTImerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const finalItems = useMemo(() => {
|
||||
return items
|
||||
.map(value =>
|
||||
typeof value === 'string' ? ({ value } as RadioItem) : value
|
||||
)
|
||||
.map(item => ({
|
||||
...item,
|
||||
ref: createRef<HTMLButtonElement>(),
|
||||
indicatorRef: createRef<HTMLDivElement>(),
|
||||
}));
|
||||
}, [items]);
|
||||
const finalStyle = useMemo(
|
||||
() => ({
|
||||
width,
|
||||
...style,
|
||||
...assignInlineVars({
|
||||
[styles.outerPadding]: withUnit(padding, 'px'),
|
||||
[styles.outerRadius]: withUnit(borderRadius, 'px'),
|
||||
[styles.itemGap]: withUnit(gap, 'px'),
|
||||
[styles.itemHeight]: withUnit(itemHeight, 'px'),
|
||||
}),
|
||||
}),
|
||||
[width, style, padding, borderRadius, gap, itemHeight]
|
||||
);
|
||||
|
||||
const animate = useCallback(
|
||||
(oldValue?: string, newValue?: string) => {
|
||||
if (!oldValue || !newValue) return;
|
||||
const oldItem = finalItems.find(item => item.value === oldValue);
|
||||
const newItem = finalItems.find(item => item.value === newValue);
|
||||
if (!oldItem || !newItem) return;
|
||||
const oldRect = oldItem.ref.current?.getBoundingClientRect();
|
||||
const newRect = newItem.ref.current?.getBoundingClientRect();
|
||||
if (!oldRect || !newRect) return;
|
||||
const activeIndicator = newItem.indicatorRef.current;
|
||||
if (!activeIndicator) return;
|
||||
|
||||
activeIndicator.style.transform = `translate3d(${oldRect.left - newRect.left}px,0,0)`;
|
||||
activeIndicator.style.transition = 'none';
|
||||
activeIndicator.style.width = `${oldRect.width}px`;
|
||||
|
||||
const animation = `${withUnit(animationDuration, 'ms')} ${animationEasing}`;
|
||||
|
||||
if (animationTImerRef.current) clearTimeout(animationTImerRef.current);
|
||||
animationTImerRef.current = setTimeout(() => {
|
||||
animationTImerRef.current = null;
|
||||
activeIndicator.style.transition = `transform ${animation}, width ${animation}`;
|
||||
activeIndicator.style.transform = 'none';
|
||||
activeIndicator.style.width = '';
|
||||
}, 50);
|
||||
},
|
||||
[animationDuration, animationEasing, finalItems]
|
||||
);
|
||||
|
||||
const onValueChange = useCallback(
|
||||
(newValue: string) => {
|
||||
const oldValue = value;
|
||||
if (oldValue !== newValue) {
|
||||
onChange(newValue);
|
||||
animate(oldValue, newValue);
|
||||
}
|
||||
},
|
||||
[animate, onChange, value]
|
||||
);
|
||||
|
||||
return (
|
||||
<RadixRadioGroup.Root
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
className={styles.radioButtonGroup}
|
||||
style={finalStyle}
|
||||
>
|
||||
{finalItems.map(({ customRender, ...item }, index) => {
|
||||
const testId = item.testId ? { 'data-testid': item.testId } : {};
|
||||
const active = item.value === value;
|
||||
|
||||
const classMap = { [styles.radioButton]: true };
|
||||
if (activeItemClassName) classMap[activeItemClassName] = active;
|
||||
if (item.className) classMap[item.className] = true;
|
||||
|
||||
const style = { ...item.style };
|
||||
if (activeItemStyle && active) Object.assign(style, activeItemStyle);
|
||||
|
||||
return (
|
||||
<RadixRadioGroup.Item
|
||||
ref={item.ref}
|
||||
key={item.value}
|
||||
value={item.value}
|
||||
className={clsx(classMap)}
|
||||
style={style}
|
||||
{...testId}
|
||||
{...item.attrs}
|
||||
>
|
||||
<RadixRadioGroup.Indicator
|
||||
forceMount
|
||||
className={styles.indicator}
|
||||
ref={item.indicatorRef}
|
||||
/>
|
||||
<span className={styles.radioButtonContent}>
|
||||
{customRender
|
||||
? customRender(item, index)
|
||||
: item.label ?? item.value}
|
||||
</span>
|
||||
</RadixRadioGroup.Item>
|
||||
);
|
||||
})}
|
||||
</RadixRadioGroup.Root>
|
||||
);
|
||||
});
|
65
packages/frontend/component/src/ui/radio/styles.css.ts
Normal file
65
packages/frontend/component/src/ui/radio/styles.css.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { createVar, globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const outerPadding = createVar('radio-outer-padding');
|
||||
export const outerRadius = createVar('radio-outer-radius');
|
||||
export const itemGap = createVar('radio-item-gap');
|
||||
export const itemHeight = createVar('radio-item-height');
|
||||
|
||||
export const radioButton = style({
|
||||
flex: 1,
|
||||
position: 'relative',
|
||||
borderRadius: `calc(${outerRadius} - ${outerPadding})`,
|
||||
height: itemHeight,
|
||||
padding: '4px 8px',
|
||||
fontSize: cssVar('fontXs'),
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
whiteSpace: 'nowrap',
|
||||
userSelect: 'none',
|
||||
fontWeight: 600,
|
||||
selectors: {
|
||||
'&[data-state="checked"]': {
|
||||
color: cssVar('textPrimaryColor'),
|
||||
},
|
||||
'&[data-state="unchecked"]:hover': {
|
||||
background: cssVar('hoverColor'),
|
||||
},
|
||||
},
|
||||
});
|
||||
export const radioButtonContent = style({
|
||||
zIndex: 1,
|
||||
display: 'block',
|
||||
});
|
||||
globalStyle(`${radioButtonContent} > svg`, { display: 'block' });
|
||||
export const radioButtonGroup = style({
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
background: cssVar('hoverColorFilled'),
|
||||
|
||||
borderRadius: outerRadius,
|
||||
padding: outerPadding,
|
||||
gap: itemGap,
|
||||
|
||||
// @ts-expect-error - fix electron drag
|
||||
WebkitAppRegion: 'no-drag',
|
||||
});
|
||||
export const indicator = style({
|
||||
position: 'absolute',
|
||||
borderRadius: 'inherit',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
left: 0,
|
||||
top: 0,
|
||||
background: cssVar('white'),
|
||||
filter: 'drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.1))',
|
||||
opacity: 0,
|
||||
transformOrigin: 'left',
|
||||
selectors: {
|
||||
'[data-state="checked"] > &': {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
});
|
68
packages/frontend/component/src/ui/radio/types.ts
Normal file
68
packages/frontend/component/src/ui/radio/types.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import type { RadioGroupItemProps } from '@radix-ui/react-radio-group';
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
|
||||
type SimpleRadioItem = string;
|
||||
|
||||
export interface RadioProps extends RadioGroupItemProps {
|
||||
items: RadioItem[] | SimpleRadioItem[];
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
|
||||
/**
|
||||
* Total width of the radio group, items will be evenly distributed
|
||||
*/
|
||||
width?: CSSProperties['width'];
|
||||
/**
|
||||
* Distance between outer wrapper and items (in pixels)
|
||||
* @default 2
|
||||
*/
|
||||
padding?: number;
|
||||
/**
|
||||
* Distance between items (in pixels)
|
||||
* @default 4
|
||||
*/
|
||||
gap?: number;
|
||||
/**
|
||||
* Outer border radius (in pixels), the inner item's border radius will be calculated based on `padding` and `borderRadius`
|
||||
* @default 10
|
||||
*/
|
||||
borderRadius?: number;
|
||||
/**
|
||||
* Height of the inner item (in pixels)
|
||||
* @default 28
|
||||
*/
|
||||
itemHeight?: number;
|
||||
|
||||
/**
|
||||
* Custom duration for the indicator animation
|
||||
* @default 250
|
||||
*/
|
||||
animationDuration?: number | string;
|
||||
/**
|
||||
* Custom easing function for the indicator animation
|
||||
* @default 'cubic-bezier(.18,.22,0,1)'
|
||||
*/
|
||||
animationEasing?: string;
|
||||
/** Customize active item's className */
|
||||
activeItemClassName?: string;
|
||||
/** Customize active item's style */
|
||||
activeItemStyle?: CSSProperties;
|
||||
}
|
||||
|
||||
export interface RadioItem {
|
||||
value: string;
|
||||
label?: ReactNode;
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
/** bind `data-testid` */
|
||||
testId?: string;
|
||||
/** Customize button-element's html attributes */
|
||||
attrs?: Partial<
|
||||
Omit<React.HTMLAttributes<HTMLButtonElement>, 'className' | 'style'>
|
||||
> &
|
||||
Record<`data-${string}`, string>;
|
||||
customRender?: (
|
||||
item: Omit<RadioItem, 'customRender'>,
|
||||
index: number
|
||||
) => ReactNode;
|
||||
}
|
@ -37,6 +37,7 @@ export const ResizePanel = ({
|
||||
}: ResizePanelProps) => {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const cornerHandleRef = useRef<HTMLDivElement | null>(null);
|
||||
const displayRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || !cornerHandleRef.current) return;
|
||||
@ -48,6 +49,7 @@ export const ResizePanel = ({
|
||||
let startSize: [number, number] = [0, 0];
|
||||
|
||||
const onDragStart = (e: MouseEvent) => {
|
||||
containerEl.dataset.resizing = 'true';
|
||||
startPos = [e.clientX, e.clientY];
|
||||
startSize = [containerEl.offsetWidth, containerEl.offsetHeight];
|
||||
document.addEventListener('mousemove', onDrag);
|
||||
@ -62,6 +64,7 @@ export const ResizePanel = ({
|
||||
};
|
||||
|
||||
const onDragEnd = () => {
|
||||
containerEl.dataset.resizing = 'false';
|
||||
document.removeEventListener('mousemove', onDrag);
|
||||
document.removeEventListener('mouseup', onDragEnd);
|
||||
};
|
||||
@ -84,6 +87,10 @@ export const ResizePanel = ({
|
||||
);
|
||||
containerEl.style.height = `${height}px`;
|
||||
}
|
||||
|
||||
if (displayRef.current) {
|
||||
displayRef.current.textContent = `${containerEl.offsetWidth}px * ${containerEl.offsetHeight}px`;
|
||||
}
|
||||
};
|
||||
|
||||
updateSize([width ?? 400, height ?? 200]);
|
||||
@ -112,7 +119,9 @@ export const ResizePanel = ({
|
||||
{...attrs}
|
||||
>
|
||||
{children}
|
||||
<div ref={cornerHandleRef} className={styles.cornerHandle}></div>
|
||||
<div ref={cornerHandleRef} className={styles.cornerHandle}>
|
||||
<div ref={displayRef} className={styles.display}></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
const HANDLE_SIZE = 24;
|
||||
export const container = style({
|
||||
@ -19,3 +20,29 @@ export const cornerHandle = style({
|
||||
transform: 'rotate(45deg)',
|
||||
cursor: 'nwse-resize',
|
||||
});
|
||||
export const display = style({
|
||||
position: 'absolute',
|
||||
left: 32,
|
||||
top: 12,
|
||||
transform: 'rotate(-45deg)',
|
||||
transformOrigin: '0 0',
|
||||
whiteSpace: 'nowrap',
|
||||
borderRadius: 6,
|
||||
background: cssVar('black'),
|
||||
color: cssVar('white'),
|
||||
borderTopLeftRadius: 0,
|
||||
|
||||
maxWidth: 0,
|
||||
maxHeight: 0,
|
||||
padding: 0,
|
||||
transition: 'all 0.23s ease',
|
||||
overflow: 'hidden',
|
||||
|
||||
selectors: {
|
||||
'[data-resizing="true"] &': {
|
||||
padding: '4px 8px',
|
||||
maxWidth: 200,
|
||||
maxHeight: 40,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
19
packages/frontend/component/src/utils/with-unit.ts
Normal file
19
packages/frontend/component/src/utils/with-unit.ts
Normal file
@ -0,0 +1,19 @@
|
||||
type AllowedUnits = 'px' | 'ms';
|
||||
|
||||
/**
|
||||
* get value with unit
|
||||
*/
|
||||
export const withUnit = (
|
||||
value: string | number,
|
||||
unit: AllowedUnits
|
||||
): string => {
|
||||
if (typeof value === 'number') {
|
||||
return `${value}${unit}`;
|
||||
}
|
||||
|
||||
if (/^\d+(\.\d+)?$/.test(value)) {
|
||||
return `${value}${unit}`;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
10
yarn.lock
10
yarn.lock
@ -354,7 +354,7 @@ __metadata:
|
||||
rxjs: "npm:^7.8.1"
|
||||
sonner: "npm:^1.4.41"
|
||||
storybook: "npm:^7.6.17"
|
||||
storybook-dark-mode: "npm:^4.0.0"
|
||||
storybook-dark-mode: "npm:4.0.1"
|
||||
swr: "npm:^2.2.5"
|
||||
typescript: "npm:^5.4.5"
|
||||
uuid: "npm:^10.0.0"
|
||||
@ -36220,9 +36220,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"storybook-dark-mode@npm:^4.0.0":
|
||||
version: 4.0.2
|
||||
resolution: "storybook-dark-mode@npm:4.0.2"
|
||||
"storybook-dark-mode@npm:4.0.1":
|
||||
version: 4.0.1
|
||||
resolution: "storybook-dark-mode@npm:4.0.1"
|
||||
dependencies:
|
||||
"@storybook/components": "npm:^8.0.0"
|
||||
"@storybook/core-events": "npm:^8.0.0"
|
||||
@ -36232,7 +36232,7 @@ __metadata:
|
||||
"@storybook/theming": "npm:^8.0.0"
|
||||
fast-deep-equal: "npm:^3.1.3"
|
||||
memoizerific: "npm:^1.11.3"
|
||||
checksum: 10/c9ef7bc6734df7486ff763c9da3c69505269eaf5fd7b5b489553f023b363ea892862241e6d701ad647ca5d1e64fd9a2646b8985c7ea8ac97a3bca87891db6fe5
|
||||
checksum: 10/3225e5bdaba0ea76b65d642202d9712d7de234e3b5673fb46e444892ab114be207dd287778e2002b662ec35bb8153d2624ff280ce51c5299fb13c711431dad40
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user