@dxos/react-ui-calendar 0.8.4-main.ef1bc66f44 → 0.8.4-main.fcfe5033a5

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