gbs-add-block 1.2.18 → 1.2.19

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.
Files changed (41) hide show
  1. package/README.md +2 -2
  2. package/index.cjs +5 -0
  3. package/package.json +1 -1
  4. package/source/beta-components/number-input/README.md +53 -0
  5. package/source/beta-components/number-input/__tests__/core.test.ts +151 -0
  6. package/source/beta-components/number-input/core/format.ts +37 -0
  7. package/source/beta-components/number-input/core/index.ts +4 -0
  8. package/source/beta-components/number-input/core/parse.ts +118 -0
  9. package/source/beta-components/number-input/core/step.ts +73 -0
  10. package/source/beta-components/number-input/core/types.ts +32 -0
  11. package/source/beta-components/number-input/index.ts +5 -0
  12. package/source/beta-components/number-input/react/NumberInput.tsx +465 -0
  13. package/source/beta-components/number-input/react/icons.tsx +17 -0
  14. package/source/beta-components/number-input/react/locale.ts +7 -0
  15. package/source/beta-components/number-input/react/props.ts +11 -0
  16. package/source/beta-components/number-input/styles.css +231 -0
  17. package/source/beta-components/radio-group/README.md +58 -0
  18. package/source/beta-components/radio-group/__tests__/core.test.ts +64 -0
  19. package/source/beta-components/radio-group/core/group.ts +36 -0
  20. package/source/beta-components/radio-group/core/index.ts +2 -0
  21. package/source/beta-components/radio-group/core/types.ts +20 -0
  22. package/source/beta-components/radio-group/index.ts +9 -0
  23. package/source/beta-components/radio-group/react/Radio.tsx +144 -0
  24. package/source/beta-components/radio-group/react/RadioGroup.tsx +179 -0
  25. package/source/beta-components/radio-group/react/context.ts +18 -0
  26. package/source/beta-components/radio-group/react/locale.ts +5 -0
  27. package/source/beta-components/radio-group/react/props.ts +12 -0
  28. package/source/beta-components/radio-group/styles.css +270 -0
  29. package/source/beta-components/shared/__tests__/position.test.ts +143 -170
  30. package/source/beta-components/shared/core/position.ts +2 -15
  31. package/source/beta-components/shared/react/Popover.tsx +5 -18
  32. package/source/beta-components/switch/README.md +46 -0
  33. package/source/beta-components/switch/__tests__/core.test.ts +64 -0
  34. package/source/beta-components/switch/core/index.ts +2 -0
  35. package/source/beta-components/switch/core/toggle.ts +54 -0
  36. package/source/beta-components/switch/core/types.ts +9 -0
  37. package/source/beta-components/switch/index.ts +5 -0
  38. package/source/beta-components/switch/react/Switch.tsx +219 -0
  39. package/source/beta-components/switch/react/locale.ts +5 -0
  40. package/source/beta-components/switch/react/props.ts +12 -0
  41. package/source/beta-components/switch/styles.css +229 -0
@@ -0,0 +1,231 @@
1
+ /*
2
+ * NumberInput styles.
3
+ *
4
+ * The field is a text input, so it looks and sizes exactly like the Input
5
+ * component; the spinner is two buttons the stylesheet owns rather than the
6
+ * browser's, which cannot be styled. Tokens read the shared --gbs-* variables
7
+ * when they are set on an ancestor (set them on :root to theme every component
8
+ * at once). Rules live in the `components` layer, so utility classes passed
9
+ * through `className` / `classNames` override them.
10
+ */
11
+ @layer theme, base, components, utilities;
12
+
13
+ @layer components {
14
+ .nm-root {
15
+ --nm-fg: var(--gbs-fg, var(--dg-fg, light-dark(#18181b, #f4f4f5)));
16
+ --nm-muted: var(--gbs-muted, var(--dg-muted, light-dark(#71717a, #a1a1aa)));
17
+ --nm-border: var(--gbs-border, var(--dg-border, light-dark(#e4e4e7, #27272a)));
18
+ --nm-input-bg: var(--gbs-input-bg, var(--dg-input-bg, light-dark(#ffffff, #121216)));
19
+ --nm-readonly-bg: var(--gbs-readonly-bg, light-dark(#fafafa, #0e0e12));
20
+ --nm-hover: var(--gbs-hover, var(--dg-hover, light-dark(#f4f4f5, #1f1f23)));
21
+ --nm-accent: var(--gbs-accent, var(--dg-accent, light-dark(#2563eb, #60a5fa)));
22
+ --nm-focus: var(--gbs-focus, var(--dg-focus, light-dark(#2563eb, #60a5fa)));
23
+ --nm-danger: var(--gbs-danger, var(--dg-danger, light-dark(#dc2626, #f87171)));
24
+ --nm-radius: var(--gbs-radius, var(--dg-radius, 8px));
25
+ --nm-font-size: var(--gbs-font-size, var(--dg-font-size, 13px));
26
+ --nm-height: 36px;
27
+ --nm-px: 10px;
28
+
29
+ color-scheme: inherit;
30
+ display: flex;
31
+ flex-direction: column;
32
+ gap: 4px;
33
+ min-width: 0;
34
+ font-size: var(--nm-font-size);
35
+ line-height: 1.4;
36
+ color: var(--nm-fg);
37
+ }
38
+
39
+ :where(.dark, [data-theme="dark"]) .nm-root {
40
+ color-scheme: dark;
41
+ }
42
+ :where(.light, [data-theme="light"]) .nm-root {
43
+ color-scheme: light;
44
+ }
45
+
46
+ :where(.nm-root) *,
47
+ :where(.nm-root) *::before,
48
+ :where(.nm-root) *::after {
49
+ box-sizing: border-box;
50
+ }
51
+
52
+ .nm-root[data-size="sm"] {
53
+ --nm-height: 30px;
54
+ --nm-px: 8px;
55
+ --nm-font-size: 12px;
56
+ }
57
+ .nm-root[data-size="lg"] {
58
+ --nm-height: 44px;
59
+ --nm-px: 12px;
60
+ --nm-font-size: 14px;
61
+ }
62
+
63
+ .nm-label {
64
+ font-weight: 500;
65
+ }
66
+ .nm-label[data-required]::after {
67
+ content: " *";
68
+ color: var(--nm-danger);
69
+ }
70
+
71
+ /* -------------------------------------------------------------- control */
72
+
73
+ .nm-control {
74
+ display: flex;
75
+ align-items: stretch;
76
+ gap: 6px;
77
+ min-width: 0;
78
+ height: var(--nm-height);
79
+ padding-inline-start: var(--nm-px);
80
+ border: 1px solid var(--nm-border);
81
+ border-radius: var(--nm-radius);
82
+ background: var(--nm-input-bg);
83
+ transition:
84
+ border-color 120ms ease,
85
+ box-shadow 120ms ease;
86
+ }
87
+ .nm-control:hover:not([data-disabled]) {
88
+ border-color: color-mix(in oklab, var(--nm-border) 60%, var(--nm-fg));
89
+ }
90
+ .nm-control:has(.nm-input:focus-visible) {
91
+ border-color: var(--nm-focus);
92
+ box-shadow: 0 0 0 1px var(--nm-focus);
93
+ }
94
+ .nm-control[data-invalid] {
95
+ border-color: var(--nm-danger);
96
+ }
97
+ .nm-control[data-invalid]:has(.nm-input:focus-visible) {
98
+ box-shadow: 0 0 0 1px var(--nm-danger);
99
+ }
100
+ .nm-control[data-disabled] {
101
+ opacity: 0.6;
102
+ cursor: not-allowed;
103
+ }
104
+ .nm-control[data-readonly] {
105
+ background: var(--nm-readonly-bg);
106
+ }
107
+
108
+ .nm-input {
109
+ flex: 1 1 auto;
110
+ min-width: 0;
111
+ padding: 0;
112
+ padding-inline-end: var(--nm-px);
113
+ border: 0;
114
+ background: none;
115
+ color: inherit;
116
+ font: inherit;
117
+ /* Figures of the same width, so a column of amounts lines up and the
118
+ number does not jitter as it is typed. */
119
+ font-variant-numeric: tabular-nums;
120
+ outline: none;
121
+ }
122
+ .nm-input::placeholder {
123
+ color: var(--nm-muted);
124
+ }
125
+ .nm-input:disabled {
126
+ cursor: not-allowed;
127
+ }
128
+
129
+ .nm-adornment {
130
+ display: inline-flex;
131
+ flex: none;
132
+ align-items: center;
133
+ color: var(--nm-muted);
134
+ white-space: nowrap;
135
+ }
136
+ .nm-adornment[data-side="trailing"] {
137
+ padding-inline-end: 2px;
138
+ }
139
+
140
+ .nm-icon-button {
141
+ display: inline-flex;
142
+ flex: none;
143
+ align-self: center;
144
+ align-items: center;
145
+ justify-content: center;
146
+ width: 20px;
147
+ height: 20px;
148
+ padding: 0;
149
+ border: 0;
150
+ border-radius: 4px;
151
+ background: none;
152
+ color: var(--nm-muted);
153
+ cursor: pointer;
154
+ }
155
+ .nm-icon-button:hover {
156
+ background: var(--nm-hover);
157
+ color: var(--nm-fg);
158
+ }
159
+
160
+ /* -------------------------------------------------------------- stepper */
161
+
162
+ .nm-stepper {
163
+ display: flex;
164
+ flex: none;
165
+ flex-direction: column;
166
+ width: 22px;
167
+ border-inline-start: 1px solid var(--nm-border);
168
+ }
169
+
170
+ .nm-step {
171
+ display: flex;
172
+ flex: 1 1 50%;
173
+ align-items: center;
174
+ justify-content: center;
175
+ min-height: 0;
176
+ padding: 0;
177
+ border: 0;
178
+ background: none;
179
+ color: var(--nm-muted);
180
+ cursor: pointer;
181
+ /* The buttons repeat while held, so the browser must not read the hold as
182
+ a scroll gesture or a text selection. */
183
+ touch-action: none;
184
+ user-select: none;
185
+ }
186
+ .nm-step:first-child {
187
+ border-start-end-radius: calc(var(--nm-radius) - 1px);
188
+ border-block-end: 1px solid var(--nm-border);
189
+ }
190
+ .nm-step:last-child {
191
+ border-end-end-radius: calc(var(--nm-radius) - 1px);
192
+ }
193
+ .nm-step:hover:not(:disabled) {
194
+ background: var(--nm-hover);
195
+ color: var(--nm-fg);
196
+ }
197
+ .nm-step:active:not(:disabled) {
198
+ background: color-mix(in oklab, var(--nm-hover) 70%, var(--nm-accent));
199
+ }
200
+ .nm-step:disabled {
201
+ opacity: 0.4;
202
+ cursor: not-allowed;
203
+ }
204
+
205
+ /* ----------------------------------------------------------------- text */
206
+
207
+ .nm-description {
208
+ color: var(--nm-muted);
209
+ text-wrap: pretty;
210
+ }
211
+ .nm-error {
212
+ color: var(--nm-danger);
213
+ text-wrap: pretty;
214
+ }
215
+
216
+ @media (prefers-reduced-motion: reduce) {
217
+ .nm-control {
218
+ transition: none;
219
+ }
220
+ }
221
+
222
+ @media (forced-colors: active) {
223
+ .nm-control {
224
+ border: 1px solid CanvasText;
225
+ }
226
+ .nm-stepper,
227
+ .nm-step:first-child {
228
+ border-color: CanvasText;
229
+ }
230
+ }
231
+ }
@@ -0,0 +1,58 @@
1
+ # Radio and RadioGroup
2
+
3
+ One choice out of a few, for React 19, styled to match the rest of the library.
4
+ The buttons are real `<input type="radio">` elements sharing a `name`, so the
5
+ arrow keys roam the group, the group is one tab stop, and the browser validates
6
+ it as one required field — without a line of JavaScript. No runtime
7
+ dependencies besides React.
8
+
9
+ ```ts
10
+ import { Radio, RadioGroup } from "@/components/radio-group";
11
+ import "@/components/radio-group/styles.css";
12
+ ```
13
+
14
+ ```tsx
15
+ <RadioGroup
16
+ label="Send the report"
17
+ name="frequency"
18
+ options={["Daily", "Weekly", "Monthly"]}
19
+ value={frequency}
20
+ onValueChange={setFrequency}
21
+ />
22
+
23
+ {/* Tiles, for choices that need explaining. */}
24
+ <RadioGroup
25
+ label="Plan"
26
+ variant="card"
27
+ orientation="horizontal"
28
+ options={[
29
+ { value: "starter", label: "Starter", description: "Up to 3 projects" },
30
+ { value: "team", label: "Team", description: "Unlimited projects and members" },
31
+ ]}
32
+ defaultValue="team"
33
+ />
34
+
35
+ {/* Or compose them, when each choice needs its own markup. */}
36
+ <RadioGroup label="Delivery" name="delivery">
37
+ <Radio value="standard" label="Standard" description="3–5 days" />
38
+ <Radio value="express" label="Express" description="Tomorrow" disabled />
39
+ </RadioGroup>
40
+ ```
41
+
42
+ **RadioGroup:** `value` / `defaultValue` / `onValueChange` (`string | null`),
43
+ `options` (plain strings or full options) or `<Radio value>` children, `label`
44
+ (legend), `description`, `error`, `required`, `disabled`, `name`, `size`,
45
+ `orientation`, `variant` (`default` `card`), `clearable`, `classNames` (`root`
46
+ `legend` `description` `items` `clear` `error`), `localeText`.
47
+
48
+ **Radio:** `value`, `label`, `description`, `error`, `size`, `disabled`,
49
+ `classNames` (`root` `control` `input` `label` `description` `error`), `ref`
50
+ (the `<input>`), and every `<input>` attribute.
51
+
52
+ A radio cannot be unselected by clicking or by keyboard — that is what the
53
+ control is — so an optional question needs either `clearable`, which offers a
54
+ button that empties the group, or a "None" option of its own.
55
+
56
+ `normalizeOptions`, `resolveValue` and `hasEnabledOption` are exported from the
57
+ framework-free `core`. `resolveValue` is what makes a value the group does not
58
+ offer select nothing, rather than guessing at an option the record never chose.
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { hasEnabledOption, normalizeOptions, resolveValue } from "../core/group";
3
+
4
+ describe("normalizeOptions", () => {
5
+ it("turns plain strings into options labelled with themselves", () => {
6
+ expect(normalizeOptions(["daily", "weekly"])).toEqual([
7
+ { value: "daily", label: "daily" },
8
+ { value: "weekly", label: "weekly" },
9
+ ]);
10
+ });
11
+
12
+ it("leaves full options as they are, mixed in with strings", () => {
13
+ const options = normalizeOptions([
14
+ "daily",
15
+ { value: "weekly", label: "Every week", description: "On Mondays", disabled: true },
16
+ ]);
17
+ expect(options[1]).toEqual({
18
+ value: "weekly",
19
+ label: "Every week",
20
+ description: "On Mondays",
21
+ disabled: true,
22
+ });
23
+ });
24
+ });
25
+
26
+ describe("resolveValue", () => {
27
+ const options = normalizeOptions([
28
+ "email",
29
+ { value: "sms", label: "SMS", disabled: true },
30
+ ]);
31
+
32
+ it("selects the matching option", () => {
33
+ expect(resolveValue(options, "email")).toBe("email");
34
+ });
35
+
36
+ it("selects a disabled option, because a locked-in choice still has to show", () => {
37
+ expect(resolveValue(options, "sms")).toBe("sms");
38
+ });
39
+
40
+ it("selects nothing for a value the group does not offer", () => {
41
+ // A stale value from a record: picking some other option instead would
42
+ // claim a choice that was never made.
43
+ expect(resolveValue(options, "pigeon")).toBeNull();
44
+ });
45
+
46
+ it("selects nothing for no value at all", () => {
47
+ expect(resolveValue(options, null)).toBeNull();
48
+ expect(resolveValue(options, undefined)).toBeNull();
49
+ });
50
+ });
51
+
52
+ describe("hasEnabledOption", () => {
53
+ const options = normalizeOptions(["email", { value: "sms", label: "SMS", disabled: true }]);
54
+
55
+ it("is true while something can still be chosen", () => {
56
+ expect(hasEnabledOption(options, false)).toBe(true);
57
+ });
58
+
59
+ it("is false when the group is disabled or has nothing to offer", () => {
60
+ expect(hasEnabledOption(options, true)).toBe(false);
61
+ expect(hasEnabledOption(normalizeOptions([{ value: "sms", label: "SMS", disabled: true }]), false)).toBe(false);
62
+ expect(hasEnabledOption([], false)).toBe(false);
63
+ });
64
+ });
@@ -0,0 +1,36 @@
1
+ import type { RadioOption } from "./types";
2
+
3
+ /**
4
+ * Accepts `["daily", "weekly"]` as well as the full option objects, because a
5
+ * list of plain values is the common case and writing `{ value: x, label: x }`
6
+ * for each one adds nothing.
7
+ */
8
+ export function normalizeOptions(
9
+ options: readonly (string | RadioOption)[],
10
+ ): RadioOption[] {
11
+ return options.map((option) =>
12
+ typeof option === "string" ? { value: option, label: option } : option,
13
+ );
14
+ }
15
+
16
+ /**
17
+ * The value the group should actually show as selected.
18
+ *
19
+ * A value that matches no option selects nothing, which is what native radios
20
+ * do and the right answer for a stale value from a server: showing an arbitrary
21
+ * option instead would claim a choice the record never made. A *disabled*
22
+ * option still counts — a locked-in choice has to be visible, or the group
23
+ * would look empty while refusing to let anything be chosen.
24
+ */
25
+ export function resolveValue(
26
+ options: readonly RadioOption[],
27
+ value: string | null | undefined,
28
+ ): string | null {
29
+ if (value === null || value === undefined) return null;
30
+ return options.some((option) => option.value === value) ? value : null;
31
+ }
32
+
33
+ /** Whether anything in the group can still be chosen, for a `clear` button. */
34
+ export function hasEnabledOption(options: readonly RadioOption[], disabled: boolean): boolean {
35
+ return !disabled && options.some((option) => !option.disabled);
36
+ }
@@ -0,0 +1,2 @@
1
+ export { hasEnabledOption, normalizeOptions, resolveValue } from "./group";
2
+ export type * from "./types";
@@ -0,0 +1,20 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export type RadioSize = "sm" | "md" | "lg";
4
+
5
+ /** `card` draws each choice as a bordered tile, for choices that carry detail. */
6
+ export type RadioVariant = "default" | "card";
7
+
8
+ export type RadioOrientation = "vertical" | "horizontal";
9
+
10
+ export interface RadioOption {
11
+ value: string;
12
+ label: ReactNode;
13
+ description?: ReactNode;
14
+ disabled?: boolean;
15
+ }
16
+
17
+ export interface RadioGroupLocaleText {
18
+ /** The button that empties a `clearable` group. */
19
+ clear: string;
20
+ }
@@ -0,0 +1,9 @@
1
+ export { Radio } from "./react/Radio";
2
+ export type { RadioProps } from "./react/Radio";
3
+ export { RadioGroup } from "./react/RadioGroup";
4
+ export type { RadioGroupProps } from "./react/RadioGroup";
5
+ export { RadioGroupContext } from "./react/context";
6
+ export type { RadioGroupContextValue } from "./react/context";
7
+ export { defaultRadioGroupText } from "./react/locale";
8
+ export type { RadioGroupSlot, RadioSlot } from "./react/props";
9
+ export * from "./core";
@@ -0,0 +1,144 @@
1
+ "use client";
2
+
3
+ import {
4
+ use,
5
+ useId,
6
+ useImperativeHandle,
7
+ useRef,
8
+ type ChangeEvent,
9
+ type ChangeEventHandler,
10
+ type InputHTMLAttributes,
11
+ type ReactNode,
12
+ type Ref,
13
+ } from "react";
14
+ import { useControllableState } from "../../shared/react/useControllableState";
15
+ import { describeField } from "../../shared/core/field";
16
+ import type { RadioSize } from "../core/types";
17
+ import { RadioGroupContext } from "./context";
18
+ import { cx, type RadioSlot } from "./props";
19
+
20
+ export interface RadioProps
21
+ extends Omit<
22
+ InputHTMLAttributes<HTMLInputElement>,
23
+ "type" | "size" | "checked" | "defaultChecked" | "onChange"
24
+ > {
25
+ /** The value this choice contributes to its group. */
26
+ value: string;
27
+ /** Selected (controlled). Inside a RadioGroup the group decides instead. */
28
+ checked?: boolean;
29
+ defaultChecked?: boolean;
30
+ onCheckedChange?(checked: boolean): void;
31
+ onChange?: ChangeEventHandler<HTMLInputElement>;
32
+ label?: ReactNode;
33
+ description?: ReactNode;
34
+ error?: ReactNode;
35
+ size?: RadioSize;
36
+ classNames?: Partial<Record<RadioSlot, string>>;
37
+ /** The `<input>` element. */
38
+ ref?: Ref<HTMLInputElement>;
39
+ }
40
+
41
+ /**
42
+ * One choice, as a native radio. Inside a `RadioGroup` the group owns the
43
+ * value; on its own it behaves like any other radio sharing a `name`.
44
+ *
45
+ * Nothing here manages focus: radios that share a `name` already roam with the
46
+ * arrow keys, skip the disabled ones, wrap around and leave one tab stop for
47
+ * the whole group — in every browser, and without JavaScript.
48
+ */
49
+ export function Radio(props: RadioProps) {
50
+ const {
51
+ ref,
52
+ value,
53
+ checked: checkedProp,
54
+ defaultChecked = false,
55
+ onCheckedChange,
56
+ onChange,
57
+ label,
58
+ description,
59
+ error,
60
+ size,
61
+ className,
62
+ classNames,
63
+ style,
64
+ id: idProp,
65
+ disabled,
66
+ required,
67
+ name,
68
+ "aria-describedby": describedByProp,
69
+ ...inputProps
70
+ } = props;
71
+
72
+ const group = use(RadioGroupContext);
73
+ const reactId = useId();
74
+ const id = idProp ?? reactId;
75
+ const inputRef = useRef<HTMLInputElement>(null);
76
+ useImperativeHandle(ref, () => inputRef.current as HTMLInputElement, []);
77
+
78
+ const [own, setOwn] = useControllableState(checkedProp, defaultChecked);
79
+ const checked = group !== null ? group.value === value : own;
80
+
81
+ const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
82
+ if (group !== null) group.select(value);
83
+ else setOwn(event.target.checked);
84
+ onChange?.(event);
85
+ onCheckedChange?.(event.target.checked);
86
+ };
87
+
88
+ const isDisabled = disabled ?? group?.disabled;
89
+ const invalid = Boolean(error) || (group?.invalid ?? false);
90
+ const describedBy = describeField(id, { extra: describedByProp, description, error });
91
+
92
+ return (
93
+ <div
94
+ className={cx("rd-root", classNames?.root, className)}
95
+ style={style}
96
+ data-size={size ?? group?.size ?? "md"}
97
+ data-variant={group?.variant ?? "default"}
98
+ data-checked={checked || undefined}
99
+ data-disabled={isDisabled || undefined}
100
+ data-invalid={invalid ? "" : undefined}
101
+ >
102
+ <span className={cx("rd-control", classNames?.control)}>
103
+ <input
104
+ {...inputProps}
105
+ ref={inputRef}
106
+ id={id}
107
+ type="radio"
108
+ className={cx("rd-input", classNames?.input)}
109
+ checked={checked}
110
+ value={value}
111
+ name={name ?? group?.name}
112
+ disabled={isDisabled}
113
+ // Every radio in a group carries `required`, which is how a browser
114
+ // validates the group as a whole rather than one button in it.
115
+ required={required ?? group?.required}
116
+ aria-invalid={invalid || undefined}
117
+ aria-describedby={describedBy}
118
+ onChange={handleChange}
119
+ />
120
+ <span className="rd-dot" aria-hidden="true" />
121
+ </span>
122
+
123
+ {(label || description || error) && (
124
+ <span className="rd-text">
125
+ {label && (
126
+ <label htmlFor={id} className={cx("rd-label", classNames?.label)}>
127
+ {label}
128
+ </label>
129
+ )}
130
+ {description && (
131
+ <span id={`${id}-description`} className={cx("rd-description", classNames?.description)}>
132
+ {description}
133
+ </span>
134
+ )}
135
+ {error && (
136
+ <span id={`${id}-error`} className={cx("rd-error", classNames?.error)} role="alert">
137
+ {error}
138
+ </span>
139
+ )}
140
+ </span>
141
+ )}
142
+ </div>
143
+ );
144
+ }