@dxos/react-ui-calendar 0.8.4-main.bc674ce → 0.8.4-main.bcb3aa67d6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-calendar",
3
- "version": "0.8.4-main.bc674ce",
3
+ "version": "0.8.4-main.bcb3aa67d6",
4
4
  "description": "A calendar component.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -29,36 +29,35 @@
29
29
  ],
30
30
  "dependencies": {
31
31
  "@radix-ui/react-context": "1.1.1",
32
- "date-fns": "^3.3.1",
32
+ "date-fns": "^3.6.0",
33
33
  "react-resize-detector": "^11.0.1",
34
34
  "react-virtualized": "^9.22.6",
35
35
  "react-window": "^2.2.3",
36
- "@dxos/debug": "0.8.4-main.bc674ce",
37
- "@dxos/async": "0.8.4-main.bc674ce",
38
- "@dxos/invariant": "0.8.4-main.bc674ce",
39
- "@dxos/log": "0.8.4-main.bc674ce",
40
- "@dxos/util": "0.8.4-main.bc674ce"
36
+ "@dxos/async": "0.8.4-main.bcb3aa67d6",
37
+ "@dxos/invariant": "0.8.4-main.bcb3aa67d6",
38
+ "@dxos/util": "0.8.4-main.bcb3aa67d6",
39
+ "@dxos/log": "0.8.4-main.bcb3aa67d6",
40
+ "@dxos/debug": "0.8.4-main.bcb3aa67d6"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/react": "~19.2.7",
44
44
  "@types/react-dom": "~19.2.3",
45
45
  "@types/react-virtualized": "^9.22.3",
46
- "@types/react-window": "^2.0.0",
47
- "effect": "3.19.11",
46
+ "effect": "3.20.0",
48
47
  "react": "~19.2.3",
49
48
  "react-dom": "~19.2.3",
50
- "vite": "7.1.9",
51
- "@dxos/random": "0.8.4-main.bc674ce",
52
- "@dxos/react-ui": "0.8.4-main.bc674ce",
53
- "@dxos/storybook-utils": "0.8.4-main.bc674ce",
54
- "@dxos/ui-theme": "0.8.4-main.bc674ce"
49
+ "vite": "^7.1.11",
50
+ "@dxos/random": "0.8.4-main.bcb3aa67d6",
51
+ "@dxos/react-ui": "0.8.4-main.bcb3aa67d6",
52
+ "@dxos/ui-theme": "0.8.4-main.bcb3aa67d6",
53
+ "@dxos/storybook-utils": "0.8.4-main.bcb3aa67d6"
55
54
  },
56
55
  "peerDependencies": {
57
- "effect": "3.19.11",
56
+ "effect": "3.20.0",
58
57
  "react": "~19.2.3",
59
58
  "react-dom": "~19.2.3",
60
- "@dxos/react-ui": "0.8.4-main.bc674ce",
61
- "@dxos/ui-theme": "0.8.4-main.bc674ce"
59
+ "@dxos/react-ui": "0.8.4-main.bcb3aa67d6",
60
+ "@dxos/ui-theme": "0.8.4-main.bcb3aa67d6"
62
61
  },
63
62
  "publishConfig": {
64
63
  "access": "public"
@@ -5,7 +5,8 @@
5
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import React from 'react';
7
7
 
8
- import { withTheme } from '@dxos/react-ui/testing';
8
+ import { Panel } from '@dxos/react-ui';
9
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
9
10
 
10
11
  import { translations } from '../../translations';
11
12
 
@@ -14,9 +15,7 @@ import { Calendar } from './Calendar';
14
15
  const meta = {
15
16
  title: 'ui/react-ui-calendar/Calendar',
16
17
  component: Calendar.Grid,
17
- decorators: [withTheme],
18
18
  parameters: {
19
- layout: 'centered',
20
19
  translations,
21
20
  },
22
21
  } satisfies Meta<typeof Calendar.Grid>;
@@ -26,51 +25,27 @@ export default meta;
26
25
  type Story = StoryObj<typeof meta>;
27
26
 
28
27
  export const Default: Story = {
28
+ decorators: [withTheme(), withLayout({ layout: 'centered' })],
29
29
  render: () => (
30
30
  <Calendar.Root>
31
- <Calendar.Viewport>
32
- <Calendar.Toolbar />
33
- <Calendar.Grid rows={6} />
34
- </Calendar.Viewport>
35
- </Calendar.Root>
36
- ),
37
- };
38
-
39
- export const Border: Story = {
40
- render: () => (
41
- <Calendar.Root>
42
- <Calendar.Viewport classNames='bg-modalSurface border border-separator rounded'>
43
- <Calendar.Toolbar />
44
- <Calendar.Grid rows={6} />
45
- </Calendar.Viewport>
31
+ <Calendar.Toolbar />
32
+ <Calendar.Grid rows={6} />
46
33
  </Calendar.Root>
47
34
  ),
48
35
  };
49
36
 
50
37
  export const Column: Story = {
38
+ decorators: [withTheme(), withLayout({ layout: 'column', classNames: 'w-auto' })],
51
39
  render: () => (
52
- <div className='absolute inset-0 flex bs-full justify-center'>
53
- <Calendar.Root>
54
- <Calendar.Viewport>
40
+ <Calendar.Root>
41
+ <Panel.Root>
42
+ <Panel.Toolbar asChild>
55
43
  <Calendar.Toolbar />
44
+ </Panel.Toolbar>
45
+ <Panel.Content asChild>
56
46
  <Calendar.Grid />
57
- </Calendar.Viewport>
58
- </Calendar.Root>
59
- </div>
60
- ),
61
- };
62
-
63
- export const Mobile: Story = {
64
- render: () => (
65
- <div className='absolute inset-0 flex bs-full justify-center'>
66
- <div className='flex bs-full is-[400px] justify-center'>
67
- <Calendar.Root>
68
- <Calendar.Viewport classNames='is-full'>
69
- <Calendar.Toolbar />
70
- <Calendar.Grid />
71
- </Calendar.Viewport>
72
- </Calendar.Root>
73
- </div>
74
- </div>
47
+ </Panel.Content>
48
+ </Panel.Root>
49
+ </Calendar.Root>
75
50
  ),
76
51
  };
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { createContext } from '@radix-ui/react-context';
6
- import { type Day, addDays, differenceInWeeks, format, startOfWeek } from 'date-fns';
6
+ import { type Day, addDays, differenceInWeeks, format, startOfDay, startOfWeek } from 'date-fns';
7
7
  import React, {
8
8
  type Dispatch,
9
9
  type PropsWithChildren,
@@ -20,8 +20,8 @@ import { useResizeDetector } from 'react-resize-detector';
20
20
  import { List, type ListProps, type ListRowRenderer } from 'react-virtualized';
21
21
 
22
22
  import { Event } from '@dxos/async';
23
- import { Icon, IconButton, type ThemedClassName, useTranslation } from '@dxos/react-ui';
24
- import { mx } from '@dxos/ui-theme';
23
+ import { IconButton, useTranslation } from '@dxos/react-ui';
24
+ import { composable, composableProps, mx } from '@dxos/ui-theme';
25
25
 
26
26
  import { translationKey } from '../../translations';
27
27
 
@@ -97,31 +97,17 @@ const CalendarRoot = forwardRef<CalendarController, CalendarRootProps>(
97
97
  },
98
98
  );
99
99
 
100
- //
101
- // Viewport
102
- //
103
-
104
- type CalendarViewportProps = PropsWithChildren<ThemedClassName>;
105
-
106
- const CalendarViewport = ({ children, classNames }: CalendarViewportProps) => {
107
- return (
108
- <div role='none' className={mx('flex flex-col items-center overflow-hidden bg-inputSurface', classNames)}>
109
- {children}
110
- </div>
111
- );
112
- };
113
-
114
- CalendarViewport.displayName = 'CalendarContent';
115
-
116
100
  //
117
101
  // Header
118
102
  //
119
103
 
120
- type CalendarToolbarProps = ThemedClassName;
104
+ const CALENDAR_TOOLBAR_NAME = 'CalendarHeader';
105
+
106
+ type CalendarToolbarProps = {};
121
107
 
122
- const CalendarToolbar = ({ classNames }: CalendarToolbarProps) => {
108
+ const CalendarToolbar = composable<HTMLDivElement, CalendarToolbarProps>(({ classNames, ...props }, forwardedRef) => {
123
109
  const { t } = useTranslation(translationKey);
124
- const { weekStartsOn, event, index, selected } = useCalendarContext(CalendarToolbar.displayName);
110
+ const { weekStartsOn, event, index, selected } = useCalendarContext(CALENDAR_TOOLBAR_NAME);
125
111
  const top = useMemo(() => getDate(start, index ?? 0, 6, weekStartsOn), [index, weekStartsOn]);
126
112
  const today = useMemo(() => new Date(), []);
127
113
 
@@ -131,18 +117,20 @@ const CalendarToolbar = ({ classNames }: CalendarToolbarProps) => {
131
117
 
132
118
  return (
133
119
  <div
134
- role='none'
135
- className={mx('shink-0 is-full grid grid-cols-3 items-center bg-barSurface', classNames)}
120
+ {...composableProps(props, {
121
+ role: 'none',
122
+ classNames: ['shrink-0 grid! grid-cols-3 items-center bg-toolbar-surface', classNames],
123
+ })}
124
+ ref={forwardedRef}
136
125
  style={{ width: defaultWidth }}
137
126
  >
138
127
  <div className='flex justify-start'>
139
128
  <IconButton
140
129
  variant='ghost'
141
- size={5}
142
130
  icon='ph--calendar--regular'
143
131
  iconOnly
144
132
  classNames='aspect-square'
145
- label={t('today button')}
133
+ label={t('today.button')}
146
134
  onClick={handleToday}
147
135
  />
148
136
  </div>
@@ -150,9 +138,9 @@ const CalendarToolbar = ({ classNames }: CalendarToolbarProps) => {
150
138
  <div className='flex justify-end p-2 text-description'>{(selected ?? top).getFullYear()}</div>
151
139
  </div>
152
140
  );
153
- };
141
+ });
154
142
 
155
- CalendarToolbar.displayName = 'CalendarHeader';
143
+ CalendarToolbar.displayName = CALENDAR_TOOLBAR_NAME;
156
144
 
157
145
  //
158
146
  // Grid
@@ -160,149 +148,149 @@ CalendarToolbar.displayName = 'CalendarHeader';
160
148
  // TODO(burdon): Drag range.
161
149
  //
162
150
 
163
- type CalendarGridProps = ThemedClassName<{
151
+ const CALENDAR_GRID_NAME = 'CalendarGrid';
152
+
153
+ type CalendarGridProps = {
164
154
  rows?: number;
155
+ /** Dates to highlight on the grid. Each date that appears in this array receives a border indicator. */
156
+ dates?: Date[];
165
157
  onSelect?: (event: { date: Date }) => void;
166
- }>;
167
-
168
- const CalendarGrid = ({ classNames, rows, onSelect }: CalendarGridProps) => {
169
- const { weekStartsOn, event, setIndex, selected, setSelected } = useCalendarContext(CalendarGrid.displayName);
170
- const { ref: containerRef, width = 0, height = 0 } = useResizeDetector();
171
- const maxHeight = rows ? rows * size : undefined;
172
- const listRef = useRef<List>(null);
173
- const today = useMemo(() => new Date(), []);
158
+ };
174
159
 
175
- const [initialized, setInitialized] = useState(false);
176
- useEffect(() => {
177
- const index = differenceInWeeks(today, start);
178
- listRef.current?.scrollToRow(index);
179
- }, [initialized, start, today]);
180
-
181
- useEffect(() => {
182
- return event.on((event) => {
183
- switch (event.type) {
184
- case 'scroll': {
185
- const index = differenceInWeeks(event.date, start);
186
- listRef.current?.scrollToRow(index);
187
- break;
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
+ }
188
187
  }
189
- }
190
- });
191
- }, [event]);
192
-
193
- const days = useMemo(() => {
194
- const weekStart = startOfWeek(new Date(), { weekStartsOn });
195
- return Array.from({ length: 7 }, (_, i) => {
196
- const day = addDays(weekStart, i);
197
- return format(day, 'EEE'); // Short day name (Mon, Tue, etc.)
198
- });
199
- }, []);
200
-
201
- // TODO(burdon): Get info by range.
202
- // TODO(burdon): Border marker for "all day events?"
203
- const getNumAppointments = useCallback((_date: Date) => {
204
- // return Math.floor(Math.random() * 10);
205
- return 0;
206
- }, []);
207
-
208
- const handleDaySelect = useCallback(
209
- (date: Date) => {
210
- setSelected((current) => (isSameDay(date, current) ? undefined : date));
211
- onSelect?.({ date });
212
- },
213
- [onSelect],
214
- );
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
+ );
215
208
 
216
- const handleScroll = useCallback<NonNullable<ListProps['onScroll']>>((info) => {
217
- setIndex(Math.round(info.scrollTop / size));
218
- }, []);
219
-
220
- const rowRenderer = useCallback<ListRowRenderer>(
221
- ({ key, index, style }) => {
222
- const getBgColor = (date: Date) => date.getMonth() % 2 === 0 && 'bg-modalSurface';
223
- return (
224
- <div key={key} role='none' style={style} className='is-full grid grid-cols-[1fr_max-content_1fr] snap-center'>
225
- <div role='none' className={mx(getBgColor(getDate(start, index, 0, weekStartsOn)))} />
226
- <div role='none' className='grid grid-cols-7' style={{ gridTemplateColumns: `repeat(7, ${size}px)` }}>
227
- {Array.from({ length: 7 }).map((_, i) => {
228
- const date = getDate(start, index, i, weekStartsOn);
229
- const num = getNumAppointments(date);
230
- const border = isSameDay(date, selected)
231
- ? 'border-primary-500'
232
- : isSameDay(date, today)
233
- ? 'border-amber-500'
234
- : undefined;
235
-
236
- return (
237
- <div
238
- key={i}
239
- role='none'
240
- className={mx('relative flex justify-center items-center cursor-pointer', getBgColor(date))}
241
- onClick={() => handleDaySelect(date)}
242
- >
243
- <span className='text-description'>{date.getDate()}</span>
244
- {!border && date.getDate() === 1 && (
245
- <span className='absolute top-0 text-xs text-description'>{format(date, 'MMM')}</span>
246
- )}
247
- {border && (
248
- <div
249
- role='none'
250
- className={mx('absolute top-0 left-0 is-full bs-full border-2 rounded-full', border)}
251
- />
252
- )}
253
- {num > 0 && (
254
- <Icon
255
- classNames='absolute bottom-0'
256
- icon={num > 3 ? 'ph--dots-three--regular' : 'ph--dot--regular'}
257
- size={5}
258
- />
259
- )}
260
- </div>
261
- );
262
- })}
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} role='none' style={style} className='grid'>
218
+ <div
219
+ role='none'
220
+ className='grid grid-cols-7 bg-input-surface'
221
+ style={{ gridTemplateColumns: `repeat(7, ${size}px)` }}
222
+ >
223
+ {Array.from({ length: 7 }).map((_, i) => {
224
+ const date = getDate(start, index, i, weekStartsOn);
225
+ const border = isSameDay(date, selected)
226
+ ? 'border-primary-500'
227
+ : isSameDay(date, today)
228
+ ? 'border-amber-500'
229
+ : hasDate(date)
230
+ ? 'border-neutral-700 border-dashed'
231
+ : undefined;
232
+
233
+ return (
234
+ <div
235
+ key={i}
236
+ role='none'
237
+ className={mx('relative flex justify-center items-center cursor-pointer', getBgColor(date))}
238
+ onClick={() => handleDaySelect(date)}
239
+ >
240
+ <span className='text-description'>{date.getDate()}</span>
241
+ {!border && date.getDate() === 1 && (
242
+ <span className='absolute top-0 text-xs text-description'>{format(date, 'MMM')}</span>
243
+ )}
244
+ {border && <div role='none' className={mx('absolute inset-1 border-2 rounded-full', border)} />}
245
+ </div>
246
+ );
247
+ })}
248
+ </div>
263
249
  </div>
264
- <div className={mx(getBgColor(getDate(start, index, 6, weekStartsOn)))} />
265
- </div>
266
- );
267
- },
268
- [handleDaySelect, getNumAppointments, selected, weekStartsOn],
269
- );
250
+ );
251
+ },
252
+ [handleDaySelect, hasDate, selected, weekStartsOn],
253
+ );
270
254
 
271
- return (
272
- <div role='none' className={mx('flex flex-col bs-full is-full justify-center overflow-hidden', classNames)}>
273
- {/* Day labels */}
274
- <div role='none' className='flex justify-center bg-groupSurface'>
275
- <div role='none' className='flex is-full grid grid-cols-7' style={{ width: defaultWidth }}>
255
+ return (
256
+ <div
257
+ {...composableProps(props, {
258
+ role: 'none',
259
+ classNames: ['flex flex-col h-full w-full justify-center overflow-hidden', classNames],
260
+ })}
261
+ ref={forwardedRef}
262
+ >
263
+ {/* Day of week labels */}
264
+ <div role='none' className='grid w-full grid-cols-7' style={{ width: defaultWidth }}>
276
265
  {days.map((date, i) => (
277
266
  <div key={i} role='none' className='flex justify-center p-2 text-sm font-thin'>
278
267
  {date}
279
268
  </div>
280
269
  ))}
281
270
  </div>
282
- </div>
283
271
 
284
- {/* Grid */}
285
- <div role='none' className='flex flex-col bs-full is-full justify-center overflow-hidden' ref={containerRef}>
286
- <List
287
- ref={listRef}
288
- role='none'
289
- // TODO(burdon): Snap isn't working.
290
- className='[&>div]:snap-y scrollbar-none outline-none'
291
- width={width}
292
- height={maxHeight ?? height}
293
- rowCount={maxRows}
294
- rowHeight={size}
295
- rowRenderer={rowRenderer}
296
- scrollToAlignment='start'
297
- onScroll={handleScroll}
298
- onRowsRendered={() => setInitialized(true)}
299
- />
272
+ {/* Grid */}
273
+ <div role='none' className='flex flex-col h-full w-full justify-center overflow-hidden' ref={containerRef}>
274
+ <List
275
+ ref={listRef}
276
+ role='none'
277
+ className='scrollbar-none outline-hidden'
278
+ width={width}
279
+ height={maxHeight ?? height}
280
+ rowCount={maxRows}
281
+ rowHeight={size}
282
+ rowRenderer={rowRenderer}
283
+ scrollToAlignment='start'
284
+ onScroll={handleScroll}
285
+ onRowsRendered={() => setInitialized(true)}
286
+ />
287
+ </div>
300
288
  </div>
301
- </div>
302
- );
303
- };
289
+ );
290
+ },
291
+ );
304
292
 
305
- CalendarGrid.displayName = 'CalendarGrid';
293
+ CalendarGrid.displayName = CALENDAR_GRID_NAME;
306
294
 
307
295
  //
308
296
  // Calendar
@@ -310,9 +298,8 @@ CalendarGrid.displayName = 'CalendarGrid';
310
298
 
311
299
  export const Calendar = {
312
300
  Root: CalendarRoot,
313
- Viewport: CalendarViewport,
314
301
  Toolbar: CalendarToolbar,
315
302
  Grid: CalendarGrid,
316
303
  };
317
304
 
318
- export type { CalendarController, CalendarRootProps, CalendarViewportProps, CalendarToolbarProps, CalendarGridProps };
305
+ export type { CalendarController, CalendarRootProps, CalendarToolbarProps, CalendarGridProps };
@@ -10,7 +10,7 @@ export const translations = [
10
10
  {
11
11
  'en-US': {
12
12
  [translationKey]: {
13
- 'today button': 'Today',
13
+ 'today.button': 'Today',
14
14
  },
15
15
  },
16
16
  },