Thank you!
diff --git a/packages/frontend/component/src/components/block-suite-editor/index.css.ts b/packages/frontend/component/src/components/block-suite-editor/index.css.ts
deleted file mode 100644
index 3b7bd0186d..0000000000
--- a/packages/frontend/component/src/components/block-suite-editor/index.css.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { style } from '@vanilla-extract/css';
-
-export const blockSuiteEditorStyle = style({
- maxWidth: 'var(--affine-editor-width)',
- margin: '0 2rem',
- padding: '0 24px',
-});
-
-export const blockSuiteEditorHeaderStyle = style({
- marginTop: '40px',
- marginBottom: '40px',
-});
diff --git a/packages/frontend/component/src/components/block-suite-editor/index.tsx b/packages/frontend/component/src/components/block-suite-editor/index.tsx
deleted file mode 100644
index 568895e9af..0000000000
--- a/packages/frontend/component/src/components/block-suite-editor/index.tsx
+++ /dev/null
@@ -1,275 +0,0 @@
-import { assertExists } from '@blocksuite/global/utils';
-import { AffineEditorContainer } from '@blocksuite/presets';
-import type { Page } from '@blocksuite/store';
-import clsx from 'clsx';
-import { use } from 'foxact/use';
-import type { CSSProperties, ReactElement } from 'react';
-import {
- forwardRef,
- memo,
- Suspense,
- useEffect,
- useLayoutEffect,
- useRef,
- useState,
-} from 'react';
-import { type Map as YMap } from 'yjs';
-
-import { Skeleton } from '../../ui/skeleton';
-import {
- blockSuiteEditorHeaderStyle,
- blockSuiteEditorStyle,
-} from './index.css';
-import { editorSpecs } from './specs';
-
-interface BlockElement extends Element {
- path: string[];
-}
-
-export type EditorProps = {
- page: Page;
- mode: 'page' | 'edgeless';
- defaultSelectedBlockId?: string;
- onModeChange?: (mode: 'page' | 'edgeless') => void;
- // on Editor instance instantiated
- onLoadEditor?: (editor: AffineEditorContainer) => () => void;
- style?: CSSProperties;
- className?: string;
-};
-
-export type ErrorBoundaryProps = {
- onReset?: () => void;
-};
-
-// a workaround for returning the webcomponent for the given block id
-// by iterating over the children of the rendered dom tree
-const useBlockElementById = (
- container: HTMLElement | null,
- blockId: string | undefined,
- timeout = 1000
-) => {
- const [blockElement, setBlockElement] = useState(null);
- useEffect(() => {
- if (!blockId) {
- return;
- }
- let canceled = false;
- const start = Date.now();
- function run() {
- if (canceled || !container) {
- return;
- }
- const element = container.querySelector(
- `[data-block-id="${blockId}"]`
- ) as BlockElement | null;
- if (element) {
- setBlockElement(element);
- } else if (Date.now() - start < timeout) {
- setTimeout(run, 100);
- }
- }
- run();
- return () => {
- canceled = true;
- };
- }, [container, blockId, timeout]);
- return blockElement;
-};
-
-/**
- * TODO: Define error to unexpected state together in the future.
- */
-export class NoPageRootError extends Error {
- constructor(public page: Page) {
- super('Page root not found when render editor!');
-
- // Log info to let sentry collect more message
- const hasExpectSpace = Array.from(page.rootDoc.spaces.values()).some(
- doc => page.spaceDoc.guid === doc.guid
- );
- const blocks = page.spaceDoc.getMap('blocks') as YMap>;
- const havePageBlock = Array.from(blocks.values()).some(
- block => block.get('sys:flavour') === 'affine:page'
- );
- console.info(
- 'NoPageRootError current data: %s',
- JSON.stringify({
- expectPageId: page.id,
- expectGuid: page.spaceDoc.guid,
- hasExpectSpace,
- blockSize: blocks.size,
- havePageBlock,
- })
- );
- }
-}
-
-/**
- * TODO: Defined async cache to support suspense, instead of reflect symbol to provider persistent error cache.
- */
-const PAGE_LOAD_KEY = Symbol('PAGE_LOAD');
-const PAGE_ROOT_KEY = Symbol('PAGE_ROOT');
-function usePageRoot(page: Page) {
- let load$ = Reflect.get(page, PAGE_LOAD_KEY);
- if (!load$) {
- load$ = page.load();
- Reflect.set(page, PAGE_LOAD_KEY, load$);
- }
- use(load$);
-
- if (!page.root) {
- let root$: Promise | undefined = Reflect.get(page, PAGE_ROOT_KEY);
- if (!root$) {
- root$ = new Promise((resolve, reject) => {
- const disposable = page.slots.rootAdded.once(() => {
- resolve();
- });
- window.setTimeout(() => {
- disposable.dispose();
- reject(new NoPageRootError(page));
- }, 20 * 1000);
- });
- Reflect.set(page, PAGE_ROOT_KEY, root$);
- }
- use(root$);
- }
-
- return page.root;
-}
-
-const BlockSuiteEditorImpl = forwardRef(
- (
- {
- mode,
- page,
- className,
- defaultSelectedBlockId,
- onLoadEditor,
- onModeChange,
- style,
- },
- ref
- ): ReactElement => {
- usePageRoot(page);
-
- assertExists(page, 'page should not be null');
- const editorRef = useRef(null);
- if (editorRef.current === null) {
- editorRef.current = new AffineEditorContainer();
- editorRef.current.autofocus = true;
- }
- const editor = editorRef.current;
- assertExists(editorRef, 'editorRef.current should not be null');
-
- if (editor.mode !== mode) {
- editor.mode = mode;
- }
-
- if (editor.page !== page) {
- editor.page = page;
- editor.docSpecs = editorSpecs.docModeSpecs;
- editor.edgelessSpecs = editorSpecs.edgelessModeSpecs;
- }
-
- if (ref) {
- if (typeof ref === 'function') {
- ref(editor);
- } else {
- ref.current = editor;
- }
- }
-
- useLayoutEffect(() => {
- if (editor) {
- const disposes: (() => void)[] = [];
- const disposeModeSwitch = editor.slots.pageModeSwitched.on(mode => {
- onModeChange?.(mode);
- });
- disposes.push(() => disposeModeSwitch?.dispose());
- if (onLoadEditor) {
- disposes.push(onLoadEditor(editor));
- }
- return () => {
- disposes.forEach(dispose => dispose());
- };
- }
- return;
- }, [editor, onModeChange, onLoadEditor]);
-
- const containerRef = useRef(null);
-
- useEffect(() => {
- const container = containerRef.current;
- if (!container) {
- return;
- }
- container.append(editor);
- return () => {
- editor.remove();
- };
- }, [editor]);
-
- const blockElement = useBlockElementById(
- containerRef.current,
- defaultSelectedBlockId
- );
-
- useEffect(() => {
- if (blockElement) {
- requestIdleCallback(() => {
- blockElement.scrollIntoView({
- behavior: 'smooth',
- block: 'center',
- inline: 'center',
- });
- const selectManager = editor.host?.selection;
- if (!blockElement.path.length || !selectManager) {
- return;
- }
- const newSelection = selectManager.create('block', {
- path: blockElement.path,
- });
- selectManager.set([newSelection]);
- });
- }
- }, [editor, blockElement]);
-
- // issue: https://github.com/toeverything/AFFiNE/issues/2004
- return (
-
- );
- }
-);
-BlockSuiteEditorImpl.displayName = 'BlockSuiteEditorImpl';
-
-export const EditorLoading = memo(function EditorLoading() {
- return (
-
-
-
-
- );
-});
-
-export const BlockSuiteEditor = memo(
- forwardRef(
- function BlockSuiteEditor(props, ref): ReactElement {
- return (
- }>
-
-
- );
- }
- )
-);
-
-BlockSuiteEditor.displayName = 'BlockSuiteEditor';
diff --git a/packages/frontend/component/src/components/block-suite-editor/specs.ts b/packages/frontend/component/src/components/block-suite-editor/specs.ts
deleted file mode 100644
index 8fd3879595..0000000000
--- a/packages/frontend/component/src/components/block-suite-editor/specs.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import {
- AttachmentService,
- DocEditorBlockSpecs,
- EdgelessEditorBlockSpecs,
-} from '@blocksuite/blocks';
-import bytes from 'bytes';
-
-class CustomAttachmentService extends AttachmentService {
- override mounted(): void {
- //TODO: get user type from store
- const userType = 'pro';
- if (userType === 'pro') {
- this.maxFileSize = bytes.parse('100MB');
- } else {
- this.maxFileSize = bytes.parse('10MB');
- }
- }
-}
-
-function getSpecs() {
- const docModeSpecs = DocEditorBlockSpecs.map(spec => {
- if (spec.schema.model.flavour === 'affine:attachment') {
- return {
- ...spec,
- service: CustomAttachmentService,
- };
- }
- return spec;
- });
- const edgelessModeSpecs = EdgelessEditorBlockSpecs.map(spec => {
- if (spec.schema.model.flavour === 'affine:attachment') {
- return {
- ...spec,
- service: CustomAttachmentService,
- };
- }
- return spec;
- });
-
- return {
- docModeSpecs,
- edgelessModeSpecs,
- };
-}
-
-export const editorSpecs = getSpecs();
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 e154676a79..0000000000
--- 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/components/date-picker/index.tsx b/packages/frontend/component/src/components/date-picker/index.tsx
deleted file mode 100644
index e5ce37c674..0000000000
--- a/packages/frontend/component/src/components/date-picker/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export * from './date-picker';
diff --git a/packages/frontend/component/src/components/notification-center/index.jotai.ts b/packages/frontend/component/src/components/notification-center/index.jotai.ts
index c793112cdc..26bc44de42 100644
--- a/packages/frontend/component/src/components/notification-center/index.jotai.ts
+++ b/packages/frontend/component/src/components/notification-center/index.jotai.ts
@@ -11,7 +11,8 @@ export type Notification = {
progressingBar?: boolean;
multimedia?: React.ReactNode | JSX.Element;
// actions
- undo?: () => Promise;
+ action?: () => Promise;
+ actionLabel?: string;
};
const notificationsBaseAtom = atom([]);
@@ -48,19 +49,19 @@ export const pushNotificationAtom = atom(
set(notificationsBaseAtom, notifications =>
notifications.filter(notification => notification.key !== key)
);
- const undo: (() => Promise) | undefined = newNotification.undo
+ const action: (() => Promise) | undefined = newNotification.action
? (() => {
- const undo: () => Promise = newNotification.undo;
- return async function undoNotificationWrapper() {
+ const action: () => Promise = newNotification.action;
+ return async function actionNotificationWrapper() {
removeNotification();
- return undo();
+ return action();
};
})()
: undefined;
set(notificationsBaseAtom, notifications => [
// push to the top
- { ...newNotification, undo },
+ { ...newNotification, action },
...notifications,
]);
}
diff --git a/packages/frontend/component/src/components/notification-center/index.tsx b/packages/frontend/component/src/components/notification-center/index.tsx
index 73870ad836..712301f437 100644
--- a/packages/frontend/component/src/components/notification-center/index.tsx
+++ b/packages/frontend/component/src/components/notification-center/index.tsx
@@ -2,6 +2,7 @@
// License on the MIT
// https://github.com/emilkowalski/sonner/blob/5cb703edc108a23fd74979235c2f3c4005edd2a7/src/index.tsx
+import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CloseIcon, InformationFillDuotoneIcon } from '@blocksuite/icons';
import * as Toast from '@radix-ui/react-toast';
import clsx from 'clsx';
@@ -71,6 +72,7 @@ const typeColorMap = {
};
function NotificationCard(props: NotificationCardProps): ReactNode {
+ const t = useAFFiNEI18N();
const removeNotification = useSetAtom(removeNotificationAtom);
const { notification, notifications, setHeights, heights, index } = props;
@@ -134,7 +136,6 @@ function NotificationCard(props: NotificationCardProps): ReactNode {
notificationNode.style.height = 'auto';
const newHeight = notificationNode.getBoundingClientRect().height;
notificationNode.style.height = originalHeight;
-
setInitialHeight(newHeight);
setHeights(heights => {
@@ -190,9 +191,9 @@ function NotificationCard(props: NotificationCardProps): ReactNode {
};
}, [duration, expand, onClickRemove]);
- const onClickUndo = useCallback(() => {
- if (notification.undo) {
- notification.undo().catch(err => {
+ const onClickAction = useCallback(() => {
+ if (notification.action) {
+ notification.action().catch(err => {
console.error(err);
});
}
@@ -298,7 +299,7 @@ function NotificationCard(props: NotificationCardProps): ReactNode {
{notification.title}
- {notification.undo && (
+ {notification.action && (
- UNDO
+ {notification.actionLabel ??
+ t['com.affine.keyboardShortcuts.undo']()}
)}
{notification.multimedia ? null : (
{
+ return (
+
+
+
+
+ );
+};
+
export const PageDetailSkeleton = () => {
return (
diff --git a/packages/frontend/component/src/components/setting-components/storage-progess.tsx b/packages/frontend/component/src/components/setting-components/storage-progess.tsx
index 174b6c83d8..e5cb1c7053 100644
--- a/packages/frontend/component/src/components/setting-components/storage-progess.tsx
+++ b/packages/frontend/component/src/components/setting-components/storage-progess.tsx
@@ -68,7 +68,11 @@ export const StorageProgress = ({
{upgradable ? (
= T extends any
+ ? K extends keyof T
+ ? Omit
+ : T
+ : T;
+type PropsWithoutRef = DistributiveOmit;
+
+/**
+ * Creates a type to be used for the props of a web component used directly in
+ * React JSX.
+ *
+ * Example:
+ *
+ * ```ts
+ * declare module "react" {
+ * namespace JSX {
+ * interface IntrinsicElements {
+ * 'x-foo': WebComponentProps;
+ * }
+ * }
+ * }
+ * ```
+ */
+export type WebComponentProps = React.DetailedHTMLProps<
+ React.HTMLAttributes,
+ I
+> &
+ ElementProps;
+
+// eslint-disable-next-line @typescript-eslint/ban-types
+type EmptyObject = {};
+
+/**
+ * Type of the React component wrapping the web component. This is the return
+ * type of `createComponent`.
+ */
+export type ReactWebComponent<
+ I extends HTMLElement,
+ E extends EventNames = EmptyObject,
+> = React.ForwardRefExoticComponent<
+ // TODO(augustjk): Remove and use `React.PropsWithoutRef` when
+ // https://github.com/preactjs/preact/issues/4124 is fixed.
+ PropsWithoutRef> & React.RefAttributes
+>;
+
+// Props derived from custom element class. Currently has limitations of making
+// all properties optional and also surfaces life cycle methods in autocomplete.
+// TODO(augustjk) Consider omitting keyof LitElement to remove "internal"
+// lifecycle methods or allow user to explicitly provide props.
+type ElementProps = Partial>;
+
+// Acceptable props to the React component.
+type ComponentProps = Omit<
+ React.HTMLAttributes,
+ // Prefer type of provided event handler props or those on element over
+ // built-in HTMLAttributes
+ keyof E | keyof ElementProps
+> &
+ EventListeners &
+ ElementProps;
+
+/**
+ * Type used to cast an event name with an event type when providing the
+ * `events` option to `createComponent` for better typing of the event handler
+ * prop.
+ *
+ * Example:
+ *
+ * ```ts
+ * const FooComponent = createComponent({
+ * ...
+ * events: {
+ * onfoo: 'foo' as EventName,
+ * }
+ * });
+ * ```
+ *
+ * `onfoo` prop will have the type `(e: FooEvent) => void`.
+ */
+export type EventName = string & {
+ __eventType: T;
+};
+
+// A key value map matching React prop names to event names.
+type EventNames = Record;
+
+// A map of expected event listener types based on EventNames.
+type EventListeners = {
+ [K in keyof R]?: R[K] extends EventName
+ ? (e: R[K]['__eventType']) => void
+ : (e: Event) => void;
+};
+
+export interface Options<
+ I extends HTMLElement,
+ E extends EventNames = EmptyObject,
+> {
+ react: typeof React;
+ tagName?: string; // default to `div`
+ elementClass: Constructor;
+ events?: E;
+ displayName?: string;
+}
+
+type Constructor = { new (): T };
+
+const reservedReactProperties = new Set([
+ 'children',
+ 'localName',
+ 'ref',
+ 'style',
+ 'className',
+]);
+
+const listenedEvents = new WeakMap>();
+
+/**
+ * Adds an event listener for the specified event to the given node. In the
+ * React setup, there should only ever be one event listener. Thus, for
+ * efficiency only one listener is added and the handler for that listener is
+ * updated to point to the given listener function.
+ */
+const addOrUpdateEventListener = (
+ node: Element,
+ event: string,
+ listener: (event?: Event) => void
+) => {
+ let events = listenedEvents.get(node);
+ if (events === undefined) {
+ listenedEvents.set(node, (events = new Map()));
+ }
+ let handler = events.get(event);
+ if (listener !== undefined) {
+ // If necessary, add listener and track handler
+ if (handler === undefined) {
+ events.set(event, (handler = { handleEvent: listener }));
+ node.addEventListener(event, handler);
+ // Otherwise just update the listener with new value
+ } else {
+ handler.handleEvent = listener;
+ }
+ // Remove listener if one exists and value is undefined
+ } else if (handler !== undefined) {
+ events.delete(event);
+ node.removeEventListener(event, handler);
+ }
+};
+
+/**
+ * Sets properties and events on custom elements. These properties and events
+ * have been pre-filtered so we know they should apply to the custom element.
+ */
+const setProperty = (
+ node: E,
+ name: string,
+ value: unknown,
+ old: unknown,
+ events?: EventNames
+) => {
+ const event = events?.[name];
+ // Dirty check event value.
+ if (event !== undefined && value !== old) {
+ addOrUpdateEventListener(node, event, value as (e?: Event) => void);
+ return;
+ }
+ // But don't dirty check properties; elements are assumed to do this.
+ node[name as keyof E] = value as E[keyof E];
+
+ // This block is to replicate React's behavior for attributes of native
+ // elements where `undefined` or `null` values result in attributes being
+ // removed.
+ // https://github.com/facebook/react/blob/899cb95f52cc83ab5ca1eb1e268c909d3f0961e7/packages/react-dom-bindings/src/client/DOMPropertyOperations.js#L107-L141
+ //
+ // It's only needed here for native HTMLElement properties that reflect
+ // attributes of the same name but don't have that behavior like "id" or
+ // "draggable".
+ if (
+ (value === undefined || value === null) &&
+ name in HTMLElement.prototype
+ ) {
+ node.removeAttribute(name);
+ }
+};
+
+/**
+ * Creates a React component for a custom element. Properties are distinguished
+ * from attributes automatically, and events can be configured so they are added
+ * to the custom element as event listeners.
+ *
+ * note from pengx17:
+ * This is a workaround for https://github.com/lit/lit/issues/4435
+ *
+ * Instead of directly using tag names of the custom elements, we create instance and then
+ * append to the container instead. An issue in this workaround is that the custom element
+ * will always be wrapped in an additional tag (the container, e.g. a div).
+ *
+ * @param options An options bag containing the parameters needed to generate a
+ * wrapped web component.
+ *
+ * @param options.react The React module, typically imported from the `react`
+ * npm package.
+ * @param options.tagName The custom element tag name registered via
+ * `customElements.define`.
+ * @param options.elementClass The custom element class registered via
+ * `customElements.define`.
+ * @param options.events An object listing events to which the component can
+ * listen. The object keys are the event property names passed in via React
+ * props and the object values are the names of the corresponding events
+ * generated by the custom element. For example, given `{onactivate:
+ * 'activate'}` an event function may be passed via the component's `onactivate`
+ * prop and will be called when the custom element fires its `activate` event.
+ * @param options.displayName A React component display name, used in debugging
+ * messages. Default value is inferred from the name of custom element class
+ * registered via `customElements.define`.
+ */
+export const createComponent = <
+ I extends HTMLElement,
+ E extends EventNames = EmptyObject,
+>({
+ react: React,
+ tagName = 'div',
+ elementClass,
+ events,
+ displayName,
+}: Options): ReactWebComponent => {
+ const eventProps = new Set(Object.keys(events ?? {}));
+
+ if (DEV_MODE) {
+ for (const p of reservedReactProperties) {
+ if (p in elementClass.prototype && !(p in HTMLElement.prototype)) {
+ // Note, this effectively warns only for `ref` since the other
+ // reserved props are on HTMLElement.prototype. To address this
+ // would require crawling down the prototype, which doesn't feel worth
+ // it since implementing these properties on an element is extremely
+ // rare.
+ console.warn(
+ `${tagName} contains property ${p} which is a React reserved ` +
+ `property. It will be used by React and not set on the element.`
+ );
+ }
+ }
+ }
+
+ type Props = ComponentProps;
+
+ const ReactComponent = React.forwardRef((props, ref) => {
+ const containerRef = React.useRef(null);
+ const prevPropsRef = React.useRef(null);
+ const elementRef = React.useRef(null);
+
+ // Props to be passed to React.createElement
+ const reactProps: Record = {
+ 'data-lit-react-wrapper': elementClass.name,
+ };
+ const elementProps: Record = {};
+
+ if (elementRef.current === null) {
+ const element = new elementClass();
+ elementRef.current = element;
+ if (typeof ref === 'function') {
+ ref(elementRef.current);
+ } else if (ref) {
+ ref.current = element;
+ }
+ }
+
+ const element = elementRef.current;
+
+ for (const [k, v] of Object.entries(props)) {
+ if (reservedReactProperties.has(k)) {
+ reactProps[k] = v;
+ continue;
+ }
+
+ if (eventProps.has(k) || k in elementClass.prototype) {
+ elementProps[k] = v;
+ continue;
+ }
+
+ reactProps[k] = v;
+ }
+
+ // This one has no dependency array so it'll run on every re-render.
+ React.useLayoutEffect(() => {
+ if (elementRef.current === null) {
+ return;
+ }
+ for (const prop in elementProps) {
+ setProperty(
+ elementRef.current,
+ prop,
+ props[prop],
+ prevPropsRef.current ? prevPropsRef.current[prop] : undefined,
+ events
+ );
+ }
+ // Note, the spirit of React might be to "unset" any old values that
+ // are no longer included; however, there's no reasonable value to set
+ // them to so we just leave the previous state as is.
+
+ prevPropsRef.current = props;
+ });
+
+ React.useLayoutEffect(() => {
+ const container = containerRef.current;
+ if (!container) {
+ return;
+ }
+ container.append(element);
+ return () => {
+ element.remove();
+ };
+ }, [element]);
+
+ return React.createElement(tagName, {
+ ...reactProps,
+ ref: React.useCallback(
+ (node: HTMLElement) => {
+ containerRef.current = node;
+ },
+ [containerRef]
+ ),
+ });
+ });
+
+ ReactComponent.displayName = displayName ?? elementClass.name;
+
+ return ReactComponent;
+};
diff --git a/packages/frontend/component/src/lit-react/index.ts b/packages/frontend/component/src/lit-react/index.ts
new file mode 100644
index 0000000000..25f45aab2c
--- /dev/null
+++ b/packages/frontend/component/src/lit-react/index.ts
@@ -0,0 +1 @@
+export { createComponent as createReactComponentFromLit } from './create-component';
diff --git a/packages/frontend/component/src/lit-react/readme.md b/packages/frontend/component/src/lit-react/readme.md
new file mode 100644
index 0000000000..4ea197fb3c
--- /dev/null
+++ b/packages/frontend/component/src/lit-react/readme.md
@@ -0,0 +1,3 @@
+# our custom @lit/react wrapper
+
+The official @lit/react createComponent has an issue with properties that accessed in `connectedCallback` lifecycle hook in lit.
diff --git a/packages/frontend/component/src/theme/global.css b/packages/frontend/component/src/theme/global.css
index 8f51d01a76..dcd9f41f95 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';
* {
@@ -310,3 +309,10 @@ body {
position: relative;
overflow: hidden;
}
+
+/**
+ * A hack to make the anchor wrapper not affect the layout of the page.
+ */
+[data-lit-react-wrapper] {
+ display: contents;
+}
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 0000000000..b18a90a7c4
--- /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 0000000000..fa97bc93c1
--- /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 0000000000..dd652b5ea4
--- /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 0000000000..268728a641
--- /dev/null
+++ b/packages/frontend/component/src/ui/date-picker/date-picker.tsx
@@ -0,0 +1,274 @@
+import {
+ ArrowDownSmallIcon,
+ ArrowLeftSmallIcon,
+ ArrowRightSmallIcon,
+} from '@blocksuite/icons';
+import clsx from 'clsx';
+import dayjs from 'dayjs';
+import { type HTMLAttributes, useCallback, useEffect, 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;
+ onSelect?: (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,
+ onSelect,
+
+ calendarClassName,
+
+ ...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 handleDateChange = (date: Date | null) => {
+ if (date) {
+ setSelectedDate(date);
+ onChange?.(dayjs(date).format('YYYY-MM-DD'));
+ setOpenMonthPicker(false);
+ }
+ };
+ const handleDateSelect = (date: Date | null) => {
+ if (date) {
+ onSelect?.(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={
+