diff --git a/packages/twenty-front/src/modules/object-record/record-calendar/components/RecordCalendar.tsx b/packages/twenty-front/src/modules/object-record/record-calendar/components/RecordCalendar.tsx
new file mode 100644
index 00000000000..508b72fb9ce
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-calendar/components/RecordCalendar.tsx
@@ -0,0 +1,31 @@
+import styled from '@emotion/styled';
+
+import { RecordCalendarTopBar } from '@/object-record/record-calendar/components/RecordCalendarTopBar';
+import { RecordCalendarMonth } from '@/object-record/record-calendar/month/components/RecordCalendarMonth';
+import { RecordCalendarComponentInstanceContext } from '@/object-record/record-calendar/states/contexts/RecordCalendarComponentInstanceContext';
+import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
+import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
+
+const StyledContainerContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ min-height: calc(100% - ${({ theme }) => theme.spacing(2)});
+ height: min-content;
+`;
+
+export const RecordCalendar = () => {
+ const recordCalendarId = useAvailableComponentInstanceIdOrThrow(
+ RecordCalendarComponentInstanceContext,
+ );
+
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-calendar/components/RecordCalendarTopBar.tsx b/packages/twenty-front/src/modules/object-record/record-calendar/components/RecordCalendarTopBar.tsx
new file mode 100644
index 00000000000..a1e6e39879c
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-calendar/components/RecordCalendarTopBar.tsx
@@ -0,0 +1,32 @@
+import { RecordCalendarComponentInstanceContext } from '@/object-record/record-calendar/states/contexts/RecordCalendarComponentInstanceContext';
+import { Select } from '@/ui/input/components/Select';
+import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
+import styled from '@emotion/styled';
+
+const StyledContainer = styled.div`
+ display: flex;
+ height: 24px;
+`;
+
+export const RecordCalendarTopBar = () => {
+ const recordCalendarId = useAvailableComponentInstanceIdOrThrow(
+ RecordCalendarComponentInstanceContext,
+ );
+
+ return (
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-calendar/month/components/RecordCalendarMonth.tsx b/packages/twenty-front/src/modules/object-record/record-calendar/month/components/RecordCalendarMonth.tsx
new file mode 100644
index 00000000000..b3467a7c80f
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-calendar/month/components/RecordCalendarMonth.tsx
@@ -0,0 +1,44 @@
+import { RecordCalendarMonthBody } from '@/object-record/record-calendar/month/components/RecordCalendarMonthBody';
+import { RecordCalendarMonthHeader } from '@/object-record/record-calendar/month/components/RecordCalendarMonthHeader';
+import { RecordCalendarMonthContextProvider } from '@/object-record/record-calendar/month/contexts/RecordCalendarMonthContext';
+import { useRecordCalendarMonthDaysRange } from '@/object-record/record-calendar/month/hooks/useRecordCalendarMonthDaysRange';
+import styled from '@emotion/styled';
+
+const StyledContainer = styled.div`
+ display: flex;
+ height: 24px;
+ flex-direction: column;
+`;
+
+export const RecordCalendarMonth = () => {
+ const currentDay = new Date();
+
+ const {
+ firstDayOfMonth,
+ lastDayOfMonth,
+ firstDayOfFirstWeek,
+ lastDayOfLastWeek,
+ weekDayLabels,
+ weekFirstDays,
+ weekStartsOnDayIndex,
+ } = useRecordCalendarMonthDaysRange(currentDay);
+
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-calendar/month/components/RecordCalendarMonthBody.tsx b/packages/twenty-front/src/modules/object-record/record-calendar/month/components/RecordCalendarMonthBody.tsx
new file mode 100644
index 00000000000..473635f455b
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-calendar/month/components/RecordCalendarMonthBody.tsx
@@ -0,0 +1,24 @@
+import { RecordCalendarMonthBodyWeek } from '@/object-record/record-calendar/month/components/RecordCalendarMonthBodyWeek';
+import { useRecordCalendarMonthContextOrThrow } from '@/object-record/record-calendar/month/contexts/RecordCalendarMonthContext';
+import styled from '@emotion/styled';
+
+const StyledContainer = styled.div`
+ display: flex;
+ height: 24px;
+ flex-direction: column;
+`;
+
+export const RecordCalendarMonthBody = () => {
+ const { weekFirstDays } = useRecordCalendarMonthContextOrThrow();
+
+ return (
+
+ {weekFirstDays.map((weekFirstDay) => (
+
+ ))}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-calendar/month/components/RecordCalendarMonthBodyDay.tsx b/packages/twenty-front/src/modules/object-record/record-calendar/month/components/RecordCalendarMonthBodyDay.tsx
new file mode 100644
index 00000000000..fa12479b0a3
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-calendar/month/components/RecordCalendarMonthBodyDay.tsx
@@ -0,0 +1,16 @@
+import styled from '@emotion/styled';
+
+const StyledContainer = styled.div`
+ display: flex;
+ width: calc(100% / 7);
+`;
+
+type RecordCalendarMonthBodyDayProps = {
+ day: Date;
+};
+
+export const RecordCalendarMonthBodyDay = ({
+ day,
+}: RecordCalendarMonthBodyDayProps) => {
+ return {day.getDate()};
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-calendar/month/components/RecordCalendarMonthBodyWeek.tsx b/packages/twenty-front/src/modules/object-record/record-calendar/month/components/RecordCalendarMonthBodyWeek.tsx
new file mode 100644
index 00000000000..5adb1f4c708
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-calendar/month/components/RecordCalendarMonthBodyWeek.tsx
@@ -0,0 +1,34 @@
+import { RecordCalendarMonthBodyDay } from '@/object-record/record-calendar/month/components/RecordCalendarMonthBodyDay';
+import { useRecordCalendarMonthContextOrThrow } from '@/object-record/record-calendar/month/contexts/RecordCalendarMonthContext';
+import styled from '@emotion/styled';
+import { eachDayOfInterval, endOfWeek } from 'date-fns';
+
+const StyledContainer = styled.div`
+ display: flex;
+ min-height: 122px;
+`;
+
+type RecordCalendarMonthBodyWeekProps = {
+ startDayOfWeek: Date;
+};
+
+export const RecordCalendarMonthBodyWeek = ({
+ startDayOfWeek,
+}: RecordCalendarMonthBodyWeekProps) => {
+ const { weekStartsOnDayIndex } = useRecordCalendarMonthContextOrThrow();
+
+ const daysOfWeek = eachDayOfInterval({
+ start: startDayOfWeek,
+ end: endOfWeek(startDayOfWeek, {
+ weekStartsOn: weekStartsOnDayIndex,
+ }),
+ });
+
+ return (
+
+ {daysOfWeek.map((day, index) => (
+
+ ))}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-calendar/month/components/RecordCalendarMonthHeader.tsx b/packages/twenty-front/src/modules/object-record/record-calendar/month/components/RecordCalendarMonthHeader.tsx
new file mode 100644
index 00000000000..2ae5e79b328
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-calendar/month/components/RecordCalendarMonthHeader.tsx
@@ -0,0 +1,21 @@
+import { RecordCalendarMonthHeaderDay } from '@/object-record/record-calendar/month/components/RecordCalendarMonthHeaderDay';
+import { useRecordCalendarMonthContextOrThrow } from '@/object-record/record-calendar/month/contexts/RecordCalendarMonthContext';
+import styled from '@emotion/styled';
+
+const StyledContainer = styled.div`
+ display: flex;
+ height: 24px;
+ width: 100%;
+`;
+
+export const RecordCalendarMonthHeader = () => {
+ const { weekDayLabels } = useRecordCalendarMonthContextOrThrow();
+
+ return (
+
+ {weekDayLabels.map((label, index) => (
+
+ ))}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-calendar/month/components/RecordCalendarMonthHeaderDay.tsx b/packages/twenty-front/src/modules/object-record/record-calendar/month/components/RecordCalendarMonthHeaderDay.tsx
new file mode 100644
index 00000000000..643f88c02b8
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-calendar/month/components/RecordCalendarMonthHeaderDay.tsx
@@ -0,0 +1,17 @@
+import styled from '@emotion/styled';
+
+type RecordCalendarMonthHeaderDayProps = {
+ label: string;
+};
+
+const StyledLabel = styled.div`
+ display: flex;
+ width: calc(100% / 7);
+ height: 24px;
+`;
+
+export const RecordCalendarMonthHeaderDay = ({
+ label,
+}: RecordCalendarMonthHeaderDayProps) => {
+ return {label};
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-calendar/month/components/__stories__/RecordCalendarMonth.stories.tsx b/packages/twenty-front/src/modules/object-record/record-calendar/month/components/__stories__/RecordCalendarMonth.stories.tsx
new file mode 100644
index 00000000000..0427d1b5dc9
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-calendar/month/components/__stories__/RecordCalendarMonth.stories.tsx
@@ -0,0 +1,133 @@
+import { type Meta, type StoryObj } from '@storybook/react';
+
+import { type TaskGroups } from '@/activities/tasks/components/TaskGroups';
+import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
+import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
+import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
+import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
+import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
+import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
+
+import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
+import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
+import { RecordCalendarMonth } from '@/object-record/record-calendar/month/components/RecordCalendarMonth';
+import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
+import { type RecordField } from '@/object-record/record-field/types/RecordField';
+import { useRecordIndexFieldMetadataDerivedStates } from '@/object-record/record-index/hooks/useRecordIndexFieldMetadataDerivedStates';
+import { VIEW_BAR_FILTER_DROPDOWN_ID } from '@/views/constants/ViewBarFilterDropdownId';
+import { coreViewsState } from '@/views/states/coreViewState';
+import { useSetRecoilState } from 'recoil';
+import { ComponentDecorator } from 'twenty-ui/testing';
+import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
+import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
+import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator';
+import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
+import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
+import { mockedCoreViewsData } from '~/testing/mock-data/views';
+import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
+
+const meta: Meta = {
+ title: 'Modules/ObjectRecord/RecordCalendar/Month',
+ component: RecordCalendarMonth,
+ decorators: [
+ (Story) => {
+ const companyObjectMetadataItem = generatedMockObjectMetadataItems.find(
+ (item) => item.nameSingular === CoreObjectNameSingular.Company,
+ )!;
+ const instanceId = companyObjectMetadataItem.id;
+
+ const setCurrentRecordFields = useSetRecoilComponentState(
+ currentRecordFieldsComponentState,
+ instanceId,
+ );
+
+ const setCoreViews = useSetRecoilState(coreViewsState);
+
+ const mockCoreView = mockedCoreViewsData[0];
+
+ setCoreViews([mockCoreView]);
+
+ const setCurrentViewId = useSetRecoilComponentState(
+ contextStoreCurrentViewIdComponentState,
+ MAIN_CONTEXT_STORE_INSTANCE_ID,
+ );
+
+ setCurrentViewId(mockCoreView.id);
+
+ const columns = companyObjectMetadataItem.fields.map(
+ (fieldMetadataItem, index) =>
+ ({
+ id: fieldMetadataItem.id,
+ fieldMetadataItemId: fieldMetadataItem.id,
+ isVisible: true,
+ position: index,
+ size: 100,
+ }) satisfies RecordField,
+ );
+
+ setCurrentRecordFields(columns);
+
+ const {
+ fieldDefinitionByFieldMetadataItemId,
+ fieldMetadataItemByFieldMetadataItemId,
+ labelIdentifierFieldMetadataItem,
+ recordFieldByFieldMetadataItemId,
+ } = useRecordIndexFieldMetadataDerivedStates(
+ companyObjectMetadataItem,
+ instanceId,
+ );
+
+ return (
+ '',
+ onIndexRecordsLoaded: () => {},
+ objectNamePlural: CoreObjectNamePlural.Company,
+ objectNameSingular: CoreObjectNameSingular.Company,
+ objectMetadataItem: companyObjectMetadataItem,
+ recordIndexId: instanceId,
+ viewBarInstanceId: instanceId,
+ labelIdentifierFieldMetadataItem,
+ recordFieldByFieldMetadataItemId,
+ fieldDefinitionByFieldMetadataItemId,
+ fieldMetadataItemByFieldMetadataItemId,
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+ );
+ },
+ ContextStoreDecorator,
+ ObjectMetadataItemsDecorator,
+ SnackBarDecorator,
+ ComponentDecorator,
+ IconsProviderDecorator,
+ I18nFrontDecorator,
+ ],
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ play: async () => {},
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-calendar/month/contexts/RecordCalendarMonthContext.ts b/packages/twenty-front/src/modules/object-record/record-calendar/month/contexts/RecordCalendarMonthContext.ts
new file mode 100644
index 00000000000..3b0ba782ace
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-calendar/month/contexts/RecordCalendarMonthContext.ts
@@ -0,0 +1,18 @@
+import { createRequiredContext } from '~/utils/createRequiredContext';
+
+type RecordCalendarMonthContextValue = {
+ firstDayOfMonth: Date;
+ lastDayOfMonth: Date;
+ firstDayOfFirstWeek: Date;
+ lastDayOfLastWeek: Date;
+ weekDayLabels: string[];
+ weekFirstDays: Date[];
+ weekStartsOnDayIndex: 0 | 1 | 2 | 3 | 4 | 5 | 6;
+};
+
+export const [
+ RecordCalendarMonthContextProvider,
+ useRecordCalendarMonthContextOrThrow,
+] = createRequiredContext(
+ 'RecordCalendarMonthContext',
+);
diff --git a/packages/twenty-front/src/modules/object-record/record-calendar/month/hooks/useRecordCalendarMonthDaysRange.tsx b/packages/twenty-front/src/modules/object-record/record-calendar/month/hooks/useRecordCalendarMonthDaysRange.tsx
new file mode 100644
index 00000000000..718a6b4e2b4
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-calendar/month/hooks/useRecordCalendarMonthDaysRange.tsx
@@ -0,0 +1,71 @@
+import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
+import { CalendarStartDay } from '@/localization/constants/CalendarStartDay';
+import { detectCalendarStartDay } from '@/localization/utils/detectCalendarStartDay';
+import {
+ addDays,
+ eachWeekOfInterval,
+ endOfWeek,
+ format,
+ lastDayOfMonth as lastDayOfMonthFn,
+ startOfMonth,
+ startOfWeek,
+} from 'date-fns';
+import { useRecoilValue } from 'recoil';
+
+import { dateLocaleState } from '~/localization/states/dateLocaleState';
+
+export const useRecordCalendarMonthDaysRange = (selectedDate: Date) => {
+ const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
+ const dateLocale = useRecoilValue(dateLocaleState);
+
+ if (!currentWorkspaceMember) {
+ throw new Error('Current workspace member not found');
+ }
+
+ const weekStartsOnDayIndex = (
+ currentWorkspaceMember?.calendarStartDay === CalendarStartDay.SYSTEM
+ ? CalendarStartDay[detectCalendarStartDay()]
+ : (currentWorkspaceMember?.calendarStartDay ?? 0)
+ ) as 0 | 1 | 2 | 3 | 4 | 5 | 6;
+
+ const firstDayOfMonth = startOfMonth(selectedDate);
+ const lastDayOfMonth = lastDayOfMonthFn(selectedDate);
+
+ const firstDayOfFirstWeek = startOfWeek(firstDayOfMonth, {
+ weekStartsOn: weekStartsOnDayIndex,
+ locale: dateLocale.localeCatalog,
+ });
+
+ const lastDayOfLastWeek = endOfWeek(lastDayOfMonth, {
+ weekStartsOn: weekStartsOnDayIndex,
+ locale: dateLocale.localeCatalog,
+ });
+
+ const daysOfWeekLabels: string[] = [];
+
+ for (let i = 0; i < 7; i++) {
+ const day = addDays(firstDayOfFirstWeek, i);
+ const label = format(day, 'EEE', { locale: dateLocale.localeCatalog });
+ daysOfWeekLabels.push(label);
+ }
+
+ const weekFirstDays = eachWeekOfInterval(
+ {
+ start: firstDayOfFirstWeek,
+ end: lastDayOfLastWeek,
+ },
+ {
+ weekStartsOn: weekStartsOnDayIndex,
+ },
+ );
+
+ return {
+ weekStartsOnDayIndex,
+ weekDayLabels: daysOfWeekLabels,
+ firstDayOfFirstWeek,
+ firstDayOfMonth,
+ lastDayOfMonth,
+ lastDayOfLastWeek,
+ weekFirstDays,
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexCalendarContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexCalendarContainer.tsx
index 3a53cf779b9..50998c06138 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexCalendarContainer.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexCalendarContainer.tsx
@@ -1,5 +1,6 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
+import { RecordCalendar } from '@/object-record/record-calendar/components/RecordCalendar';
import { RecordCalendarContextProvider } from '@/object-record/record-calendar/contexts/RecordCalendarContext';
import { RecordCalendarComponentInstanceContext } from '@/object-record/record-calendar/states/contexts/RecordCalendarComponentInstanceContext';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
@@ -38,7 +39,7 @@ export const RecordIndexCalendarContainer = ({
objectPermissions,
}}
>
- Calendar View
+
);