@octanejs/window 0.0.1

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.
Files changed (43) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +89 -0
  3. package/UPSTREAM.md +116 -0
  4. package/package.json +68 -0
  5. package/src/components/grid/Grid.tsx +329 -0
  6. package/src/components/grid/types.ts +334 -0
  7. package/src/components/grid/useGridCallbackRef.ts +16 -0
  8. package/src/components/grid/useGridRef.ts +12 -0
  9. package/src/components/list/List.tsx +216 -0
  10. package/src/components/list/isDynamicRowHeight.ts +10 -0
  11. package/src/components/list/types.ts +214 -0
  12. package/src/components/list/useDynamicRowHeight.ts +170 -0
  13. package/src/components/list/useListCallbackRef.ts +16 -0
  14. package/src/components/list/useListRef.ts +12 -0
  15. package/src/constants.ts +3 -0
  16. package/src/core/createCachedBounds.ts +65 -0
  17. package/src/core/getEstimatedSize.ts +25 -0
  18. package/src/core/getOffsetForIndex.ts +75 -0
  19. package/src/core/getStartStopIndices.ts +68 -0
  20. package/src/core/types.ts +14 -0
  21. package/src/core/useCachedBounds.ts +29 -0
  22. package/src/core/useIsRtl.ts +27 -0
  23. package/src/core/useItemSize.ts +33 -0
  24. package/src/core/useVirtualizer.ts +269 -0
  25. package/src/hooks/useIsomorphicLayoutEffect.ts +10 -0
  26. package/src/hooks/useMemoizedObject.ts +14 -0
  27. package/src/hooks/useResizeObserver.ts +98 -0
  28. package/src/hooks/useStableCallback.ts +39 -0
  29. package/src/index.ts +20 -0
  30. package/src/internal.ts +37 -0
  31. package/src/types.ts +5 -0
  32. package/src/utils/adjustScrollOffsetForRtl.ts +35 -0
  33. package/src/utils/areArraysEqual.ts +13 -0
  34. package/src/utils/arePropsEqual.ts +19 -0
  35. package/src/utils/assert.ts +10 -0
  36. package/src/utils/colors/getContrastColor.ts +47 -0
  37. package/src/utils/colors/stringToColor.ts +13 -0
  38. package/src/utils/debug.ts +13 -0
  39. package/src/utils/getRTLOffsetType.ts +46 -0
  40. package/src/utils/getScrollbarSize.ts +23 -0
  41. package/src/utils/isRtl.ts +12 -0
  42. package/src/utils/parseNumericStyleValue.ts +17 -0
  43. package/src/utils/shallowCompare.ts +26 -0
@@ -0,0 +1,334 @@
1
+ import type {
2
+ ComponentProps,
3
+ CSSProperties,
4
+ HTMLAttributes,
5
+ ReactElement,
6
+ ReactNode,
7
+ Ref,
8
+ } from 'react';
9
+ import type { TagNames } from '../../types.js';
10
+
11
+ type ForbiddenKeys = 'ariaAttributes' | 'columnIndex' | 'rowIndex' | 'style';
12
+ type ExcludeForbiddenKeys<Type> = {
13
+ [Key in keyof Type]: Key extends ForbiddenKeys ? never : Type[Key];
14
+ };
15
+
16
+ export type GridProps<CellProps extends object, TagName extends TagNames = 'div'> = Omit<
17
+ HTMLAttributes<HTMLDivElement>,
18
+ 'onResize'
19
+ > & {
20
+ /**
21
+ * React component responsible for rendering a cell.
22
+ *
23
+ * This component will receive an `index` and `style` prop by default.
24
+ * Additionally it will receive prop values passed to `cellProps`.
25
+ *
26
+ * ℹ️ The prop types for this component are exported as `CellComponentProps`
27
+ */
28
+ cellComponent: (
29
+ props: {
30
+ ariaAttributes: {
31
+ 'aria-colindex': number;
32
+ role: 'gridcell';
33
+ };
34
+ columnIndex: number;
35
+ rowIndex: number;
36
+ style: CSSProperties;
37
+ } & CellProps,
38
+ ) => ReactElement | null;
39
+
40
+ /**
41
+ * Additional props to be passed to the cell-rendering component.
42
+ * Grid will automatically re-render cells when values in this object change.
43
+ *
44
+ * ⚠️ This object must not contain `ariaAttributes`, `columnIndex`, `rowIndex`, or `style` props.
45
+ */
46
+ cellProps: ExcludeForbiddenKeys<CellProps>;
47
+
48
+ /**
49
+ * Additional content to be rendered within the grid (above cells).
50
+ * This property can be used to render things like overlays or tooltips.
51
+ */
52
+ children?: ReactNode;
53
+
54
+ /**
55
+ * CSS class name.
56
+ */
57
+ className?: string;
58
+
59
+ /**
60
+ * Number of columns to be rendered in the grid.
61
+ */
62
+ columnCount: number;
63
+
64
+ /**
65
+ * Grids use the column index as a `key` by default.
66
+ * This prop can be used along with the `rowKey` prop to provide a custom `key` value.
67
+ *
68
+ * ℹ️ Custom keys can ensure better UX for sortable or filterable grids,
69
+ * particularly if your cell components are stateful.
70
+ * Refer to the [React documentation](https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key) for more info.
71
+ *
72
+ * ⚠️ This prop cannot be auto-memoized because it is called during render.
73
+ * It is important to always `useCallback` for this prop; do not use an inline function.
74
+ */
75
+ columnKey?: (args: { columnIndex: number; data: CellProps; rowIndex: number }) => React.Key;
76
+
77
+ /**
78
+ * Column width; the following formats are supported:
79
+ * - number of pixels (number)
80
+ * - percentage of the grid's current width (string)
81
+ * - function that returns the column width (in pixels) given an index and `cellProps`
82
+ */
83
+ columnWidth: number | string | ((index: number, cellProps: CellProps) => number);
84
+
85
+ /**
86
+ * Default height of grid for initial render.
87
+ * This value is important for server rendering.
88
+ */
89
+ defaultHeight?: number;
90
+
91
+ /**
92
+ * Default width of grid for initial render.
93
+ * This value is important for server rendering.
94
+ */
95
+ defaultWidth?: number;
96
+
97
+ /**
98
+ * Indicates the directionality of grid cells.
99
+ *
100
+ * ℹ️ See HTML `dir` [global attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/dir) for more information.
101
+ */
102
+ dir?: 'ltr' | 'rtl';
103
+
104
+ /**
105
+ * Imperative Grid API.
106
+ *
107
+ * ℹ️ The `useGridRef` and `useGridCallbackRef` hooks are exported for convenience use in TypeScript projects.
108
+ */
109
+ gridRef?: Ref<{
110
+ /**
111
+ * Outermost HTML element for the grid if mounted and null (if not mounted.
112
+ */
113
+ get element(): HTMLDivElement | null;
114
+
115
+ /**
116
+ * Scrolls the grid so that the specified row and column are visible.
117
+ *
118
+ * @param behavior Determines whether scrolling is instant or animates smoothly
119
+ * @param columnAlign Determines the horizontal alignment of the element within the list
120
+ * @param columnIndex Index of the column to scroll to (0-based)
121
+ * @param rowAlign Determines the vertical alignment of the element within the list
122
+ * @param rowIndex Index of the row to scroll to (0-based)
123
+ *
124
+ * @throws RangeError if an invalid row or column index is provided
125
+ */
126
+ scrollToCell(config: {
127
+ behavior?: 'auto' | 'instant' | 'smooth';
128
+ columnAlign?: 'auto' | 'center' | 'end' | 'smart' | 'start';
129
+ columnIndex: number;
130
+ rowAlign?: 'auto' | 'center' | 'end' | 'smart' | 'start';
131
+ rowIndex: number;
132
+ }): void;
133
+
134
+ /**
135
+ * Scrolls the grid so that the specified column is visible.
136
+ *
137
+ * @param align Determines the horizontal alignment of the element within the list
138
+ * @param behavior Determines whether scrolling is instant or animates smoothly
139
+ * @param index Index of the column to scroll to (0-based)
140
+ *
141
+ * @throws RangeError if an invalid column index is provided
142
+ */
143
+ scrollToColumn(config: {
144
+ align?: 'auto' | 'center' | 'end' | 'smart' | 'start';
145
+ behavior?: 'auto' | 'instant' | 'smooth';
146
+ index: number;
147
+ }): void;
148
+
149
+ /**
150
+ * Scrolls the grid so that the specified row is visible.
151
+ *
152
+ * @param align Determines the vertical alignment of the element within the list
153
+ * @param behavior Determines whether scrolling is instant or animates smoothly
154
+ * @param index Index of the row to scroll to (0-based)
155
+ *
156
+ * @throws RangeError if an invalid row index is provided
157
+ */
158
+ scrollToRow(config: {
159
+ align?: 'auto' | 'center' | 'end' | 'smart' | 'start';
160
+ behavior?: 'auto' | 'instant' | 'smooth';
161
+ index: number;
162
+ }): void;
163
+ }>;
164
+
165
+ /**
166
+ * Callback notified when the range of rendered cells changes.
167
+ */
168
+ onCellsRendered?: (
169
+ visibleCells: {
170
+ columnStartIndex: number;
171
+ columnStopIndex: number;
172
+ rowStartIndex: number;
173
+ rowStopIndex: number;
174
+ },
175
+ allCells: {
176
+ columnStartIndex: number;
177
+ columnStopIndex: number;
178
+ rowStartIndex: number;
179
+ rowStopIndex: number;
180
+ },
181
+ ) => void;
182
+
183
+ /**
184
+ * Callback notified when the Grid's outermost HTMLElement resizes.
185
+ * This may be used to (re)scroll a cell into view.
186
+ */
187
+ onResize?: (
188
+ size: { height: number; width: number },
189
+ prevSize: { height: number; width: number },
190
+ ) => void;
191
+
192
+ /**
193
+ * How many additional rows/columns to render outside of the visible area.
194
+ * This can reduce visual flickering near the edges of a grid when scrolling.
195
+ */
196
+ overscanCount?: number;
197
+
198
+ /**
199
+ * Number of rows to be rendered in the grid.
200
+ */
201
+ rowCount: number;
202
+
203
+ /**
204
+ * Row height; the following formats are supported:
205
+ * - number of pixels (number)
206
+ * - percentage of the grid's current height (string)
207
+ * - function that returns the row height (in pixels) given an index and `cellProps`
208
+ */
209
+ rowHeight: number | string | ((index: number, cellProps: CellProps) => number);
210
+
211
+ /**
212
+ * Grids use the row index as a `key` by default.
213
+ * This prop can be used along with the `columnKey` prop to provide a custom `key` value.
214
+ *
215
+ * ℹ️ Custom keys can ensure better UX for sortable or filterable grids,
216
+ * particularly if your cell components are stateful.
217
+ * Refer to the [React documentation](https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key) for more info.
218
+ *
219
+ * ⚠️ This prop cannot be auto-memoized because it is called during render.
220
+ * It is important to always `useCallback` for this prop; do not use an inline function.
221
+ */
222
+ rowKey?: (args: { data: CellProps; rowIndex: number }) => React.Key;
223
+
224
+ /**
225
+ * Optional CSS properties.
226
+ * The grid of cells will fill the height and width defined by this style.
227
+ */
228
+ style?: CSSProperties;
229
+
230
+ /**
231
+ * Can be used to override the root HTML element rendered by the List component.
232
+ * The default value is "div", meaning that List renders an HTMLDivElement as its root.
233
+ *
234
+ * ⚠️ In most use cases the default ARIA roles are sufficient and this prop is not needed.
235
+ */
236
+ tagName?: TagName;
237
+ };
238
+
239
+ export type CellComponent<CellProps extends object> = GridProps<CellProps>['cellComponent'];
240
+ export type CellComponentProps<CellProps extends object = object> = ComponentProps<
241
+ CellComponent<CellProps>
242
+ >;
243
+
244
+ export type ScrollState = {
245
+ prevScrollTop: number;
246
+ scrollTop: number;
247
+ };
248
+
249
+ export type OnCellsRendered = NonNullable<GridProps<object>['onCellsRendered']>;
250
+
251
+ export type CachedBounds = Map<
252
+ number,
253
+ {
254
+ height: number;
255
+ scrollTop: number;
256
+ }
257
+ >;
258
+
259
+ /**
260
+ * Ref used to interact with this component's imperative API.
261
+ *
262
+ * This API has imperative methods for scrolling and a getter for the outermost DOM element.
263
+ *
264
+ * ℹ️ The `useGridRef` and `useGridCallbackRef` hooks are exported for convenience use in TypeScript projects.
265
+ */
266
+ export interface GridImperativeAPI {
267
+ /**
268
+ * Outermost HTML element for the grid if mounted and null (if not mounted.
269
+ */
270
+ get element(): HTMLDivElement | null;
271
+
272
+ /**
273
+ * Scrolls the grid so that the specified row and column are visible.
274
+ *
275
+ * @param behavior Determines whether scrolling is instant or animates smoothly
276
+ * @param columnAlign Determines the horizontal alignment of the element within the list
277
+ * @param columnIndex Index of the column to scroll to (0-based)
278
+ * @param rowAlign Determines the vertical alignment of the element within the list
279
+ * @param rowIndex Index of the row to scroll to (0-based)
280
+ *
281
+ * @throws RangeError if an invalid row or column index is provided
282
+ */
283
+ scrollToCell: ({
284
+ behavior,
285
+ columnAlign,
286
+ columnIndex,
287
+ rowAlign,
288
+ rowIndex,
289
+ }: {
290
+ behavior?: 'auto' | 'instant' | 'smooth';
291
+ columnAlign?: 'auto' | 'center' | 'end' | 'smart' | 'start';
292
+ columnIndex: number;
293
+ rowAlign?: 'auto' | 'center' | 'end' | 'smart' | 'start';
294
+ rowIndex: number;
295
+ }) => void;
296
+
297
+ /**
298
+ * Scrolls the grid so that the specified column is visible.
299
+ *
300
+ * @param align Determines the horizontal alignment of the element within the list
301
+ * @param behavior Determines whether scrolling is instant or animates smoothly
302
+ * @param index Index of the column to scroll to (0-based)
303
+ *
304
+ * @throws RangeError if an invalid column index is provided
305
+ */
306
+ scrollToColumn: ({
307
+ align,
308
+ behavior,
309
+ index,
310
+ }: {
311
+ align?: 'auto' | 'center' | 'end' | 'smart' | 'start';
312
+ behavior?: 'auto' | 'instant' | 'smooth';
313
+ index: number;
314
+ }) => void;
315
+
316
+ /**
317
+ * Scrolls the grid so that the specified row is visible.
318
+ *
319
+ * @param align Determines the vertical alignment of the element within the list
320
+ * @param behavior Determines whether scrolling is instant or animates smoothly
321
+ * @param index Index of the row to scroll to (0-based)
322
+ *
323
+ * @throws RangeError if an invalid row index is provided
324
+ */
325
+ scrollToRow: ({
326
+ align,
327
+ behavior,
328
+ index,
329
+ }: {
330
+ align?: 'auto' | 'center' | 'end' | 'smart' | 'start';
331
+ behavior?: 'auto' | 'instant' | 'smooth';
332
+ index: number;
333
+ }) => void;
334
+ }
@@ -0,0 +1,16 @@
1
+ import { useState } from 'octane';
2
+ import { getPublicArgument, getSlot, subSlot } from '../../internal.js';
3
+ import type { GridImperativeAPI } from './types.js';
4
+
5
+ /**
6
+ * Convenience hook to return a properly typed ref callback for the Grid component.
7
+ *
8
+ * Use this hook when you need to share the ref with another component or hook.
9
+ */
10
+ export const useGridCallbackRef = ((...args: unknown[]) => {
11
+ const slot = getSlot(args);
12
+ const initialValue = getPublicArgument(args, 0) as
13
+ GridImperativeAPI | null | (() => GridImperativeAPI | null) | undefined;
14
+ const [value, setValue] = useState(initialValue, subSlot(slot, 'grid-callback-ref'));
15
+ return [value, setValue];
16
+ }) as unknown as typeof import('react').useState<GridImperativeAPI | null>;
@@ -0,0 +1,12 @@
1
+ import { useRef } from 'octane';
2
+ import { getPublicArgument, getSlot, subSlot } from '../../internal.js';
3
+ import type { GridImperativeAPI } from './types.js';
4
+
5
+ /**
6
+ * Convenience hook to return a properly typed ref for the Grid component.
7
+ */
8
+ export const useGridRef = ((...args: unknown[]) => {
9
+ const slot = getSlot(args);
10
+ const initialValue = getPublicArgument(args, 0) as GridImperativeAPI | null | undefined;
11
+ return useRef(initialValue, subSlot(slot, 'grid-ref'));
12
+ }) as typeof import('react').useRef<GridImperativeAPI>;
@@ -0,0 +1,216 @@
1
+ /** @jsxImportSource octane */
2
+ 'use client';
3
+
4
+ import { createElement, memo, useEffect, useImperativeHandle, useMemo, useState } from 'octane';
5
+ import { useVirtualizer } from '../../core/useVirtualizer.js';
6
+ import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect.js';
7
+ import { useMemoizedObject } from '../../hooks/useMemoizedObject.js';
8
+ import type { Align, TagNames } from '../../types.js';
9
+ import { arePropsEqual } from '../../utils/arePropsEqual.js';
10
+ import { isDynamicRowHeight as isDynamicRowHeightUtil } from './isDynamicRowHeight.js';
11
+ import type { ListProps } from './types.js';
12
+
13
+ export const DATA_ATTRIBUTE_LIST_INDEX = 'data-react-window-index';
14
+
15
+ /**
16
+ * Renders data with many rows.
17
+ */
18
+ export function List<RowProps extends object, TagName extends TagNames = 'div'>({
19
+ children,
20
+ className,
21
+ defaultHeight = 0,
22
+ listRef,
23
+ onResize,
24
+ onRowsRendered,
25
+ overscanCount = 3,
26
+ rowComponent: RowComponentProp,
27
+ rowCount,
28
+ rowHeight: rowHeightProp,
29
+ rowKey,
30
+ rowProps: rowPropsUnstable,
31
+ tagName = 'div' as TagName,
32
+ style,
33
+ ...rest
34
+ }: ListProps<RowProps, TagName>) {
35
+ const rowProps = useMemoizedObject(rowPropsUnstable);
36
+ const RowComponent = useMemo(
37
+ () => memo(RowComponentProp as any, arePropsEqual as any) as typeof RowComponentProp,
38
+ [RowComponentProp],
39
+ );
40
+
41
+ const [element, setElement] = useState<HTMLDivElement | null>(null);
42
+
43
+ const isDynamicRowHeight = isDynamicRowHeightUtil(rowHeightProp);
44
+
45
+ const rowHeight = useMemo(() => {
46
+ if (isDynamicRowHeight) {
47
+ return (index: number) => {
48
+ return rowHeightProp.getRowHeight(index) ?? rowHeightProp.getAverageRowHeight();
49
+ };
50
+ }
51
+
52
+ return rowHeightProp;
53
+ }, [isDynamicRowHeight, rowHeightProp]);
54
+
55
+ const {
56
+ getCellBounds,
57
+ getEstimatedSize,
58
+ scrollToIndex,
59
+ startIndexOverscan,
60
+ startIndexVisible,
61
+ stopIndexOverscan,
62
+ stopIndexVisible,
63
+ } = useVirtualizer({
64
+ containerElement: element,
65
+ containerStyle: style,
66
+ defaultContainerSize: defaultHeight,
67
+ direction: 'vertical',
68
+ itemCount: rowCount,
69
+ itemProps: rowProps,
70
+ itemSize: rowHeight,
71
+ onResize,
72
+ overscanCount,
73
+ });
74
+
75
+ useImperativeHandle(
76
+ listRef,
77
+ () => ({
78
+ get element() {
79
+ return element;
80
+ },
81
+
82
+ scrollToRow({
83
+ align = 'auto',
84
+ behavior = 'auto',
85
+ index,
86
+ }: {
87
+ align?: Align;
88
+ behavior?: ScrollBehavior;
89
+ index: number;
90
+ }) {
91
+ const top = scrollToIndex({
92
+ align,
93
+ containerScrollOffset: element?.scrollTop ?? 0,
94
+ index,
95
+ });
96
+
97
+ if (typeof element?.scrollTo === 'function') {
98
+ element.scrollTo({
99
+ behavior,
100
+ top,
101
+ });
102
+ }
103
+ },
104
+ }),
105
+ [element, scrollToIndex],
106
+ );
107
+
108
+ useIsomorphicLayoutEffect(() => {
109
+ if (!element) {
110
+ return;
111
+ }
112
+
113
+ const rows = Array.from(element.children).filter((item, index) => {
114
+ if (item.hasAttribute('aria-hidden')) {
115
+ // Ignore sizing element
116
+ return false;
117
+ }
118
+
119
+ const attribute = `${startIndexOverscan + index}`;
120
+ item.setAttribute(DATA_ATTRIBUTE_LIST_INDEX, attribute);
121
+
122
+ return true;
123
+ });
124
+
125
+ if (isDynamicRowHeight) {
126
+ return rowHeightProp.observeRowElements(rows);
127
+ }
128
+ }, [element, isDynamicRowHeight, rowHeightProp, startIndexOverscan, stopIndexOverscan]);
129
+
130
+ useEffect(() => {
131
+ if (startIndexOverscan >= 0 && stopIndexOverscan >= 0 && onRowsRendered) {
132
+ onRowsRendered(
133
+ {
134
+ startIndex: startIndexVisible,
135
+ stopIndex: stopIndexVisible,
136
+ },
137
+ {
138
+ startIndex: startIndexOverscan,
139
+ stopIndex: stopIndexOverscan,
140
+ },
141
+ );
142
+ }
143
+ }, [onRowsRendered, startIndexOverscan, startIndexVisible, stopIndexOverscan, stopIndexVisible]);
144
+
145
+ const rows = useMemo(() => {
146
+ const children: unknown[] = [];
147
+ if (rowCount > 0) {
148
+ for (let index = startIndexOverscan; index <= stopIndexOverscan; index++) {
149
+ const bounds = getCellBounds(index);
150
+
151
+ children.push(
152
+ <RowComponent
153
+ {...(rowProps as RowProps)}
154
+ ariaAttributes={{
155
+ 'aria-posinset': index + 1,
156
+ 'aria-setsize': rowCount,
157
+ role: 'listitem',
158
+ }}
159
+ key={rowKey ? rowKey(index, rowProps) : index}
160
+ index={index}
161
+ style={{
162
+ position: 'absolute',
163
+ left: 0,
164
+ transform: `translateY(${bounds.scrollOffset}px)`,
165
+ // In case of dynamic row heights, don't specify a height style
166
+ // otherwise a default/estimated height would mask the actual height
167
+ height: isDynamicRowHeight ? undefined : bounds.size,
168
+ width: '100%',
169
+ }}
170
+ />,
171
+ );
172
+ }
173
+ }
174
+ return children;
175
+ }, [
176
+ RowComponent,
177
+ getCellBounds,
178
+ isDynamicRowHeight,
179
+ rowCount,
180
+ rowKey,
181
+ rowProps,
182
+ startIndexOverscan,
183
+ stopIndexOverscan,
184
+ ]);
185
+
186
+ const sizingElement = (
187
+ <div
188
+ aria-hidden
189
+ style={{
190
+ height: getEstimatedSize(),
191
+ width: '100%',
192
+ zIndex: -1,
193
+ }}
194
+ ></div>
195
+ );
196
+
197
+ return createElement(
198
+ tagName,
199
+ {
200
+ role: 'list',
201
+ ...rest,
202
+ className,
203
+ ref: setElement,
204
+ style: {
205
+ position: 'relative',
206
+ maxHeight: '100%',
207
+ flexGrow: 1,
208
+ overflowY: 'auto',
209
+ ...style,
210
+ },
211
+ },
212
+ rows,
213
+ children,
214
+ sizingElement,
215
+ );
216
+ }
@@ -0,0 +1,10 @@
1
+ import type { DynamicRowHeight } from './types.js';
2
+
3
+ export function isDynamicRowHeight(value: unknown): value is DynamicRowHeight {
4
+ return (
5
+ value != null &&
6
+ typeof value === 'object' &&
7
+ 'getAverageRowHeight' in value &&
8
+ typeof value.getAverageRowHeight === 'function'
9
+ );
10
+ }