@khanacademy/wonder-blocks-cell 1.0.0
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/CHANGELOG.md +17 -0
- package/dist/es/index.js +351 -0
- package/dist/index.js +577 -0
- package/dist/index.js.flow +2 -0
- package/package.json +33 -0
- package/src/components/__docs__/basic-cell.argtypes.js +178 -0
- package/src/components/__docs__/basic-cell.stories.js +302 -0
- package/src/components/__docs__/detail-cell.argtypes.js +28 -0
- package/src/components/__docs__/detail-cell.stories.js +154 -0
- package/src/components/__tests__/basic-cell.test.js +98 -0
- package/src/components/__tests__/detail-cell.test.js +103 -0
- package/src/components/basic-cell.js +40 -0
- package/src/components/detail-cell.js +100 -0
- package/src/components/internal/__tests__/cell-core.test.js +95 -0
- package/src/components/internal/__tests__/common.test.js +47 -0
- package/src/components/internal/cell-core.js +288 -0
- package/src/components/internal/common.js +73 -0
- package/src/index.js +5 -0
- package/src/util/types.js +129 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {StyleSheet} from "aphrodite";
|
|
4
|
+
|
|
5
|
+
import Clickable from "@khanacademy/wonder-blocks-clickable";
|
|
6
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
7
|
+
import Color, {fade} from "@khanacademy/wonder-blocks-color";
|
|
8
|
+
import {Strut} from "@khanacademy/wonder-blocks-layout";
|
|
9
|
+
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
10
|
+
|
|
11
|
+
import type {ClickableState} from "@khanacademy/wonder-blocks-clickable";
|
|
12
|
+
import {CellMeasurements, getHorizontalRuleStyles} from "./common.js";
|
|
13
|
+
|
|
14
|
+
import type {CellProps, TypographyText} from "../../util/types.js";
|
|
15
|
+
|
|
16
|
+
type LeftAccessoryProps = {|
|
|
17
|
+
leftAccessory?: CellProps["leftAccessory"],
|
|
18
|
+
leftAccessoryStyle?: CellProps["leftAccessoryStyle"],
|
|
19
|
+
disabled?: CellProps["disabled"],
|
|
20
|
+
|};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Left Accessories can be defined using WB components such as Icon, IconButton,
|
|
24
|
+
* or it can even be used for a custom node/component if needed.
|
|
25
|
+
*/
|
|
26
|
+
const LeftAccessory = ({
|
|
27
|
+
leftAccessory,
|
|
28
|
+
leftAccessoryStyle,
|
|
29
|
+
disabled,
|
|
30
|
+
}: LeftAccessoryProps): React.Node => {
|
|
31
|
+
if (!leftAccessory) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<>
|
|
37
|
+
<View
|
|
38
|
+
style={[
|
|
39
|
+
styles.accessory,
|
|
40
|
+
disabled && styles.accessoryDisabled,
|
|
41
|
+
{...leftAccessoryStyle},
|
|
42
|
+
]}
|
|
43
|
+
>
|
|
44
|
+
{leftAccessory}
|
|
45
|
+
</View>
|
|
46
|
+
<Strut size={CellMeasurements.accessoryHorizontalSpacing} />
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type RightAccessoryProps = {|
|
|
52
|
+
rightAccessory?: CellProps["rightAccessory"],
|
|
53
|
+
rightAccessoryStyle?: CellProps["rightAccessoryStyle"],
|
|
54
|
+
active?: CellProps["active"],
|
|
55
|
+
disabled?: CellProps["disabled"],
|
|
56
|
+
|};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Right Accessories can be defined using WB components such as Icon,
|
|
60
|
+
* IconButton, or it can even be used for a custom node/component if needed.
|
|
61
|
+
*/
|
|
62
|
+
const RightAccessory = ({
|
|
63
|
+
rightAccessory,
|
|
64
|
+
rightAccessoryStyle,
|
|
65
|
+
active,
|
|
66
|
+
disabled,
|
|
67
|
+
}: RightAccessoryProps): React.Node => {
|
|
68
|
+
if (!rightAccessory) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<>
|
|
74
|
+
<Strut size={CellMeasurements.accessoryHorizontalSpacing} />
|
|
75
|
+
<View
|
|
76
|
+
style={[
|
|
77
|
+
styles.accessory,
|
|
78
|
+
styles.accessoryRight,
|
|
79
|
+
disabled && styles.accessoryDisabled,
|
|
80
|
+
{...rightAccessoryStyle},
|
|
81
|
+
active && styles.accessoryActive,
|
|
82
|
+
]}
|
|
83
|
+
>
|
|
84
|
+
{rightAccessory}
|
|
85
|
+
</View>
|
|
86
|
+
</>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
type CellCoreProps = {|
|
|
91
|
+
...$Rest<CellProps, {|title: TypographyText|}>,
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The content of the cell.
|
|
95
|
+
*/
|
|
96
|
+
children: React.Node,
|
|
97
|
+
|};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* CellCore is the base cell wrapper. It's used as the skeleton/layout that is
|
|
101
|
+
* used by BasicCell and DetailCell (and any other variants).
|
|
102
|
+
*
|
|
103
|
+
* Both variants share how they render their accessories, and the main
|
|
104
|
+
* responsibility of this component is to render the contents that are passed in
|
|
105
|
+
* (using the `children` prop).
|
|
106
|
+
*/
|
|
107
|
+
const CellCore = (props: CellCoreProps): React.Node => {
|
|
108
|
+
const {
|
|
109
|
+
active,
|
|
110
|
+
children,
|
|
111
|
+
disabled,
|
|
112
|
+
horizontalRule = "inset",
|
|
113
|
+
leftAccessory = undefined,
|
|
114
|
+
leftAccessoryStyle = undefined,
|
|
115
|
+
onClick,
|
|
116
|
+
rightAccessory = undefined,
|
|
117
|
+
rightAccessoryStyle = undefined,
|
|
118
|
+
style,
|
|
119
|
+
testId,
|
|
120
|
+
"aria-label": ariaLabel,
|
|
121
|
+
} = props;
|
|
122
|
+
|
|
123
|
+
const renderCell = (eventState?: ClickableState): React.Node => {
|
|
124
|
+
const horizontalRuleStyles = getHorizontalRuleStyles(horizontalRule);
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<View
|
|
128
|
+
style={[
|
|
129
|
+
styles.wrapper,
|
|
130
|
+
// focused applied to the main wrapper to make the border
|
|
131
|
+
// outline part of the wrapper
|
|
132
|
+
eventState?.focused && styles.focused,
|
|
133
|
+
// custom styles
|
|
134
|
+
style,
|
|
135
|
+
]}
|
|
136
|
+
aria-current={active ? "true" : undefined}
|
|
137
|
+
>
|
|
138
|
+
<View
|
|
139
|
+
style={[
|
|
140
|
+
styles.innerWrapper,
|
|
141
|
+
horizontalRuleStyles,
|
|
142
|
+
disabled && styles.disabled,
|
|
143
|
+
active && styles.active,
|
|
144
|
+
// other states applied to the inner wrapper to blend
|
|
145
|
+
// the background color properly
|
|
146
|
+
!disabled && eventState?.hovered && styles.hovered,
|
|
147
|
+
// active + hovered
|
|
148
|
+
active && eventState?.hovered && styles.activeHovered,
|
|
149
|
+
!disabled && eventState?.pressed && styles.pressed,
|
|
150
|
+
// active + pressed
|
|
151
|
+
!disabled &&
|
|
152
|
+
active &&
|
|
153
|
+
eventState?.pressed &&
|
|
154
|
+
styles.activePressed,
|
|
155
|
+
]}
|
|
156
|
+
>
|
|
157
|
+
{/* Left accessory */}
|
|
158
|
+
<LeftAccessory
|
|
159
|
+
leftAccessory={leftAccessory}
|
|
160
|
+
leftAccessoryStyle={leftAccessoryStyle}
|
|
161
|
+
disabled={disabled}
|
|
162
|
+
/>
|
|
163
|
+
|
|
164
|
+
{/* Cell contents */}
|
|
165
|
+
<View style={styles.content} testId={testId}>
|
|
166
|
+
{children}
|
|
167
|
+
</View>
|
|
168
|
+
|
|
169
|
+
{/* Right accessory */}
|
|
170
|
+
<RightAccessory
|
|
171
|
+
rightAccessory={rightAccessory}
|
|
172
|
+
rightAccessoryStyle={rightAccessoryStyle}
|
|
173
|
+
active={active}
|
|
174
|
+
disabled={disabled}
|
|
175
|
+
/>
|
|
176
|
+
</View>
|
|
177
|
+
</View>
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Pressable cell.
|
|
182
|
+
if (onClick) {
|
|
183
|
+
return (
|
|
184
|
+
<Clickable
|
|
185
|
+
disabled={disabled}
|
|
186
|
+
onClick={onClick}
|
|
187
|
+
hideDefaultFocusRing={true}
|
|
188
|
+
aria-label={ariaLabel ? ariaLabel : undefined}
|
|
189
|
+
>
|
|
190
|
+
{(eventState) => renderCell(eventState)}
|
|
191
|
+
</Clickable>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// No click event attached, so just render the cell as-is.
|
|
196
|
+
return renderCell();
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const styles = StyleSheet.create({
|
|
200
|
+
wrapper: {
|
|
201
|
+
background: Color.white,
|
|
202
|
+
color: Color.offBlack,
|
|
203
|
+
minHeight: CellMeasurements.cellMinHeight,
|
|
204
|
+
textAlign: "left",
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
innerWrapper: {
|
|
208
|
+
padding: `${CellMeasurements.cellPadding.paddingVertical}px ${CellMeasurements.cellPadding.paddingHorizontal}px`,
|
|
209
|
+
flexDirection: "row",
|
|
210
|
+
flex: 1,
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
content: {
|
|
214
|
+
alignSelf: "center",
|
|
215
|
+
overflowWrap: "break-word",
|
|
216
|
+
padding: `${CellMeasurements.contentVerticalSpacing}px 0`,
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
accessory: {
|
|
220
|
+
// Use content width by default.
|
|
221
|
+
minWidth: "auto",
|
|
222
|
+
// Horizontal alignment of the accessory.
|
|
223
|
+
alignItems: "center",
|
|
224
|
+
// Vertical alignment.
|
|
225
|
+
alignSelf: "center",
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
accessoryRight: {
|
|
229
|
+
// The right accessory will have this color by default. Unless the
|
|
230
|
+
// accessory element overrides that color internally.
|
|
231
|
+
color: Color.offBlack64,
|
|
232
|
+
// Align the right accessory to the right side of the cell, so we can
|
|
233
|
+
// prevent the accessory from shifting left, if the content is too
|
|
234
|
+
// short.
|
|
235
|
+
marginLeft: "auto",
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* States
|
|
240
|
+
*/
|
|
241
|
+
hovered: {
|
|
242
|
+
background: Color.offBlack8,
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
// Handling the focus ring internally because clickable doesn't support
|
|
246
|
+
// rounded focus ring.
|
|
247
|
+
focused: {
|
|
248
|
+
borderRadius: Spacing.xxxSmall_4,
|
|
249
|
+
outline: `solid ${Spacing.xxxxSmall_2}px ${Color.blue}`,
|
|
250
|
+
// The focus ring is not visible when there are stacked cells.
|
|
251
|
+
// Using outlineOffset to display the focus ring inside the cell.
|
|
252
|
+
outlineOffset: -Spacing.xxxxSmall_2,
|
|
253
|
+
// To hide the internal corners of the cell.
|
|
254
|
+
overflow: "hidden",
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
pressed: {
|
|
258
|
+
background: Color.offBlack16,
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
active: {
|
|
262
|
+
background: fade(Color.blue, 0.08),
|
|
263
|
+
color: Color.blue,
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
activeHovered: {
|
|
267
|
+
background: fade(Color.blue, 0.16),
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
activePressed: {
|
|
271
|
+
background: fade(Color.blue, 0.24),
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
disabled: {
|
|
275
|
+
color: Color.offBlack32,
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
accessoryActive: {
|
|
279
|
+
color: Color.blue,
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
accessoryDisabled: {
|
|
283
|
+
color: Color.offBlack,
|
|
284
|
+
opacity: 0.32,
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
export default CellCore;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {StyleSheet} from "aphrodite";
|
|
3
|
+
|
|
4
|
+
import Color from "@khanacademy/wonder-blocks-color";
|
|
5
|
+
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
6
|
+
|
|
7
|
+
import type {StyleType} from "@khanacademy/wonder-blocks-core";
|
|
8
|
+
import type {HorizontalRuleVariant} from "../../util/types.js";
|
|
9
|
+
|
|
10
|
+
export const CellMeasurements = {
|
|
11
|
+
cellMinHeight: Spacing.xxLarge_48,
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The cell wrapper's gap.
|
|
15
|
+
*/
|
|
16
|
+
cellPadding: {
|
|
17
|
+
paddingVertical: Spacing.xSmall_8,
|
|
18
|
+
paddingHorizontal: Spacing.medium_16,
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The extra vertical spacing added to the title/content wrapper.
|
|
23
|
+
*/
|
|
24
|
+
contentVerticalSpacing: Spacing.xxxSmall_4,
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The horizontal spacing between the left and right accessory.
|
|
28
|
+
*/
|
|
29
|
+
accessoryHorizontalSpacing: Spacing.medium_16,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Gets the horizontalRule style based on the variant.
|
|
34
|
+
* @param {HorizontalRuleVariant} horizontalRule The variant of the horizontal
|
|
35
|
+
* rule.
|
|
36
|
+
* @returns A styled horizontal rule.
|
|
37
|
+
*/
|
|
38
|
+
export const getHorizontalRuleStyles = (
|
|
39
|
+
horizontalRule: HorizontalRuleVariant,
|
|
40
|
+
): StyleType => {
|
|
41
|
+
switch (horizontalRule) {
|
|
42
|
+
case "inset":
|
|
43
|
+
return [styles.horizontalRule, styles.horizontalRuleInset];
|
|
44
|
+
case "full-width":
|
|
45
|
+
return styles.horizontalRule;
|
|
46
|
+
case "none":
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const styles = StyleSheet.create({
|
|
52
|
+
horizontalRule: {
|
|
53
|
+
position: "relative",
|
|
54
|
+
":after": {
|
|
55
|
+
width: "100%",
|
|
56
|
+
content: "''",
|
|
57
|
+
position: "absolute",
|
|
58
|
+
// align to the bottom of the cell
|
|
59
|
+
bottom: 0,
|
|
60
|
+
// align border to the right of the cell
|
|
61
|
+
right: 0,
|
|
62
|
+
height: Spacing.xxxxSmall_2,
|
|
63
|
+
boxShadow: `inset 0px -1px 0px ${Color.offBlack8}`,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
horizontalRuleInset: {
|
|
68
|
+
":after": {
|
|
69
|
+
// Inset doesn't include the left padding of the cell.
|
|
70
|
+
width: `calc(100% - ${CellMeasurements.cellPadding.paddingHorizontal}px)`,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
});
|
package/src/index.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
import type {StyleType} from "@khanacademy/wonder-blocks-core";
|
|
5
|
+
import type {Typography} from "@khanacademy/wonder-blocks-typography";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A set of values that can be used to configure the horizontal rule appearance.
|
|
9
|
+
*/
|
|
10
|
+
export type HorizontalRuleVariant = "full-width" | "inset" | "none";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* An element or component that represents an accessory within a cell.
|
|
14
|
+
*/
|
|
15
|
+
type Accessory = React.Node;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A subset of CSS Properties to allow overriding some of the default styles set
|
|
19
|
+
* on the accessory wrapper (loosely based on StyleType).
|
|
20
|
+
*/
|
|
21
|
+
export type AccessoryStyle = {|
|
|
22
|
+
/**
|
|
23
|
+
* A subset of valid Spacing values.
|
|
24
|
+
*/
|
|
25
|
+
minWidth?: 16 | 24 | 32 | 48,
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* To vertically align the accessory.
|
|
29
|
+
*/
|
|
30
|
+
alignSelf?: "flex-start" | "flex-end" | "center",
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* To horizontally align the accessory.
|
|
34
|
+
*/
|
|
35
|
+
alignItems?: "flex-start" | "flex-end" | "center",
|
|
36
|
+
|};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A union that allows using plain text or WB Typography elements.
|
|
40
|
+
*/
|
|
41
|
+
export type TypographyText = string | React.Element<Typography>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Common properties for all cells.
|
|
45
|
+
*/
|
|
46
|
+
export type CellProps = {|
|
|
47
|
+
/**
|
|
48
|
+
* The title / main content of the cell. You can either provide a string or
|
|
49
|
+
* a Typography component. If a string is provided, typography defaults to
|
|
50
|
+
* LabelLarge.
|
|
51
|
+
*/
|
|
52
|
+
title: TypographyText,
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* If provided, this adds a left accessory to the cell. Left
|
|
56
|
+
* Accessories can be defined using WB components such as Icon,
|
|
57
|
+
* IconButton, or it can even be used for a custom node/component if
|
|
58
|
+
* needed. What ever is passed in will occupy the "LeftAccessory” area
|
|
59
|
+
* of the Cell.
|
|
60
|
+
*/
|
|
61
|
+
leftAccessory?: Accessory,
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Optional custom styles applied to the leftAccessory wrapper. For
|
|
65
|
+
* example, it can be used to set a custom minWidth or a custom
|
|
66
|
+
* alignment.
|
|
67
|
+
*
|
|
68
|
+
* NOTE: leftAccessoryStyle can only be used if leftAccessory is set.
|
|
69
|
+
*/
|
|
70
|
+
leftAccessoryStyle?: AccessoryStyle,
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* If provided, this adds a right accessory to the cell. Right
|
|
74
|
+
* Accessories can be defined using WB components such as Icon,
|
|
75
|
+
* IconButton, or it can even be used for a custom node/component if
|
|
76
|
+
* needed. What ever is passed in will occupy the “RightAccessory”
|
|
77
|
+
* area of the Cell.
|
|
78
|
+
*/
|
|
79
|
+
rightAccessory?: Accessory,
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Optional custom styles applied to the rightAccessory wrapper. For
|
|
83
|
+
* example, it can be used to set a custom minWidth or a custom
|
|
84
|
+
* alignment.
|
|
85
|
+
*
|
|
86
|
+
* NOTE: rightAccessoryStyle can only be used if rightAccessory is
|
|
87
|
+
* set.
|
|
88
|
+
*/
|
|
89
|
+
rightAccessoryStyle?: AccessoryStyle,
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Adds a horizontal rule at the bottom of the cell that can be used to
|
|
93
|
+
* separate cells within groups such as lists. Defaults to `inset`.
|
|
94
|
+
*/
|
|
95
|
+
horizontalRule?: HorizontalRuleVariant,
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Optional custom styles applied to the cell container.
|
|
99
|
+
*/
|
|
100
|
+
style?: StyleType,
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Optional test ID for e2e testing.
|
|
104
|
+
*/
|
|
105
|
+
testId?: string,
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Called when the cell is clicked.
|
|
109
|
+
*
|
|
110
|
+
* If not provided, the Cell can’t be hovered and/or pressed (highlighted on
|
|
111
|
+
* hover).
|
|
112
|
+
*/
|
|
113
|
+
onClick?: (e: SyntheticEvent<>) => mixed,
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Whether the cell is active (or currently selected).
|
|
117
|
+
*/
|
|
118
|
+
active?: boolean,
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Whether the cell is disabled.
|
|
122
|
+
*/
|
|
123
|
+
disabled?: boolean,
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Used to announce the cell's content to screen readers.
|
|
127
|
+
*/
|
|
128
|
+
"aria-label"?: string,
|
|
129
|
+
|};
|