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

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.fcc0d83b33",
4
4
  "description": "A calendar component.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -11,54 +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.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"
42
+ "@dxos/async": "0.8.4-main.fcc0d83b33",
43
+ "@dxos/debug": "0.8.4-main.fcc0d83b33",
44
+ "@dxos/log": "0.8.4-main.fcc0d83b33",
45
+ "@dxos/invariant": "0.8.4-main.fcc0d83b33",
46
+ "@dxos/util": "0.8.4-main.fcc0d83b33"
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
- "@types/react-window": "^2.0.0",
47
- "effect": "3.19.16",
52
+ "effect": "3.20.0",
48
53
  "react": "~19.2.3",
49
54
  "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"
55
+ "vite": "^8.0.10",
56
+ "@dxos/react-ui": "0.8.4-main.fcc0d83b33",
57
+ "@dxos/random": "0.8.4-main.fcc0d83b33",
58
+ "@dxos/storybook-utils": "0.8.4-main.fcc0d83b33",
59
+ "@dxos/ui-theme": "0.8.4-main.fcc0d83b33"
55
60
  },
56
61
  "peerDependencies": {
57
- "effect": "3.19.16",
62
+ "effect": "3.20.0",
58
63
  "react": "~19.2.3",
59
64
  "react-dom": "~19.2.3",
60
- "@dxos/react-ui": "0.8.4-main.ef1bc66f44",
61
- "@dxos/ui-theme": "0.8.4-main.ef1bc66f44"
65
+ "@dxos/react-ui": "0.8.4-main.fcc0d83b33",
66
+ "@dxos/ui-theme": "0.8.4-main.fcc0d83b33"
62
67
  },
63
68
  "publishConfig": {
64
69
  "access": "public"
@@ -5,18 +5,17 @@
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
- import { translations } from '../../translations';
11
+ import { translations } from '#translations';
11
12
 
12
13
  import { Calendar } from './Calendar';
13
14
 
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,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
 
@@ -97,33 +97,15 @@ const CalendarRoot = forwardRef<CalendarController, CalendarRootProps>(
97
97
  },
98
98
  );
99
99
 
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
100
  //
119
101
  // Header
120
102
  //
121
103
 
122
104
  const CALENDAR_TOOLBAR_NAME = 'CalendarHeader';
123
105
 
124
- type CalendarToolbarProps = ThemedClassName;
106
+ type CalendarToolbarProps = {};
125
107
 
126
- const CalendarToolbar = ({ classNames }: CalendarToolbarProps) => {
108
+ const CalendarToolbar = composable<HTMLDivElement, CalendarToolbarProps>(({ classNames, ...props }, forwardedRef) => {
127
109
  const { t } = useTranslation(translationKey);
128
110
  const { weekStartsOn, event, index, selected } = useCalendarContext(CALENDAR_TOOLBAR_NAME);
129
111
  const top = useMemo(() => getDate(start, index ?? 0, 6, weekStartsOn), [index, weekStartsOn]);
@@ -135,18 +117,20 @@ const CalendarToolbar = ({ classNames }: CalendarToolbarProps) => {
135
117
 
136
118
  return (
137
119
  <div
138
- role='none'
139
- 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}
140
125
  style={{ width: defaultWidth }}
141
126
  >
142
127
  <div className='flex justify-start'>
143
128
  <IconButton
144
129
  variant='ghost'
145
- size={5}
146
130
  icon='ph--calendar--regular'
147
131
  iconOnly
148
132
  classNames='aspect-square'
149
- label={t('today button')}
133
+ label={t('today.button')}
150
134
  onClick={handleToday}
151
135
  />
152
136
  </div>
@@ -154,7 +138,7 @@ const CalendarToolbar = ({ classNames }: CalendarToolbarProps) => {
154
138
  <div className='flex justify-end p-2 text-description'>{(selected ?? top).getFullYear()}</div>
155
139
  </div>
156
140
  );
157
- };
141
+ });
158
142
 
159
143
  CalendarToolbar.displayName = CALENDAR_TOOLBAR_NAME;
160
144
 
@@ -166,147 +150,145 @@ CalendarToolbar.displayName = CALENDAR_TOOLBAR_NAME;
166
150
 
167
151
  const CALENDAR_GRID_NAME = 'CalendarGrid';
168
152
 
169
- type CalendarGridProps = ThemedClassName<{
153
+ type CalendarGridProps = {
170
154
  rows?: number;
155
+ /** Dates to highlight on the grid. Each date that appears in this array receives a border indicator. */
156
+ dates?: Date[];
171
157
  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(), []);
158
+ };
180
159
 
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;
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
+ }
194
187
  }
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
- );
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
+ );
221
208
 
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
- })}
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>
269
249
  </div>
270
- <div className={mx(getBgColor(getDate(start, index, 6, weekStartsOn)))} />
271
- </div>
272
- );
273
- },
274
- [handleDaySelect, getNumAppointments, selected, weekStartsOn],
275
- );
250
+ );
251
+ },
252
+ [handleDaySelect, hasDate, selected, weekStartsOn],
253
+ );
276
254
 
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 }}>
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 }}>
282
265
  {days.map((date, i) => (
283
266
  <div key={i} role='none' className='flex justify-center p-2 text-sm font-thin'>
284
267
  {date}
285
268
  </div>
286
269
  ))}
287
270
  </div>
288
- </div>
289
271
 
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
- />
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>
306
288
  </div>
307
- </div>
308
- );
309
- };
289
+ );
290
+ },
291
+ );
310
292
 
311
293
  CalendarGrid.displayName = CALENDAR_GRID_NAME;
312
294
 
@@ -316,9 +298,8 @@ CalendarGrid.displayName = CALENDAR_GRID_NAME;
316
298
 
317
299
  export const Calendar = {
318
300
  Root: CalendarRoot,
319
- Viewport: CalendarViewport,
320
301
  Toolbar: CalendarToolbar,
321
302
  Grid: CalendarGrid,
322
303
  };
323
304
 
324
- 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
  },