diff --git a/packages/frontend/component/.storybook/preview.tsx b/packages/frontend/component/.storybook/preview.tsx index 942413a87..f47232491 100644 --- a/packages/frontend/component/.storybook/preview.tsx +++ b/packages/frontend/component/.storybook/preview.tsx @@ -1,3 +1,4 @@ +import '../src/theme/global.css'; import './preview.css'; import { ThemeProvider, useTheme } from 'next-themes'; import type { ComponentType } from 'react'; diff --git a/packages/frontend/component/package.json b/packages/frontend/component/package.json index 63fb5f515..40fec0331 100644 --- a/packages/frontend/component/package.json +++ b/packages/frontend/component/package.json @@ -31,6 +31,7 @@ "@emotion/react": "^11.11.1", "@emotion/server": "^11.11.0", "@emotion/styled": "^11.11.0", + "@lit/react": "^1.0.2", "@popperjs/core": "^2.11.8", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", diff --git a/packages/frontend/component/src/components/date-picker/date-picker.tsx b/packages/frontend/component/src/components/date-picker/date-picker.tsx deleted file mode 100644 index e154676a7..000000000 --- a/packages/frontend/component/src/components/date-picker/date-picker.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import { - ArrowDownSmallIcon, - ArrowLeftSmallIcon, - ArrowRightSmallIcon, -} from '@blocksuite/icons'; -import dayjs from 'dayjs'; -import { useCallback, useState } from 'react'; -import DatePicker from 'react-datepicker'; - -import * as styles from './index.css'; -const months = [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December', -]; -type DatePickerProps = { - value?: string; - onChange: (value: string) => void; -}; - -export const AFFiNEDatePicker = (props: DatePickerProps) => { - const { value, onChange } = props; - const [openMonthPicker, setOpenMonthPicker] = useState(false); - const [selectedDate, setSelectedDate] = useState( - value ? dayjs(value).toDate() : null - ); - const handleOpenMonthPicker = useCallback(() => { - setOpenMonthPicker(true); - }, []); - const handleCloseMonthPicker = useCallback(() => { - setOpenMonthPicker(false); - }, []); - const handleSelectDate = (date: Date | null) => { - if (date) { - setSelectedDate(date); - onChange(dayjs(date).format('YYYY-MM-DD')); - setOpenMonthPicker(false); - } - }; - const renderCustomHeader = ({ - date, - decreaseMonth, - increaseMonth, - prevMonthButtonDisabled, - nextMonthButtonDisabled, - }: { - date: Date; - decreaseMonth: () => void; - increaseMonth: () => void; - prevMonthButtonDisabled: boolean; - nextMonthButtonDisabled: boolean; - }) => { - const selectedYear = dayjs(date).year(); - const selectedMonth = dayjs(date).month(); - return ( -
-
- {months[selectedMonth]} -
-
- {selectedYear} -
-
- -
- - -
- ); - }; - const renderCustomMonthHeader = ({ - date, - decreaseYear, - increaseYear, - prevYearButtonDisabled, - nextYearButtonDisabled, - }: { - date: Date; - decreaseYear: () => void; - increaseYear: () => void; - prevYearButtonDisabled: boolean; - nextYearButtonDisabled: boolean; - }) => { - const selectedYear = dayjs(date).year(); - return ( -
-
- {selectedYear} -
- - -
- ); - }; - return ( - styles.weekStyle} - dayClassName={() => styles.dayStyle} - popperClassName={styles.popperStyle} - monthClassName={() => styles.mouthsStyle} - selected={selectedDate} - onChange={handleSelectDate} - showPopperArrow={false} - dateFormat="MMM dd" - showMonthYearPicker={openMonthPicker} - shouldCloseOnSelect={!openMonthPicker} - renderCustomHeader={({ - date, - decreaseYear, - increaseYear, - decreaseMonth, - increaseMonth, - prevYearButtonDisabled, - nextYearButtonDisabled, - prevMonthButtonDisabled, - nextMonthButtonDisabled, - }) => - openMonthPicker - ? renderCustomMonthHeader({ - date, - decreaseYear, - increaseYear, - prevYearButtonDisabled, - nextYearButtonDisabled, - }) - : renderCustomHeader({ - date, - decreaseMonth, - increaseMonth, - prevMonthButtonDisabled, - nextMonthButtonDisabled, - }) - } - /> - ); -}; - -export default AFFiNEDatePicker; diff --git a/packages/frontend/component/src/index.ts b/packages/frontend/component/src/index.ts index aed6a0731..d325cdb79 100644 --- a/packages/frontend/component/src/index.ts +++ b/packages/frontend/component/src/index.ts @@ -3,6 +3,7 @@ export * from './styles'; export * from './ui/avatar'; export * from './ui/button'; export * from './ui/checkbox'; +export * from './ui/date-picker'; export * from './ui/divider'; export * from './ui/empty'; export * from './ui/input'; diff --git a/packages/frontend/component/src/theme/global.css b/packages/frontend/component/src/theme/global.css index 8f51d01a7..66a35fc6e 100644 --- a/packages/frontend/component/src/theme/global.css +++ b/packages/frontend/component/src/theme/global.css @@ -1,4 +1,3 @@ -@import 'react-datepicker/dist/react-datepicker.css'; @import './fonts.css'; * { diff --git a/packages/frontend/component/src/ui/date-picker/blocksuite-date-picker.stories.tsx b/packages/frontend/component/src/ui/date-picker/blocksuite-date-picker.stories.tsx new file mode 100644 index 000000000..b18a90a7c --- /dev/null +++ b/packages/frontend/component/src/ui/date-picker/blocksuite-date-picker.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryFn } from '@storybook/react'; + +import { BlocksuiteDatePicker } from './blocksuite-date-picker'; + +export default { + title: 'UI/Date Picker/Blocksuite Date Picker', +} satisfies Meta; + +export const Basic: StoryFn = () => { + return ( +
+ +
+ ); +}; diff --git a/packages/frontend/component/src/ui/date-picker/blocksuite-date-picker.tsx b/packages/frontend/component/src/ui/date-picker/blocksuite-date-picker.tsx new file mode 100644 index 000000000..fa97bc93c --- /dev/null +++ b/packages/frontend/component/src/ui/date-picker/blocksuite-date-picker.tsx @@ -0,0 +1,11 @@ +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { DatePicker } from '@blocksuite/blocks/src/_common/components/date-picker/index'; +import { createComponent } from '@lit/react'; +import React from 'react'; + +export const BlocksuiteDatePicker = createComponent({ + tagName: 'date-picker', + elementClass: DatePicker, + react: React, + events: {}, +}); diff --git a/packages/frontend/component/src/ui/date-picker/date-picker.stories.tsx b/packages/frontend/component/src/ui/date-picker/date-picker.stories.tsx new file mode 100644 index 000000000..dd652b5ea --- /dev/null +++ b/packages/frontend/component/src/ui/date-picker/date-picker.stories.tsx @@ -0,0 +1,37 @@ +import type { Meta, StoryFn } from '@storybook/react'; +import dayjs from 'dayjs'; +import { useState } from 'react'; + +import { AFFiNEDatePicker } from '.'; + +export default { + title: 'UI/Date Picker/Date Picker', +} satisfies Meta; + +const _format = 'YYYY-MM-DD'; + +const Template: StoryFn = args => { + const [date, setDate] = useState(dayjs().format(_format)); + return ( +
+
Selected Date: {date}
+ + { + setDate(dayjs(e, _format).format(_format)); + }} + /> +
+ ); +}; + +export const Basic: StoryFn = Template.bind(undefined); +Basic.args = {}; + +export const Inline: StoryFn = + Template.bind(undefined); +Inline.args = { + inline: true, +}; diff --git a/packages/frontend/component/src/ui/date-picker/date-picker.tsx b/packages/frontend/component/src/ui/date-picker/date-picker.tsx new file mode 100644 index 000000000..99cb499e8 --- /dev/null +++ b/packages/frontend/component/src/ui/date-picker/date-picker.tsx @@ -0,0 +1,257 @@ +import { + ArrowDownSmallIcon, + ArrowLeftSmallIcon, + ArrowRightSmallIcon, +} from '@blocksuite/icons'; +import clsx from 'clsx'; +import dayjs from 'dayjs'; +import { type HTMLAttributes, useCallback, useState } from 'react'; +import DatePicker, { type ReactDatePickerProps } from 'react-datepicker'; + +import * as styles from './index.css'; +const months = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', +]; +export interface AFFiNEDatePickerProps + extends Omit { + value?: string; + onChange: (value: string) => void; +} + +interface HeaderLayoutProps extends HTMLAttributes { + length: number; + left: React.ReactNode; + right: React.ReactNode; +} + +/** + * The `DatePicker` should work with different width + * This is a hack to make header's item align with calendar cell's label, **instead of the cell** + * @param length: number of items that calendar body row has + */ +const HeaderLayout = ({ + length, + left, + right, + className, + ...attrs +}: HeaderLayoutProps) => { + return ( +
+ {Array.from({ length }) + .fill(0) + .map((_, index) => { + const isLeft = index === 0; + const isRight = index === length - 1; + return ( +
+
+ {isLeft ? left : isRight ? right : null} +
+
+ ); + })} +
+ ); +}; + +export const AFFiNEDatePicker = ({ + value, + onChange, + ...props +}: AFFiNEDatePickerProps) => { + const [openMonthPicker, setOpenMonthPicker] = useState(false); + const [selectedDate, setSelectedDate] = useState( + value ? dayjs(value).toDate() : null + ); + const handleOpenMonthPicker = useCallback(() => { + setOpenMonthPicker(true); + }, []); + const handleCloseMonthPicker = useCallback(() => { + setOpenMonthPicker(false); + }, []); + const handleSelectDate = (date: Date | null) => { + if (date) { + setSelectedDate(date); + onChange(dayjs(date).format('YYYY-MM-DD')); + setOpenMonthPicker(false); + } + }; + const renderCustomHeader = ({ + date, + decreaseMonth, + increaseMonth, + prevMonthButtonDisabled, + nextMonthButtonDisabled, + }: { + date: Date; + decreaseMonth: () => void; + increaseMonth: () => void; + prevMonthButtonDisabled: boolean; + nextMonthButtonDisabled: boolean; + }) => { + const selectedYear = dayjs(date).year(); + const selectedMonth = dayjs(date).month(); + return ( + +
+ {months[selectedMonth]} +
+
+ {selectedYear} +
+
+ +
+ + } + right={ +
+ + +
+ } + /> + ); + }; + const renderCustomMonthHeader = ({ + date, + decreaseYear, + increaseYear, + prevYearButtonDisabled, + nextYearButtonDisabled, + }: { + date: Date; + decreaseYear: () => void; + increaseYear: () => void; + prevYearButtonDisabled: boolean; + nextYearButtonDisabled: boolean; + }) => { + const selectedYear = dayjs(date).year(); + return ( + + {selectedYear} + + } + right={ +
+ + +
+ } + /> + ); + }; + return ( + styles.weekStyle} + dayClassName={() => styles.dayStyle} + popperClassName={styles.popperStyle} + monthClassName={() => styles.mouthsStyle} + selected={selectedDate} + onChange={handleSelectDate} + showPopperArrow={false} + dateFormat="MMM dd" + showMonthYearPicker={openMonthPicker} + shouldCloseOnSelect={!openMonthPicker} + renderCustomHeader={({ + date, + decreaseYear, + increaseYear, + decreaseMonth, + increaseMonth, + prevYearButtonDisabled, + nextYearButtonDisabled, + prevMonthButtonDisabled, + nextMonthButtonDisabled, + }) => + openMonthPicker + ? renderCustomMonthHeader({ + date, + decreaseYear, + increaseYear, + prevYearButtonDisabled, + nextYearButtonDisabled, + }) + : renderCustomHeader({ + date, + decreaseMonth, + increaseMonth, + prevMonthButtonDisabled, + nextMonthButtonDisabled, + }) + } + {...props} + /> + ); +}; + +export default AFFiNEDatePicker; diff --git a/packages/frontend/component/src/components/date-picker/index.css.ts b/packages/frontend/component/src/ui/date-picker/index.css.ts similarity index 51% rename from packages/frontend/component/src/components/date-picker/index.css.ts rename to packages/frontend/component/src/ui/date-picker/index.css.ts index ddbccbed4..f7b775708 100644 --- a/packages/frontend/component/src/components/date-picker/index.css.ts +++ b/packages/frontend/component/src/ui/date-picker/index.css.ts @@ -1,5 +1,38 @@ import { globalStyle, style } from '@vanilla-extract/css'; +/** + * we do not import css from 'react-date-picker' anymore + **/ +globalStyle('.react-datepicker__aria-live', { + display: 'none', +}); + +export const basicCell = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + minWidth: '28px', + maxWidth: '50px', + flex: '1', + userSelect: 'none', +}); +export const headerLayoutCell = style([basicCell]); +export const headerLayoutCellOrigin = style({ + width: 0, + height: 'fit-content', + display: 'flex', + selectors: { + '[data-is-left="true"] &': { + justifyContent: 'flex-start', + marginLeft: '-12px', + }, + '[data-is-right="true"] &': { + justifyContent: 'flex-end', + marginRight: '-24px', + }, + }, +}); + export const inputStyle = style({ fontSize: 'var(--affine-font-xs)', width: '50px', @@ -16,52 +49,40 @@ export const inputStyle = style({ }); export const popperStyle = style({ boxShadow: 'var(--affine-shadow-2)', - padding: '0 10px', + // TODO: for menu offset, need to be optimized marginTop: '16px', background: 'var(--affine-background-overlay-panel-color)', borderRadius: '12px', width: '300px', + zIndex: 'var(--affine-z-index-popover)', }); globalStyle('.react-datepicker__header', { - background: 'var(--affine-background-overlay-panel-color)', + background: 'none', border: 'none', - marginBottom: '6px', + marginBottom: '8px', +}); +globalStyle('.react-datepicker__header, .react-datepicker__month', { + display: 'flex', + flexDirection: 'column', + gap: '8px', }); export const headerStyle = style({ - background: 'var(--affine-background-overlay-panel-color)', border: 'none', display: 'flex', - width: '100%', alignItems: 'center', - marginBottom: '12px', - padding: '0 14px', - position: 'relative', + justifyContent: 'space-between', + width: '100%', }); export const monthHeaderStyle = style({ - background: 'var(--affine-background-overlay-panel-color)', border: 'none', - display: 'flex', width: '100%', - alignItems: 'center', marginBottom: '18px', - padding: '0 14px', - position: 'relative', - '::after': { - content: '""', - position: 'absolute', - width: 'calc(100% - 24px)', - height: '1px', - background: 'var(--affine-border-color)', - bottom: '-18px', - left: '12px', - }, }); export const monthTitleStyle = style({ color: 'var(--affine-text-primary-color)', fontWeight: '600', fontSize: 'var(--affine-font-sm)', - marginLeft: '12px', }); export const yearStyle = style({ marginLeft: '8px', @@ -73,77 +94,92 @@ export const mouthStyle = style({ color: 'var(--affine-text-primary-color)', fontWeight: '600', fontSize: 'var(--affine-font-sm)', + cursor: 'pointer', + textAlign: 'center', }); -export const arrowLeftStyle = style({ +export const headerLabel = style({ + display: 'flex', + alignItems: 'center', +}); +export const headerActionWrapper = style({ + display: 'flex', + alignItems: 'center', + gap: 24, +}); +export const headerAction = style({ width: '16px', height: '16px', - textAlign: 'right', - position: 'absolute', - right: '50px', }); -export const arrowRightStyle = style({ - width: '16px', - height: '16px', - right: '14px', - position: 'absolute', + +globalStyle('.react-datepicker__day-names, .react-datepicker__week', { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: 8, }); -export const weekStyle = style({ - fontSize: 'var(--affine-font-xs)', - color: 'var(--affine-text-secondary-color)', - display: 'inline-block', - width: '28px', - height: '28px', - lineHeight: '28px', - padding: '0 4px', - margin: '0px 6px', - verticalAlign: 'middle', +export const row = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: 8, }); +// header day cell +export const weekStyle = style([ + basicCell, + { + height: '28px', + fontSize: 'var(--affine-font-xs)', + fontWeight: 500, + color: 'var(--affine-text-secondary-color)', + }, +]); export const calendarStyle = style({ - background: 'var(--affine-background-overlay-panel-color)', + background: 'none', border: 'none', width: '100%', + padding: '20px', }); -export const dayStyle = style({ - fontSize: 'var(--affine-font-xs)', - color: 'var(--affine-text-primary-color)', - display: 'inline-block', - width: '28px', - height: '28px', - lineHeight: '28px', - padding: '0 4px', - margin: '6px 12px 6px 0px', - verticalAlign: 'middle', - fontWeight: '400', - borderRadius: '8px', - selectors: { - '&:hover': { - background: 'var(--affine-hover-color)', - borderRadius: '8px', - transition: 'background-color 0.3s ease-in-out', - }, - '&[aria-selected="true"]': { - color: 'var(--affine-black)', - background: 'var(--affine-hover-color)', - }, - '&[aria-selected="true"]:hover': { - background: 'var(--affine-hover-color)', - }, - '&[tabindex="0"][aria-selected="false"]': { - background: 'var(--affine-background-overlay-panel-color)', - }, - '&.react-datepicker__day--today[aria-selected="false"]': { - background: 'var(--affine-primary-color)', - color: 'var(--affine-palette-line-white)', - }, - '&.react-datepicker__day--today[aria-selected="false"]:hover': { - color: 'var(--affine-black)', - background: 'var(--affine-hover-color)', - }, - '&.react-datepicker__day--outside-month[aria-selected="false"]': { - color: 'var(--affine-text-disable-color)', + +export const dayStyle = style([ + basicCell, + { + height: '28px', + fontSize: 'var(--affine-font-xs)', + color: 'var(--affine-text-primary-color)', + cursor: 'pointer', + + fontWeight: '400', + borderRadius: '8px', + selectors: { + '&:hover': { + background: 'var(--affine-hover-color)', + borderRadius: '8px', + transition: 'background-color 0.3s ease-in-out', + }, + '&[aria-selected="true"]': { + color: 'var(--affine-black)', + background: 'var(--affine-hover-color)', + }, + '&[aria-selected="true"]:hover': { + background: 'var(--affine-hover-color)', + }, + '&[tabindex="0"][aria-selected="false"]': { + background: 'var(--affine-background-overlay-panel-color)', + }, + '&.react-datepicker__day--today[aria-selected="false"]': { + background: 'var(--affine-primary-color)', + color: 'var(--affine-palette-line-white)', + }, + '&.react-datepicker__day--today[aria-selected="false"]:hover': { + color: 'var(--affine-black)', + background: 'var(--affine-hover-color)', + }, + '&.react-datepicker__day--outside-month[aria-selected="false"]': { + color: 'var(--affine-text-disable-color)', + }, }, }, -}); +]); export const arrowDownStyle = style({ width: '16px', height: '16px', diff --git a/packages/frontend/component/src/components/date-picker/index.tsx b/packages/frontend/component/src/ui/date-picker/index.ts similarity index 100% rename from packages/frontend/component/src/components/date-picker/index.tsx rename to packages/frontend/component/src/ui/date-picker/index.ts diff --git a/packages/frontend/core/src/components/page-list/filter/literal-matcher.tsx b/packages/frontend/core/src/components/page-list/filter/literal-matcher.tsx index 83cacf6f8..fc3e259fe 100644 --- a/packages/frontend/core/src/components/page-list/filter/literal-matcher.tsx +++ b/packages/frontend/core/src/components/page-list/filter/literal-matcher.tsx @@ -1,5 +1,4 @@ -import { Input, Menu, MenuItem } from '@affine/component'; -import { AFFiNEDatePicker } from '@affine/component/date-picker'; +import { AFFiNEDatePicker, Input, Menu, MenuItem } from '@affine/component'; import type { LiteralValue, Tag } from '@affine/env/filter'; import dayjs from 'dayjs'; import { type ReactNode } from 'react'; diff --git a/tests/storybook/src/stories/datepicker.stories.tsx b/tests/storybook/src/stories/datepicker.stories.tsx index f69cc9635..ccfcb532b 100644 --- a/tests/storybook/src/stories/datepicker.stories.tsx +++ b/tests/storybook/src/stories/datepicker.stories.tsx @@ -1,4 +1,4 @@ -import { AFFiNEDatePicker } from '@affine/component/date-picker'; +import { AFFiNEDatePicker } from '@affine/component'; import type { Meta, StoryFn } from '@storybook/react'; import { useState } from 'react'; @@ -8,7 +8,7 @@ export default { parameters: { chromatic: { disableSnapshot: true }, }, -} satisfies Meta; +} satisfies Meta; export const Default: StoryFn = () => { const [value, setValue] = useState(new Date().toString()); diff --git a/yarn.lock b/yarn.lock index 2eb4fc0da..1f9e6e321 100644 --- a/yarn.lock +++ b/yarn.lock @@ -219,6 +219,7 @@ __metadata: "@emotion/react": "npm:^11.11.1" "@emotion/server": "npm:^11.11.0" "@emotion/styled": "npm:^11.11.0" + "@lit/react": "npm:^1.0.2" "@popperjs/core": "npm:^2.11.8" "@radix-ui/react-avatar": "npm:^1.0.4" "@radix-ui/react-collapsible": "npm:^1.0.3" @@ -6488,6 +6489,15 @@ __metadata: languageName: node linkType: hard +"@lit/react@npm:^1.0.2": + version: 1.0.2 + resolution: "@lit/react@npm:1.0.2" + peerDependencies: + "@types/react": 17 || 18 + checksum: 78bc607f9022ceefa5c714a701b71e444df8d6c88c75b0fe728b7157d3ec00a7fa459bc3c4ce5a4b5315079159c9759be4738e5ade656f8d74e32f729f75efc4 + languageName: node + linkType: hard + "@lit/reactive-element@npm:^2.0.0": version: 2.0.2 resolution: "@lit/reactive-element@npm:2.0.2"