@dxos/react-ui-calendar 0.8.4-main.e8ec1fe → 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,9 +1,13 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-calendar",
3
- "version": "0.8.4-main.e8ec1fe",
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",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dxos/dxos"
10
+ },
7
11
  "license": "MIT",
8
12
  "author": "DXOS.org",
9
13
  "type": "module",
@@ -24,39 +28,36 @@
24
28
  "src"
25
29
  ],
26
30
  "dependencies": {
27
- "@preact-signals/safe-react": "^0.9.0",
28
- "@preact/signals-core": "^1.12.1",
29
31
  "@radix-ui/react-context": "1.1.1",
30
- "date-fns": "^3.3.1",
32
+ "date-fns": "^3.6.0",
31
33
  "react-resize-detector": "^11.0.1",
32
34
  "react-virtualized": "^9.22.6",
33
35
  "react-window": "^2.2.3",
34
- "@dxos/async": "0.8.4-main.e8ec1fe",
35
- "@dxos/debug": "0.8.4-main.e8ec1fe",
36
- "@dxos/invariant": "0.8.4-main.e8ec1fe",
37
- "@dxos/util": "0.8.4-main.e8ec1fe",
38
- "@dxos/log": "0.8.4-main.e8ec1fe"
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"
39
41
  },
40
42
  "devDependencies": {
41
- "@types/react": "~19.2.2",
42
- "@types/react-dom": "~19.2.2",
43
+ "@types/react": "~19.2.7",
44
+ "@types/react-dom": "~19.2.3",
43
45
  "@types/react-virtualized": "^9.22.3",
44
- "@types/react-window": "^2.0.0",
45
- "effect": "3.18.3",
46
- "react": "~19.2.0",
47
- "react-dom": "~19.2.0",
48
- "vite": "7.1.9",
49
- "@dxos/random": "0.8.4-main.e8ec1fe",
50
- "@dxos/react-ui": "0.8.4-main.e8ec1fe",
51
- "@dxos/react-ui-theme": "0.8.4-main.e8ec1fe",
52
- "@dxos/storybook-utils": "0.8.4-main.e8ec1fe"
46
+ "effect": "3.20.0",
47
+ "react": "~19.2.3",
48
+ "react-dom": "~19.2.3",
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"
53
54
  },
54
55
  "peerDependencies": {
55
- "effect": "3.13.3",
56
- "react": "^19.0.0",
57
- "react-dom": "^19.0.0",
58
- "@dxos/react-ui": "0.8.4-main.e8ec1fe",
59
- "@dxos/react-ui-theme": "0.8.4-main.e8ec1fe"
56
+ "effect": "3.20.0",
57
+ "react": "~19.2.3",
58
+ "react-dom": "~19.2.3",
59
+ "@dxos/react-ui": "0.8.4-main.fcfe5033a5",
60
+ "@dxos/ui-theme": "0.8.4-main.fcfe5033a5"
60
61
  },
61
62
  "publishConfig": {
62
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/react-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,31 +96,17 @@ const CalendarRoot = forwardRef<CalendarController, CalendarRootProps>(
97
96
  },
98
97
  );
99
98
 
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
99
  //
117
100
  // Header
118
101
  //
119
102
 
120
- type CalendarToolbarProps = ThemedClassName;
103
+ const CALENDAR_TOOLBAR_NAME = 'CalendarHeader';
104
+
105
+ type CalendarToolbarProps = {};
121
106
 
122
- const CalendarToolbar = ({ classNames }: CalendarToolbarProps) => {
107
+ const CalendarToolbar = composable<HTMLDivElement, CalendarToolbarProps>(({ classNames, ...props }, forwardedRef) => {
123
108
  const { t } = useTranslation(translationKey);
124
- const { weekStartsOn, event, index, selected } = useCalendarContext(CalendarToolbar.displayName);
109
+ const { weekStartsOn, event, index, selected } = useCalendarContext(CALENDAR_TOOLBAR_NAME);
125
110
  const top = useMemo(() => getDate(start, index ?? 0, 6, weekStartsOn), [index, weekStartsOn]);
126
111
  const today = useMemo(() => new Date(), []);
127
112
 
@@ -131,18 +116,20 @@ const CalendarToolbar = ({ classNames }: CalendarToolbarProps) => {
131
116
 
132
117
  return (
133
118
  <div
134
- role='none'
135
- 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}
136
124
  style={{ width: defaultWidth }}
137
125
  >
138
126
  <div className='flex justify-start'>
139
127
  <IconButton
140
128
  variant='ghost'
141
- size={5}
142
129
  icon='ph--calendar--regular'
143
130
  iconOnly
144
131
  classNames='aspect-square'
145
- label={t('today button')}
132
+ label={t('today.button')}
146
133
  onClick={handleToday}
147
134
  />
148
135
  </div>
@@ -150,9 +137,9 @@ const CalendarToolbar = ({ classNames }: CalendarToolbarProps) => {
150
137
  <div className='flex justify-end p-2 text-description'>{(selected ?? top).getFullYear()}</div>
151
138
  </div>
152
139
  );
153
- };
140
+ });
154
141
 
155
- CalendarToolbar.displayName = 'CalendarHeader';
142
+ CalendarToolbar.displayName = CALENDAR_TOOLBAR_NAME;
156
143
 
157
144
  //
158
145
  // Grid
@@ -160,149 +147,149 @@ CalendarToolbar.displayName = 'CalendarHeader';
160
147
  // TODO(burdon): Drag range.
161
148
  //
162
149
 
163
- type CalendarGridProps = ThemedClassName<{
150
+ const CALENDAR_GRID_NAME = 'CalendarGrid';
151
+
152
+ type CalendarGridProps = {
164
153
  rows?: number;
154
+ /** Dates to highlight on the grid. Each date that appears in this array receives a border indicator. */
155
+ dates?: Date[];
165
156
  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(), []);
157
+ };
174
158
 
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;
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
+ }
188
186
  }
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
- );
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
+ );
215
207
 
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
- })}
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>
263
248
  </div>
264
- <div className={mx(getBgColor(getDate(start, index, 6, weekStartsOn)))} />
265
- </div>
266
- );
267
- },
268
- [handleDaySelect, getNumAppointments, selected, weekStartsOn],
269
- );
249
+ );
250
+ },
251
+ [handleDaySelect, hasDate, selected, weekStartsOn],
252
+ );
270
253
 
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 }}>
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 }}>
276
264
  {days.map((date, i) => (
277
265
  <div key={i} role='none' className='flex justify-center p-2 text-sm font-thin'>
278
266
  {date}
279
267
  </div>
280
268
  ))}
281
269
  </div>
282
- </div>
283
270
 
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
- />
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>
300
287
  </div>
301
- </div>
302
- );
303
- };
288
+ );
289
+ },
290
+ );
304
291
 
305
- CalendarGrid.displayName = 'CalendarGrid';
292
+ CalendarGrid.displayName = CALENDAR_GRID_NAME;
306
293
 
307
294
  //
308
295
  // Calendar
@@ -310,9 +297,8 @@ CalendarGrid.displayName = 'CalendarGrid';
310
297
 
311
298
  export const Calendar = {
312
299
  Root: CalendarRoot,
313
- Viewport: CalendarViewport,
314
300
  Toolbar: CalendarToolbar,
315
301
  Grid: CalendarGrid,
316
302
  };
317
303
 
318
- export type { CalendarController, CalendarRootProps, CalendarViewportProps, CalendarToolbarProps, CalendarGridProps };
304
+ export type { CalendarController, CalendarRootProps, CalendarToolbarProps, CalendarGridProps };
@@ -4,13 +4,13 @@
4
4
 
5
5
  import { type Resource } from '@dxos/react-ui';
6
6
 
7
- export const translationKey = 'react-ui-calendar';
7
+ export const translationKey = '@dxos/react-ui-calendar';
8
8
 
9
9
  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
  },