@dxos/react-ui-grid 0.8.4-main.72ec0f3 → 0.8.4-main.765dc60934
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/LICENSE +102 -5
- package/dist/lib/browser/index.mjs +149 -160
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/CellEditor/CellEditor.d.ts +3 -2
- package/dist/types/src/CellEditor/CellEditor.d.ts.map +1 -1
- package/dist/types/src/CellEditor/CellEditor.stories.d.ts +1 -1
- package/dist/types/src/CellEditor/CellEditor.stories.d.ts.map +1 -1
- package/dist/types/src/CellEditor/GridCellEditor.d.ts.map +1 -1
- package/dist/types/src/Grid/Grid.d.ts +8 -8
- package/dist/types/src/Grid/Grid.d.ts.map +1 -1
- package/dist/types/src/Grid/Grid.stories.d.ts +4 -0
- package/dist/types/src/Grid/Grid.stories.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +29 -26
- package/src/CellEditor/CellEditor.stories.tsx +2 -2
- package/src/CellEditor/CellEditor.tsx +26 -16
- package/src/CellEditor/GridCellEditor.tsx +0 -1
- package/src/Grid/Grid.stories.tsx +53 -19
- package/src/Grid/Grid.tsx +10 -8
package/package.json
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-grid",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.765dc60934",
|
|
4
4
|
"description": "React component which manages a `dx-grid` Lit web component.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
7
|
-
"
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/dxos/dxos"
|
|
10
|
+
},
|
|
11
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
8
12
|
"author": "DXOS.org",
|
|
9
|
-
"sideEffects":
|
|
13
|
+
"sideEffects": false,
|
|
10
14
|
"type": "module",
|
|
11
15
|
"exports": {
|
|
12
16
|
".": {
|
|
13
17
|
"source": "./src/index.ts",
|
|
14
18
|
"types": "./dist/types/src/index.d.ts",
|
|
15
|
-
"browser": "./dist/lib/browser/index.mjs"
|
|
19
|
+
"browser": "./dist/lib/browser/index.mjs",
|
|
20
|
+
"node": "./dist/lib/browser/index.mjs"
|
|
16
21
|
}
|
|
17
22
|
},
|
|
18
23
|
"types": "dist/types/src/index.d.ts",
|
|
19
|
-
"typesVersions": {
|
|
20
|
-
"*": {}
|
|
21
|
-
},
|
|
22
24
|
"files": [
|
|
23
25
|
"dist",
|
|
24
26
|
"src"
|
|
@@ -26,33 +28,34 @@
|
|
|
26
28
|
"dependencies": {
|
|
27
29
|
"@codemirror/autocomplete": "^6.19.0",
|
|
28
30
|
"@codemirror/state": "^6.5.2",
|
|
29
|
-
"@codemirror/view": "^6.38.
|
|
31
|
+
"@codemirror/view": "^6.38.5",
|
|
30
32
|
"@lit/react": "^1.0.8",
|
|
31
|
-
"@preact-signals/safe-react": "^0.9.0",
|
|
32
33
|
"@radix-ui/react-context": "1.1.1",
|
|
33
34
|
"@radix-ui/react-popper": "1.2.2",
|
|
34
35
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
|
35
|
-
"@dxos/
|
|
36
|
-
"@dxos/
|
|
37
|
-
"@dxos/
|
|
36
|
+
"@dxos/lit-grid": "0.8.4-main.765dc60934",
|
|
37
|
+
"@dxos/ui-editor": "0.8.4-main.765dc60934",
|
|
38
|
+
"@dxos/react-ui-editor": "0.8.4-main.765dc60934",
|
|
39
|
+
"@dxos/util": "0.8.4-main.765dc60934"
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
40
|
-
"@types/react": "~19.2.
|
|
41
|
-
"@types/react-dom": "~19.2.
|
|
42
|
-
"react": "~19.2.
|
|
43
|
-
"react-dom": "~19.2.
|
|
44
|
-
"vite": "
|
|
45
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
46
|
-
"@dxos/react-ui-
|
|
47
|
-
"@dxos/
|
|
48
|
-
"@dxos/storybook-utils": "0.8.4-main.
|
|
49
|
-
"@dxos/
|
|
42
|
+
"@types/react": "~19.2.7",
|
|
43
|
+
"@types/react-dom": "~19.2.3",
|
|
44
|
+
"react": "~19.2.3",
|
|
45
|
+
"react-dom": "~19.2.3",
|
|
46
|
+
"vite": "^8.0.13",
|
|
47
|
+
"@dxos/react-ui-list": "0.8.4-main.765dc60934",
|
|
48
|
+
"@dxos/react-ui-search": "0.8.4-main.765dc60934",
|
|
49
|
+
"@dxos/random": "0.8.4-main.765dc60934",
|
|
50
|
+
"@dxos/storybook-utils": "0.8.4-main.765dc60934",
|
|
51
|
+
"@dxos/react-ui": "0.8.4-main.765dc60934",
|
|
52
|
+
"@dxos/ui-theme": "0.8.4-main.765dc60934"
|
|
50
53
|
},
|
|
51
54
|
"peerDependencies": {
|
|
52
|
-
"react": "
|
|
53
|
-
"react-dom": "
|
|
54
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
55
|
-
"@dxos/
|
|
55
|
+
"react": "~19.2.3",
|
|
56
|
+
"react-dom": "~19.2.3",
|
|
57
|
+
"@dxos/react-ui": "0.8.4-main.765dc60934",
|
|
58
|
+
"@dxos/ui-theme": "0.8.4-main.765dc60934"
|
|
56
59
|
},
|
|
57
60
|
"publishConfig": {
|
|
58
61
|
"access": "public"
|
|
@@ -47,7 +47,7 @@ const DefaultStory = (props: CellEditorProps) => {
|
|
|
47
47
|
<div className='text-sm'>
|
|
48
48
|
Last action: <span className='font-mono'>{lastAction}</span>
|
|
49
49
|
</div>
|
|
50
|
-
<div className='relative border border-separator
|
|
50
|
+
<div className='relative border border-separator h-[100px] w-[300px]'>
|
|
51
51
|
<CellEditor
|
|
52
52
|
value={value}
|
|
53
53
|
extensions={extensions}
|
|
@@ -70,7 +70,7 @@ const meta = {
|
|
|
70
70
|
title: 'ui/react-ui-grid/CellEditor',
|
|
71
71
|
component: CellEditor,
|
|
72
72
|
render: DefaultStory,
|
|
73
|
-
decorators: [withTheme],
|
|
73
|
+
decorators: [withTheme()],
|
|
74
74
|
parameters: {
|
|
75
75
|
layout: 'centered',
|
|
76
76
|
},
|
|
@@ -8,15 +8,14 @@ import { EditorView, keymap } from '@codemirror/view';
|
|
|
8
8
|
import React, { type KeyboardEvent } from 'react';
|
|
9
9
|
|
|
10
10
|
import { useThemeContext } from '@dxos/react-ui';
|
|
11
|
+
import { type UseTextEditorProps, useTextEditor } from '@dxos/react-ui-editor';
|
|
11
12
|
import {
|
|
12
13
|
type ThemeExtensionsOptions,
|
|
13
|
-
type UseTextEditorProps,
|
|
14
14
|
createBasicExtensions,
|
|
15
15
|
createThemeExtensions,
|
|
16
16
|
filterChars,
|
|
17
|
-
|
|
18
|
-
} from '@dxos/
|
|
19
|
-
import { mx } from '@dxos/react-ui-theme';
|
|
17
|
+
} from '@dxos/ui-editor';
|
|
18
|
+
import { mx } from '@dxos/ui-theme';
|
|
20
19
|
|
|
21
20
|
import { type GridEditBox } from '../Grid';
|
|
22
21
|
|
|
@@ -123,7 +122,7 @@ export type CellEditorProps = {
|
|
|
123
122
|
} & Pick<UseTextEditorProps, 'autoFocus'> &
|
|
124
123
|
Pick<ThemeExtensionsOptions, 'slots'>;
|
|
125
124
|
|
|
126
|
-
export const CellEditor = ({ value, extensions, box, gridId,
|
|
125
|
+
export const CellEditor = ({ value, extensions, box, gridId, autoFocus, slots, onBlur }: CellEditorProps) => {
|
|
127
126
|
const { themeMode } = useThemeContext();
|
|
128
127
|
const { parentRef } = useTextEditor(() => {
|
|
129
128
|
return {
|
|
@@ -133,11 +132,22 @@ export const CellEditor = ({ value, extensions, box, gridId, onBlur, autoFocus,
|
|
|
133
132
|
extensions: [
|
|
134
133
|
extensions ?? [],
|
|
135
134
|
filterChars(/[\n\r]+/),
|
|
136
|
-
EditorView.focusChangeEffect
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
135
|
+
// Observe the underlying blur DOM event rather than `EditorView.focusChangeEffect`. The
|
|
136
|
+
// focus-change facet fires asynchronously (CodeMirror schedules it on a 10ms setTimeout),
|
|
137
|
+
// which means in React strict mode the destroy → blur of the first EditorView runs the
|
|
138
|
+
// callback after the second view has mounted — committing stale data and closing the
|
|
139
|
+
// editor on the user's first keystroke. Deferring via `queueMicrotask` runs the check
|
|
140
|
+
// *after* `view.destroy()` finishes its synchronous body (which calls `dom.remove()`),
|
|
141
|
+
// so `view.dom.isConnected === false` reliably distinguishes a programmatic teardown
|
|
142
|
+
// from a real user blur. Pass `undefined` on teardown so downstream handlers can consume
|
|
143
|
+
// any pending suppress-next-blur flag without committing stale data.
|
|
144
|
+
EditorView.domEventObservers({
|
|
145
|
+
blur: (_event, view) => {
|
|
146
|
+
const doc = view.state.doc.toString();
|
|
147
|
+
queueMicrotask(() => {
|
|
148
|
+
onBlur?.(view.dom.isConnected ? doc : undefined);
|
|
149
|
+
});
|
|
150
|
+
},
|
|
141
151
|
}),
|
|
142
152
|
createBasicExtensions({ lineWrapping: true }),
|
|
143
153
|
createThemeExtensions({
|
|
@@ -145,18 +155,18 @@ export const CellEditor = ({ value, extensions, box, gridId, onBlur, autoFocus,
|
|
|
145
155
|
slots: {
|
|
146
156
|
editor: {
|
|
147
157
|
className: mx(
|
|
148
|
-
'
|
|
158
|
+
'min-w-full! w-min! !max-w-(--dx-grid-cell-editor-max-w-size) min-h-full! !max-h-(--dx-grid-cell-editor-max-h-size)',
|
|
149
159
|
slots?.editor?.className,
|
|
150
160
|
),
|
|
151
161
|
},
|
|
152
|
-
|
|
162
|
+
scroller: {
|
|
153
163
|
className: mx(
|
|
154
|
-
'
|
|
155
|
-
slots?.
|
|
164
|
+
'overflow-x-hidden! !py-[max(0,calc(var(--dx-grid-cell-editor-padding-block)-1px))] pe-0! !pl-(--dx-grid-cell-editor-padding-inline)',
|
|
165
|
+
slots?.scroller?.className,
|
|
156
166
|
),
|
|
157
167
|
},
|
|
158
168
|
content: {
|
|
159
|
-
className: mx('
|
|
169
|
+
className: mx('break-normal!', slots?.content?.className),
|
|
160
170
|
},
|
|
161
171
|
},
|
|
162
172
|
}),
|
|
@@ -174,7 +184,7 @@ export const CellEditor = ({ value, extensions, box, gridId, onBlur, autoFocus,
|
|
|
174
184
|
insetBlockStart: box?.insetBlockStart ?? '0px',
|
|
175
185
|
minInlineSize: box?.inlineSize ?? '180px',
|
|
176
186
|
minBlockSize: box?.blockSize ?? '30px',
|
|
177
|
-
...{ '--dx-
|
|
187
|
+
...{ '--dx-grid-cell-width': `${box?.inlineSize ?? 200}px` },
|
|
178
188
|
}}
|
|
179
189
|
{...(gridId && { 'data-grid': gridId })}
|
|
180
190
|
/>
|
|
@@ -7,15 +7,16 @@ import React, { type MouseEvent, type MutableRefObject, useCallback, useRef, use
|
|
|
7
7
|
|
|
8
8
|
import { defaultRowSize } from '@dxos/lit-grid';
|
|
9
9
|
import { type DxGridPlaneCells } from '@dxos/lit-grid';
|
|
10
|
-
import {
|
|
10
|
+
import { random } from '@dxos/random';
|
|
11
11
|
import { DropdownMenu } from '@dxos/react-ui';
|
|
12
|
-
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
13
12
|
import { toPlaneCellIndex } from '@dxos/react-ui-grid';
|
|
14
|
-
import { Combobox, type ComboboxRootProps } from '@dxos/react-ui-
|
|
13
|
+
import { Combobox, type ComboboxRootProps } from '@dxos/react-ui-list';
|
|
14
|
+
import { useSearchListResults } from '@dxos/react-ui-search';
|
|
15
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
15
16
|
|
|
16
17
|
import { Grid, type GridContentProps, type GridEditing, type GridRootProps } from './Grid';
|
|
17
18
|
|
|
18
|
-
const storybookItems =
|
|
19
|
+
const storybookItems = random.helpers.uniqueArray(random.commerce.productName, 16);
|
|
19
20
|
|
|
20
21
|
type GridStoryProps = GridContentProps & Pick<GridRootProps, 'onEditingChange'>;
|
|
21
22
|
|
|
@@ -64,7 +65,7 @@ const GridStory = ({ initialCells, ...props }: GridStoryProps) => {
|
|
|
64
65
|
}, []);
|
|
65
66
|
|
|
66
67
|
return (
|
|
67
|
-
<div
|
|
68
|
+
<div className='contents'>
|
|
68
69
|
<Grid.Root id='story' editing={editing} onEditingChange={handleEditingChange}>
|
|
69
70
|
{/* TODO(burdon): Why is this property not just "cells" or "values" */}
|
|
70
71
|
<Grid.Content {...props} initialCells={cells} onClick={handleClick} />
|
|
@@ -87,24 +88,34 @@ const GridStory = ({ initialCells, ...props }: GridStoryProps) => {
|
|
|
87
88
|
onValueChange={setMultiselectValue}
|
|
88
89
|
>
|
|
89
90
|
<Combobox.VirtualTrigger virtualRef={triggerRef} />
|
|
90
|
-
<
|
|
91
|
-
<Combobox.Input placeholder='Search...' />
|
|
92
|
-
<Combobox.List>
|
|
93
|
-
{storybookItems.map((value) => (
|
|
94
|
-
<Combobox.Item key={value}>{value}</Combobox.Item>
|
|
95
|
-
))}
|
|
96
|
-
</Combobox.List>
|
|
97
|
-
<Combobox.Arrow />
|
|
98
|
-
</Combobox.Content>
|
|
91
|
+
<ComboboxContentWithFiltering />
|
|
99
92
|
</Combobox.Root>
|
|
100
93
|
</div>
|
|
101
94
|
);
|
|
102
95
|
};
|
|
103
96
|
|
|
97
|
+
const ComboboxContentWithFiltering = () => {
|
|
98
|
+
const { results, query, handleSearch } = useSearchListResults({
|
|
99
|
+
items: storybookItems,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Combobox.Content>
|
|
104
|
+
<Combobox.Input placeholder='Search...' value={query} onValueChange={handleSearch} />
|
|
105
|
+
<Combobox.List>
|
|
106
|
+
{results.map((value) => (
|
|
107
|
+
<Combobox.Item key={value} value={value} label={value} />
|
|
108
|
+
))}
|
|
109
|
+
</Combobox.List>
|
|
110
|
+
<Combobox.Arrow />
|
|
111
|
+
</Combobox.Content>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
104
115
|
const meta = {
|
|
105
116
|
title: 'ui/react-ui-grid/Grid',
|
|
106
117
|
component: GridStory,
|
|
107
|
-
decorators: [withTheme, withLayout({
|
|
118
|
+
decorators: [withTheme(), withLayout({ layout: 'column' })],
|
|
108
119
|
parameters: {
|
|
109
120
|
layout: 'fullscreen',
|
|
110
121
|
},
|
|
@@ -116,6 +127,29 @@ type Story = StoryObj<typeof meta>;
|
|
|
116
127
|
|
|
117
128
|
export const Default: Story = {};
|
|
118
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Single focusable cell — for verifying focus-ring alignment with grid lines.
|
|
132
|
+
*/
|
|
133
|
+
export const SingleCell: Story = {
|
|
134
|
+
args: {
|
|
135
|
+
id: 'story',
|
|
136
|
+
limitColumns: 1,
|
|
137
|
+
limitRows: 1,
|
|
138
|
+
columnDefault: { grid: { size: 200, resizeable: false } },
|
|
139
|
+
rowDefault: { grid: { size: 32, resizeable: false } },
|
|
140
|
+
initialCells: {
|
|
141
|
+
grid: {
|
|
142
|
+
'0,0': { value: 'Focus me' },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
render: (args) => (
|
|
147
|
+
<div className='h-full grid place-items-center'>
|
|
148
|
+
<GridStory {...args} />
|
|
149
|
+
</div>
|
|
150
|
+
),
|
|
151
|
+
};
|
|
152
|
+
|
|
119
153
|
export const Basic: Story = {
|
|
120
154
|
args: {
|
|
121
155
|
id: 'story',
|
|
@@ -145,7 +179,7 @@ export const Basic: Story = {
|
|
|
145
179
|
'1,1': {
|
|
146
180
|
value: 'Demo decoration',
|
|
147
181
|
accessoryHtml: `
|
|
148
|
-
<button class="dx-button
|
|
182
|
+
<button class="dx-button w-6 px-0.5 min-h-0 absolute inset-y-1 right-1" data-story-action="menu">
|
|
149
183
|
<svg><use href="/icons.svg#ph--arrow-right--regular"/></svg>
|
|
150
184
|
</button>
|
|
151
185
|
`,
|
|
@@ -189,7 +223,7 @@ export const Calendar: Story = {
|
|
|
189
223
|
// TODO(burdon): Formatting changes when cell is selected.
|
|
190
224
|
cells[toPlaneCellIndex({ col, row })] = {
|
|
191
225
|
readonly: true,
|
|
192
|
-
accessoryHtml: '<div class="flex
|
|
226
|
+
accessoryHtml: '<div class="flex h-full w-full justify-center items-center overflow-hidden">0</div>',
|
|
193
227
|
className: '',
|
|
194
228
|
};
|
|
195
229
|
}
|
|
@@ -199,8 +233,8 @@ export const Calendar: Story = {
|
|
|
199
233
|
},
|
|
200
234
|
},
|
|
201
235
|
render: (args) => (
|
|
202
|
-
<div className='
|
|
203
|
-
<div className='
|
|
236
|
+
<div className='h-full flex justify-center'>
|
|
237
|
+
<div className='h-full w-[288px] border-x border-separator'>
|
|
204
238
|
<GridStory {...args} />
|
|
205
239
|
</div>
|
|
206
240
|
</div>
|
package/src/Grid/Grid.tsx
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import '@dxos/lit-grid/dx-grid.pcss';
|
|
6
|
-
|
|
7
5
|
import { type EventName, createComponent } from '@lit/react';
|
|
8
6
|
import { type Scope, createContextScope } from '@radix-ui/react-context';
|
|
9
7
|
import { useControllableState } from '@radix-ui/react-use-controllable-state';
|
|
@@ -16,6 +14,7 @@ import React, {
|
|
|
16
14
|
useState,
|
|
17
15
|
} from 'react';
|
|
18
16
|
|
|
17
|
+
import '@dxos/lit-grid/dx-grid.pcss';
|
|
19
18
|
import { type DxAxisResize, type DxEditRequest, type DxGridCellsSelect, DxGrid as NaturalDxGrid } from '@dxos/lit-grid';
|
|
20
19
|
|
|
21
20
|
type DxGridElement = NaturalDxGrid;
|
|
@@ -63,20 +62,23 @@ const [createGridContext, createGridScope] = createContextScope(GRID_NAME, []);
|
|
|
63
62
|
const [GridProvider, useGridContext] = createGridContext<GridContextValue>(GRID_NAME);
|
|
64
63
|
|
|
65
64
|
type GridRootProps = PropsWithChildren<
|
|
66
|
-
{
|
|
65
|
+
{
|
|
66
|
+
id: string;
|
|
67
|
+
} & Partial<{
|
|
67
68
|
editing: GridEditing;
|
|
68
69
|
defaultEditing: GridEditing;
|
|
69
70
|
onEditingChange: (nextEditing: GridEditing) => void;
|
|
70
71
|
}>
|
|
71
72
|
>;
|
|
72
73
|
|
|
74
|
+
// TODO(burdon): Make headless.
|
|
73
75
|
const GridRoot = ({
|
|
76
|
+
__gridScope,
|
|
77
|
+
children,
|
|
74
78
|
id,
|
|
75
79
|
editing: propsEditing,
|
|
76
80
|
defaultEditing,
|
|
77
81
|
onEditingChange,
|
|
78
|
-
children,
|
|
79
|
-
__gridScope,
|
|
80
82
|
}: GridScopedProps<GridRootProps>) => {
|
|
81
83
|
const [editing = null, setEditing] = useControllableState({
|
|
82
84
|
prop: propsEditing,
|
|
@@ -150,12 +152,12 @@ GridContent.displayName = GRID_CONTENT_NAME;
|
|
|
150
152
|
// Fragments
|
|
151
153
|
//
|
|
152
154
|
|
|
153
|
-
// NOTE(Zan): These fragments add border to
|
|
155
|
+
// NOTE(Zan): These fragments add border to w-end and h-end of the grid using pseudo-elements.
|
|
154
156
|
// These are offset by 1px to avoid double borders in planks.
|
|
155
157
|
const gridSeparatorInlineEnd =
|
|
156
|
-
'[&>.dx-grid]:relative [&>.dx-grid]:after:absolute [&>.dx-grid]:after:inset-
|
|
158
|
+
'[&>.dx-grid]:relative [&>.dx-grid]:after:absolute [&>.dx-grid]:after:inset-y-0 [&>.dx-grid]:after:-right-px [&>.dx-grid]:after:w-px [&>.dx-grid]:after:bg-subdued-separator';
|
|
157
159
|
const gridSeparatorBlockEnd =
|
|
158
|
-
'[&>.dx-grid]:relative [&>.dx-grid]:before:absolute [&>.dx-grid]:before:inset-
|
|
160
|
+
'[&>.dx-grid]:relative [&>.dx-grid]:before:absolute [&>.dx-grid]:before:inset-x-0 [&>.dx-grid]:before:-bottom-px [&>.dx-grid]:before:h-px [&>.dx-grid]:before:bg-subdued-separator';
|
|
159
161
|
|
|
160
162
|
//
|
|
161
163
|
// Exports
|