@dxos/react-ui-calendar 0.8.4-main.c85a9c8dae → 0.8.4-main.e00bdcdb52

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