hallo-kit 0.3.0 → 0.3.1

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.
@@ -1,215 +1,215 @@
1
- 'use client'
2
-
3
- import * as React from 'react'
4
-
5
- import * as SelectPrimitive from '@radix-ui/react-select'
6
-
7
- import { Icon } from '../Icon'
8
- import { cn } from '../../lib/utils'
9
-
10
- export interface SelectOption {
11
- value: string
12
- label: string
13
- /** Контент в начале пункта (флаг/иконка/что угодно). Показывается и в списке, и в значении триггера. */
14
- startContent?: React.ReactNode
15
- disabled?: boolean
16
- }
17
-
18
- // group — чтобы шеврон в триггере доворачивался по data-state. Размер — через
19
- // responsiveSize (CSS-адаптив без флэша), а не cva. Иконки — в em (масштаб за font-size).
20
- const SELECT_TRIGGER_BASE =
21
- 'group inline-flex w-full cursor-pointer items-center bg-card text-foreground shadow-sm transition-[color,box-shadow] outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background data-disabled:pointer-events-none data-disabled:opacity-50 data-placeholder:text-muted-foreground rounded-full'
22
- const SELECT_TRIGGER_SIZE: Record<'sm' | 'md' | 'lg', string> = {
23
- sm: 'gap-fl-10/12 min-h-fl-28/32 px-fl-10/12 py-fl-2/4 text-button-s',
24
- md: 'gap-fl-14/16 min-h-fl-36/40 px-fl-14/16 py-fl-6/8 text-button-m',
25
- lg: 'gap-fl-18/20 min-h-fl-44/48 px-fl-18/20 py-fl-10/12 text-button-l',
26
- }
27
- // Пункт = носитель подсветки; текст инсетит px ВНУТРИ пункта (выравнивание с триггером).
28
- const SELECT_ITEM_SIZE: Record<'sm' | 'md' | 'lg', string> = {
29
- sm: 'rounded-sm px-fl-6/8 py-fl-6/8 text-button-s',
30
- md: 'rounded-md px-fl-10/12 py-fl-8/10 text-button-m',
31
- lg: 'rounded-lg px-fl-14/16 py-fl-10/12 text-button-l',
32
- }
33
- // Боковой инсет разделителя = px пункта → линия выровнена с текстом и уже подсветки.
34
- const SELECT_DIVIDER_INSET: Record<'sm' | 'md' | 'lg', string> = {
35
- sm: 'mx-2',
36
- md: 'mx-3',
37
- lg: 'mx-4',
38
- }
39
- const SELECT_ROUNDED_CONTENT: Record<'sm' | 'md' | 'lg', string> = {
40
- sm: 'rounded-sm',
41
- md: 'rounded-md',
42
- lg: 'rounded-lg',
43
- }
44
-
45
- export interface SelectProps {
46
- options: SelectOption[]
47
- /** Размер: `sm | md | lg`. По умолчанию md. Каждый размер сам fluid-клампится. */
48
- size?: 'sm' | 'md' | 'lg'
49
- value?: string
50
- defaultValue?: string
51
- onValueChange?: (value: string) => void
52
- placeholder?: string
53
- disabled?: boolean
54
- /** Фиксированный контент в начале триггера (помимо иконки выбранного пункта): лейбл, иконка и т.п. */
55
- startContent?: React.ReactNode
56
- /** Класс на триггер. */
57
- className?: string
58
- /** Класс на выпадающую панель. */
59
- contentClassName?: string
60
- /**
61
- * В триггере показывать только `startContent` выбранного пункта (напр. флаг),
62
- * без текста; в списке пункты остаются с текстом. Для компактных переключателей
63
- * (язык). По умолчанию false.
64
- */
65
- compactValue?: boolean
66
- /**
67
- * Доступное имя контрола. Заведено явно: у компонента нет rest-спреда, а TypeScript
68
- * не проверяет лишние JSX-атрибуты с дефисом — `aria-label` на <Select> молча
69
- * компилировался и никуда не попадал, оставляя контрол без доступного имени.
70
- */
71
- 'aria-label'?: string
72
- 'aria-labelledby'?: string
73
- }
74
-
75
- // Флаг/иконка + подпись. Кладём ВНУТРЬ ItemText, чтобы триггер (Select.Value)
76
- // отрисовал ровно то же — иконку слева и текст, как в макете.
77
- function OptionContent({ option }: { option: SelectOption }) {
78
- return (
79
- // leading-normal: токены text-button-* имеют line-height:1, из-за чего нижние
80
- // выносные элементы букв (g/y/р) обрезаются строчным боксом. Локально возвращаем запас.
81
- <span className='flex items-center gap-2.5 leading-normal'>
82
- {option.startContent ? (
83
- <span className='inline-flex shrink-0 items-center'>
84
- {option.startContent}
85
- </span>
86
- ) : null}
87
- {option.label}
88
- </span>
89
- )
90
- }
91
-
92
- export function Select({
93
- options,
94
- value,
95
- defaultValue,
96
- onValueChange,
97
- placeholder,
98
- disabled,
99
- startContent,
100
- size,
101
- className,
102
- contentClassName,
103
- compactValue = false,
104
- 'aria-label': ariaLabel,
105
- 'aria-labelledby': ariaLabelledBy,
106
- }: SelectProps) {
107
- // Только для контролируемого значения: в неконтролируемом режиме выбранный пункт
108
- // знает лишь Radix, и подменять его нечем.
109
- const selectedOption = value != null ? options.find((o) => o.value === value) : undefined
110
-
111
- const triggerSize = SELECT_TRIGGER_SIZE[size ?? 'md']
112
- const itemSize = SELECT_ITEM_SIZE[size ?? 'md']
113
- const dividerInset = SELECT_DIVIDER_INSET[size ?? 'md']
114
- const roundedContent = SELECT_ROUNDED_CONTENT[size ?? 'md']
115
-
116
- return (
117
- <SelectPrimitive.Root
118
- value={value}
119
- defaultValue={defaultValue}
120
- onValueChange={onValueChange}
121
- disabled={disabled}
122
- >
123
- <SelectPrimitive.Trigger
124
- aria-label={ariaLabel}
125
- aria-labelledby={ariaLabelledBy}
126
- className={cn(SELECT_TRIGGER_BASE, triggerSize, className)}
127
- >
128
- {startContent ? (
129
- <span className='flex shrink-0 items-center text-muted-foreground'>
130
- {startContent}
131
- </span>
132
- ) : null}
133
- {/* Значение рендерим САМИ, а не отдаём на откуп SelectPrimitive.Value.
134
- Radix берёт текст из смонтированных Item'ов, а они живут в портале и
135
- на сервере не рендерятся — из-за чего триггер приезжает ПУСТЫМ и
136
- остаётся таким до гидрации. На медленной сети это сотни миллисекунд
137
- пустого поля там, где уже должно стоять выбранное значение.
138
- Когда значение контролируемое, лейбл известен сразу — печатаем его. */}
139
- <SelectPrimitive.Value placeholder={placeholder}>
140
- {selectedOption ? (
141
- compactValue ? (
142
- selectedOption.startContent
143
- ) : (
144
- <OptionContent option={selectedOption} />
145
- )
146
- ) : undefined}
147
- </SelectPrimitive.Value>
148
- <SelectPrimitive.Icon asChild>
149
- <span className='ml-auto flex shrink-0 items-center text-muted-foreground transition-transform duration-200 group-data-[state=open]:rotate-180'>
150
- <Icon type='chevron_down' isCurrentColor size='0.72em' />
151
- </span>
152
- </SelectPrimitive.Icon>
153
- </SelectPrimitive.Trigger>
154
-
155
- <SelectPrimitive.Portal>
156
- <SelectPrimitive.Content
157
- position='popper'
158
- sideOffset={8}
159
- className={cn(
160
- 'z-50 max-h-[var(--radix-select-content-available-height)] min-w-[var(--radix-select-trigger-width)] overflow-hidden border border-border bg-popover p-1 text-popover-foreground shadow-lg outline-none data-[state=open]:animate-in data-[state=open]:fade-in data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=closed]:zoom-out-95',
161
- contentClassName,
162
- roundedContent,
163
- )}
164
- >
165
- <SelectPrimitive.Viewport className='flex flex-col'>
166
- {options.map((option, index) => (
167
- <React.Fragment key={option.value}>
168
- <SelectPrimitive.Item
169
- value={option.value}
170
- disabled={option.disabled}
171
- textValue={option.label}
172
- className={cn(
173
- 'flex w-full cursor-pointer items-center gap-2.5 text-foreground outline-none select-none data-highlighted:bg-secondary data-disabled:pointer-events-none data-disabled:opacity-50',
174
- itemSize,
175
- )}
176
- >
177
- <SelectPrimitive.ItemText>
178
- {compactValue && option.startContent ? (
179
- <span className='inline-flex shrink-0 items-center'>
180
- {option.startContent}
181
- </span>
182
- ) : (
183
- <OptionContent option={option} />
184
- )}
185
- </SelectPrimitive.ItemText>
186
- {/* compactValue: название живёт ВНЕ ItemText → видно в списке, но НЕ в триггере.
187
- leading-normal — иначе truncate (overflow-hidden) при line-height:1 режет
188
- нижние выносы букв («g» в English). */}
189
- {compactValue ? (
190
- <span className='truncate leading-normal'>
191
- {option.label}
192
- </span>
193
- ) : null}
194
- {/* галочка прижата вправо; рендерится только у выбранного */}
195
- <SelectPrimitive.ItemIndicator className='ml-auto flex items-center text-primary'>
196
- <Icon type='check' isCurrentColor size='0.85em' />
197
- </SelectPrimitive.ItemIndicator>
198
- </SelectPrimitive.Item>
199
- {/* разделитель между пунктами — тонкая линия */}
200
- {index < options.length - 1 ? (
201
- <SelectPrimitive.Separator
202
- className={cn(
203
- 'my-1 h-px bg-border rounded-full',
204
- dividerInset,
205
- )}
206
- />
207
- ) : null}
208
- </React.Fragment>
209
- ))}
210
- </SelectPrimitive.Viewport>
211
- </SelectPrimitive.Content>
212
- </SelectPrimitive.Portal>
213
- </SelectPrimitive.Root>
214
- )
215
- }
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+
5
+ import * as SelectPrimitive from '@radix-ui/react-select'
6
+
7
+ import { Icon } from '../Icon'
8
+ import { cn } from '../../lib/utils'
9
+
10
+ export interface SelectOption {
11
+ value: string
12
+ label: string
13
+ /** Контент в начале пункта (флаг/иконка/что угодно). Показывается и в списке, и в значении триггера. */
14
+ startContent?: React.ReactNode
15
+ disabled?: boolean
16
+ }
17
+
18
+ // group — чтобы шеврон в триггере доворачивался по data-state. Размер — через
19
+ // responsiveSize (CSS-адаптив без флэша), а не cva. Иконки — в em (масштаб за font-size).
20
+ const SELECT_TRIGGER_BASE =
21
+ 'group inline-flex w-full cursor-pointer items-center bg-card text-foreground shadow-sm transition-[color,box-shadow] outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background data-disabled:pointer-events-none data-disabled:opacity-50 data-placeholder:text-muted-foreground rounded-full'
22
+ const SELECT_TRIGGER_SIZE: Record<'sm' | 'md' | 'lg', string> = {
23
+ sm: 'gap-fl-10/12 min-h-fl-28/32 px-fl-10/12 py-fl-2/4 text-button-s',
24
+ md: 'gap-fl-14/16 min-h-fl-36/40 px-fl-14/16 py-fl-6/8 text-button-m',
25
+ lg: 'gap-fl-18/20 min-h-fl-44/48 px-fl-18/20 py-fl-10/12 text-button-l',
26
+ }
27
+ // Пункт = носитель подсветки; текст инсетит px ВНУТРИ пункта (выравнивание с триггером).
28
+ const SELECT_ITEM_SIZE: Record<'sm' | 'md' | 'lg', string> = {
29
+ sm: 'rounded-sm px-fl-6/8 py-fl-6/8 text-button-s',
30
+ md: 'rounded-md px-fl-10/12 py-fl-8/10 text-button-m',
31
+ lg: 'rounded-lg px-fl-14/16 py-fl-10/12 text-button-l',
32
+ }
33
+ // Боковой инсет разделителя = px пункта → линия выровнена с текстом и уже подсветки.
34
+ const SELECT_DIVIDER_INSET: Record<'sm' | 'md' | 'lg', string> = {
35
+ sm: 'mx-2',
36
+ md: 'mx-3',
37
+ lg: 'mx-4',
38
+ }
39
+ const SELECT_ROUNDED_CONTENT: Record<'sm' | 'md' | 'lg', string> = {
40
+ sm: 'rounded-sm',
41
+ md: 'rounded-md',
42
+ lg: 'rounded-lg',
43
+ }
44
+
45
+ export interface SelectProps {
46
+ options: SelectOption[]
47
+ /** Размер: `sm | md | lg`. По умолчанию md. Каждый размер сам fluid-клампится. */
48
+ size?: 'sm' | 'md' | 'lg'
49
+ value?: string
50
+ defaultValue?: string
51
+ onValueChange?: (value: string) => void
52
+ placeholder?: string
53
+ disabled?: boolean
54
+ /** Фиксированный контент в начале триггера (помимо иконки выбранного пункта): лейбл, иконка и т.п. */
55
+ startContent?: React.ReactNode
56
+ /** Класс на триггер. */
57
+ className?: string
58
+ /** Класс на выпадающую панель. */
59
+ contentClassName?: string
60
+ /**
61
+ * В триггере показывать только `startContent` выбранного пункта (напр. флаг),
62
+ * без текста; в списке пункты остаются с текстом. Для компактных переключателей
63
+ * (язык). По умолчанию false.
64
+ */
65
+ compactValue?: boolean
66
+ /**
67
+ * Доступное имя контрола. Заведено явно: у компонента нет rest-спреда, а TypeScript
68
+ * не проверяет лишние JSX-атрибуты с дефисом — `aria-label` на <Select> молча
69
+ * компилировался и никуда не попадал, оставляя контрол без доступного имени.
70
+ */
71
+ 'aria-label'?: string
72
+ 'aria-labelledby'?: string
73
+ }
74
+
75
+ // Флаг/иконка + подпись. Кладём ВНУТРЬ ItemText, чтобы триггер (Select.Value)
76
+ // отрисовал ровно то же — иконку слева и текст, как в макете.
77
+ function OptionContent({ option }: { option: SelectOption }) {
78
+ return (
79
+ // leading-normal: токены text-button-* имеют line-height:1, из-за чего нижние
80
+ // выносные элементы букв (g/y/р) обрезаются строчным боксом. Локально возвращаем запас.
81
+ <span className='flex items-center gap-2.5 leading-normal'>
82
+ {option.startContent ? (
83
+ <span className='inline-flex shrink-0 items-center'>
84
+ {option.startContent}
85
+ </span>
86
+ ) : null}
87
+ {option.label}
88
+ </span>
89
+ )
90
+ }
91
+
92
+ export function Select({
93
+ options,
94
+ value,
95
+ defaultValue,
96
+ onValueChange,
97
+ placeholder,
98
+ disabled,
99
+ startContent,
100
+ size,
101
+ className,
102
+ contentClassName,
103
+ compactValue = false,
104
+ 'aria-label': ariaLabel,
105
+ 'aria-labelledby': ariaLabelledBy,
106
+ }: SelectProps) {
107
+ // Только для контролируемого значения: в неконтролируемом режиме выбранный пункт
108
+ // знает лишь Radix, и подменять его нечем.
109
+ const selectedOption = value != null ? options.find((o) => o.value === value) : undefined
110
+
111
+ const triggerSize = SELECT_TRIGGER_SIZE[size ?? 'md']
112
+ const itemSize = SELECT_ITEM_SIZE[size ?? 'md']
113
+ const dividerInset = SELECT_DIVIDER_INSET[size ?? 'md']
114
+ const roundedContent = SELECT_ROUNDED_CONTENT[size ?? 'md']
115
+
116
+ return (
117
+ <SelectPrimitive.Root
118
+ value={value}
119
+ defaultValue={defaultValue}
120
+ onValueChange={onValueChange}
121
+ disabled={disabled}
122
+ >
123
+ <SelectPrimitive.Trigger
124
+ aria-label={ariaLabel}
125
+ aria-labelledby={ariaLabelledBy}
126
+ className={cn(SELECT_TRIGGER_BASE, triggerSize, className)}
127
+ >
128
+ {startContent ? (
129
+ <span className='flex shrink-0 items-center text-muted-foreground'>
130
+ {startContent}
131
+ </span>
132
+ ) : null}
133
+ {/* Значение рендерим САМИ, а не отдаём на откуп SelectPrimitive.Value.
134
+ Radix берёт текст из смонтированных Item'ов, а они живут в портале и
135
+ на сервере не рендерятся — из-за чего триггер приезжает ПУСТЫМ и
136
+ остаётся таким до гидрации. На медленной сети это сотни миллисекунд
137
+ пустого поля там, где уже должно стоять выбранное значение.
138
+ Когда значение контролируемое, лейбл известен сразу — печатаем его. */}
139
+ <SelectPrimitive.Value placeholder={placeholder}>
140
+ {selectedOption ? (
141
+ compactValue ? (
142
+ selectedOption.startContent
143
+ ) : (
144
+ <OptionContent option={selectedOption} />
145
+ )
146
+ ) : undefined}
147
+ </SelectPrimitive.Value>
148
+ <SelectPrimitive.Icon asChild>
149
+ <span className='ml-auto flex shrink-0 items-center text-muted-foreground transition-transform duration-200 group-data-[state=open]:rotate-180'>
150
+ <Icon type='chevron_down' isCurrentColor size='0.72em' />
151
+ </span>
152
+ </SelectPrimitive.Icon>
153
+ </SelectPrimitive.Trigger>
154
+
155
+ <SelectPrimitive.Portal>
156
+ <SelectPrimitive.Content
157
+ position='popper'
158
+ sideOffset={8}
159
+ className={cn(
160
+ 'z-50 max-h-[var(--radix-select-content-available-height)] min-w-[var(--radix-select-trigger-width)] overflow-hidden border border-border bg-popover p-1 text-popover-foreground shadow-lg outline-none data-[state=open]:animate-in data-[state=open]:fade-in data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=closed]:zoom-out-95',
161
+ contentClassName,
162
+ roundedContent,
163
+ )}
164
+ >
165
+ <SelectPrimitive.Viewport className='flex flex-col'>
166
+ {options.map((option, index) => (
167
+ <React.Fragment key={option.value}>
168
+ <SelectPrimitive.Item
169
+ value={option.value}
170
+ disabled={option.disabled}
171
+ textValue={option.label}
172
+ className={cn(
173
+ 'flex w-full cursor-pointer items-center gap-2.5 text-foreground outline-none select-none data-highlighted:bg-secondary data-disabled:pointer-events-none data-disabled:opacity-50',
174
+ itemSize,
175
+ )}
176
+ >
177
+ <SelectPrimitive.ItemText>
178
+ {compactValue && option.startContent ? (
179
+ <span className='inline-flex shrink-0 items-center'>
180
+ {option.startContent}
181
+ </span>
182
+ ) : (
183
+ <OptionContent option={option} />
184
+ )}
185
+ </SelectPrimitive.ItemText>
186
+ {/* compactValue: название живёт ВНЕ ItemText → видно в списке, но НЕ в триггере.
187
+ leading-normal — иначе truncate (overflow-hidden) при line-height:1 режет
188
+ нижние выносы букв («g» в English). */}
189
+ {compactValue ? (
190
+ <span className='truncate leading-normal'>
191
+ {option.label}
192
+ </span>
193
+ ) : null}
194
+ {/* галочка прижата вправо; рендерится только у выбранного */}
195
+ <SelectPrimitive.ItemIndicator className='ml-auto flex items-center text-primary'>
196
+ <Icon type='check' isCurrentColor size='0.85em' />
197
+ </SelectPrimitive.ItemIndicator>
198
+ </SelectPrimitive.Item>
199
+ {/* разделитель между пунктами — тонкая линия */}
200
+ {index < options.length - 1 ? (
201
+ <SelectPrimitive.Separator
202
+ className={cn(
203
+ 'my-1 h-px bg-border rounded-full',
204
+ dividerInset,
205
+ )}
206
+ />
207
+ ) : null}
208
+ </React.Fragment>
209
+ ))}
210
+ </SelectPrimitive.Viewport>
211
+ </SelectPrimitive.Content>
212
+ </SelectPrimitive.Portal>
213
+ </SelectPrimitive.Root>
214
+ )
215
+ }
@@ -1,44 +1,44 @@
1
- import * as React from 'react'
2
-
3
- import { cn } from '../../lib/utils'
4
-
5
- export interface SkeletonProps extends React.ComponentProps<'div'> {
6
- /**
7
- * Идёт ли загрузка. По умолчанию `true`. Если `false` и переданы `children` —
8
- * рендерим реальный контент без заглушки (wrapper-режим).
9
- */
10
- loading?: boolean
11
- }
12
-
13
- // Skeleton — заглушка-плейсхолдер на время загрузки. Два режима:
14
- // - без children: пустой блок, форму задаёшь className (w-*/h-*/size-*/rounded-*);
15
- // - с children (wrapper): children держат размер (invisible), а заглушка ложится
16
- // поверх — удобно, когда контент уже есть, но «не готов».
17
- // Анимация — shimmer-блик пробегает слева-направо (keyframes shimmer в globals.css).
18
- export function Skeleton({
19
- className,
20
- loading = true,
21
- children,
22
- ...props
23
- }: SkeletonProps) {
24
- // Загрузка завершена и есть что показать — отдаём реальный контент как есть.
25
- if (!loading && children !== undefined) return <>{children}</>
26
-
27
- return (
28
- <div
29
- aria-hidden
30
- className={cn(
31
- 'relative overflow-hidden rounded-md bg-accent',
32
- className,
33
- )}
34
- {...props}
35
- >
36
- {/* блик-полоса: едет translateX(-100% → 100%), overflow-hidden её обрезает */}
37
- <span className='pointer-events-none absolute inset-0 animate-shimmer bg-linear-to-r from-transparent via-skeleton-sheen to-transparent' />
38
- {/* children задают размер обёртки, но скрыты на время загрузки */}
39
- {children !== undefined ? (
40
- <div className='invisible'>{children}</div>
41
- ) : null}
42
- </div>
43
- )
44
- }
1
+ import * as React from 'react'
2
+
3
+ import { cn } from '../../lib/utils'
4
+
5
+ export interface SkeletonProps extends React.ComponentProps<'div'> {
6
+ /**
7
+ * Идёт ли загрузка. По умолчанию `true`. Если `false` и переданы `children` —
8
+ * рендерим реальный контент без заглушки (wrapper-режим).
9
+ */
10
+ loading?: boolean
11
+ }
12
+
13
+ // Skeleton — заглушка-плейсхолдер на время загрузки. Два режима:
14
+ // - без children: пустой блок, форму задаёшь className (w-*/h-*/size-*/rounded-*);
15
+ // - с children (wrapper): children держат размер (invisible), а заглушка ложится
16
+ // поверх — удобно, когда контент уже есть, но «не готов».
17
+ // Анимация — shimmer-блик пробегает слева-направо (keyframes shimmer в globals.css).
18
+ export function Skeleton({
19
+ className,
20
+ loading = true,
21
+ children,
22
+ ...props
23
+ }: SkeletonProps) {
24
+ // Загрузка завершена и есть что показать — отдаём реальный контент как есть.
25
+ if (!loading && children !== undefined) return <>{children}</>
26
+
27
+ return (
28
+ <div
29
+ aria-hidden
30
+ className={cn(
31
+ 'relative overflow-hidden rounded-md bg-accent',
32
+ className,
33
+ )}
34
+ {...props}
35
+ >
36
+ {/* блик-полоса: едет translateX(-100% → 100%), overflow-hidden её обрезает */}
37
+ <span className='pointer-events-none absolute inset-0 animate-shimmer bg-linear-to-r from-transparent via-skeleton-sheen to-transparent' />
38
+ {/* children задают размер обёртки, но скрыты на время загрузки */}
39
+ {children !== undefined ? (
40
+ <div className='invisible'>{children}</div>
41
+ ) : null}
42
+ </div>
43
+ )
44
+ }