mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 18:26:05 +08:00
feat(component): new week-date-picker component (#5477)
<picture> <source media="(prefers-color-scheme: dark)" srcset="https://github.com/toeverything/AFFiNE/assets/39363750/49d7a1ee-2832-4b61-a427-e627dae92952"> <img height="100" alt="" src="https://github.com/toeverything/AFFiNE/assets/39363750/819d6ee9-38e0-4537-ad0f-ec9faf96f505"> </picture>
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
export * from './date-picker';
|
export * from './date-picker';
|
||||||
|
export * from './week-date-picker';
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const weekDatePicker = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 4,
|
||||||
|
|
||||||
|
height: '100%',
|
||||||
|
maxHeight: '39px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const weekDatePickerContent = style({
|
||||||
|
width: 0,
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'stretch',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 4,
|
||||||
|
userSelect: 'none',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const dayCell = style({
|
||||||
|
position: 'relative',
|
||||||
|
width: 0,
|
||||||
|
flexGrow: 1,
|
||||||
|
minWidth: 30,
|
||||||
|
maxWidth: 130,
|
||||||
|
|
||||||
|
cursor: 'pointer',
|
||||||
|
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
|
||||||
|
padding: '2px 4px 1px 4px',
|
||||||
|
borderRadius: 4,
|
||||||
|
|
||||||
|
fontFamily: 'var(--affine-font-family)',
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: 12,
|
||||||
|
|
||||||
|
selectors: {
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'var(--affine-hover-color)',
|
||||||
|
},
|
||||||
|
'&[data-today="true"]:not([data-active="true"])': {
|
||||||
|
vars: {
|
||||||
|
'--cell-color': 'var(--affine-brand-color)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'&[data-active="true"]': {
|
||||||
|
vars: {
|
||||||
|
'--cell-color': 'var(--affine-pure-white)',
|
||||||
|
},
|
||||||
|
background: 'var(--affine-brand-color)',
|
||||||
|
},
|
||||||
|
|
||||||
|
// interactive
|
||||||
|
'&::before, &::after': {
|
||||||
|
content: '',
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
zIndex: 1,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
borderRadius: 'inherit',
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
'&::before': {
|
||||||
|
boxShadow: '0 0 0 2px var(--affine-brand-color)',
|
||||||
|
},
|
||||||
|
'&::after': {
|
||||||
|
border: '1px solid var(--affine-brand-color)',
|
||||||
|
},
|
||||||
|
'&:focus-visible::before': {
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
'&:focus-visible::after': {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export const dayCellWeek = style({
|
||||||
|
width: '100%',
|
||||||
|
height: 16,
|
||||||
|
lineHeight: '16px',
|
||||||
|
textAlign: 'center',
|
||||||
|
|
||||||
|
textOverflow: 'clip',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
color: 'var(--cell-color, var(--affine-text-secondary-color))',
|
||||||
|
});
|
||||||
|
export const dayCellDate = style({
|
||||||
|
width: '100%',
|
||||||
|
height: 20,
|
||||||
|
lineHeight: '20px',
|
||||||
|
textAlign: 'center',
|
||||||
|
|
||||||
|
color: 'var(--cell-color, var(--affine-text-primary-color))',
|
||||||
|
});
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import type { Meta, StoryFn } from '@storybook/react';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { ResizePanel } from '../resize-panel/resize-panel';
|
||||||
|
import { WeekDatePicker } from './week-date-picker';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'UI/Date Picker/Week Date Picker',
|
||||||
|
} satisfies Meta<typeof WeekDatePicker>;
|
||||||
|
|
||||||
|
const _format = 'YYYY-MM-DD';
|
||||||
|
|
||||||
|
const Template: StoryFn<typeof WeekDatePicker> = args => {
|
||||||
|
const [date, setDate] = useState(dayjs().format(_format));
|
||||||
|
return (
|
||||||
|
<div style={{ paddingBottom: 100 }}>
|
||||||
|
<div style={{ marginBottom: 20 }}>Selected Date: {date}</div>
|
||||||
|
|
||||||
|
<ResizePanel
|
||||||
|
width={600}
|
||||||
|
height={56}
|
||||||
|
minHeight={56}
|
||||||
|
minWidth={100}
|
||||||
|
maxWidth={1400}
|
||||||
|
horizontal={true}
|
||||||
|
vertical={false}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'stretch',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<WeekDatePicker
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={date}
|
||||||
|
{...args}
|
||||||
|
onChange={e => {
|
||||||
|
setDate(dayjs(e, _format).format(_format));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ResizePanel>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Basic: StoryFn<typeof WeekDatePicker> = Template.bind(undefined);
|
||||||
|
Basic.args = {};
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
import { ArrowLeftSmallIcon, ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import {
|
||||||
|
type ForwardedRef,
|
||||||
|
type HTMLAttributes,
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import { IconButton } from '../button';
|
||||||
|
import * as styles from './week-date-picker.css';
|
||||||
|
|
||||||
|
export interface WeekDatePickerHandle {
|
||||||
|
/** control cursor manually */
|
||||||
|
setCursor?: (cursor: dayjs.Dayjs) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeekDatePickerProps
|
||||||
|
extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
||||||
|
value?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
handleRef?: ForwardedRef<WeekDatePickerHandle>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: i18n
|
||||||
|
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||||
|
// const weekDays = ['日', '一', '二', '三', '四', '五', '六'];
|
||||||
|
const format = 'YYYY-MM-DD';
|
||||||
|
|
||||||
|
export const WeekDatePicker = memo(function WeekDatePicker({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
handleRef,
|
||||||
|
...attrs
|
||||||
|
}: WeekDatePickerProps) {
|
||||||
|
const weekRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const [cursor, setCursor] = useState(dayjs(value));
|
||||||
|
const [range, setRange] = useState([0, 7]);
|
||||||
|
const [dense, setDense] = useState(false);
|
||||||
|
const [allDays, setAllDays] = useState<dayjs.Dayjs[]>([]);
|
||||||
|
const [viewPortSize, setViewPortSize] = useState(7);
|
||||||
|
|
||||||
|
useImperativeHandle(handleRef, () => ({
|
||||||
|
setCursor,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const displayDays = allDays.slice(...range);
|
||||||
|
|
||||||
|
const updateRange = useCallback(
|
||||||
|
(newRange: [number, number]) => {
|
||||||
|
if (range && newRange[0] === range[0] && newRange[1] === range[1]) return;
|
||||||
|
setRange(newRange);
|
||||||
|
},
|
||||||
|
[range]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onNext = useCallback(() => {
|
||||||
|
if (viewPortSize === 7) {
|
||||||
|
setCursor(cursor.add(1, 'week'));
|
||||||
|
} else if (range[1] === 7) {
|
||||||
|
setCursor(cursor.add(1, 'week'));
|
||||||
|
updateRange([0, viewPortSize]);
|
||||||
|
} else {
|
||||||
|
const end = Math.min(range[1] + viewPortSize, 7);
|
||||||
|
const start = Math.min(range[0] + viewPortSize, end - viewPortSize);
|
||||||
|
updateRange([start, end]);
|
||||||
|
}
|
||||||
|
}, [cursor, range, updateRange, viewPortSize]);
|
||||||
|
const onPrev = useCallback(() => {
|
||||||
|
if (viewPortSize === 7) {
|
||||||
|
setCursor(cursor.add(-1, 'week'));
|
||||||
|
} else if (range[0] === 0) {
|
||||||
|
setCursor(cursor.add(-1, 'week'));
|
||||||
|
updateRange([7 - viewPortSize, 7]);
|
||||||
|
} else {
|
||||||
|
const start = Math.max(range[0] - viewPortSize, 0);
|
||||||
|
const end = Math.max(range[1] - viewPortSize, start + viewPortSize);
|
||||||
|
updateRange([start, end]);
|
||||||
|
}
|
||||||
|
}, [cursor, range, updateRange, viewPortSize]);
|
||||||
|
const onDayClick = useCallback(
|
||||||
|
(day: dayjs.Dayjs) => {
|
||||||
|
onChange?.(day.format(format));
|
||||||
|
},
|
||||||
|
[onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Observe weekRef to update viewPortSize
|
||||||
|
useEffect(() => {
|
||||||
|
const el = weekRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(entries => {
|
||||||
|
const rect = entries[0].contentRect;
|
||||||
|
const width = rect.width;
|
||||||
|
if (!width) return;
|
||||||
|
|
||||||
|
const minWidth = 30;
|
||||||
|
const gap = 4;
|
||||||
|
const viewPortCount = Math.floor(width / (minWidth + gap));
|
||||||
|
setViewPortSize(Math.max(1, Math.min(viewPortCount, 7)));
|
||||||
|
setDense(width < 300);
|
||||||
|
});
|
||||||
|
resizeObserver.observe(el);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// update allDays when cursor changes
|
||||||
|
useEffect(() => {
|
||||||
|
const firstDay = dayjs(cursor).startOf('week');
|
||||||
|
if (allDays[0] && firstDay.isSame(allDays[0], 'day')) return;
|
||||||
|
|
||||||
|
setAllDays(
|
||||||
|
Array.from({ length: 7 }).map((_, index) =>
|
||||||
|
firstDay.add(index, 'day').startOf('day')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [allDays, cursor]);
|
||||||
|
|
||||||
|
// when viewPortSize changes, reset range
|
||||||
|
useEffect(() => {
|
||||||
|
if (viewPortSize >= 7) updateRange([0, 7]);
|
||||||
|
else {
|
||||||
|
const end = Math.min(7, range[0] + viewPortSize);
|
||||||
|
const start = Math.max(0, end - viewPortSize);
|
||||||
|
updateRange([start, end]);
|
||||||
|
}
|
||||||
|
}, [range, updateRange, viewPortSize]);
|
||||||
|
|
||||||
|
// when value changes, reset cursor
|
||||||
|
useEffect(() => {
|
||||||
|
value && setCursor(dayjs(value));
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
// TODO: keyboard navigation
|
||||||
|
useEffect(() => {
|
||||||
|
if (!weekRef.current) return;
|
||||||
|
const el = weekRef.current;
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const focused = document.activeElement as HTMLElement;
|
||||||
|
if (!focused) return el.querySelector('button')?.focus();
|
||||||
|
const day = dayjs(focused.dataset.value);
|
||||||
|
if (
|
||||||
|
(day.day() === 0 && e.key === 'ArrowLeft') ||
|
||||||
|
(e.key === 'ArrowLeft' && !focused.previousElementSibling)
|
||||||
|
) {
|
||||||
|
onPrev();
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
el.querySelector('button')?.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(day.day() === 6 && e.key === 'ArrowRight') ||
|
||||||
|
(e.key === 'ArrowRight' && !focused.nextElementSibling)
|
||||||
|
) {
|
||||||
|
onNext();
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
(el.querySelector('button:last-child') as HTMLElement)?.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'ArrowLeft' && focused.previousElementSibling) {
|
||||||
|
(focused.previousElementSibling as HTMLElement).focus();
|
||||||
|
}
|
||||||
|
if (e.key === 'ArrowRight' && focused.nextElementSibling) {
|
||||||
|
(focused.nextElementSibling as HTMLElement).focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
el.addEventListener('keydown', onKeyDown);
|
||||||
|
return () => {
|
||||||
|
el.removeEventListener('keydown', onKeyDown);
|
||||||
|
};
|
||||||
|
}, [onNext, onPrev]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx(styles.weekDatePicker, className)} {...attrs}>
|
||||||
|
<IconButton onClick={onPrev}>
|
||||||
|
<ArrowLeftSmallIcon />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<div ref={weekRef} className={styles.weekDatePickerContent}>
|
||||||
|
{displayDays.map(day => (
|
||||||
|
<Cell
|
||||||
|
key={day.toISOString()}
|
||||||
|
dense={dense}
|
||||||
|
value={value}
|
||||||
|
day={day}
|
||||||
|
cursor={cursor}
|
||||||
|
onClick={onDayClick}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<IconButton onClick={onNext}>
|
||||||
|
<ArrowRightSmallIcon />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
interface CellProps {
|
||||||
|
dense: boolean;
|
||||||
|
day: dayjs.Dayjs;
|
||||||
|
cursor: dayjs.Dayjs;
|
||||||
|
value?: string;
|
||||||
|
onClick: (day: dayjs.Dayjs) => void;
|
||||||
|
}
|
||||||
|
const Cell = ({ day, dense, value, cursor, onClick }: CellProps) => {
|
||||||
|
const isActive = day.format(format) === value;
|
||||||
|
const isCurrentMonth = day.month() === cursor.month();
|
||||||
|
const isToday = day.isSame(dayjs(), 'day');
|
||||||
|
|
||||||
|
const dayIndex = day.day();
|
||||||
|
const label = weekDays[dayIndex];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={day.format(format)}
|
||||||
|
data-active={isActive}
|
||||||
|
data-curr-month={isCurrentMonth}
|
||||||
|
data-today={isToday}
|
||||||
|
data-value={day.format(format)}
|
||||||
|
key={day.toISOString()}
|
||||||
|
className={styles.dayCell}
|
||||||
|
onClick={() => onClick(day)}
|
||||||
|
>
|
||||||
|
<div className={styles.dayCellWeek}>
|
||||||
|
{dense ? label.slice(0, 1) : label}
|
||||||
|
</div>
|
||||||
|
<div className={styles.dayCellDate}>{day.format('D')}</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
122
packages/frontend/component/src/ui/resize-panel/resize-panel.tsx
Normal file
122
packages/frontend/component/src/ui/resize-panel/resize-panel.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import {
|
||||||
|
type HTMLAttributes,
|
||||||
|
type PropsWithChildren,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
|
export interface ResizePanelProps
|
||||||
|
extends PropsWithChildren,
|
||||||
|
HTMLAttributes<HTMLDivElement> {
|
||||||
|
horizontal?: boolean;
|
||||||
|
vertical?: boolean;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
minWidth?: number;
|
||||||
|
minHeight?: number;
|
||||||
|
maxWidth?: number;
|
||||||
|
maxHeight?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component is used for debugging responsive layout in storybook
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export const ResizePanel = ({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
children,
|
||||||
|
minHeight,
|
||||||
|
minWidth,
|
||||||
|
maxHeight,
|
||||||
|
maxWidth,
|
||||||
|
className,
|
||||||
|
horizontal = true,
|
||||||
|
vertical = true,
|
||||||
|
|
||||||
|
...attrs
|
||||||
|
}: ResizePanelProps) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const cornerHandleRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current || !cornerHandleRef.current) return;
|
||||||
|
|
||||||
|
const containerEl = containerRef.current;
|
||||||
|
const cornerHandleEl = cornerHandleRef.current;
|
||||||
|
|
||||||
|
let startPos: [number, number] = [0, 0];
|
||||||
|
let startSize: [number, number] = [0, 0];
|
||||||
|
|
||||||
|
const onDragStart = (e: MouseEvent) => {
|
||||||
|
startPos = [e.clientX, e.clientY];
|
||||||
|
startSize = [containerEl.offsetWidth, containerEl.offsetHeight];
|
||||||
|
document.addEventListener('mousemove', onDrag);
|
||||||
|
document.addEventListener('mouseup', onDragEnd);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDrag = (e: MouseEvent) => {
|
||||||
|
const pos = [e.clientX, e.clientY];
|
||||||
|
const delta = [pos[0] - startPos[0], pos[1] - startPos[1]];
|
||||||
|
const newSize = [startSize[0] + delta[0], startSize[1] + delta[1]];
|
||||||
|
updateSize(newSize);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragEnd = () => {
|
||||||
|
document.removeEventListener('mousemove', onDrag);
|
||||||
|
document.removeEventListener('mouseup', onDragEnd);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSize = (size: number[]) => {
|
||||||
|
if (!containerEl) return;
|
||||||
|
|
||||||
|
if (horizontal) {
|
||||||
|
const width = Math.max(
|
||||||
|
Math.min(size[0], maxWidth ?? Infinity),
|
||||||
|
minWidth ?? 0
|
||||||
|
);
|
||||||
|
containerEl.style.width = `${width}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vertical) {
|
||||||
|
const height = Math.max(
|
||||||
|
Math.min(size[1], maxHeight ?? Infinity),
|
||||||
|
minHeight ?? 0
|
||||||
|
);
|
||||||
|
containerEl.style.height = `${height}px`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateSize([width ?? 400, height ?? 200]);
|
||||||
|
cornerHandleEl.addEventListener('mousedown', onDragStart);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cornerHandleEl.removeEventListener('mousedown', onDragStart);
|
||||||
|
document.removeEventListener('mousemove', onDrag);
|
||||||
|
document.removeEventListener('mouseup', onDragEnd);
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
height,
|
||||||
|
horizontal,
|
||||||
|
maxHeight,
|
||||||
|
maxWidth,
|
||||||
|
minHeight,
|
||||||
|
minWidth,
|
||||||
|
vertical,
|
||||||
|
width,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={clsx(styles.container, className)}
|
||||||
|
{...attrs}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<div ref={cornerHandleRef} className={styles.cornerHandle}></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
const HANDLE_SIZE = 24;
|
||||||
|
|
||||||
|
export const container = style({
|
||||||
|
position: 'relative',
|
||||||
|
border: '1px solid rgba(100, 100, 100, 0.2)',
|
||||||
|
padding: 8,
|
||||||
|
borderRadius: 4,
|
||||||
|
borderBottomRightRadius: HANDLE_SIZE / 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const cornerHandle = style({
|
||||||
|
position: 'absolute',
|
||||||
|
top: `calc(100% - ${HANDLE_SIZE / 1.5}px)`,
|
||||||
|
left: `calc(100% - ${HANDLE_SIZE / 1.5}px)`,
|
||||||
|
width: HANDLE_SIZE,
|
||||||
|
height: HANDLE_SIZE,
|
||||||
|
|
||||||
|
borderRadius: '50%',
|
||||||
|
border: '2px solid transparent',
|
||||||
|
borderRightColor: 'rgba(100, 100, 100, 0.3)',
|
||||||
|
transform: 'rotate(45deg)',
|
||||||
|
|
||||||
|
cursor: 'nwse-resize',
|
||||||
|
});
|
||||||
@@ -1028,5 +1028,12 @@
|
|||||||
"com.affine.page-operation.add-linked-page": "Add linked page",
|
"com.affine.page-operation.add-linked-page": "Add linked page",
|
||||||
"com.affine.onboarding.workspace-guide.title": "Start AFFiNE by creating your own Workspace here!",
|
"com.affine.onboarding.workspace-guide.title": "Start AFFiNE by creating your own Workspace here!",
|
||||||
"com.affine.onboarding.workspace-guide.content": "A Workspace is your virtual space to capture, create and plan as just one person or together as a team.",
|
"com.affine.onboarding.workspace-guide.content": "A Workspace is your virtual space to capture, create and plan as just one person or together as a team.",
|
||||||
"com.affine.onboarding.workspace-guide.got-it": "Got it!"
|
"com.affine.onboarding.workspace-guide.got-it": "Got it!",
|
||||||
|
"com.affine.calendar.weekdays.sun": "Sun",
|
||||||
|
"com.affine.calendar.weekdays.mon": "Mon",
|
||||||
|
"com.affine.calendar.weekdays.tue": "Tue",
|
||||||
|
"com.affine.calendar.weekdays.wed": "Wed",
|
||||||
|
"com.affine.calendar.weekdays.thu": "Thu",
|
||||||
|
"com.affine.calendar.weekdays.fri": "Fri",
|
||||||
|
"com.affine.calendar.weekdays.sat": "Sat"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user