@juwel-development/design-system 3.9.1 → 3.10.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/README.md +9 -298
- package/dist/design-system.js +1026 -582
- package/dist/index.css +1 -1
- package/dist/types/Arrangement/ColumnLayout/ColumnLayout.d.ts +92 -0
- package/dist/types/Arrangement/ColumnLayout/ColumnLayoutCompositionError.d.ts +3 -0
- package/dist/types/Arrangement/ColumnLayout/ColumnLayoutConfigurationError.d.ts +3 -0
- package/dist/types/Arrangement/FieldRow/FieldRow.d.ts +87 -0
- package/dist/types/Arrangement/FieldRow/FieldRowCompositionError.d.ts +3 -0
- package/dist/types/Arrangement/FieldRow/FieldRowConfigurationError.d.ts +3 -0
- package/dist/types/Arrangement/FieldRow/IControlEdge.d.ts +6 -0
- package/dist/types/Arrangement/FieldRow/alignControlEdges.d.ts +7 -0
- package/dist/types/Display/Box/Box.d.ts +41 -0
- package/dist/types/Display/Brandmark/Brandmark.d.ts +1 -1
- package/dist/types/Display/DefinitionList/DefinitionList.d.ts +59 -9
- package/dist/types/Display/DefinitionList/DefinitionListConfigurationError.d.ts +3 -0
- package/dist/types/Display/Figure/Figure.d.ts +2 -2
- package/dist/types/Display/Icon/Icon.d.ts +26 -0
- package/dist/types/Display/Table/Table.d.ts +118 -8
- package/dist/types/Display/Table/TableConfigurationError.d.ts +3 -0
- package/dist/types/Display/Typography/Eyebrow/Eyebrow.d.ts +1 -1
- package/dist/types/Display/Typography/H1/H1.d.ts +3 -2
- package/dist/types/Display/Typography/H2/H2.d.ts +3 -2
- package/dist/types/Display/Typography/H3/H3.d.ts +3 -2
- package/dist/types/Display/Typography/H4/H4.d.ts +3 -2
- package/dist/types/Display/Typography/H5/H5.d.ts +3 -2
- package/dist/types/Display/Typography/H6/H6.d.ts +3 -2
- package/dist/types/Display/Typography/Note/Note.d.ts +1 -1
- package/dist/types/Display/Typography/P/P.d.ts +3 -2
- package/dist/types/Display/Typography/Prose/Prose.d.ts +1 -1
- package/dist/types/Interaction/Button/Button.d.ts +23 -3
- package/dist/types/Interaction/Tabs/Tabs.d.ts +31 -6
- package/dist/types/Layout/Header/Header.d.ts +67 -8
- package/dist/types/Layout/ScrollContainer/ScrollContainer.d.ts +37 -0
- package/dist/types/Layout/Section/Section.d.ts +1 -1
- package/dist/types/Layout/Sidebar/Sidebar.d.ts +4 -0
- package/dist/types/Theme/Palette.d.ts +31 -7
- package/dist/types/index.d.ts +6 -0
- package/package.json +1 -1
- package/src/Arrangement/ColumnLayout/ColumnLayout.tsx +249 -0
- package/src/Arrangement/ColumnLayout/ColumnLayoutCompositionError.ts +8 -0
- package/src/Arrangement/ColumnLayout/ColumnLayoutConfigurationError.ts +6 -0
- package/src/Arrangement/FieldRow/FieldRow.tsx +313 -0
- package/src/Arrangement/FieldRow/FieldRowCompositionError.ts +6 -0
- package/src/Arrangement/FieldRow/FieldRowConfigurationError.ts +6 -0
- package/src/Arrangement/FieldRow/IControlEdge.ts +6 -0
- package/src/Arrangement/FieldRow/alignControlEdges.ts +35 -0
- package/src/Display/Box/Box.tsx +77 -0
- package/src/Display/Checklist/Checklist.tsx +1 -1
- package/src/Display/DefinitionList/DefinitionList.tsx +164 -22
- package/src/Display/DefinitionList/DefinitionListConfigurationError.ts +6 -0
- package/src/Display/Icon/Icon.tsx +63 -0
- package/src/Display/Table/Table.tsx +434 -44
- package/src/Display/Table/TableConfigurationError.ts +6 -0
- package/src/Display/Typography/H1/H1.tsx +3 -2
- package/src/Display/Typography/H2/H2.tsx +3 -2
- package/src/Display/Typography/H3/H3.tsx +3 -2
- package/src/Display/Typography/H4/H4.tsx +3 -2
- package/src/Display/Typography/H5/H5.tsx +3 -2
- package/src/Display/Typography/H6/H6.tsx +3 -2
- package/src/Display/Typography/P/P.tsx +3 -2
- package/src/Display/Typography/Prose/Prose.tsx +3 -3
- package/src/Interaction/Button/Button.tsx +45 -17
- package/src/Interaction/Input/Input.tsx +1 -1
- package/src/Interaction/MultiSelect/MultiSelect.tsx +1 -1
- package/src/Interaction/NumberInput/NumberInput.tsx +1 -1
- package/src/Interaction/Select/Select.tsx +1 -1
- package/src/Interaction/Tabs/Tabs.tsx +38 -11
- package/src/Interaction/TextArea/TextArea.tsx +1 -1
- package/src/Layout/Dialog/Dialog.tsx +4 -3
- package/src/Layout/Header/Header.tsx +139 -39
- package/src/Layout/PageHead/PageHead.tsx +6 -5
- package/src/Layout/ScrollContainer/ScrollContainer.tsx +149 -0
- package/src/Layout/Sidebar/Sidebar.tsx +8 -4
- package/src/Theme/Palette.ts +37 -9
- package/src/Theme/renderTokens.ts +101 -5
- package/src/index.ts +6 -0
- package/src/tokens.css +68 -4
- package/src/tokens.dark.css +66 -4
- package/src/tokens.light.css +64 -2
|
@@ -1,50 +1,161 @@
|
|
|
1
|
+
import type { VariantProps } from 'class-variance-authority';
|
|
1
2
|
import { cva } from 'class-variance-authority';
|
|
2
|
-
import type { FunctionComponent, ReactNode } from 'react';
|
|
3
|
+
import type { CSSProperties, FunctionComponent, ReactNode } from 'react';
|
|
4
|
+
import { DefinitionListConfigurationError } from './DefinitionListConfigurationError';
|
|
3
5
|
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
6
|
+
// The dl is the inline-size container its items measure, and the one place density lands: it pads
|
|
7
|
+
// its items from the role of its treatment, so Item, Term and Description carry no density of
|
|
8
|
+
// their own and nothing is passed down (docs/agents/standards/design-system-components.md,
|
|
9
|
+
// "Compound components": a Root styles the descendants it owns through its one recipe).
|
|
10
|
+
const definitionList = cva('@container', {
|
|
11
|
+
variants: {
|
|
12
|
+
density: {
|
|
13
|
+
comfortable: '[&>div]:py-[var(--space-definition-item)]',
|
|
14
|
+
compact: '[&>div]:py-[var(--space-definition-item-compact)]',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: { density: 'comfortable' },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Each item shares the list's tracks. Below the fit threshold the term track is full width.
|
|
21
|
+
// The registered shortfall resolves on the item and inherits as a length into its description.
|
|
22
|
+
// tan(atan2(length, 1px)) converts the 0px/1px switch to a number for grid-column-start (2/1).
|
|
23
|
+
// Dense placement fills column two beside the first term, or the next full row when stacked;
|
|
24
|
+
// it preserves DOM reading order and supports several terms without counting them in JavaScript.
|
|
8
25
|
const item = cva(
|
|
9
26
|
[
|
|
10
|
-
'grid gap-[var(--space-stack)]
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
27
|
+
'grid grid-flow-row-dense gap-[var(--space-stack)] border-b border-solid border-border first:border-t',
|
|
28
|
+
'[--definition-stack:clamp(0px,(var(--definition-threshold)-100%)*1000000,100%)]',
|
|
29
|
+
'[--definition-shortfall:clamp(0px,(var(--definition-threshold)-100cqi)*1000000,1px)]',
|
|
30
|
+
'grid-cols-[max(calc((100%-var(--space-definition-column))*var(--definition-term-share)),var(--definition-stack))_minmax(0,1fr)]',
|
|
31
|
+
'gap-x-[max(0px,var(--space-definition-column)-var(--definition-stack))]',
|
|
32
|
+
'[&>dt]:col-start-1 [&>dt]:self-baseline [&>dd]:col-span-full [&>dd]:self-baseline',
|
|
33
|
+
'[&>dd]:col-start-[calc(2-tan(atan2(var(--definition-shortfall),1px)))]',
|
|
14
34
|
].join(' '),
|
|
15
35
|
);
|
|
16
36
|
|
|
17
37
|
// The term at the subtitle role - a step below title (docs/adr/0005 fixes the role, no size prop), so
|
|
18
|
-
// terms never tie with the heading introducing the list. Foreground, and no measure cap.
|
|
19
|
-
|
|
38
|
+
// terms never tie with the heading introducing the list. Foreground, and no measure cap. The term
|
|
39
|
+
// is sized like a heading and is not one: with its description it is the reading matter, so both
|
|
40
|
+
// take the body face, never the heading face (docs/adr/0004, #120).
|
|
41
|
+
const term = cva(
|
|
42
|
+
'font-body text-subtitle leading-subtitle text-foreground wrap-break-word',
|
|
43
|
+
);
|
|
20
44
|
|
|
21
45
|
// The description at the body role, muted, capped at the reading measure in both layout modes.
|
|
22
46
|
const description = cva(
|
|
23
|
-
'font-
|
|
47
|
+
'font-body text-body leading-body text-muted max-w-[var(--measure)] wrap-break-word',
|
|
24
48
|
);
|
|
25
49
|
|
|
26
|
-
|
|
50
|
+
/** The name of a CSS custom property, as written in a stylesheet: `--summary-term-min-width`. */
|
|
51
|
+
type MinWidthToken = `--${string}`;
|
|
52
|
+
|
|
53
|
+
/** One column's allocation: its share of the row relative to the other's, and the theme token that
|
|
54
|
+
* holds its minimum readable width. Named only through `Root`'s props. */
|
|
55
|
+
type ColumnAllocation = { weight: number; minWidth: MinWidthToken };
|
|
56
|
+
|
|
57
|
+
const TERM_COLUMN: ColumnAllocation = {
|
|
58
|
+
weight: 1,
|
|
59
|
+
minWidth: '--definition-term-min-width',
|
|
60
|
+
};
|
|
61
|
+
const DESCRIPTION_COLUMN: ColumnAllocation = {
|
|
62
|
+
weight: 2,
|
|
63
|
+
minWidth: '--definition-description-min-width',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// The ident grammar CSS gives a custom property name, restricted to ASCII so a `var()`, a length,
|
|
67
|
+
// a space or a brace can never ride in on the name.
|
|
68
|
+
const TOKEN_NAME = /^--[A-Za-z0-9_-]+$/;
|
|
69
|
+
|
|
70
|
+
const validateColumn = (
|
|
71
|
+
name: 'termColumn' | 'descriptionColumn',
|
|
72
|
+
column: ColumnAllocation,
|
|
73
|
+
): ColumnAllocation => {
|
|
74
|
+
if (!Number.isFinite(column.weight) || column.weight <= 0) {
|
|
75
|
+
throw new DefinitionListConfigurationError(
|
|
76
|
+
`${name} needs a positive finite weight (got ${column.weight})`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (!TOKEN_NAME.test(column.minWidth)) {
|
|
80
|
+
throw new DefinitionListConfigurationError(
|
|
81
|
+
`${name} needs a custom-property name such as --summary-term-min-width for minWidth (got ${JSON.stringify(column.minWidth)})`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return column;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// The documented threshold, `g + max(m_i * S / w_i)` with the column gap as g, left to the browser
|
|
88
|
+
// to resolve so a theme re-pointing a minimum - or the gap - moves it with no script in between.
|
|
89
|
+
const thresholdOf = (
|
|
90
|
+
termColumn: ColumnAllocation,
|
|
91
|
+
descriptionColumn: ColumnAllocation,
|
|
92
|
+
): string => {
|
|
93
|
+
const total = termColumn.weight + descriptionColumn.weight;
|
|
94
|
+
return `calc(var(--space-definition-column) + max(var(${termColumn.minWidth}) * ${total / termColumn.weight}, var(${descriptionColumn.minWidth}) * ${total / descriptionColumn.weight}))`;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// React's CSSProperties is closed over known properties; the two custom properties the item
|
|
98
|
+
// recipe reads are declared here so the style object stays typed without an assertion.
|
|
99
|
+
type DefinitionListRootStyle = CSSProperties & {
|
|
100
|
+
'--definition-threshold': string;
|
|
101
|
+
'--definition-term-share': string;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export interface IDefinitionListRootProps
|
|
105
|
+
extends VariantProps<typeof definitionList> {
|
|
106
|
+
/** The term column's share of the row and its minimum readable width, shared by every item.
|
|
107
|
+
* Defaults to weight `1` and the library's `--definition-term-min-width`; a consumer's own
|
|
108
|
+
* token is declared in the theme with a nonnegative CSS length, as ColumnLayout's are. */
|
|
109
|
+
termColumn?: ColumnAllocation;
|
|
110
|
+
/** The description column's share and minimum, likewise. Defaults to weight `2` and
|
|
111
|
+
* `--definition-description-min-width`. */
|
|
112
|
+
descriptionColumn?: ColumnAllocation;
|
|
27
113
|
children?: ReactNode;
|
|
28
114
|
testId?: string;
|
|
29
115
|
}
|
|
30
116
|
|
|
31
|
-
interface IDefinitionListItemProps {
|
|
117
|
+
export interface IDefinitionListItemProps {
|
|
32
118
|
children?: ReactNode;
|
|
33
119
|
testId?: string;
|
|
34
120
|
}
|
|
35
121
|
|
|
36
|
-
interface IDefinitionListTermProps {
|
|
122
|
+
export interface IDefinitionListTermProps {
|
|
37
123
|
children?: ReactNode;
|
|
38
124
|
}
|
|
39
125
|
|
|
40
|
-
interface IDefinitionListDescriptionProps {
|
|
126
|
+
export interface IDefinitionListDescriptionProps {
|
|
41
127
|
children?: ReactNode;
|
|
42
128
|
}
|
|
43
129
|
|
|
44
130
|
const DefinitionListRoot: FunctionComponent<IDefinitionListRootProps> = ({
|
|
131
|
+
density,
|
|
132
|
+
termColumn = TERM_COLUMN,
|
|
133
|
+
descriptionColumn = DESCRIPTION_COLUMN,
|
|
45
134
|
children,
|
|
46
135
|
testId,
|
|
47
|
-
}) =>
|
|
136
|
+
}) => {
|
|
137
|
+
const termAllocation = validateColumn('termColumn', termColumn);
|
|
138
|
+
const descriptionAllocation = validateColumn(
|
|
139
|
+
'descriptionColumn',
|
|
140
|
+
descriptionColumn,
|
|
141
|
+
);
|
|
142
|
+
const style: DefinitionListRootStyle = {
|
|
143
|
+
'--definition-threshold': thresholdOf(
|
|
144
|
+
termAllocation,
|
|
145
|
+
descriptionAllocation,
|
|
146
|
+
),
|
|
147
|
+
'--definition-term-share': `calc(${termAllocation.weight} / ${termAllocation.weight + descriptionAllocation.weight})`,
|
|
148
|
+
};
|
|
149
|
+
return (
|
|
150
|
+
<dl
|
|
151
|
+
className={definitionList({ density })}
|
|
152
|
+
style={style}
|
|
153
|
+
data-testid={testId}
|
|
154
|
+
>
|
|
155
|
+
{children}
|
|
156
|
+
</dl>
|
|
157
|
+
);
|
|
158
|
+
};
|
|
48
159
|
|
|
49
160
|
const DefinitionListItem: FunctionComponent<IDefinitionListItemProps> = ({
|
|
50
161
|
children,
|
|
@@ -70,17 +181,48 @@ const DefinitionListDescription: FunctionComponent<
|
|
|
70
181
|
* `Description` a `<dd>`.
|
|
71
182
|
*
|
|
72
183
|
* @Guarantees — enforced on every render
|
|
73
|
-
* - Renders semantic `dl`/`div`/`dt`/`dd
|
|
184
|
+
* - Renders semantic `dl`/`div`/`dt`/`dd` at either density, and works with JavaScript off: the
|
|
185
|
+
* arrangement is a stylesheet rule, so nothing is measured, reordered or remounted, and resizing
|
|
186
|
+
* keeps every descendant's state, focus and keyboard order.
|
|
74
187
|
* - The term is fixed to the subtitle type role and exposes no size prop or variant (docs/adr/0005).
|
|
75
|
-
*
|
|
188
|
+
* The description is body, muted, and capped at `--measure`; the term carries no measure cap.
|
|
189
|
+
* Density changes neither: `compact` pads each item from `--space-definition-item-compact` where
|
|
190
|
+
* `comfortable` (the default) pads from `--space-definition-item`, and nothing else moves.
|
|
76
191
|
* - A hairline sits above the first item and below every item, in `border`, and the block closes at
|
|
77
192
|
* the foot. No card, box, fill, icon or bullet - it reads from the rules alone.
|
|
78
|
-
* -
|
|
79
|
-
*
|
|
193
|
+
* - Every item shares one allocation: the term column and the description column each take a share
|
|
194
|
+
* of the width left after the column gap in proportion to their weights, `1` and `2` unless the
|
|
195
|
+
* caller says otherwise. The row holds while both shares are at least their minimum readable
|
|
196
|
+
* width; the moment one falls short, every item in the list puts its terms above its description
|
|
197
|
+
* at the full width, separated by the stack role. There is no in-between, and the minimum decides
|
|
198
|
+
* the switch rather than flooring a width. For column gap g, total weight S and minimums m_i the
|
|
199
|
+
* row fits at and above `g + max(m_i * S / w_i)`.
|
|
200
|
+
* - The width measured is the list's own, never the viewport, so a narrow list on a wide screen
|
|
201
|
+
* stacks while a wide one beside it keeps its columns, and a theme that re-points a minimum -
|
|
202
|
+
* at any scope - moves the threshold with it.
|
|
203
|
+
* - A long phrase or an unbroken value wraps inside its column; no content is truncated, nothing
|
|
204
|
+
* overlaps, and the list never widens past its holder.
|
|
205
|
+
* - A non-positive or non-finite `weight`, or a `minWidth` that is not a custom-property name,
|
|
206
|
+
* throws {@link DefinitionListConfigurationError}: a programmer error, raised loud and early.
|
|
80
207
|
*
|
|
81
|
-
* @
|
|
208
|
+
* @CallerMustEnsure — the component cannot see these and does not check them
|
|
82
209
|
* - Several `Term`s may share one `Description`; keep them inside one `Item` so the term column
|
|
83
210
|
* stays intact.
|
|
211
|
+
* - A `minWidth` token the caller names is declared, on the list or an ancestor of it, as a valid
|
|
212
|
+
* nonnegative CSS length. The library's own two defaults are declared in every token stylesheet.
|
|
213
|
+
* A missing or invalid token is not a responsive configuration: the threshold has nothing to
|
|
214
|
+
* compare and the list stays stacked at every width.
|
|
215
|
+
* - The holder gives the list a definite width, as any block, grid track or Dialog content region
|
|
216
|
+
* does. Inside a shrink-to-fit frame an inline-size container contributes no width of its own.
|
|
217
|
+
*
|
|
218
|
+
* @UXGuidelines
|
|
219
|
+
* - Choose `compact` for a fact list - short values beside their labels in a panel or a content
|
|
220
|
+
* Dialog - and `comfortable` for a glossary. The choice is what the list holds, never how much
|
|
221
|
+
* air a page wants.
|
|
222
|
+
* - A minimum is the width below which a column's content stops being readable - the narrowest
|
|
223
|
+
* the labels keep their lines - not the width the designer would like; the stack is the readable
|
|
224
|
+
* fallback. Weights are structural: `1` and `3` say the value is the subject and the term its
|
|
225
|
+
* label, and a ratio chosen to hit a pixel width is a measurement, which the theme owns.
|
|
84
226
|
*/
|
|
85
227
|
export const DefinitionList = {
|
|
86
228
|
Root: DefinitionListRoot,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { cva } from 'class-variance-authority';
|
|
2
|
+
import type { FunctionComponent } from 'react';
|
|
3
|
+
|
|
4
|
+
// The glyph is sized in `em` and stroked in `currentColor`, so it takes the size and colour of the
|
|
5
|
+
// text it sits in and needs no colour or size prop. Baseline at -0.125em is what seats a 1em square
|
|
6
|
+
// beside lowercase letters; the inline-box exists so an icon reads as a word in its line.
|
|
7
|
+
const icon = cva('inline-block shrink-0 align-[-0.125em]');
|
|
8
|
+
|
|
9
|
+
// Each name selects a drawing and nothing else: the three sort indicators are told apart by their
|
|
10
|
+
// arrows, not by colour or size, so they survive forced-colors mode and a monochrome print.
|
|
11
|
+
const drawings = {
|
|
12
|
+
sort: [
|
|
13
|
+
'M5.5 13V3',
|
|
14
|
+
'M3 5.5l2.5-2.5L8 5.5',
|
|
15
|
+
'M10.5 3v10',
|
|
16
|
+
'M8 10.5l2.5 2.5 2.5-2.5',
|
|
17
|
+
],
|
|
18
|
+
'sort-ascending': ['M8 13V3', 'M4 7l4-4 4 4'],
|
|
19
|
+
'sort-descending': ['M8 3v10', 'M4 9l4 4 4-4'],
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
export interface IIconProps {
|
|
23
|
+
/** Which drawing. A name selects a shape, never a state or a behaviour. */
|
|
24
|
+
name: keyof typeof drawings;
|
|
25
|
+
testId?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A reusable visual glyph: the three sort indicators a consumer composes inside a header cell's
|
|
30
|
+
* plain Button, or beside any text that names what the icon reinforces.
|
|
31
|
+
*
|
|
32
|
+
* @Guarantees — enforced on every render
|
|
33
|
+
* - Hidden from assistive technology and out of the tab order: it adds no stop and no spoken name.
|
|
34
|
+
* - Scales with the surrounding font-size and takes the surrounding text colour.
|
|
35
|
+
* - The three drawings differ in shape, so the ordering is never carried by colour alone.
|
|
36
|
+
*
|
|
37
|
+
* @CallerMustEnsure
|
|
38
|
+
* - The control or text beside it carries the meaning: a `Button` label, a header cell's `ariaSort`.
|
|
39
|
+
* An icon standing alone says nothing to a screen reader.
|
|
40
|
+
*/
|
|
41
|
+
export const Icon: FunctionComponent<IIconProps> = ({ name, testId }) => (
|
|
42
|
+
<svg
|
|
43
|
+
aria-hidden={true}
|
|
44
|
+
focusable={false}
|
|
45
|
+
viewBox={'0 0 16 16'}
|
|
46
|
+
width={'1em'}
|
|
47
|
+
height={'1em'}
|
|
48
|
+
className={icon()}
|
|
49
|
+
data-testid={testId}
|
|
50
|
+
>
|
|
51
|
+
{drawings[name].map((segment) => (
|
|
52
|
+
<path
|
|
53
|
+
key={segment}
|
|
54
|
+
d={segment}
|
|
55
|
+
fill={'none'}
|
|
56
|
+
stroke={'currentColor'}
|
|
57
|
+
strokeWidth={1.5}
|
|
58
|
+
strokeLinecap={'round'}
|
|
59
|
+
strokeLinejoin={'round'}
|
|
60
|
+
/>
|
|
61
|
+
))}
|
|
62
|
+
</svg>
|
|
63
|
+
);
|