@oneli8/react 1.0.0-beta.10
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/AGENTS.md +16 -0
- package/LICENSE +202 -0
- package/NOTICE +8 -0
- package/PRINCIPLES.md +48 -0
- package/README.md +136 -0
- package/ai-context.json +264 -0
- package/package.json +66 -0
- package/skills/oneli8-figma-to-code/SKILL.md +25 -0
- package/skills/oneli8-icons/SKILL.md +36 -0
- package/skills/oneli8-ui/SKILL.md +52 -0
- package/src/atoms/Icon.js +34 -0
- package/src/atoms/NavigationBadge.js +19 -0
- package/src/atoms/SelectionIndicator.js +43 -0
- package/src/foundations/geometry.js +143 -0
- package/src/foundations/icons.generated.d.ts +48 -0
- package/src/foundations/icons.generated.js +562 -0
- package/src/index.d.ts +381 -0
- package/src/index.js +23 -0
- package/src/molecules/Button.js +50 -0
- package/src/molecules/ChoiceChip.js +101 -0
- package/src/molecules/ChoiceItem.js +55 -0
- package/src/molecules/FormMessage.js +33 -0
- package/src/molecules/IconButton.js +33 -0
- package/src/molecules/Link.js +31 -0
- package/src/molecules/NavigationItem.js +55 -0
- package/src/molecules/Select.js +109 -0
- package/src/molecules/SelectionOption.js +44 -0
- package/src/molecules/TextField.js +174 -0
- package/src/molecules/Token.js +49 -0
- package/src/organisms/ChoiceGroup.js +78 -0
- package/src/organisms/ChoicePicker.js +114 -0
- package/src/organisms/Combobox.js +122 -0
- package/src/organisms/MultiSelectField.js +138 -0
- package/src/organisms/SegmentedControl.js +114 -0
- package/src/organisms/SelectionPopup.js +73 -0
- package/src/organisms/TabBar.js +68 -0
- package/src/organisms/Tabs.js +123 -0
- package/src/styles/button.css +195 -0
- package/src/styles/choice-chip.css +216 -0
- package/src/styles/choice-group.css +95 -0
- package/src/styles/choice-item.css +267 -0
- package/src/styles/choice-picker.css +100 -0
- package/src/styles/combobox.css +42 -0
- package/src/styles/form-message.css +48 -0
- package/src/styles/gem.css +376 -0
- package/src/styles/icon-button.css +224 -0
- package/src/styles/icon.css +56 -0
- package/src/styles/link.css +116 -0
- package/src/styles/multi-select-field.css +78 -0
- package/src/styles/navigation-badge.css +37 -0
- package/src/styles/navigation-item.css +155 -0
- package/src/styles/segmented-control.css +115 -0
- package/src/styles/select.css +33 -0
- package/src/styles/selection-indicator.css +162 -0
- package/src/styles/selection-option.css +123 -0
- package/src/styles/selection-popup.css +92 -0
- package/src/styles/tab-bar.css +47 -0
- package/src/styles/tabs.css +55 -0
- package/src/styles/text-field.css +268 -0
- package/src/styles/token.css +65 -0
- package/styles.css +29 -0
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ComponentPropsWithoutRef, ForwardRefExoticComponent, ReactElement, ReactNode, RefAttributes,
|
|
3
|
+
} from 'react';
|
|
4
|
+
import type { Ol8IconName } from './foundations/icons.generated.js';
|
|
5
|
+
|
|
6
|
+
export type { Ol8IconName };
|
|
7
|
+
export { OL8_ICONS, OL8_ICON_VIEWBOX } from './foundations/icons.generated.js';
|
|
8
|
+
|
|
9
|
+
export type Ol8Variant = 'primary' | 'secondary' | 'quiet' | 'destructive';
|
|
10
|
+
export type Ol8Size = 'compact' | 'standard' | 'comfortable' | 'large';
|
|
11
|
+
export type Ol8Material = 'regular' | 'gem';
|
|
12
|
+
export type Ol8SelectionKind = 'checkbox' | 'radio' | 'switch';
|
|
13
|
+
export type Ol8SelectionSize = 'compact' | 'comfortable';
|
|
14
|
+
export type Ol8Selection =
|
|
15
|
+
| 'unchecked' | 'checked' | 'mixed'
|
|
16
|
+
| 'unselected' | 'selected'
|
|
17
|
+
| 'off' | 'on';
|
|
18
|
+
export type Ol8LinkForm = 'inline' | 'standalone' | 'navigation';
|
|
19
|
+
export type Ol8LinkSize = 'small' | 'standard' | 'large';
|
|
20
|
+
export type Ol8LinkMotion = 'system' | 'none';
|
|
21
|
+
export declare const OL8_LINK_FORMS: readonly Ol8LinkForm[];
|
|
22
|
+
export declare const OL8_LINK_SIZES: readonly Ol8LinkSize[];
|
|
23
|
+
export declare const OL8_LINK_MOTIONS: readonly Ol8LinkMotion[];
|
|
24
|
+
|
|
25
|
+
export type Ol8TextFieldSize = 'compact' | 'standard' | 'comfortable' | 'large';
|
|
26
|
+
export type Ol8TextFieldAppearance = 'outline' | 'filled';
|
|
27
|
+
/** Pending is a validation process status, not a fifth message tone. */
|
|
28
|
+
export type Ol8MessageTone = 'critical' | 'caution' | 'positive' | 'informative';
|
|
29
|
+
export type Ol8ValidationStatus = 'idle' | 'pending' | 'resolved';
|
|
30
|
+
export type Ol8CharacterLimitBehavior = 'soft' | 'hard';
|
|
31
|
+
/** Form Message itself draws all five, because Figma 177:13 enumerates five. */
|
|
32
|
+
export type Ol8FormMessageTone = Ol8MessageTone | 'pending';
|
|
33
|
+
export declare const OL8_TEXT_FIELD_SIZES: readonly Ol8TextFieldSize[];
|
|
34
|
+
export declare const OL8_TEXT_FIELD_APPEARANCES: readonly Ol8TextFieldAppearance[];
|
|
35
|
+
export declare const OL8_TEXT_FIELD_MATERIALS: readonly Ol8Material[];
|
|
36
|
+
export declare const OL8_MESSAGE_TONES: readonly Ol8MessageTone[];
|
|
37
|
+
export declare const OL8_VALIDATION_STATUSES: readonly Ol8ValidationStatus[];
|
|
38
|
+
export declare const OL8_CHARACTER_LIMIT_BEHAVIORS: readonly Ol8CharacterLimitBehavior[];
|
|
39
|
+
export declare const OL8_FORM_MESSAGE_TONES: readonly Ol8FormMessageTone[];
|
|
40
|
+
export declare const OL8_FORM_MESSAGE_ICONS: Readonly<Record<Ol8FormMessageTone, string>>;
|
|
41
|
+
|
|
42
|
+
export type Ol8NavigationSize = 'compact' | 'standard' | 'comfortable' | 'large';
|
|
43
|
+
export type Ol8TabsHierarchy = 'primary' | 'secondary' | 'tertiary';
|
|
44
|
+
export type Ol8TabsOrientation = 'horizontal' | 'vertical';
|
|
45
|
+
export type Ol8TabsActivation = 'automatic' | 'manual';
|
|
46
|
+
export type Ol8SegmentBehavior = 'single' | 'multi' | 'momentary';
|
|
47
|
+
export type Ol8SegmentPresentation =
|
|
48
|
+
| 'inset-fill' | 'line-indicator' | 'outlined-selection'
|
|
49
|
+
| 'soft-pill' | 'icon-only' | 'stacked-label';
|
|
50
|
+
export type Ol8TabBarPresentation = 'bottom' | 'inline' | 'sidebar' | 'spatial-rail';
|
|
51
|
+
export declare const OL8_TABS_HIERARCHIES: readonly Ol8TabsHierarchy[];
|
|
52
|
+
export declare const OL8_TABS_ORIENTATIONS: readonly Ol8TabsOrientation[];
|
|
53
|
+
export declare const OL8_TABS_ACTIVATIONS: readonly Ol8TabsActivation[];
|
|
54
|
+
export declare const OL8_SEGMENT_BEHAVIORS: readonly Ol8SegmentBehavior[];
|
|
55
|
+
export declare const OL8_SEGMENT_PRESENTATIONS: readonly Ol8SegmentPresentation[];
|
|
56
|
+
export declare const OL8_TAB_BAR_PRESENTATIONS: readonly Ol8TabBarPresentation[];
|
|
57
|
+
|
|
58
|
+
export type Ol8OptionSize = 'standard' | 'large';
|
|
59
|
+
export type Ol8Aggregate = 'none' | 'some' | 'all';
|
|
60
|
+
|
|
61
|
+
export declare const OL8_BUTTON_VARIANTS: readonly Ol8Variant[];
|
|
62
|
+
export declare const OL8_BUTTON_SIZES: readonly Ol8Size[];
|
|
63
|
+
export declare const OL8_MATERIALS: readonly Ol8Material[];
|
|
64
|
+
/** Gem is approved for Primary and Secondary only (Figma 100:3). */
|
|
65
|
+
export declare const GEM_VARIANTS: readonly ('primary' | 'secondary')[];
|
|
66
|
+
export declare const OL8_ICON_BUTTON_VARIANTS: readonly Ol8Variant[];
|
|
67
|
+
export declare const OL8_ICON_BUTTON_SIZES: readonly Ol8Size[];
|
|
68
|
+
export declare const OL8_ICON_BUTTON_SHAPES: readonly ('rounded' | 'circle')[];
|
|
69
|
+
export declare const OL8_SELECTION_KINDS: readonly Ol8SelectionKind[];
|
|
70
|
+
export declare const OL8_SELECTION_SIZES: readonly Ol8SelectionSize[];
|
|
71
|
+
export declare const OL8_SELECTION_STATES: Record<Ol8SelectionKind, readonly Ol8Selection[]>;
|
|
72
|
+
export declare const OL8_CHOICE_KINDS: readonly Ol8SelectionKind[];
|
|
73
|
+
export declare const OL8_CHOICE_SIZES: readonly Ol8Size[];
|
|
74
|
+
export declare const OL8_OPTION_SIZES: readonly Ol8OptionSize[];
|
|
75
|
+
export declare const OL8_AGGREGATE: readonly Ol8Aggregate[];
|
|
76
|
+
|
|
77
|
+
export declare function isOl8IconName(name: string): name is Ol8IconName;
|
|
78
|
+
export declare function iconSizeForButton(size: Ol8Size): 18 | 24;
|
|
79
|
+
export declare function artworkSizeForIconButton(size: Ol8Size): 18 | 24;
|
|
80
|
+
export declare function indicatorSizeFor(size: Ol8Size): Ol8SelectionSize;
|
|
81
|
+
export declare function selectionStateFor(
|
|
82
|
+
kind: Ol8SelectionKind, state?: { checked?: boolean; mixed?: boolean }
|
|
83
|
+
): Ol8Selection;
|
|
84
|
+
export declare function computeAggregate(
|
|
85
|
+
children: Iterable<{ checked?: boolean; disabled?: boolean }>
|
|
86
|
+
): Ol8Aggregate;
|
|
87
|
+
|
|
88
|
+
/** ATOM. Decorative unless given a label; the parent control carries meaning. */
|
|
89
|
+
export interface IconProps extends Omit<ComponentPropsWithoutRef<'span'>, 'children'> {
|
|
90
|
+
name: Ol8IconName;
|
|
91
|
+
size?: number;
|
|
92
|
+
/** Promotes the icon to role="img". Omit for decorative. */
|
|
93
|
+
label?: string;
|
|
94
|
+
}
|
|
95
|
+
export declare function Icon(props: IconProps): ReactElement;
|
|
96
|
+
|
|
97
|
+
/** ATOM. Presentational only; the native input in ChoiceItem owns state and focus. */
|
|
98
|
+
export interface SelectionIndicatorProps extends Omit<ComponentPropsWithoutRef<'span'>, 'children'> {
|
|
99
|
+
kind: Ol8SelectionKind;
|
|
100
|
+
selection?: Ol8Selection;
|
|
101
|
+
size?: Ol8SelectionSize;
|
|
102
|
+
disabled?: boolean;
|
|
103
|
+
/** Switch thumb artwork only. */
|
|
104
|
+
icon?: Ol8IconName;
|
|
105
|
+
}
|
|
106
|
+
export declare function SelectionIndicator(props: SelectionIndicatorProps): ReactElement;
|
|
107
|
+
|
|
108
|
+
/** MOLECULE. States are CSS, never classes. */
|
|
109
|
+
export interface ButtonProps extends Omit<ComponentPropsWithoutRef<'button'>, 'type'> {
|
|
110
|
+
children?: ReactNode;
|
|
111
|
+
variant?: Ol8Variant;
|
|
112
|
+
size?: Ol8Size;
|
|
113
|
+
/** Throws for quiet and destructive: Gem has no approved treatment there. */
|
|
114
|
+
material?: Ol8Material;
|
|
115
|
+
leadingIcon?: Ol8IconName;
|
|
116
|
+
trailingIcon?: Ol8IconName;
|
|
117
|
+
loading?: boolean;
|
|
118
|
+
pressed?: boolean;
|
|
119
|
+
fullWidth?: boolean;
|
|
120
|
+
/** Renders an anchor instead of a button. */
|
|
121
|
+
href?: string;
|
|
122
|
+
type?: 'button' | 'submit' | 'reset';
|
|
123
|
+
/** Keeps the leading icon beside the spinner, Figma's literal reading. */
|
|
124
|
+
keepLeadingIconWhileLoading?: boolean;
|
|
125
|
+
}
|
|
126
|
+
export declare const Button: ForwardRefExoticComponent<ButtonProps & RefAttributes<HTMLElement>>;
|
|
127
|
+
|
|
128
|
+
/** MOLECULE. An accessible name is mandatory and enforced at runtime. */
|
|
129
|
+
export interface IconButtonProps extends Omit<ComponentPropsWithoutRef<'button'>, 'type'> {
|
|
130
|
+
icon: Ol8IconName;
|
|
131
|
+
accessibleName: string;
|
|
132
|
+
variant?: Ol8Variant;
|
|
133
|
+
size?: Ol8Size;
|
|
134
|
+
shape?: 'rounded' | 'circle';
|
|
135
|
+
material?: Ol8Material;
|
|
136
|
+
loading?: boolean;
|
|
137
|
+
pressed?: boolean;
|
|
138
|
+
type?: 'button' | 'submit' | 'reset';
|
|
139
|
+
}
|
|
140
|
+
export declare const IconButton: ForwardRefExoticComponent<IconButtonProps & RefAttributes<HTMLButtonElement>>;
|
|
141
|
+
|
|
142
|
+
/** MOLECULE. A real input owns state, focus and form participation. */
|
|
143
|
+
export interface ChoiceItemProps extends Omit<ComponentPropsWithoutRef<'input'>, 'size' | 'type' | 'children'> {
|
|
144
|
+
kind: Ol8SelectionKind;
|
|
145
|
+
children?: ReactNode;
|
|
146
|
+
size?: Ol8Size;
|
|
147
|
+
/** Checkbox only. Sets the JavaScript-only indeterminate flag. */
|
|
148
|
+
mixed?: boolean;
|
|
149
|
+
invalid?: boolean;
|
|
150
|
+
/** Associated with aria-describedby, outside the accessible name. */
|
|
151
|
+
description?: ReactNode;
|
|
152
|
+
material?: Ol8Material;
|
|
153
|
+
}
|
|
154
|
+
export declare const ChoiceItem: ForwardRefExoticComponent<ChoiceItemProps & RefAttributes<HTMLInputElement>>;
|
|
155
|
+
|
|
156
|
+
/** MOLECULE. States are CSS, never props. Icons are intentionally parked. */
|
|
157
|
+
export interface LinkProps extends Omit<ComponentPropsWithoutRef<'a'>, 'href'> {
|
|
158
|
+
children?: ReactNode;
|
|
159
|
+
href: string;
|
|
160
|
+
form?: Ol8LinkForm;
|
|
161
|
+
size?: Ol8LinkSize;
|
|
162
|
+
/** Navigation only. `true` emits aria-current="page"; a string names the kind. */
|
|
163
|
+
current?: boolean | string;
|
|
164
|
+
motion?: Ol8LinkMotion;
|
|
165
|
+
}
|
|
166
|
+
export declare const Link: ForwardRefExoticComponent<LinkProps & RefAttributes<HTMLAnchorElement>>;
|
|
167
|
+
|
|
168
|
+
/** MOLECULE. Tone maps to semantic message tone; Pending is the validating state. */
|
|
169
|
+
export interface FormMessageProps extends ComponentPropsWithoutRef<'div'> {
|
|
170
|
+
children?: ReactNode;
|
|
171
|
+
tone?: Ol8FormMessageTone;
|
|
172
|
+
/** Figma's Show Icon. */
|
|
173
|
+
icon?: boolean;
|
|
174
|
+
}
|
|
175
|
+
export declare const FormMessage: ForwardRefExoticComponent<FormMessageProps & RefAttributes<HTMLDivElement>>;
|
|
176
|
+
|
|
177
|
+
/** MOLECULE. Conditions are CSS and native state, never props. Gem changes material only. */
|
|
178
|
+
export interface TextFieldProps extends Omit<ComponentPropsWithoutRef<'input'>, 'size' | 'prefix'> {
|
|
179
|
+
/** Required and visible. A placeholder is never the label. */
|
|
180
|
+
label: string;
|
|
181
|
+
/** Default Comfortable, the cross platform size. */
|
|
182
|
+
size?: Ol8TextFieldSize;
|
|
183
|
+
appearance?: Ol8TextFieldAppearance;
|
|
184
|
+
material?: Ol8Material;
|
|
185
|
+
/** False keeps the label as the accessible name without showing it, which needs a documented exception. */
|
|
186
|
+
showLabel?: boolean;
|
|
187
|
+
/** Correction is required. Never set without also exposing a Critical message. */
|
|
188
|
+
invalid?: boolean;
|
|
189
|
+
leadingIcon?: string;
|
|
190
|
+
prefix?: ReactNode;
|
|
191
|
+
suffix?: ReactNode;
|
|
192
|
+
trailingAction?: { icon: string; label: string; onClick?: () => void; pressed?: boolean };
|
|
193
|
+
instruction?: ReactNode;
|
|
194
|
+
message?: ReactNode;
|
|
195
|
+
messageTone?: Ol8MessageTone;
|
|
196
|
+
messageIcon?: boolean;
|
|
197
|
+
/** Pending shows the loader; it never freezes typing. */
|
|
198
|
+
validationStatus?: Ol8ValidationStatus;
|
|
199
|
+
/** Counted in graphemes, so an emoji counts as one. */
|
|
200
|
+
characterLimit?: number;
|
|
201
|
+
/** Soft lets a person overshoot and then correct. Hard is for real technical limits. */
|
|
202
|
+
characterLimitBehavior?: Ol8CharacterLimitBehavior;
|
|
203
|
+
}
|
|
204
|
+
export declare const TextField: ForwardRefExoticComponent<TextFieldProps & RefAttributes<HTMLInputElement>>;
|
|
205
|
+
|
|
206
|
+
/** Counts what a reader would call characters. */
|
|
207
|
+
export declare function countGraphemes(value: unknown): number;
|
|
208
|
+
|
|
209
|
+
/** Keeps the first `limit` graphemes, so a cluster is never cut in half. */
|
|
210
|
+
export declare function clipToGraphemes(value: unknown, limit: number): string;
|
|
211
|
+
|
|
212
|
+
/** MOLECULE. Never focusable; a listbox moves a virtual cursor instead. */
|
|
213
|
+
export interface SelectionOptionProps extends Omit<ComponentPropsWithoutRef<'li'>, 'children'> {
|
|
214
|
+
children?: ReactNode;
|
|
215
|
+
value?: string;
|
|
216
|
+
size?: Ol8OptionSize;
|
|
217
|
+
selected?: boolean;
|
|
218
|
+
active?: boolean;
|
|
219
|
+
disabled?: boolean;
|
|
220
|
+
description?: ReactNode;
|
|
221
|
+
leadingIcon?: Ol8IconName;
|
|
222
|
+
focusRing?: boolean;
|
|
223
|
+
}
|
|
224
|
+
export declare function SelectionOption(props: SelectionOptionProps): ReactElement;
|
|
225
|
+
|
|
226
|
+
export interface Ol8GroupOption {
|
|
227
|
+
label: ReactNode;
|
|
228
|
+
value: string;
|
|
229
|
+
description?: ReactNode;
|
|
230
|
+
disabled?: boolean;
|
|
231
|
+
/** CheckboxGroup only; radios take a single group value. */
|
|
232
|
+
checked?: boolean;
|
|
233
|
+
}
|
|
234
|
+
interface Ol8GroupShell {
|
|
235
|
+
legend: ReactNode;
|
|
236
|
+
instruction?: ReactNode;
|
|
237
|
+
message?: ReactNode;
|
|
238
|
+
invalid?: boolean;
|
|
239
|
+
disabled?: boolean;
|
|
240
|
+
size?: Ol8Size;
|
|
241
|
+
material?: Ol8Material;
|
|
242
|
+
id?: string;
|
|
243
|
+
className?: string;
|
|
244
|
+
}
|
|
245
|
+
/** ORGANISM. Native radios already give roving focus and single selection. */
|
|
246
|
+
export interface RadioGroupProps extends Ol8GroupShell {
|
|
247
|
+
name: string;
|
|
248
|
+
options: Ol8GroupOption[];
|
|
249
|
+
value?: string;
|
|
250
|
+
onChange?: (value: string) => void;
|
|
251
|
+
orientation?: 'vertical' | 'horizontal';
|
|
252
|
+
required?: boolean;
|
|
253
|
+
}
|
|
254
|
+
export declare function RadioGroup(props: RadioGroupProps): ReactElement;
|
|
255
|
+
|
|
256
|
+
/** ORGANISM. Aggregate is computed; Mixed is never a third preference. */
|
|
257
|
+
export interface CheckboxGroupProps extends Ol8GroupShell {
|
|
258
|
+
parentLabel: ReactNode;
|
|
259
|
+
options: Ol8GroupOption[];
|
|
260
|
+
name?: string;
|
|
261
|
+
onChange?: (state: Record<string, boolean>) => void;
|
|
262
|
+
}
|
|
263
|
+
export declare function CheckboxGroup(props: CheckboxGroupProps): ReactElement;
|
|
264
|
+
|
|
265
|
+
/** ATOM. One badge serves all three navigation organisms. */
|
|
266
|
+
export interface NavigationBadgeProps extends ComponentPropsWithoutRef<'span'> {
|
|
267
|
+
children: ReactNode;
|
|
268
|
+
}
|
|
269
|
+
export declare const NavigationBadge: ForwardRefExoticComponent<NavigationBadgeProps & RefAttributes<HTMLSpanElement>>;
|
|
270
|
+
|
|
271
|
+
export interface Ol8TabItem {
|
|
272
|
+
/** Required: exactly one panel is paired with it. */
|
|
273
|
+
id: string;
|
|
274
|
+
label?: string;
|
|
275
|
+
icon?: string;
|
|
276
|
+
badge?: string | number;
|
|
277
|
+
disabled?: boolean;
|
|
278
|
+
/** Required when the item shows an icon and no label. */
|
|
279
|
+
ariaLabel?: string;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** ORGANISM. Emits tablist, tab and tabpanel, and keeps one enabled tab stop. */
|
|
283
|
+
export interface TabsProps extends Omit<ComponentPropsWithoutRef<'div'>, 'onChange'> {
|
|
284
|
+
/** Required. A region nobody can name is a region nobody can reach. */
|
|
285
|
+
label: string;
|
|
286
|
+
items: Ol8TabItem[];
|
|
287
|
+
value?: string;
|
|
288
|
+
defaultValue?: string;
|
|
289
|
+
onChange?: (id: string) => void;
|
|
290
|
+
hierarchy?: Ol8TabsHierarchy;
|
|
291
|
+
size?: Ol8NavigationSize;
|
|
292
|
+
material?: Ol8Material;
|
|
293
|
+
orientation?: Ol8TabsOrientation;
|
|
294
|
+
activation?: Ol8TabsActivation;
|
|
295
|
+
/** Supply it and Tabs renders the owned panel for each item. */
|
|
296
|
+
renderPanel?: (item: Ol8TabItem) => ReactNode;
|
|
297
|
+
}
|
|
298
|
+
export declare const Tabs: ForwardRefExoticComponent<TabsProps & RefAttributes<HTMLDivElement>>;
|
|
299
|
+
|
|
300
|
+
export interface Ol8SegmentItem {
|
|
301
|
+
id: string;
|
|
302
|
+
label?: string;
|
|
303
|
+
icon?: string;
|
|
304
|
+
badge?: string | number;
|
|
305
|
+
disabled?: boolean;
|
|
306
|
+
/** Required when the item shows an icon and no label. */
|
|
307
|
+
ariaLabel?: string;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** ORGANISM. Behaviour is declared, never inferred; presentation never changes it. */
|
|
311
|
+
export interface SegmentedControlProps extends Omit<ComponentPropsWithoutRef<'div'>, 'onChange' | 'defaultValue'> {
|
|
312
|
+
/** Required accessible name. */
|
|
313
|
+
label: string;
|
|
314
|
+
/** Exactly one: single is a radio group, multi is toggle buttons, momentary stores nothing. */
|
|
315
|
+
behavior: Ol8SegmentBehavior;
|
|
316
|
+
items: Ol8SegmentItem[];
|
|
317
|
+
/** One value for single, an array for multi, nothing for momentary. */
|
|
318
|
+
value?: string | string[];
|
|
319
|
+
defaultValue?: string | string[];
|
|
320
|
+
onChange?: (value: string | string[]) => void;
|
|
321
|
+
presentation?: Ol8SegmentPresentation;
|
|
322
|
+
size?: Ol8NavigationSize;
|
|
323
|
+
material?: Ol8Material;
|
|
324
|
+
}
|
|
325
|
+
export declare const SegmentedControl: ForwardRefExoticComponent<SegmentedControlProps & RefAttributes<HTMLDivElement>>;
|
|
326
|
+
|
|
327
|
+
export interface Ol8Destination {
|
|
328
|
+
/** Required. A tab bar navigates, so it uses real links. */
|
|
329
|
+
href: string;
|
|
330
|
+
label?: string;
|
|
331
|
+
icon?: string;
|
|
332
|
+
badge?: string | number;
|
|
333
|
+
/** Required when the destination shows an icon and no label. */
|
|
334
|
+
ariaLabel?: string;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** ORGANISM. Native destination links, one current destination, no disabled pseudo destinations. */
|
|
338
|
+
export interface TabBarProps extends ComponentPropsWithoutRef<'nav'> {
|
|
339
|
+
/** Required navigation name, so a person can tell one landmark from another. */
|
|
340
|
+
label: string;
|
|
341
|
+
items: Ol8Destination[];
|
|
342
|
+
/** The href of the destination a person is on. */
|
|
343
|
+
current?: string;
|
|
344
|
+
/** Routes an ordinary left click while the link stays a real link. */
|
|
345
|
+
onNavigate?: (href: string, event: MouseEvent) => void;
|
|
346
|
+
presentation?: Ol8TabBarPresentation;
|
|
347
|
+
size?: Ol8NavigationSize;
|
|
348
|
+
material?: Ol8Material;
|
|
349
|
+
}
|
|
350
|
+
export declare const TabBar: ForwardRefExoticComponent<TabBarProps & RefAttributes<HTMLElement>>;
|
|
351
|
+
|
|
352
|
+
export type Ol8SelectNativePolicy = 'preferred' | 'required' | 'custom-allowed';
|
|
353
|
+
export declare const OL8_SELECT_NATIVE_POLICIES: readonly Ol8SelectNativePolicy[];
|
|
354
|
+
|
|
355
|
+
export interface Ol8SelectOption {
|
|
356
|
+
value: string;
|
|
357
|
+
label: string;
|
|
358
|
+
disabled?: boolean;
|
|
359
|
+
group?: string;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** MOLECULE. Native first: a real select, wearing the Text Field geometry Figma reuses. */
|
|
363
|
+
export interface SelectProps extends Omit<ComponentPropsWithoutRef<'select'>, 'size'> {
|
|
364
|
+
/** Required and visible. A placeholder is never the label. */
|
|
365
|
+
label: string;
|
|
366
|
+
options: Ol8SelectOption[];
|
|
367
|
+
size?: Ol8TextFieldSize;
|
|
368
|
+
appearance?: Ol8TextFieldAppearance;
|
|
369
|
+
material?: Ol8Material;
|
|
370
|
+
showLabel?: boolean;
|
|
371
|
+
invalid?: boolean;
|
|
372
|
+
/** Only when an unselected value is valid, or required validation needs a prompt. */
|
|
373
|
+
placeholderOption?: string;
|
|
374
|
+
leadingIcon?: string;
|
|
375
|
+
instruction?: ReactNode;
|
|
376
|
+
message?: ReactNode;
|
|
377
|
+
messageTone?: Ol8MessageTone;
|
|
378
|
+
/** 'custom-allowed' is refused: use Combobox, which implements the full contract. */
|
|
379
|
+
native?: Ol8SelectNativePolicy;
|
|
380
|
+
}
|
|
381
|
+
export declare const Select: ForwardRefExoticComponent<SelectProps & RefAttributes<HTMLSelectElement>>;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export { Icon } from './atoms/Icon.js';
|
|
2
|
+
export { SelectionIndicator } from './atoms/SelectionIndicator.js';
|
|
3
|
+
export { Button } from './molecules/Button.js';
|
|
4
|
+
export { IconButton } from './molecules/IconButton.js';
|
|
5
|
+
export { Link } from './molecules/Link.js';
|
|
6
|
+
export { ChoiceItem } from './molecules/ChoiceItem.js';
|
|
7
|
+
export { SelectionOption } from './molecules/SelectionOption.js';
|
|
8
|
+
export { FormMessage } from './molecules/FormMessage.js';
|
|
9
|
+
export { TextField, countGraphemes, clipToGraphemes } from './molecules/TextField.js';
|
|
10
|
+
export { Select, OL8_SELECT_NATIVE_POLICIES } from './molecules/Select.js';
|
|
11
|
+
export { ChoiceChip } from './molecules/ChoiceChip.js';
|
|
12
|
+
export { Token } from './molecules/Token.js';
|
|
13
|
+
export { RadioGroup, CheckboxGroup } from './organisms/ChoiceGroup.js';
|
|
14
|
+
export { SelectionPopup } from './organisms/SelectionPopup.js';
|
|
15
|
+
export { MultiSelectField } from './organisms/MultiSelectField.js';
|
|
16
|
+
export { ChoicePicker } from './organisms/ChoicePicker.js';
|
|
17
|
+
export { Combobox } from './organisms/Combobox.js';
|
|
18
|
+
export { NavigationBadge } from './atoms/NavigationBadge.js';
|
|
19
|
+
export { Tabs } from './organisms/Tabs.js';
|
|
20
|
+
export { SegmentedControl } from './organisms/SegmentedControl.js';
|
|
21
|
+
export { TabBar } from './organisms/TabBar.js';
|
|
22
|
+
export * from './foundations/geometry.js';
|
|
23
|
+
export { OL8_ICONS, OL8_ICON_VIEWBOX } from './foundations/icons.generated.js';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createElement, forwardRef } from 'react';
|
|
2
|
+
import { Icon, IconGlyph } from '../atoms/Icon.js';
|
|
3
|
+
import { assertVariant, iconSizeForButton } from '../foundations/geometry.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MOLECULE. Figma's six States are visual evidence for review, not classes.
|
|
7
|
+
* In code they are :hover, :active, :focus-visible, [disabled] and
|
|
8
|
+
* [data-ol8-loading]. There is no .ol8-btn--hover.
|
|
9
|
+
*/
|
|
10
|
+
export const Button = forwardRef(function Button({
|
|
11
|
+
children, variant = 'primary', size = 'standard', material = 'regular',
|
|
12
|
+
leadingIcon, trailingIcon, loading = false, disabled = false, pressed,
|
|
13
|
+
fullWidth = false, href, type = 'button', className = '',
|
|
14
|
+
keepLeadingIconWhileLoading = false, ...rest
|
|
15
|
+
}, ref) {
|
|
16
|
+
assertVariant(variant, size, material);
|
|
17
|
+
const isLink = typeof href === 'string';
|
|
18
|
+
const inert = disabled || loading;
|
|
19
|
+
const iconSize = iconSizeForButton(size);
|
|
20
|
+
|
|
21
|
+
// The spinner takes the leading slot by default, so the control never shows
|
|
22
|
+
// two competing leading glyphs and its width stays predictable.
|
|
23
|
+
const content = [];
|
|
24
|
+
if (loading) {
|
|
25
|
+
if (keepLeadingIconWhileLoading && leadingIcon) {
|
|
26
|
+
content.push(createElement(Icon, { key: 'leading', name: leadingIcon, size: iconSize }));
|
|
27
|
+
}
|
|
28
|
+
content.push(createElement('span', { key: 'spinner', className: 'ol8-btn__spinner ol8-icon', 'aria-hidden': 'true' },
|
|
29
|
+
createElement(IconGlyph, { name: 'loading' })));
|
|
30
|
+
} else if (leadingIcon) {
|
|
31
|
+
content.push(createElement(Icon, { key: 'leading', name: leadingIcon, size: iconSize }));
|
|
32
|
+
}
|
|
33
|
+
content.push(createElement('span', { key: 'label', className: 'ol8-btn__label' }, children));
|
|
34
|
+
if (trailingIcon) content.push(createElement(Icon, { key: 'trailing', name: trailingIcon, size: iconSize }));
|
|
35
|
+
|
|
36
|
+
const shared = {
|
|
37
|
+
...rest, ref,
|
|
38
|
+
className: `ol8-btn ol8-btn--${variant}${className ? ` ${className}` : ''}`,
|
|
39
|
+
'data-ol8-size': size,
|
|
40
|
+
...(loading ? { 'data-ol8-loading': 'true', 'aria-busy': 'true' } : {}),
|
|
41
|
+
...(pressed !== undefined ? { 'aria-pressed': String(pressed) } : {}),
|
|
42
|
+
...(fullWidth ? { 'data-ol8-fullwidth': 'true' } : {}),
|
|
43
|
+
...(material === 'gem' ? { 'data-ol8-material': 'gem' } : {}),
|
|
44
|
+
...(loading && keepLeadingIconWhileLoading ? { 'data-ol8-loading-keep-icon': 'true' } : {}),
|
|
45
|
+
};
|
|
46
|
+
// A link cannot be natively disabled, so it leaves the tab order instead.
|
|
47
|
+
return isLink
|
|
48
|
+
? createElement('a', { ...shared, ...(inert ? { role: 'button', 'aria-disabled': 'true', tabIndex: -1 } : { href }) }, content)
|
|
49
|
+
: createElement('button', { ...shared, type, ...(inert ? { disabled: true } : {}) }, content);
|
|
50
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createElement, useId } from 'react';
|
|
2
|
+
import { IconGlyph } from '../atoms/Icon.js';
|
|
3
|
+
import { OL8_CHIP_SIZES } from '../foundations/geometry.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MOLECULE. The Soft Hexagon, built the way Figma builds it: a fixed 12 unit
|
|
7
|
+
* terminal, a rail that grows with the label, and a mirrored terminal. Only the
|
|
8
|
+
* rail grows, so peer chips of any length keep one shoulder depth and angle.
|
|
9
|
+
*
|
|
10
|
+
* The terminal paths and the shape are private, as they are in Figma, where the
|
|
11
|
+
* terminals are named "Private Geometry" and the contract says they are "not
|
|
12
|
+
* published cap atoms, layer variants, or independently swappable components".
|
|
13
|
+
* Token imports them from this module; the package index does not export them.
|
|
14
|
+
*
|
|
15
|
+
* One viewBox serves all four heights: read from Figma, every terminal path at
|
|
16
|
+
* 30, 36 and 42 shares identical X values and Y values that are the same
|
|
17
|
+
* fractions of the height, so stretching Y alone is exact.
|
|
18
|
+
*/
|
|
19
|
+
const TERMINAL_FILL =
|
|
20
|
+
'M12 0 L8.96202541 0 C7.44303812 0 6.43037944 0.83333333 5.92405035 2.5 ' +
|
|
21
|
+
'L0.45569618 12.5 C-0.15189877 13.75 -0.15189877 16.25 0.45569618 17.5 ' +
|
|
22
|
+
'L5.92405035 27.5 C6.43037944 29.16666667 7.44303812 30 8.96202541 30 L12 30 Z';
|
|
23
|
+
const TERMINAL_EDGE =
|
|
24
|
+
'M12 0 L8.96202541 0 C7.44303812 0 6.43037944 0.83333333 5.92405035 2.5 ' +
|
|
25
|
+
'L0.45569618 12.5 C-0.15189877 13.75 -0.15189877 16.25 0.45569618 17.5 ' +
|
|
26
|
+
'L5.92405035 27.5 C6.43037944 29.16666667 7.44303812 30 8.96202541 30 L12 30';
|
|
27
|
+
|
|
28
|
+
function Terminal({ uid, side }) {
|
|
29
|
+
const clip = `${uid}-clip-${side}`;
|
|
30
|
+
return createElement('svg', {
|
|
31
|
+
className: `ol8-chip__terminal ol8-chip__terminal--${side}`,
|
|
32
|
+
viewBox: '0 0 12 30', preserveAspectRatio: 'none',
|
|
33
|
+
'aria-hidden': 'true', focusable: 'false',
|
|
34
|
+
}, [
|
|
35
|
+
createElement('clipPath', { key: 'c', id: clip }, createElement('path', { d: TERMINAL_FILL })),
|
|
36
|
+
createElement('path', { key: 'fo', className: 'ol8-chip__focus-outer', d: TERMINAL_EDGE }),
|
|
37
|
+
createElement('path', { key: 'fi', className: 'ol8-chip__focus-inner', d: TERMINAL_EDGE }),
|
|
38
|
+
createElement('path', { key: 'f', className: 'ol8-chip__fill', d: TERMINAL_FILL }),
|
|
39
|
+
createElement('path', { key: 'e', className: 'ol8-chip__edge', d: TERMINAL_EDGE, clipPath: `url(#${clip})` }),
|
|
40
|
+
]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The silhouette and its rail. Figma's "Choice Chip / Geometry Owner".
|
|
45
|
+
* The mark follows the label, which is the anatomy the contract writes down as
|
|
46
|
+
* "Label + optional Remove mark", and one position serves Check and Remove
|
|
47
|
+
* alike so peer marks never end up on opposite sides.
|
|
48
|
+
*/
|
|
49
|
+
export function ChipShape({ uid, label, mark = null }) {
|
|
50
|
+
return [
|
|
51
|
+
createElement(Terminal, { key: 'l', uid, side: 'leading' }),
|
|
52
|
+
createElement('span', { key: 'r', className: 'ol8-chip__rail' }, [
|
|
53
|
+
createElement('span', { key: 'label', className: 'ol8-chip__label' }, label),
|
|
54
|
+
mark,
|
|
55
|
+
]),
|
|
56
|
+
createElement(Terminal, { key: 't', uid, side: 'trailing' }),
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A Choice Chip is a real checkbox. The contract is explicit that every picker
|
|
62
|
+
* option is "a real Checkbox rendered with the approved quiet Choice Chip
|
|
63
|
+
* appearance", with native checked state among the redundant cues, so selection
|
|
64
|
+
* never rests on colour alone.
|
|
65
|
+
*/
|
|
66
|
+
export function ChoiceChip({
|
|
67
|
+
children, size = 'standard', selected, defaultSelected, showMark = false,
|
|
68
|
+
disabled = false, name, value, id, className = '', onChange, ...rest
|
|
69
|
+
}) {
|
|
70
|
+
if (!OL8_CHIP_SIZES.includes(size)) throw new Error(`[ol8] unknown chip size "${size}"`);
|
|
71
|
+
if (!children) throw new Error('[ol8] Choice Chip requires a label');
|
|
72
|
+
const auto = useId();
|
|
73
|
+
const uid = id ?? `ol8-chip-${auto}`;
|
|
74
|
+
const isOn = selected ?? defaultSelected ?? false;
|
|
75
|
+
|
|
76
|
+
return createElement('label', {
|
|
77
|
+
className: `ol8-chip${className ? ` ${className}` : ''}`,
|
|
78
|
+
htmlFor: uid,
|
|
79
|
+
'data-ol8-size': size,
|
|
80
|
+
'data-ol8-selection': isOn ? 'selected' : 'inactive',
|
|
81
|
+
'data-ol8-availability': disabled ? 'disabled' : 'enabled',
|
|
82
|
+
}, [
|
|
83
|
+
createElement('input', {
|
|
84
|
+
key: 'input', ...rest,
|
|
85
|
+
type: 'checkbox', className: 'ol8-chip__input', id: uid, name, value, disabled, onChange,
|
|
86
|
+
// A controlled chip with nothing listening is a display of state, not a
|
|
87
|
+
// control. Saying so is what React asks for, and it is the truth: the
|
|
88
|
+
// chip cannot change anything until someone handles the change.
|
|
89
|
+
...(selected !== undefined
|
|
90
|
+
? { checked: selected, ...(onChange ? {} : { readOnly: true }) }
|
|
91
|
+
: defaultSelected !== undefined ? { defaultChecked: defaultSelected } : {}),
|
|
92
|
+
}),
|
|
93
|
+
createElement(ChipShape, {
|
|
94
|
+
key: 'shape', uid, label: children,
|
|
95
|
+
mark: showMark
|
|
96
|
+
? createElement('span', { key: 'mark', className: 'ol8-chip__mark', 'aria-hidden': 'true' },
|
|
97
|
+
createElement(IconGlyph, { name: 'check' }))
|
|
98
|
+
: null,
|
|
99
|
+
}),
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createElement, forwardRef, useId } from 'react';
|
|
2
|
+
import { SelectionIndicator } from '../atoms/SelectionIndicator.js';
|
|
3
|
+
import { OL8_CHOICE_KINDS, OL8_CHOICE_SIZES, indicatorSizeFor, selectionStateFor } from '../foundations/geometry.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MOLECULE. A real input owns state, focus and form participation; this never
|
|
7
|
+
* reimplements toggling. `indeterminate` is the one thing markup cannot express,
|
|
8
|
+
* so React sets it through a ref callback rather than an attribute.
|
|
9
|
+
*/
|
|
10
|
+
export const ChoiceItem = forwardRef(function ChoiceItem({
|
|
11
|
+
kind, children, size = 'standard', checked, defaultChecked, mixed = false,
|
|
12
|
+
disabled = false, invalid = false, description, name, value, required = false,
|
|
13
|
+
material = 'regular', id, className = '', onChange, ...rest
|
|
14
|
+
}, ref) {
|
|
15
|
+
if (!OL8_CHOICE_KINDS.includes(kind)) throw new Error(`[ol8] unknown choice kind "${kind}"`);
|
|
16
|
+
if (!OL8_CHOICE_SIZES.includes(size)) throw new Error(`[ol8] unknown choice size "${size}"`);
|
|
17
|
+
const auto = useId();
|
|
18
|
+
const rowId = id ?? `ol8-choice-${auto}`;
|
|
19
|
+
const descId = description ? `${rowId}-desc` : undefined;
|
|
20
|
+
const isOn = checked ?? defaultChecked ?? false;
|
|
21
|
+
|
|
22
|
+
return createElement('label', {
|
|
23
|
+
className: `ol8-choice ol8-choice--${kind}${className ? ` ${className}` : ''}`,
|
|
24
|
+
htmlFor: rowId,
|
|
25
|
+
'data-ol8-size': size,
|
|
26
|
+
'data-ol8-kind': kind,
|
|
27
|
+
'data-ol8-availability': disabled ? 'disabled' : 'enabled',
|
|
28
|
+
...(invalid ? { 'data-ol8-validity': 'invalid' } : {}),
|
|
29
|
+
...(material === 'gem' ? { 'data-ol8-material': 'gem' } : {}),
|
|
30
|
+
}, [
|
|
31
|
+
createElement('input', {
|
|
32
|
+
key: 'input', ...rest,
|
|
33
|
+
ref: (node) => {
|
|
34
|
+
if (node) node.indeterminate = kind === 'checkbox' && mixed;
|
|
35
|
+
if (typeof ref === 'function') ref(node); else if (ref) ref.current = node;
|
|
36
|
+
},
|
|
37
|
+
type: kind === 'radio' ? 'radio' : 'checkbox',
|
|
38
|
+
className: 'ol8-choice__input', id: rowId, name, value, required, disabled, onChange,
|
|
39
|
+
...(checked !== undefined ? { checked } : defaultChecked !== undefined ? { defaultChecked } : {}),
|
|
40
|
+
...(descId ? { 'aria-describedby': descId } : {}),
|
|
41
|
+
...(invalid ? { 'aria-invalid': 'true' } : {}),
|
|
42
|
+
...(kind === 'switch' ? { role: 'switch' } : {}),
|
|
43
|
+
}),
|
|
44
|
+
createElement('span', { key: 'surface', className: 'ol8-choice__surface', 'aria-hidden': 'true' }),
|
|
45
|
+
createElement('span', { key: 'indicator', className: 'ol8-choice__indicator' },
|
|
46
|
+
createElement(SelectionIndicator, {
|
|
47
|
+
kind, size: indicatorSizeFor(size), disabled,
|
|
48
|
+
selection: selectionStateFor(kind, { checked: isOn, mixed }),
|
|
49
|
+
})),
|
|
50
|
+
createElement('span', { key: 'text', className: 'ol8-choice__text' }, [
|
|
51
|
+
createElement('span', { key: 'label', className: 'ol8-choice__label' }, children),
|
|
52
|
+
description ? createElement('span', { key: 'desc', className: 'ol8-choice__description', id: descId }, description) : null,
|
|
53
|
+
]),
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createElement, forwardRef } from 'react';
|
|
2
|
+
import { OL8_FORM_MESSAGE_TONES, OL8_FORM_MESSAGE_ICONS } from '../foundations/geometry.js';
|
|
3
|
+
import { Icon } from '../atoms/Icon.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MOLECULE. Figma 177:13: "Tone maps to semantic message tone; Pending is
|
|
7
|
+
* resolved through validationStatus rather than treated as a fifth error tone.
|
|
8
|
+
* Runtime role, status announcement and relationships live in code."
|
|
9
|
+
*
|
|
10
|
+
* So the role and the live region are decided here, from the tone: critical is
|
|
11
|
+
* the only tone that interrupts, because it is the only one that stops a person
|
|
12
|
+
* from finishing.
|
|
13
|
+
*/
|
|
14
|
+
export const FormMessage = forwardRef(function FormMessage({
|
|
15
|
+
children, tone = 'critical', icon = true, className = '', ...rest
|
|
16
|
+
}, ref) {
|
|
17
|
+
if (!OL8_FORM_MESSAGE_TONES.includes(tone)) {
|
|
18
|
+
throw new Error(`[ol8] unknown form message tone "${tone}"`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return createElement('div', {
|
|
22
|
+
...rest, ref,
|
|
23
|
+
className: `ol8-form-message${className ? ` ${className}` : ''}`,
|
|
24
|
+
'data-ol8-tone': tone,
|
|
25
|
+
role: tone === 'critical' ? 'alert' : 'status',
|
|
26
|
+
'aria-live': tone === 'critical' ? 'assertive' : 'polite',
|
|
27
|
+
},
|
|
28
|
+
icon
|
|
29
|
+
? createElement(Icon, { key: 'icon', name: OL8_FORM_MESSAGE_ICONS[tone], size: 18, className: 'ol8-form-message__icon' })
|
|
30
|
+
: null,
|
|
31
|
+
createElement('span', { key: 'text', className: 'ol8-form-message__text' }, children),
|
|
32
|
+
);
|
|
33
|
+
});
|