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

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.f466a3d56e",
4
4
  "description": "A calendar component.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -8,57 +8,62 @@
8
8
  "type": "git",
9
9
  "url": "https://github.com/dxos/dxos"
10
10
  },
11
- "license": "MIT",
11
+ "license": "FSL-1.1-Apache-2.0",
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.f466a3d56e",
43
+ "@dxos/debug": "0.8.4-main.f466a3d56e",
44
+ "@dxos/invariant": "0.8.4-main.f466a3d56e",
45
+ "@dxos/log": "0.8.4-main.f466a3d56e",
46
+ "@dxos/util": "0.8.4-main.f466a3d56e"
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.13",
56
+ "@dxos/random": "0.8.4-main.f466a3d56e",
57
+ "@dxos/react-ui": "0.8.4-main.f466a3d56e",
58
+ "@dxos/storybook-utils": "0.8.4-main.f466a3d56e",
59
+ "@dxos/ui-theme": "0.8.4-main.f466a3d56e"
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.f466a3d56e",
66
+ "@dxos/ui-theme": "0.8.4-main.f466a3d56e"
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,140 @@ 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} style={style} className='grid'>
218
+ <div className='grid grid-cols-7 bg-input-surface' style={{ gridTemplateColumns: `repeat(7, ${size}px)` }}>
219
+ {Array.from({ length: 7 }).map((_, i) => {
220
+ const date = getDate(start, index, i, weekStartsOn);
221
+ const border = isSameDay(date, selected)
222
+ ? 'border-primary-500'
223
+ : isSameDay(date, today)
224
+ ? 'border-amber-500'
225
+ : hasDate(date)
226
+ ? 'border-neutral-700 border-dashed'
227
+ : undefined;
228
+
229
+ return (
230
+ <div
231
+ key={i}
232
+ className={mx('relative flex justify-center items-center cursor-pointer', getBgColor(date))}
233
+ onClick={() => handleDaySelect(date)}
234
+ >
235
+ <span className='text-description'>{date.getDate()}</span>
236
+ {!border && date.getDate() === 1 && (
237
+ <span className='absolute top-0 text-xs text-description'>{format(date, 'MMM')}</span>
238
+ )}
239
+ {border && <div className={mx('absolute inset-1 border-2 rounded-full', border)} />}
240
+ </div>
241
+ );
242
+ })}
243
+ </div>
269
244
  </div>
270
- <div className={mx(getBgColor(getDate(start, index, 6, weekStartsOn)))} />
271
- </div>
272
- );
273
- },
274
- [handleDaySelect, getNumAppointments, selected, weekStartsOn],
275
- );
245
+ );
246
+ },
247
+ [handleDaySelect, hasDate, selected, weekStartsOn],
248
+ );
276
249
 
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 }}>
250
+ return (
251
+ <div
252
+ {...composableProps(props, {
253
+ role: 'none',
254
+ classNames: ['flex flex-col h-full w-full justify-center overflow-hidden', classNames],
255
+ })}
256
+ ref={forwardedRef}
257
+ >
258
+ {/* Day of week labels */}
259
+ <div className='grid w-full grid-cols-7' style={{ width: defaultWidth }}>
282
260
  {days.map((date, i) => (
283
- <div key={i} role='none' className='flex justify-center p-2 text-sm font-thin'>
261
+ <div key={i} className='flex justify-center p-2 text-sm font-thin'>
284
262
  {date}
285
263
  </div>
286
264
  ))}
287
265
  </div>
288
- </div>
289
266
 
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
- />
267
+ {/* Grid */}
268
+ <div className='flex flex-col h-full w-full justify-center overflow-hidden' ref={containerRef}>
269
+ <List
270
+ ref={listRef}
271
+ role='none'
272
+ className='scrollbar-none outline-hidden'
273
+ width={width}
274
+ height={maxHeight ?? height}
275
+ rowCount={maxRows}
276
+ rowHeight={size}
277
+ rowRenderer={rowRenderer}
278
+ scrollToAlignment='start'
279
+ onScroll={handleScroll}
280
+ onRowsRendered={() => setInitialized(true)}
281
+ />
282
+ </div>
306
283
  </div>
307
- </div>
308
- );
309
- };
284
+ );
285
+ },
286
+ );
310
287
 
311
288
  CalendarGrid.displayName = CALENDAR_GRID_NAME;
312
289
 
@@ -316,9 +293,8 @@ CalendarGrid.displayName = CALENDAR_GRID_NAME;
316
293
 
317
294
  export const Calendar = {
318
295
  Root: CalendarRoot,
319
- Viewport: CalendarViewport,
320
296
  Toolbar: CalendarToolbar,
321
297
  Grid: CalendarGrid,
322
298
  };
323
299
 
324
- export type { CalendarController, CalendarRootProps, CalendarViewportProps, CalendarToolbarProps, CalendarGridProps };
300
+ 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
  },