@dxos/react-ui-calendar 0.8.4-main.03d5cd7b56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +8 -0
- package/README.md +1 -0
- package/dist/lib/browser/index.mjs +234 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/lib/browser/translations.mjs +16 -0
- package/dist/lib/browser/translations.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +236 -0
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/lib/node-esm/translations.mjs +18 -0
- package/dist/lib/node-esm/translations.mjs.map +7 -0
- package/dist/types/src/components/Calendar/Calendar.d.ts +41 -0
- package/dist/types/src/components/Calendar/Calendar.d.ts.map +1 -0
- package/dist/types/src/components/Calendar/Calendar.stories.d.ts +22 -0
- package/dist/types/src/components/Calendar/Calendar.stories.d.ts.map +1 -0
- package/dist/types/src/components/Calendar/index.d.ts +2 -0
- package/dist/types/src/components/Calendar/index.d.ts.map +1 -0
- package/dist/types/src/components/Calendar/util.d.ts +4 -0
- package/dist/types/src/components/Calendar/util.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +2 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +2 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +9 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +175 -0
- package/dist/types/src/types.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +71 -0
- package/src/components/Calendar/Calendar.stories.tsx +51 -0
- package/src/components/Calendar/Calendar.tsx +300 -0
- package/src/components/Calendar/index.ts +5 -0
- package/src/components/Calendar/util.ts +22 -0
- package/src/components/index.ts +5 -0
- package/src/index.ts +5 -0
- package/src/translations.ts +17 -0
- package/src/types.ts +193 -0
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dxos/react-ui-calendar",
|
|
3
|
+
"version": "0.8.4-main.03d5cd7b56",
|
|
4
|
+
"description": "A calendar component.",
|
|
5
|
+
"homepage": "https://dxos.org",
|
|
6
|
+
"bugs": "https://github.com/dxos/dxos/issues",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/dxos/dxos"
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "DXOS.org",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"imports": {
|
|
15
|
+
"#translations": "./src/translations.ts"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"source": "./src/index.ts",
|
|
20
|
+
"types": "./dist/types/src/index.d.ts",
|
|
21
|
+
"browser": "./dist/lib/browser/index.mjs",
|
|
22
|
+
"node": "./dist/lib/node-esm/index.mjs"
|
|
23
|
+
},
|
|
24
|
+
"./translations": {
|
|
25
|
+
"source": "./src/translations.ts",
|
|
26
|
+
"types": "./dist/types/src/translations.d.ts",
|
|
27
|
+
"browser": "./dist/lib/browser/translations.mjs",
|
|
28
|
+
"node": "./dist/lib/node-esm/translations.mjs"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"types": "dist/types/src/index.d.ts",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"src"
|
|
35
|
+
],
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@radix-ui/react-context": "1.1.1",
|
|
38
|
+
"date-fns": "^3.6.0",
|
|
39
|
+
"react-resize-detector": "^11.0.1",
|
|
40
|
+
"react-virtualized": "^9.22.6",
|
|
41
|
+
"react-window": "^2.2.3",
|
|
42
|
+
"@dxos/invariant": "0.8.4-main.03d5cd7b56",
|
|
43
|
+
"@dxos/async": "0.8.4-main.03d5cd7b56",
|
|
44
|
+
"@dxos/debug": "0.8.4-main.03d5cd7b56",
|
|
45
|
+
"@dxos/log": "0.8.4-main.03d5cd7b56",
|
|
46
|
+
"@dxos/util": "0.8.4-main.03d5cd7b56"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/react": "~19.2.7",
|
|
50
|
+
"@types/react-dom": "~19.2.3",
|
|
51
|
+
"@types/react-virtualized": "^9.22.3",
|
|
52
|
+
"effect": "3.20.0",
|
|
53
|
+
"react": "~19.2.3",
|
|
54
|
+
"react-dom": "~19.2.3",
|
|
55
|
+
"vite": "^8.0.10",
|
|
56
|
+
"@dxos/random": "0.8.4-main.03d5cd7b56",
|
|
57
|
+
"@dxos/react-ui": "0.8.4-main.03d5cd7b56",
|
|
58
|
+
"@dxos/ui-theme": "0.8.4-main.03d5cd7b56",
|
|
59
|
+
"@dxos/storybook-utils": "0.8.4-main.03d5cd7b56"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"effect": "3.20.0",
|
|
63
|
+
"react": "~19.2.3",
|
|
64
|
+
"react-dom": "~19.2.3",
|
|
65
|
+
"@dxos/react-ui": "0.8.4-main.03d5cd7b56",
|
|
66
|
+
"@dxos/ui-theme": "0.8.4-main.03d5cd7b56"
|
|
67
|
+
},
|
|
68
|
+
"publishConfig": {
|
|
69
|
+
"access": "public"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
import { Panel } from '@dxos/react-ui';
|
|
9
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
10
|
+
|
|
11
|
+
import { translations } from '#translations';
|
|
12
|
+
|
|
13
|
+
import { Calendar } from './Calendar';
|
|
14
|
+
|
|
15
|
+
const meta = {
|
|
16
|
+
title: 'ui/react-ui-calendar/Calendar',
|
|
17
|
+
component: Calendar.Grid,
|
|
18
|
+
parameters: {
|
|
19
|
+
translations,
|
|
20
|
+
},
|
|
21
|
+
} satisfies Meta<typeof Calendar.Grid>;
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
|
|
25
|
+
type Story = StoryObj<typeof meta>;
|
|
26
|
+
|
|
27
|
+
export const Default: Story = {
|
|
28
|
+
decorators: [withTheme(), withLayout({ layout: 'centered' })],
|
|
29
|
+
render: () => (
|
|
30
|
+
<Calendar.Root>
|
|
31
|
+
<Calendar.Toolbar />
|
|
32
|
+
<Calendar.Grid rows={6} />
|
|
33
|
+
</Calendar.Root>
|
|
34
|
+
),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const Column: Story = {
|
|
38
|
+
decorators: [withTheme(), withLayout({ layout: 'column', classNames: 'w-auto' })],
|
|
39
|
+
render: () => (
|
|
40
|
+
<Calendar.Root>
|
|
41
|
+
<Panel.Root>
|
|
42
|
+
<Panel.Toolbar asChild>
|
|
43
|
+
<Calendar.Toolbar />
|
|
44
|
+
</Panel.Toolbar>
|
|
45
|
+
<Panel.Content asChild>
|
|
46
|
+
<Calendar.Grid />
|
|
47
|
+
</Panel.Content>
|
|
48
|
+
</Panel.Root>
|
|
49
|
+
</Calendar.Root>
|
|
50
|
+
),
|
|
51
|
+
};
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { createContext } from '@radix-ui/react-context';
|
|
6
|
+
import { type Day, addDays, differenceInWeeks, format, startOfDay, startOfWeek } from 'date-fns';
|
|
7
|
+
import React, {
|
|
8
|
+
type Dispatch,
|
|
9
|
+
type PropsWithChildren,
|
|
10
|
+
type SetStateAction,
|
|
11
|
+
forwardRef,
|
|
12
|
+
useCallback,
|
|
13
|
+
useEffect,
|
|
14
|
+
useImperativeHandle,
|
|
15
|
+
useMemo,
|
|
16
|
+
useRef,
|
|
17
|
+
useState,
|
|
18
|
+
} from 'react';
|
|
19
|
+
import { useResizeDetector } from 'react-resize-detector';
|
|
20
|
+
import { List, type ListProps, type ListRowRenderer } from 'react-virtualized';
|
|
21
|
+
|
|
22
|
+
import { Event } from '@dxos/async';
|
|
23
|
+
import { IconButton, useTranslation } from '@dxos/react-ui';
|
|
24
|
+
import { composable, composableProps, mx } from '@dxos/ui-theme';
|
|
25
|
+
|
|
26
|
+
import { translationKey } from '#translations';
|
|
27
|
+
|
|
28
|
+
import { getDate, isSameDay } from './util';
|
|
29
|
+
|
|
30
|
+
const maxRows = 50 * 100;
|
|
31
|
+
const start = new Date('1970-01-01');
|
|
32
|
+
const size = 48;
|
|
33
|
+
const defaultWidth = 7 * size;
|
|
34
|
+
|
|
35
|
+
//
|
|
36
|
+
// Context
|
|
37
|
+
//
|
|
38
|
+
|
|
39
|
+
type CalendarEvent = {
|
|
40
|
+
type: 'scroll';
|
|
41
|
+
date: Date;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type CalendarContextValue = {
|
|
45
|
+
weekStartsOn: Day;
|
|
46
|
+
event: Event<CalendarEvent>;
|
|
47
|
+
index: number | undefined;
|
|
48
|
+
setIndex: Dispatch<SetStateAction<number | undefined>>;
|
|
49
|
+
selected: Date | undefined;
|
|
50
|
+
setSelected: Dispatch<SetStateAction<Date | undefined>>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const [CalendarContextProvider, useCalendarContext] = createContext<CalendarContextValue>('Calendar');
|
|
54
|
+
|
|
55
|
+
//
|
|
56
|
+
// Controller
|
|
57
|
+
//
|
|
58
|
+
|
|
59
|
+
type CalendarController = {
|
|
60
|
+
scrollTo: (date: Date) => void;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
//
|
|
64
|
+
// Root
|
|
65
|
+
//
|
|
66
|
+
|
|
67
|
+
type CalendarRootProps = PropsWithChildren<Partial<Pick<CalendarContextValue, 'weekStartsOn'>>>;
|
|
68
|
+
|
|
69
|
+
const CalendarRoot = forwardRef<CalendarController, CalendarRootProps>(
|
|
70
|
+
({ children, weekStartsOn = 1 }, forwardedRef) => {
|
|
71
|
+
const event = useMemo(() => new Event<CalendarEvent>(), []);
|
|
72
|
+
const [selected, setSelected] = useState<Date | undefined>();
|
|
73
|
+
const [index, setIndex] = useState<number | undefined>();
|
|
74
|
+
|
|
75
|
+
useImperativeHandle(
|
|
76
|
+
forwardedRef,
|
|
77
|
+
() => ({
|
|
78
|
+
scrollTo: (date: Date) => {
|
|
79
|
+
event.emit({ type: 'scroll', date });
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
[event],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<CalendarContextProvider
|
|
87
|
+
weekStartsOn={weekStartsOn}
|
|
88
|
+
event={event}
|
|
89
|
+
index={index}
|
|
90
|
+
setIndex={setIndex}
|
|
91
|
+
selected={selected}
|
|
92
|
+
setSelected={setSelected}
|
|
93
|
+
>
|
|
94
|
+
{children}
|
|
95
|
+
</CalendarContextProvider>
|
|
96
|
+
);
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
//
|
|
101
|
+
// Header
|
|
102
|
+
//
|
|
103
|
+
|
|
104
|
+
const CALENDAR_TOOLBAR_NAME = 'CalendarHeader';
|
|
105
|
+
|
|
106
|
+
type CalendarToolbarProps = {};
|
|
107
|
+
|
|
108
|
+
const CalendarToolbar = composable<HTMLDivElement, CalendarToolbarProps>(({ classNames, ...props }, forwardedRef) => {
|
|
109
|
+
const { t } = useTranslation(translationKey);
|
|
110
|
+
const { weekStartsOn, event, index, selected } = useCalendarContext(CALENDAR_TOOLBAR_NAME);
|
|
111
|
+
const top = useMemo(() => getDate(start, index ?? 0, 6, weekStartsOn), [index, weekStartsOn]);
|
|
112
|
+
const today = useMemo(() => new Date(), []);
|
|
113
|
+
|
|
114
|
+
const handleToday = useCallback(() => {
|
|
115
|
+
event.emit({ type: 'scroll', date: today });
|
|
116
|
+
}, [event, start, today]);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div
|
|
120
|
+
{...composableProps(props, {
|
|
121
|
+
role: 'none',
|
|
122
|
+
classNames: ['shrink-0 grid! grid-cols-3 items-center bg-toolbar-surface', classNames],
|
|
123
|
+
})}
|
|
124
|
+
ref={forwardedRef}
|
|
125
|
+
style={{ width: defaultWidth }}
|
|
126
|
+
>
|
|
127
|
+
<div className='flex justify-start'>
|
|
128
|
+
<IconButton
|
|
129
|
+
variant='ghost'
|
|
130
|
+
icon='ph--calendar--regular'
|
|
131
|
+
iconOnly
|
|
132
|
+
classNames='aspect-square'
|
|
133
|
+
label={t('today.button')}
|
|
134
|
+
onClick={handleToday}
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
<div className='flex justify-center p-2 text-description'>{format(selected ?? top, 'MMMM')}</div>
|
|
138
|
+
<div className='flex justify-end p-2 text-description'>{(selected ?? top).getFullYear()}</div>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
CalendarToolbar.displayName = CALENDAR_TOOLBAR_NAME;
|
|
144
|
+
|
|
145
|
+
//
|
|
146
|
+
// Grid
|
|
147
|
+
// TODO(burdon): Key nav.
|
|
148
|
+
// TODO(burdon): Drag range.
|
|
149
|
+
//
|
|
150
|
+
|
|
151
|
+
const CALENDAR_GRID_NAME = 'CalendarGrid';
|
|
152
|
+
|
|
153
|
+
type CalendarGridProps = {
|
|
154
|
+
rows?: number;
|
|
155
|
+
/** Dates to highlight on the grid. Each date that appears in this array receives a border indicator. */
|
|
156
|
+
dates?: Date[];
|
|
157
|
+
onSelect?: (event: { date: Date }) => void;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const CalendarGrid = composable<HTMLDivElement, CalendarGridProps>(
|
|
161
|
+
({ classNames, rows, dates = [], onSelect, ...props }, forwardedRef) => {
|
|
162
|
+
const { weekStartsOn, event, setIndex, selected, setSelected } = useCalendarContext(CALENDAR_GRID_NAME);
|
|
163
|
+
const { ref: containerRef, width = 0, height = 0 } = useResizeDetector();
|
|
164
|
+
const maxHeight = rows ? rows * size : undefined;
|
|
165
|
+
const listRef = useRef<List>(null);
|
|
166
|
+
const today = useMemo(() => new Date(), []);
|
|
167
|
+
|
|
168
|
+
// Build a set of ISO date strings (YYYY-MM-DD) for O(1) per-cell lookup.
|
|
169
|
+
const dateSet = useMemo(() => new Set(dates.map((date) => startOfDay(date).toISOString())), [dates]);
|
|
170
|
+
|
|
171
|
+
const hasDate = useCallback((date: Date) => dateSet.has(startOfDay(date).toISOString()), [dateSet]);
|
|
172
|
+
|
|
173
|
+
const [initialized, setInitialized] = useState(false);
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
const index = differenceInWeeks(today, start);
|
|
176
|
+
listRef.current?.scrollToRow(index);
|
|
177
|
+
}, [initialized, start, today]);
|
|
178
|
+
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
return event.on((event) => {
|
|
181
|
+
switch (event.type) {
|
|
182
|
+
case 'scroll': {
|
|
183
|
+
const index = differenceInWeeks(event.date, start);
|
|
184
|
+
listRef.current?.scrollToRow(index);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}, [event]);
|
|
190
|
+
|
|
191
|
+
const days = useMemo(() => {
|
|
192
|
+
const weekStart = startOfWeek(new Date(), { weekStartsOn });
|
|
193
|
+
return Array.from({ length: 7 }, (_, i) => {
|
|
194
|
+
const day = addDays(weekStart, i);
|
|
195
|
+
return format(day, 'EEE'); // Short day name (Mon, Tue, etc.)
|
|
196
|
+
});
|
|
197
|
+
}, []);
|
|
198
|
+
|
|
199
|
+
// TODO(burdon): Get info by range.
|
|
200
|
+
|
|
201
|
+
const handleDaySelect = useCallback(
|
|
202
|
+
(date: Date) => {
|
|
203
|
+
setSelected((current) => (isSameDay(date, current) ? undefined : date));
|
|
204
|
+
onSelect?.({ date });
|
|
205
|
+
},
|
|
206
|
+
[onSelect],
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const handleScroll = useCallback<NonNullable<ListProps['onScroll']>>((info) => {
|
|
210
|
+
setIndex(Math.round(info.scrollTop / size));
|
|
211
|
+
}, []);
|
|
212
|
+
|
|
213
|
+
const rowRenderer = useCallback<ListRowRenderer>(
|
|
214
|
+
({ key, index, style }) => {
|
|
215
|
+
const getBgColor = (date: Date) => date.getMonth() % 2 === 0 && 'bg-modal-surface';
|
|
216
|
+
return (
|
|
217
|
+
<div key={key} style={style} className='grid'>
|
|
218
|
+
<div className='grid grid-cols-7 bg-input-surface' style={{ gridTemplateColumns: `repeat(7, ${size}px)` }}>
|
|
219
|
+
{Array.from({ length: 7 }).map((_, i) => {
|
|
220
|
+
const date = getDate(start, index, i, weekStartsOn);
|
|
221
|
+
const border = isSameDay(date, selected)
|
|
222
|
+
? 'border-primary-500'
|
|
223
|
+
: isSameDay(date, today)
|
|
224
|
+
? 'border-amber-500'
|
|
225
|
+
: hasDate(date)
|
|
226
|
+
? 'border-neutral-700 border-dashed'
|
|
227
|
+
: undefined;
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<div
|
|
231
|
+
key={i}
|
|
232
|
+
className={mx('relative flex justify-center items-center cursor-pointer', getBgColor(date))}
|
|
233
|
+
onClick={() => handleDaySelect(date)}
|
|
234
|
+
>
|
|
235
|
+
<span className='text-description'>{date.getDate()}</span>
|
|
236
|
+
{!border && date.getDate() === 1 && (
|
|
237
|
+
<span className='absolute top-0 text-xs text-description'>{format(date, 'MMM')}</span>
|
|
238
|
+
)}
|
|
239
|
+
{border && <div className={mx('absolute inset-1 border-2 rounded-full', border)} />}
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
})}
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
},
|
|
247
|
+
[handleDaySelect, hasDate, selected, weekStartsOn],
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<div
|
|
252
|
+
{...composableProps(props, {
|
|
253
|
+
role: 'none',
|
|
254
|
+
classNames: ['flex flex-col h-full w-full justify-center overflow-hidden', classNames],
|
|
255
|
+
})}
|
|
256
|
+
ref={forwardedRef}
|
|
257
|
+
>
|
|
258
|
+
{/* Day of week labels */}
|
|
259
|
+
<div className='grid w-full grid-cols-7' style={{ width: defaultWidth }}>
|
|
260
|
+
{days.map((date, i) => (
|
|
261
|
+
<div key={i} className='flex justify-center p-2 text-sm font-thin'>
|
|
262
|
+
{date}
|
|
263
|
+
</div>
|
|
264
|
+
))}
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
{/* Grid */}
|
|
268
|
+
<div className='flex flex-col h-full w-full justify-center overflow-hidden' ref={containerRef}>
|
|
269
|
+
<List
|
|
270
|
+
ref={listRef}
|
|
271
|
+
role='none'
|
|
272
|
+
className='scrollbar-none outline-hidden'
|
|
273
|
+
width={width}
|
|
274
|
+
height={maxHeight ?? height}
|
|
275
|
+
rowCount={maxRows}
|
|
276
|
+
rowHeight={size}
|
|
277
|
+
rowRenderer={rowRenderer}
|
|
278
|
+
scrollToAlignment='start'
|
|
279
|
+
onScroll={handleScroll}
|
|
280
|
+
onRowsRendered={() => setInitialized(true)}
|
|
281
|
+
/>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
},
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
CalendarGrid.displayName = CALENDAR_GRID_NAME;
|
|
289
|
+
|
|
290
|
+
//
|
|
291
|
+
// Calendar
|
|
292
|
+
//
|
|
293
|
+
|
|
294
|
+
export const Calendar = {
|
|
295
|
+
Root: CalendarRoot,
|
|
296
|
+
Toolbar: CalendarToolbar,
|
|
297
|
+
Grid: CalendarGrid,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
export type { CalendarController, CalendarRootProps, CalendarToolbarProps, CalendarGridProps };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Day } from 'date-fns';
|
|
6
|
+
|
|
7
|
+
export const getDate = (start: Date, weekNumber: number, dayOfWeek: number, weekStartsOn: Day): Date => {
|
|
8
|
+
const result = new Date(start);
|
|
9
|
+
const startDayOfWeek = start.getDay(); // 0 = Sunday, 1 = Monday, etc.
|
|
10
|
+
const adjustedStartDay = (startDayOfWeek === 0 ? 7 : startDayOfWeek) - weekStartsOn; // Adjust for weekStartsOn.
|
|
11
|
+
result.setDate(start.getDate() - adjustedStartDay + weekNumber * 7 + dayOfWeek);
|
|
12
|
+
return result;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const isSameDay = (date1: Date, date2: Date | undefined): boolean => {
|
|
16
|
+
return (
|
|
17
|
+
!!date2 &&
|
|
18
|
+
date1.getFullYear() === date2.getFullYear() &&
|
|
19
|
+
date1.getMonth() === date2.getMonth() &&
|
|
20
|
+
date1.getDate() === date2.getDate()
|
|
21
|
+
);
|
|
22
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Resource } from '@dxos/react-ui';
|
|
6
|
+
|
|
7
|
+
export const translationKey = '@dxos/react-ui-calendar';
|
|
8
|
+
|
|
9
|
+
export const translations = [
|
|
10
|
+
{
|
|
11
|
+
'en-US': {
|
|
12
|
+
[translationKey]: {
|
|
13
|
+
'today.button': 'Today',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
] as const satisfies Resource[];
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Locale } from 'date-fns';
|
|
6
|
+
import { type RefObject } from 'react';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Calendar types and interfaces.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type DateString = string; // YYYY-MM-DD format
|
|
13
|
+
|
|
14
|
+
export type DisplayMode = 'days' | 'years';
|
|
15
|
+
|
|
16
|
+
export type LayoutMode = 'portrait' | 'landscape';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Locale configuration for the calendar.
|
|
20
|
+
*/
|
|
21
|
+
export interface CalendarLocale {
|
|
22
|
+
blank?: string;
|
|
23
|
+
headerFormat?: string;
|
|
24
|
+
todayLabel?: {
|
|
25
|
+
long: string;
|
|
26
|
+
short?: string;
|
|
27
|
+
};
|
|
28
|
+
weekdays?: string[];
|
|
29
|
+
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
30
|
+
locale?: Locale; // date-fns locale
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Theme configuration for the calendar.
|
|
35
|
+
*/
|
|
36
|
+
export interface CalendarTheme {
|
|
37
|
+
floatingNav?: {
|
|
38
|
+
background?: string;
|
|
39
|
+
chevron?: string;
|
|
40
|
+
color?: string;
|
|
41
|
+
};
|
|
42
|
+
headerColor?: string;
|
|
43
|
+
selectionColor?: string | ((date: DateString) => string);
|
|
44
|
+
textColor?: {
|
|
45
|
+
active?: string;
|
|
46
|
+
default?: string;
|
|
47
|
+
};
|
|
48
|
+
todayColor?: string;
|
|
49
|
+
weekdayColor?: string;
|
|
50
|
+
overlayColor?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Display options for the calendar.
|
|
55
|
+
*/
|
|
56
|
+
export interface DisplayOptions {
|
|
57
|
+
hideYearsOnSelect?: boolean;
|
|
58
|
+
layout?: LayoutMode;
|
|
59
|
+
overscanMonthCount?: number;
|
|
60
|
+
shouldHeaderAnimate?: boolean;
|
|
61
|
+
showHeader?: boolean;
|
|
62
|
+
showMonthsForYears?: boolean;
|
|
63
|
+
showOverlay?: boolean;
|
|
64
|
+
showTodayHelper?: boolean;
|
|
65
|
+
showWeekdays?: boolean;
|
|
66
|
+
todayHelperRowOffset?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Month data structure.
|
|
71
|
+
*/
|
|
72
|
+
export interface MonthData {
|
|
73
|
+
month: number;
|
|
74
|
+
year: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Month rendering data.
|
|
79
|
+
*/
|
|
80
|
+
export interface MonthRenderData {
|
|
81
|
+
date: Date;
|
|
82
|
+
rows: number[][];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Day props for rendering.
|
|
87
|
+
*/
|
|
88
|
+
export interface DayProps {
|
|
89
|
+
date: DateString;
|
|
90
|
+
day: number;
|
|
91
|
+
month: number;
|
|
92
|
+
year: number;
|
|
93
|
+
monthShort: string;
|
|
94
|
+
currentYear: number;
|
|
95
|
+
isDisabled: boolean;
|
|
96
|
+
isToday: boolean;
|
|
97
|
+
isSelected: boolean;
|
|
98
|
+
isHighlighted?: boolean;
|
|
99
|
+
onClick?: (date: Date) => void;
|
|
100
|
+
handlers?: Record<string, any>;
|
|
101
|
+
className?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Calendar props.
|
|
106
|
+
*/
|
|
107
|
+
export interface CalendarProps {
|
|
108
|
+
autoFocus?: boolean;
|
|
109
|
+
className?: string;
|
|
110
|
+
disabledDates?: Date[];
|
|
111
|
+
disabledDays?: number[];
|
|
112
|
+
display?: DisplayMode;
|
|
113
|
+
displayOptions?: DisplayOptions;
|
|
114
|
+
height?: number;
|
|
115
|
+
keyboardSupport?: boolean;
|
|
116
|
+
locale?: CalendarLocale;
|
|
117
|
+
max?: Date;
|
|
118
|
+
maxDate?: Date;
|
|
119
|
+
min?: Date;
|
|
120
|
+
minDate?: Date;
|
|
121
|
+
onScroll?: (scrollTop: number, event?: Event) => void;
|
|
122
|
+
onScrollEnd?: (scrollTop: number) => void;
|
|
123
|
+
onSelect?: (date: Date) => void;
|
|
124
|
+
onHighlightedDateChange?: (date: Date) => void;
|
|
125
|
+
rowHeight?: number;
|
|
126
|
+
scrollDate?: Date;
|
|
127
|
+
selected?: Date | DateString;
|
|
128
|
+
tabIndex?: number;
|
|
129
|
+
theme?: CalendarTheme;
|
|
130
|
+
width?: number | string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Month props.
|
|
135
|
+
*/
|
|
136
|
+
export interface MonthProps {
|
|
137
|
+
monthDate: Date;
|
|
138
|
+
rows: number[][];
|
|
139
|
+
rowHeight: number;
|
|
140
|
+
selected?: DateString;
|
|
141
|
+
disabledDates?: DateString[];
|
|
142
|
+
disabledDays?: number[];
|
|
143
|
+
minDate: Date;
|
|
144
|
+
maxDate: Date;
|
|
145
|
+
today: Date;
|
|
146
|
+
locale: Required<CalendarLocale>;
|
|
147
|
+
theme: Required<CalendarTheme>;
|
|
148
|
+
showOverlay?: boolean;
|
|
149
|
+
isScrolling?: boolean;
|
|
150
|
+
onDayClick?: (date: Date) => void;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Hook return types for headless implementation.
|
|
155
|
+
*/
|
|
156
|
+
export interface UseCalendarReturn {
|
|
157
|
+
containerProps: {
|
|
158
|
+
ref: RefObject<HTMLDivElement>;
|
|
159
|
+
tabIndex?: number;
|
|
160
|
+
className?: string;
|
|
161
|
+
'aria-label': string;
|
|
162
|
+
};
|
|
163
|
+
display: DisplayMode;
|
|
164
|
+
locale: Required<CalendarLocale>;
|
|
165
|
+
theme: Required<CalendarTheme>;
|
|
166
|
+
displayOptions: Required<DisplayOptions>;
|
|
167
|
+
months: MonthData[];
|
|
168
|
+
today: Date;
|
|
169
|
+
scrollToDate: (date: Date, offset?: number, shouldAnimate?: boolean) => void;
|
|
170
|
+
scrollTo: (offset: number) => void;
|
|
171
|
+
getCurrentOffset: () => number;
|
|
172
|
+
getDateOffset: (date: Date) => number;
|
|
173
|
+
setDisplay: (display: DisplayMode) => void;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface UseDayReturn {
|
|
177
|
+
dayProps: {
|
|
178
|
+
onClick: () => void;
|
|
179
|
+
'data-date': DateString;
|
|
180
|
+
className?: string;
|
|
181
|
+
};
|
|
182
|
+
isSelected: boolean;
|
|
183
|
+
isToday: boolean;
|
|
184
|
+
isDisabled: boolean;
|
|
185
|
+
isHighlighted: boolean;
|
|
186
|
+
selectionColor?: string;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface UseMonthReturn {
|
|
190
|
+
days: DayProps[];
|
|
191
|
+
monthLabel: string;
|
|
192
|
+
showOverlay: boolean;
|
|
193
|
+
}
|