@12-apps/ui 1.7.0 → 1.8.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/package.json +4 -4
- package/src/components/data-display/DataViews/base-card.tsx +93 -23
- package/src/components/data-display/DataViews/base-list-card-slots.tsx +256 -0
- package/src/components/data-display/DataViews/base-list-card.tsx +352 -0
- package/src/components/data-display/DataViews/card-surface.ts +297 -0
- package/src/components/data-display/DataViews/data-views-drag.tsx +135 -0
- package/src/components/data-display/DataViews/index.ts +7 -0
- package/src/components/data-display/DataViews/list-card-rails.tsx +141 -0
- package/src/components/data-display/DescriptionItem/DescriptionItem.tsx +26 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"packageManager": "pnpm@9.0.0",
|
|
6
6
|
"type": "module",
|
|
@@ -194,7 +194,7 @@
|
|
|
194
194
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
195
195
|
},
|
|
196
196
|
"dependencies": {
|
|
197
|
-
"@12-apps/forms-core": "^1.
|
|
197
|
+
"@12-apps/forms-core": "^1.7.0",
|
|
198
198
|
"@emotion/react": "^11.14.0",
|
|
199
199
|
"@emotion/styled": "^11.14.0",
|
|
200
200
|
"@monaco-editor/react": "^4.7.0",
|
|
@@ -209,8 +209,8 @@
|
|
|
209
209
|
"recharts": "^3.5.1"
|
|
210
210
|
},
|
|
211
211
|
"devDependencies": {
|
|
212
|
-
"@12-apps/eslint-config": "^1.
|
|
213
|
-
"@12-apps/typescript-config": "^1.
|
|
212
|
+
"@12-apps/eslint-config": "^1.0.1",
|
|
213
|
+
"@12-apps/typescript-config": "^1.0.2",
|
|
214
214
|
"@playwright/test": "^1.55.0",
|
|
215
215
|
"@storybook/addon-docs": "^9.1.10",
|
|
216
216
|
"@storybook/addon-links": "^9.1.10",
|
|
@@ -2,16 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
import { type ReactNode } from "react";
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import { useTheme, type CSSObject } from "@mui/material/styles";
|
|
6
|
+
|
|
7
|
+
/** See the note on `CardSx` in `card-surface` — these fragments get merged. */
|
|
8
|
+
type CardSx = CSSObject | Record<string, unknown>;
|
|
6
9
|
|
|
7
10
|
import { Checkbox } from "../../form/Checkbox";
|
|
8
11
|
import { Card } from "../../layout/Card";
|
|
9
12
|
import { Box } from "../../../mui/Box";
|
|
10
13
|
import { Text } from "../../typography/Text";
|
|
11
14
|
|
|
15
|
+
import { DragHandle, useDragItem } from "./data-views-drag";
|
|
16
|
+
import {
|
|
17
|
+
cardSurfaceStyles,
|
|
18
|
+
isActionable,
|
|
19
|
+
isSelectable,
|
|
20
|
+
slotTestIds,
|
|
21
|
+
type CardSurfaceProps,
|
|
22
|
+
} from "./card-surface";
|
|
12
23
|
import { CARD_ASPECT_RATIOS, type CardAspectRatio } from "./data-views-types";
|
|
13
24
|
|
|
14
|
-
interface BaseCardProps {
|
|
25
|
+
export interface BaseCardProps extends CardSurfaceProps {
|
|
15
26
|
/** Extra body content below the caption (chips, meta). Optional. */
|
|
16
27
|
children?: ReactNode;
|
|
17
28
|
/** Tile proportion. Defaults to "4:3". */
|
|
@@ -33,15 +44,17 @@ interface BaseCardProps {
|
|
|
33
44
|
subtitle?: ReactNode;
|
|
34
45
|
/** The 3-dots menu (or any action node) pinned to the top-right corner. */
|
|
35
46
|
menu?: ReactNode;
|
|
36
|
-
/** Whether this card is in the grid's selection. */
|
|
37
|
-
selected?: boolean;
|
|
38
47
|
/** Toggle this card's selection. Omit to hide the checkbox (non-selectable). */
|
|
39
48
|
onToggleSelect?: () => void;
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
/**
|
|
50
|
+
* This card's id for drag purposes.
|
|
51
|
+
*
|
|
52
|
+
* Draggable ONLY inside a {@link DragContainerProvider} — see
|
|
53
|
+
* `data-views-drag`. On the board that is how a card moves between columns;
|
|
54
|
+
* in a grade it is how the operator reorders. Outside a container this is
|
|
55
|
+
* inert and no grip appears.
|
|
56
|
+
*/
|
|
57
|
+
dragId?: string | number;
|
|
45
58
|
checkboxTestId?: string;
|
|
46
59
|
}
|
|
47
60
|
|
|
@@ -50,10 +63,9 @@ function cardSx(opts: {
|
|
|
50
63
|
aspectRatio: CardAspectRatio;
|
|
51
64
|
pad: number;
|
|
52
65
|
interactive: boolean;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
const { aspectRatio, pad, interactive, dimmed, selected } = opts;
|
|
66
|
+
draggable: boolean;
|
|
67
|
+
}): CardSx {
|
|
68
|
+
const { aspectRatio, pad, interactive, draggable } = opts;
|
|
57
69
|
return {
|
|
58
70
|
position: "relative",
|
|
59
71
|
aspectRatio: String(CARD_ASPECT_RATIOS[aspectRatio]),
|
|
@@ -61,12 +73,8 @@ function cardSx(opts: {
|
|
|
61
73
|
flexDirection: "column",
|
|
62
74
|
overflow: "hidden",
|
|
63
75
|
p: pad,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
opacity: dimmed ? 0.6 : 1,
|
|
67
|
-
borderColor: selected ? "primary.main" : undefined,
|
|
68
|
-
boxShadow: selected ? (theme) => `inset 0 0 0 1px ${theme.palette.primary.main}` : undefined,
|
|
69
|
-
backgroundColor: selected ? "action.selected" : undefined,
|
|
76
|
+
cursor: draggable ? "grab" : interactive ? "pointer" : undefined,
|
|
77
|
+
...(draggable ? { touchAction: "none", "&:active": { cursor: "grabbing" } } : {}),
|
|
70
78
|
};
|
|
71
79
|
}
|
|
72
80
|
|
|
@@ -244,9 +252,29 @@ function CardContents({
|
|
|
244
252
|
* per-slot helpers above; this shell just assembles them within a fixed-ratio
|
|
245
253
|
* tile. Reads no context, so it renders standalone as well as inside the grid.
|
|
246
254
|
*/
|
|
255
|
+
/**
|
|
256
|
+
* Everything the tile derives from its props before it can render — pulled out
|
|
257
|
+
* so `BaseCard` itself stays inside the complexity budget.
|
|
258
|
+
*/
|
|
259
|
+
function useCardShell(props: BaseCardProps) {
|
|
260
|
+
const actionable = isActionable(props.state);
|
|
261
|
+
return {
|
|
262
|
+
selectable: isSelectable(props),
|
|
263
|
+
actionable,
|
|
264
|
+
theme: useTheme(),
|
|
265
|
+
slot: slotTestIds(props.testId),
|
|
266
|
+
scale: props.scale ?? 1,
|
|
267
|
+
// The card's own veto over the container's decision — see `CardSurfaceProps`.
|
|
268
|
+
drag: useDragItem(props.draggable === false || !actionable ? undefined : props.dragId),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
247
272
|
export function BaseCard(props: BaseCardProps): React.JSX.Element {
|
|
248
|
-
const { aspectRatio = "4:3",
|
|
249
|
-
const { menu, onToggleSelect, onClick
|
|
273
|
+
const { aspectRatio = "4:3", selected = false } = props;
|
|
274
|
+
const { menu, onToggleSelect, onClick } = props;
|
|
275
|
+
const { selectable, actionable, theme, slot, scale, drag } = useCardShell(props);
|
|
276
|
+
const dataTestId = props.testId;
|
|
277
|
+
const checkboxTestId = slot("checkbox");
|
|
250
278
|
const pad = 1.5 * scale;
|
|
251
279
|
|
|
252
280
|
return (
|
|
@@ -255,11 +283,53 @@ export function BaseCard(props: BaseCardProps): React.JSX.Element {
|
|
|
255
283
|
borderRadius="lg"
|
|
256
284
|
onClick={onClick}
|
|
257
285
|
dataTestId={dataTestId}
|
|
258
|
-
|
|
286
|
+
className={props.className}
|
|
287
|
+
aria-label={props["aria-label"]}
|
|
288
|
+
aria-disabled={actionable ? undefined : true}
|
|
289
|
+
{...drag.itemProps}
|
|
290
|
+
// A PLAIN OBJECT, not a function. `Card` merges by spreading
|
|
291
|
+
// (`sx={{ ...defaults, ...sx }}`), and spreading a function yields
|
|
292
|
+
// nothing at all — the styles vanish silently, which is exactly how the
|
|
293
|
+
// first version of this shipped with no variants and no effects.
|
|
294
|
+
sx={{
|
|
295
|
+
// The shared half — variant, colour, selection, effects — identical to
|
|
296
|
+
// the one BaseListCard uses, so a Grade and a Lista of the same records
|
|
297
|
+
// are dressed by one set of rules.
|
|
298
|
+
...cardSurfaceStyles(
|
|
299
|
+
{
|
|
300
|
+
...props,
|
|
301
|
+
selectable,
|
|
302
|
+
shape: "tile",
|
|
303
|
+
// A card mid-drag is a ghost of itself; the drop target is what the
|
|
304
|
+
// eye should be on.
|
|
305
|
+
state: drag.dragging ? "disabled" : props.state,
|
|
306
|
+
},
|
|
307
|
+
theme,
|
|
308
|
+
),
|
|
309
|
+
// …and the tile's own geometry on top.
|
|
310
|
+
...cardSx({
|
|
311
|
+
aspectRatio,
|
|
312
|
+
pad,
|
|
313
|
+
interactive: onClick != null,
|
|
314
|
+
draggable: drag.draggable && drag.handleProps === undefined,
|
|
315
|
+
}),
|
|
316
|
+
}}
|
|
259
317
|
>
|
|
318
|
+
{/* Bottom-left, opposite the menu and clear of the checkbox — a grip in
|
|
319
|
+
the same corner as a control that toggles selection means every drag
|
|
320
|
+
starts with a near-miss on it. */}
|
|
321
|
+
{drag.draggable && (
|
|
322
|
+
<Box sx={{ position: "absolute", bottom: 4, left: 4, zIndex: 2 }}>
|
|
323
|
+
<DragHandle
|
|
324
|
+
handleProps={drag.handleProps}
|
|
325
|
+
gated={drag.handleProps !== undefined}
|
|
326
|
+
testId={slot("drag")}
|
|
327
|
+
/>
|
|
328
|
+
</Box>
|
|
329
|
+
)}
|
|
260
330
|
<CardOverlays
|
|
261
331
|
selected={selected}
|
|
262
|
-
onToggleSelect={onToggleSelect}
|
|
332
|
+
onToggleSelect={selectable ? onToggleSelect ?? (() => {}) : undefined}
|
|
263
333
|
checkboxTestId={checkboxTestId}
|
|
264
334
|
menu={menu}
|
|
265
335
|
/>
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import { DescriptionItem, type DescriptionItemProps } from "../DescriptionItem";
|
|
6
|
+
import { Box } from "../../../mui/Box";
|
|
7
|
+
import { Text } from "../../typography/Text";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* WHAT SITS IN EACH RAIL.
|
|
11
|
+
*
|
|
12
|
+
* Split from `base-list-card` at the size gate: that module is the shell, the
|
|
13
|
+
* rails and the states; this is the content of each column.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Below this the row drops its middle columns. */
|
|
17
|
+
export const META_BREAK = 520;
|
|
18
|
+
/** …and below this it leaves the shared rails and goes two-line. */
|
|
19
|
+
export const STACK_BREAK = 360;
|
|
20
|
+
|
|
21
|
+
/** Money and dates line up only if their digits are the same width. */
|
|
22
|
+
const TABULAR = { fontVariantNumeric: "tabular-nums" } as const;
|
|
23
|
+
|
|
24
|
+
const CLAMP = {
|
|
25
|
+
overflow: "hidden",
|
|
26
|
+
textOverflow: "ellipsis",
|
|
27
|
+
whiteSpace: "nowrap",
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Title over subtitle — and, when the row navigates, the anchor that covers it.
|
|
32
|
+
*
|
|
33
|
+
* The link is a real `<a>` stretched by `::after` over the whole card rather
|
|
34
|
+
* than a click handler on a `<div>`. That buys cmd-click, middle-click, "copy
|
|
35
|
+
* link address", the status bar preview and correct tab order, and it stops the
|
|
36
|
+
* buttons elsewhere in the row from being interactive elements nested inside a
|
|
37
|
+
* clickable one.
|
|
38
|
+
*/
|
|
39
|
+
export function ListCardCaption({
|
|
40
|
+
title,
|
|
41
|
+
subtitle,
|
|
42
|
+
href,
|
|
43
|
+
target,
|
|
44
|
+
scale,
|
|
45
|
+
testId,
|
|
46
|
+
}: {
|
|
47
|
+
title?: ReactNode;
|
|
48
|
+
subtitle?: ReactNode;
|
|
49
|
+
href?: string;
|
|
50
|
+
target?: string;
|
|
51
|
+
scale: number;
|
|
52
|
+
testId?: string;
|
|
53
|
+
}): React.JSX.Element {
|
|
54
|
+
return (
|
|
55
|
+
<Box data-slot="caption" sx={{ minWidth: 0 }}>
|
|
56
|
+
{title != null && (
|
|
57
|
+
<Text variant="heading" size="sm" weight="bold" as="p">
|
|
58
|
+
<Box
|
|
59
|
+
{...(href ? { component: "a", href, target } : { component: "span" })}
|
|
60
|
+
data-testid={testId}
|
|
61
|
+
sx={{
|
|
62
|
+
display: "block",
|
|
63
|
+
lineHeight: 1.2,
|
|
64
|
+
fontSize: `${0.9 * scale}rem`,
|
|
65
|
+
color: "inherit",
|
|
66
|
+
textDecoration: "none",
|
|
67
|
+
...CLAMP,
|
|
68
|
+
...(href
|
|
69
|
+
? {
|
|
70
|
+
"&::after": {
|
|
71
|
+
content: '""',
|
|
72
|
+
position: "absolute",
|
|
73
|
+
inset: 0,
|
|
74
|
+
// Under the checkbox, drag grip, actions and menu, which
|
|
75
|
+
// all raise themselves to 1.
|
|
76
|
+
zIndex: 0,
|
|
77
|
+
},
|
|
78
|
+
"&:hover": { textDecoration: "underline" },
|
|
79
|
+
}
|
|
80
|
+
: {}),
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
{title}
|
|
84
|
+
</Box>
|
|
85
|
+
</Text>
|
|
86
|
+
)}
|
|
87
|
+
{subtitle != null && (
|
|
88
|
+
<Text variant="caption" size="xs" color="secondary" as="p">
|
|
89
|
+
<Box component="span" sx={{ display: "block", ...CLAMP }}>
|
|
90
|
+
{subtitle}
|
|
91
|
+
</Box>
|
|
92
|
+
</Text>
|
|
93
|
+
)}
|
|
94
|
+
</Box>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The middle columns: stacked label-over-value pairs, separated by a rule.
|
|
100
|
+
*
|
|
101
|
+
* Stacked rather than side by side because `DATA 05/08/2026` inline spends the
|
|
102
|
+
* horizontal room the value rail needs, and the separator is what stops two
|
|
103
|
+
* adjacent pairs reading as one four-word phrase.
|
|
104
|
+
*/
|
|
105
|
+
export function ListCardMeta({
|
|
106
|
+
meta,
|
|
107
|
+
metaSlot,
|
|
108
|
+
}: {
|
|
109
|
+
meta?: DescriptionItemProps[];
|
|
110
|
+
metaSlot?: ReactNode;
|
|
111
|
+
}): React.JSX.Element {
|
|
112
|
+
const items = meta ?? [];
|
|
113
|
+
return (
|
|
114
|
+
<Box
|
|
115
|
+
data-slot="meta"
|
|
116
|
+
sx={{
|
|
117
|
+
display: "flex",
|
|
118
|
+
alignItems: "center",
|
|
119
|
+
gap: 2,
|
|
120
|
+
whiteSpace: "nowrap",
|
|
121
|
+
...TABULAR,
|
|
122
|
+
// Rung 1 of the ladder.
|
|
123
|
+
[`@container (max-width: ${META_BREAK}px)`]: { display: "none" },
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
{items.map((item, index) => (
|
|
127
|
+
<Box key={`${item.label}-${index}`} sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
|
128
|
+
{index > 0 && (
|
|
129
|
+
<Box
|
|
130
|
+
aria-hidden
|
|
131
|
+
sx={{ color: "text.disabled", opacity: 0.5, userSelect: "none", fontSize: "0.875rem" }}
|
|
132
|
+
>
|
|
133
|
+
|
|
|
134
|
+
</Box>
|
|
135
|
+
)}
|
|
136
|
+
{/* Centred: a stacked pair sitting in its own column reads as one
|
|
137
|
+
unit only when the label is over the value rather than flush left
|
|
138
|
+
of a value shorter than it — `MÉTODO` above `PIX` was visibly
|
|
139
|
+
unhinged from it. Overridable per item. */}
|
|
140
|
+
<DescriptionItem orientation="vertical" align="center" {...item} />
|
|
141
|
+
</Box>
|
|
142
|
+
))}
|
|
143
|
+
{metaSlot}
|
|
144
|
+
</Box>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The value, on its own rail, never truncated and never shunted by the chip. */
|
|
149
|
+
function ListCardValue({ value, scale }: { value: ReactNode; scale: number }): React.JSX.Element {
|
|
150
|
+
return (
|
|
151
|
+
<Text variant="body" size="sm" weight="medium" as="span">
|
|
152
|
+
<Box
|
|
153
|
+
component="span"
|
|
154
|
+
sx={{ fontSize: `${0.875 * scale}rem`, whiteSpace: "nowrap", ...TABULAR }}
|
|
155
|
+
>
|
|
156
|
+
{value}
|
|
157
|
+
</Box>
|
|
158
|
+
</Text>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The actions and the menu, at the end of the row.
|
|
164
|
+
*
|
|
165
|
+
* Actions are revealed on hover and `:focus-within` — always-visible buttons on
|
|
166
|
+
* every row of fifty is a lot of chrome for something used on one of them.
|
|
167
|
+
* `:focus-within` is what keeps them reachable by keyboard, and `visibility`
|
|
168
|
+
* rather than `display` keeps their space reserved so nothing shifts.
|
|
169
|
+
*/
|
|
170
|
+
function ListCardActions({
|
|
171
|
+
actions,
|
|
172
|
+
alwaysVisible,
|
|
173
|
+
menu,
|
|
174
|
+
testId,
|
|
175
|
+
}: {
|
|
176
|
+
actions?: ReactNode;
|
|
177
|
+
alwaysVisible?: boolean;
|
|
178
|
+
menu?: ReactNode;
|
|
179
|
+
testId: (slot: string) => string | undefined;
|
|
180
|
+
}): React.JSX.Element | null {
|
|
181
|
+
if (actions == null && menu == null) return null;
|
|
182
|
+
return (
|
|
183
|
+
// z-index 1: above the stretched link, so these stay clickable.
|
|
184
|
+
<Box
|
|
185
|
+
data-slot="actions"
|
|
186
|
+
sx={{ display: "flex", alignItems: "center", gap: 0.5, position: "relative", zIndex: 1 }}
|
|
187
|
+
>
|
|
188
|
+
{actions && (
|
|
189
|
+
<Box
|
|
190
|
+
data-testid={testId("actions")}
|
|
191
|
+
onClick={(event) => event.stopPropagation()}
|
|
192
|
+
sx={{
|
|
193
|
+
display: "flex",
|
|
194
|
+
alignItems: "center",
|
|
195
|
+
gap: 0.5,
|
|
196
|
+
...(alwaysVisible
|
|
197
|
+
? {}
|
|
198
|
+
: {
|
|
199
|
+
visibility: "hidden",
|
|
200
|
+
".MuiPaper-root:hover &, .MuiPaper-root:focus-within &": {
|
|
201
|
+
visibility: "visible",
|
|
202
|
+
},
|
|
203
|
+
}),
|
|
204
|
+
}}
|
|
205
|
+
>
|
|
206
|
+
{actions}
|
|
207
|
+
</Box>
|
|
208
|
+
)}
|
|
209
|
+
{menu && (
|
|
210
|
+
<Box data-testid={testId("menu")} onClick={(event) => event.stopPropagation()}>
|
|
211
|
+
{menu}
|
|
212
|
+
</Box>
|
|
213
|
+
)}
|
|
214
|
+
</Box>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Value, status and actions — three rails, not one flex run. */
|
|
219
|
+
export function ListCardTail({
|
|
220
|
+
value,
|
|
221
|
+
status,
|
|
222
|
+
actions,
|
|
223
|
+
actionsAlwaysVisible,
|
|
224
|
+
menu,
|
|
225
|
+
scale,
|
|
226
|
+
testId,
|
|
227
|
+
}: {
|
|
228
|
+
value?: ReactNode;
|
|
229
|
+
status?: ReactNode;
|
|
230
|
+
actions?: ReactNode;
|
|
231
|
+
actionsAlwaysVisible?: boolean;
|
|
232
|
+
menu?: ReactNode;
|
|
233
|
+
scale: number;
|
|
234
|
+
testId: (slot: string) => string | undefined;
|
|
235
|
+
}): React.JSX.Element {
|
|
236
|
+
return (
|
|
237
|
+
<>
|
|
238
|
+
<Box data-slot="value" sx={{ textAlign: "right" }} data-testid={testId("value")}>
|
|
239
|
+
{value != null && <ListCardValue value={value} scale={scale} />}
|
|
240
|
+
</Box>
|
|
241
|
+
<Box
|
|
242
|
+
data-slot="status"
|
|
243
|
+
sx={{ display: "flex", justifyContent: "flex-start" }}
|
|
244
|
+
data-testid={testId("status")}
|
|
245
|
+
>
|
|
246
|
+
{status}
|
|
247
|
+
</Box>
|
|
248
|
+
<ListCardActions
|
|
249
|
+
actions={actions}
|
|
250
|
+
alwaysVisible={actionsAlwaysVisible}
|
|
251
|
+
menu={menu}
|
|
252
|
+
testId={testId}
|
|
253
|
+
/>
|
|
254
|
+
</>
|
|
255
|
+
);
|
|
256
|
+
}
|