@vobs/layout 1.0.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/src/layout.ts ADDED
@@ -0,0 +1,294 @@
1
+ import { effect, memo, state } from '@vobs/reactivity'
2
+ import {
3
+ addEventListener,
4
+ createComponent,
5
+ createElement,
6
+ createText,
7
+ insertBefore,
8
+ insertDynamic,
9
+ provide,
10
+ setAttribute,
11
+ setProperty,
12
+ type VobsNode
13
+ } from '@vobs/vobs'
14
+ import { KIT_LAYOUT_KEY } from './context'
15
+ import { KitHeader } from './header'
16
+ import { KitSidebar } from './sidebar'
17
+ import {
18
+ bindClassList,
19
+ bindCommonAttributes,
20
+ bindUserStyle,
21
+ hasProp,
22
+ mountSlot,
23
+ readProp,
24
+ resolveSlot,
25
+ setOptionalAttribute
26
+ } from './utils'
27
+ import {
28
+ createKitViewport,
29
+ DEFAULT_MOBILE_BREAKPOINT,
30
+ normalizeBreakpoint
31
+ } from './viewport'
32
+ import type {
33
+ KitLayoutContext,
34
+ KitLayoutProps,
35
+ KitMenuItem,
36
+ KitSidebarVariant
37
+ } from './types'
38
+
39
+ export function KitLayout(props: KitLayoutProps = {}): VobsNode {
40
+ const root = createElement('div')
41
+ const header = createElement('div')
42
+ const body = createElement('div')
43
+ const sidebar = createElement('div')
44
+ const main = createElement('main')
45
+ const breadcrumb = createElement('div')
46
+ const tabs = createElement('div')
47
+ const content = createElement('div')
48
+ const backdrop = createElement('div')
49
+ const footer = createElement('footer')
50
+
51
+ const breakpoint = normalizeBreakpoint(
52
+ readProp(props, 'mobileBreakpoint', DEFAULT_MOBILE_BREAKPOINT)
53
+ )
54
+ const viewport = createKitViewport(breakpoint)
55
+ const sidebarExpanded = readProp(props, 'sidebarExpanded', false)
56
+ const internalCollapsed = state(
57
+ readProp<boolean | undefined>(props, 'sidebarCollapsed', undefined) ?? !sidebarExpanded
58
+ )
59
+ const internalMobileOpen = state(readProp<boolean | undefined>(props, 'mobileOpen', undefined) ?? false)
60
+ const collapsed = memo(() => readProp<boolean | undefined>(props, 'sidebarCollapsed', undefined) ?? internalCollapsed.value)
61
+ const mobileOpen = memo(() => readProp<boolean | undefined>(props, 'mobileOpen', undefined) ?? internalMobileOpen.value)
62
+ const headerVisible = state(!isProvided(props, 'header'))
63
+ const sidebarVisible = state(isProvided(props, 'sidebar') || hasProp(props, 'menu'))
64
+ const backdropVisible = state(!isProvided(props, 'mobileBackdrop'))
65
+ const breadcrumbVisible = state(false)
66
+ const tabsVisible = state(false)
67
+ const footerVisible = state(false)
68
+
69
+ const context: KitLayoutContext = {
70
+ sidebarCollapsed: collapsed,
71
+ mobileOpen,
72
+ isMobile: viewport.isMobile,
73
+ breakpoint,
74
+
75
+ toggleSidebar(): void {
76
+ if (viewport.isMobile.value) context.setMobileOpen(!mobileOpen.value)
77
+ else context.setSidebarCollapsed(!collapsed.value)
78
+ },
79
+
80
+ setSidebarCollapsed(value: boolean): void {
81
+ if (!isBoolean(value)) throw new Error('VOBS_KIT001: sidebarCollapsed 必须是 boolean')
82
+ if (readProp<boolean | undefined>(props, 'sidebarCollapsed', undefined) === undefined) {
83
+ internalCollapsed.value = value
84
+ }
85
+ const handler = readProp<unknown>(props, 'onSidebarCollapsedChange', undefined)
86
+ if (typeof handler === 'function') {
87
+ (handler as KitLayoutProps['onSidebarCollapsedChange'])!(value)
88
+ }
89
+ },
90
+
91
+ setMobileOpen(value: boolean): void {
92
+ if (!isBoolean(value)) throw new Error('VOBS_KIT001: mobileOpen 必须是 boolean')
93
+ if (readProp<boolean | undefined>(props, 'mobileOpen', undefined) === undefined) {
94
+ internalMobileOpen.value = value
95
+ }
96
+ const handler = readProp<unknown>(props, 'onMobileOpenChange', undefined)
97
+ if (typeof handler === 'function') (handler as KitLayoutProps['onMobileOpenChange'])!(value)
98
+ }
99
+ }
100
+ provide(KIT_LAYOUT_KEY, context)
101
+
102
+ bindClassList(root, props, () => [
103
+ 'vobs-kit-layout',
104
+ collapsed.value ? 'vobs-kit-layout--sidebar-collapsed' : undefined,
105
+ viewport.isMobile.value ? 'vobs-kit-layout--mobile' : undefined,
106
+ mobileOpen.value ? 'vobs-kit-layout--mobile-open' : undefined
107
+ ])
108
+ bindCommonAttributes(root, props, [
109
+ 'header',
110
+ 'sidebar',
111
+ 'breadcrumb',
112
+ 'tabs',
113
+ 'footer',
114
+ 'mobileBackdrop',
115
+ 'menu',
116
+ 'logo',
117
+ 'userMenu',
118
+ 'headerActions',
119
+ 'activeKey',
120
+ 'sidebarExpanded',
121
+ 'sidebarCollapsed',
122
+ 'mobileOpen',
123
+ 'sidebarWidth',
124
+ 'mobileBreakpoint',
125
+ 'sidebarVariant',
126
+ 'onSidebarCollapsedChange',
127
+ 'onMobileOpenChange',
128
+ 'onToggleSidebar',
129
+ 'onCloseSidebar'
130
+ ])
131
+ bindUserStyle(root, props, () => {
132
+ const sidebarWidth = readProp<number | undefined>(props, 'sidebarWidth', undefined)
133
+ if (sidebarWidth === undefined) return ''
134
+ return `--vobs-kit-sidebar-width: ${normalizeSidebarWidth(sidebarWidth)}px`
135
+ })
136
+ setAttribute(header, 'class', 'vobs-kit-layout__header')
137
+ insertDynamic(header, null, () => createHeaderSlot(props, context, headerVisible))
138
+ bindVisibility(header, headerVisible)
139
+
140
+ setAttribute(body, 'class', 'vobs-kit-layout__body')
141
+ setAttribute(sidebar, 'class', 'vobs-kit-layout__sidebar')
142
+ setAttribute(main, 'class', 'vobs-kit-layout__main')
143
+ setAttribute(breadcrumb, 'class', 'vobs-kit-layout__breadcrumb')
144
+ setAttribute(tabs, 'class', 'vobs-kit-layout__tabs')
145
+ setAttribute(content, 'class', 'vobs-kit-layout__content')
146
+
147
+ insertDynamic(sidebar, null, () => createSidebarSlot(props, context, sidebarVisible))
148
+ bindVisibility(sidebar, sidebarVisible)
149
+
150
+ insertDynamic(breadcrumb, null, () => createNamedSlot(props, 'breadcrumb', breadcrumbVisible))
151
+ bindVisibility(breadcrumb, breadcrumbVisible)
152
+ insertDynamic(tabs, null, () => createNamedSlot(props, 'tabs', tabsVisible))
153
+ bindVisibility(tabs, tabsVisible)
154
+ if (hasProp(props, 'children')) mountSlot(content, props, 'children')
155
+
156
+ insertBefore(main, breadcrumb, null)
157
+ insertBefore(main, tabs, null)
158
+ insertBefore(main, content, null)
159
+ insertBefore(body, sidebar, null)
160
+ insertBefore(body, main, null)
161
+
162
+ setAttribute(backdrop, 'class', 'vobs-kit-layout__backdrop')
163
+ insertDynamic(backdrop, null, () => createBackdropSlot(props, context, sidebarVisible, backdropVisible))
164
+ bindVisibility(backdrop, memo(() => backdropVisible.value && sidebarVisible.value && mobileOpen.value))
165
+
166
+ setAttribute(footer, 'class', 'vobs-kit-layout__footer')
167
+ insertDynamic(footer, null, () => {
168
+ const value = isProvided(props, 'footer') ? resolveSlot(readProp(props, 'footer', undefined)) : null
169
+ footerVisible.value = value !== null
170
+ return value
171
+ })
172
+ bindVisibility(footer, footerVisible)
173
+
174
+ insertBefore(root, header, null)
175
+ insertBefore(root, body, null)
176
+ insertBefore(root, backdrop, null)
177
+ insertBefore(root, footer, null)
178
+ return root
179
+ }
180
+
181
+ function createHeaderSlot(
182
+ props: KitLayoutProps,
183
+ context: KitLayoutContext,
184
+ visible: { value: boolean }
185
+ ): VobsNode | null {
186
+ if (isProvided(props, 'header')) {
187
+ const value = resolveSlot(readProp(props, 'header', undefined))
188
+ visible.value = value !== null
189
+ return value
190
+ }
191
+ visible.value = true
192
+ return createComponent(KitHeader, {
193
+ get logo() { return readProp(props, 'logo', undefined) },
194
+ get title() { return readProp<string | undefined>(props, 'title', undefined) },
195
+ get headerActions() { return readProp(props, 'headerActions', undefined) },
196
+ get userMenu() { return readProp(props, 'userMenu', undefined) },
197
+ onToggleSidebar: () => {
198
+ context.toggleSidebar()
199
+ const handler = readProp<unknown>(props, 'onToggleSidebar', undefined)
200
+ if (typeof handler === 'function') (handler as KitLayoutProps['onToggleSidebar'])!()
201
+ }
202
+ })
203
+ }
204
+
205
+ function createSidebarSlot(
206
+ props: KitLayoutProps,
207
+ context: KitLayoutContext,
208
+ visible: { value: boolean }
209
+ ): VobsNode | null {
210
+ if (isProvided(props, 'sidebar')) {
211
+ const value = resolveSlot(readProp(props, 'sidebar', undefined))
212
+ visible.value = value !== null
213
+ return value
214
+ }
215
+ if (!hasProp(props, 'menu')) {
216
+ visible.value = false
217
+ return null
218
+ }
219
+ visible.value = true
220
+ return createComponent(KitSidebar, {
221
+ get menu() { return readProp<readonly KitMenuItem[]>(props, 'menu', []) },
222
+ get collapsed() { return context.sidebarCollapsed.value },
223
+ get mobileOpen() { return context.mobileOpen.value },
224
+ get activeKey() { return readProp<string | undefined>(props, 'activeKey', undefined) },
225
+ get variant() { return readProp<KitSidebarVariant>(props, 'sidebarVariant', 'default') },
226
+ get onSelect() { return readProp<KitLayoutProps['onMenuSelect'] | undefined>(props, 'onMenuSelect', undefined) },
227
+ onClose: () => {
228
+ context.setMobileOpen(false)
229
+ const handler = readProp<unknown>(props, 'onCloseSidebar', undefined)
230
+ if (typeof handler === 'function') (handler as KitLayoutProps['onCloseSidebar'])!()
231
+ }
232
+ })
233
+ }
234
+
235
+ function createNamedSlot(
236
+ props: KitLayoutProps,
237
+ name: 'breadcrumb' | 'tabs',
238
+ visible: { value: boolean }
239
+ ): VobsNode | null {
240
+ const value = hasProp(props, name) ? resolveSlot(readProp(props, name, undefined)) : null
241
+ visible.value = value !== null
242
+ return value
243
+ }
244
+
245
+ function createBackdropSlot(
246
+ props: KitLayoutProps,
247
+ context: KitLayoutContext,
248
+ sidebarVisible: { value: boolean },
249
+ visible: { value: boolean }
250
+ ): VobsNode | null {
251
+ if (isProvided(props, 'mobileBackdrop')) {
252
+ const value = resolveSlot(readProp(props, 'mobileBackdrop', undefined))
253
+ visible.value = value !== null
254
+ return value
255
+ }
256
+ if (!sidebarVisible.value) {
257
+ visible.value = false
258
+ return null
259
+ }
260
+ visible.value = true
261
+ const button = createElement('button')
262
+ setAttribute(button, 'type', 'button')
263
+ setAttribute(button, 'class', 'vobs-kit-layout__backdrop-button')
264
+ setAttribute(button, 'aria-label', 'Close navigation')
265
+ insertBefore(button, createText(''), null)
266
+ addEventListener(button, 'click', () => {
267
+ context.setMobileOpen(false)
268
+ const handler = readProp<unknown>(props, 'onCloseSidebar', undefined)
269
+ if (typeof handler === 'function') (handler as KitLayoutProps['onCloseSidebar'])!()
270
+ })
271
+ return button
272
+ }
273
+
274
+ function bindVisibility(node: Element, visible: { value: boolean }): void {
275
+ effect(() => {
276
+ setProperty(node, 'hidden', !visible.value)
277
+ setOptionalAttribute(node, 'aria-hidden', visible.value ? undefined : 'true')
278
+ })
279
+ }
280
+
281
+ function normalizeSidebarWidth(value: number): number {
282
+ if (!Number.isFinite(value) || value <= 0) {
283
+ throw new Error('VOBS_KIT001: sidebarWidth 必须是大于 0 的有限数字')
284
+ }
285
+ return Math.round(value)
286
+ }
287
+
288
+ function isBoolean(value: unknown): value is boolean {
289
+ return typeof value === 'boolean'
290
+ }
291
+
292
+ function isProvided(props: object, name: string): boolean {
293
+ return hasProp(props, name) && Reflect.get(props, name) !== undefined
294
+ }
package/src/menu.ts ADDED
@@ -0,0 +1,161 @@
1
+ import { state } from '@vobs/reactivity'
2
+ import {
3
+ addEventListener,
4
+ createElement,
5
+ createFragment,
6
+ createText,
7
+ insertBefore,
8
+ insertDynamic,
9
+ setAttribute,
10
+ setProperty,
11
+ type VobsNode
12
+ } from '@vobs/vobs'
13
+ import {
14
+ bindClassList,
15
+ bindCommonAttributes,
16
+ hasProp,
17
+ mountSlot,
18
+ readProp,
19
+ setOptionalAttribute
20
+ } from './utils'
21
+ import type { KitMenuItem, KitMenuProps } from './types'
22
+
23
+ export function KitMenu(props: KitMenuProps): VobsNode {
24
+ const root = createElement('ul')
25
+ const expanded = state<ReadonlySet<string>>(new Set())
26
+
27
+ bindClassList(root, props, () => ['vobs-kit-menu'])
28
+ bindCommonAttributes(root, props, ['items', 'collapsed', 'activeKey', 'onSelect'])
29
+ if (!hasProp(props, 'role')) setAttribute(root, 'role', 'menu')
30
+ insertDynamic(root, null, () => createMenuTree(props, expanded))
31
+ if (hasProp(props, 'children')) mountSlot(root, props, 'children')
32
+ return root
33
+ }
34
+
35
+ function createMenuTree(
36
+ props: KitMenuProps,
37
+ expanded: { value: ReadonlySet<string> }
38
+ ): VobsNode {
39
+ const items = readProp<readonly KitMenuItem[]>(props, 'items', [])
40
+ const activeKey = readProp<string | undefined>(props, 'activeKey', undefined)
41
+ const collapsed = readProp(props, 'collapsed', false)
42
+
43
+ return createFragment((parent, anchor) => {
44
+ for (const item of items) {
45
+ insertBefore(parent, createMenuItem(item, props, activeKey, collapsed, expanded), anchor)
46
+ }
47
+ })
48
+ }
49
+
50
+ function createMenuItem(
51
+ item: KitMenuItem,
52
+ props: KitMenuProps,
53
+ activeKey: string | undefined,
54
+ collapsed: boolean,
55
+ expanded: { value: ReadonlySet<string> }
56
+ ): VobsNode {
57
+ const li = createElement('li')
58
+ const children = item.children ?? []
59
+ const hasChildren = children.length > 0
60
+ const active = item.key === activeKey
61
+ const activeDescendant = children.some(child => hasActiveDescendant(child, activeKey))
62
+ const isExpanded = expanded.value.has(item.key) || activeDescendant
63
+
64
+ setAttribute(li, 'class', `vobs-kit-menu__item${active || activeDescendant ? ' is-active' : ''}`)
65
+ setOptionalAttribute(li, 'data-menu-key', item.key)
66
+
67
+ if (hasChildren) {
68
+ const groupButton = createElement('button')
69
+ setAttribute(groupButton, 'type', 'button')
70
+ setAttribute(groupButton, 'class', 'vobs-kit-menu__link vobs-kit-menu__group-toggle')
71
+ setAttribute(groupButton, 'aria-expanded', isExpanded ? 'true' : 'false')
72
+ if (collapsed) setOptionalAttribute(groupButton, 'title', item.label)
73
+ appendMenuContent(groupButton, item, collapsed)
74
+ addEventListener(groupButton, 'click', () => {
75
+ toggleExpanded(expanded, item.key)
76
+ if (item.href !== undefined) emitSelect(props, item)
77
+ })
78
+ insertBefore(li, groupButton, null)
79
+
80
+ if (isExpanded) {
81
+ const nested = createElement('ul')
82
+ setAttribute(nested, 'class', 'vobs-kit-menu__children')
83
+ setAttribute(nested, 'role', 'menu')
84
+ for (const child of children) {
85
+ insertBefore(nested, createMenuItem(child, props, activeKey, collapsed, expanded), null)
86
+ }
87
+ insertBefore(li, nested, null)
88
+ }
89
+ } else {
90
+ const link = createElement(item.href === undefined ? 'button' : 'a')
91
+ setAttribute(link, 'class', 'vobs-kit-menu__link')
92
+ if (item.href === undefined) setAttribute(link, 'type', 'button')
93
+ else {
94
+ setAttribute(link, 'href', item.href)
95
+ setOptionalAttribute(link, 'target', item.target)
96
+ setOptionalAttribute(link, 'rel', item.rel)
97
+ }
98
+ if (active) setAttribute(link, 'aria-current', 'page')
99
+ if (item.disabled === true) {
100
+ setAttribute(link, 'aria-disabled', 'true')
101
+ setProperty(link, 'disabled', true)
102
+ }
103
+ if (collapsed) setOptionalAttribute(link, 'title', item.label)
104
+ appendMenuContent(link, item, collapsed)
105
+ addEventListener(link, 'click', event => {
106
+ if (item.disabled === true) {
107
+ event.preventDefault?.()
108
+ return
109
+ }
110
+ emitSelect(props, item)
111
+ })
112
+ insertBefore(li, link, null)
113
+ }
114
+
115
+ return li
116
+ }
117
+
118
+ function appendMenuContent(parent: Element, item: KitMenuItem, collapsed: boolean): void {
119
+ if (item.icon !== undefined) {
120
+ const icon = createElement('span')
121
+ setAttribute(icon, 'class', 'vobs-kit-menu__icon')
122
+ mountSlot(icon, item, 'icon')
123
+ insertBefore(parent, icon, null)
124
+ }
125
+
126
+ const label = createElement('span')
127
+ setAttribute(label, 'class', 'vobs-kit-menu__label')
128
+ insertBefore(label, createText(item.label), null)
129
+ insertBefore(parent, label, null)
130
+
131
+ if (item.badge !== undefined && !collapsed) {
132
+ const badge = createElement('span')
133
+ setAttribute(badge, 'class', 'vobs-kit-menu__badge')
134
+ mountSlot(badge, item, 'badge')
135
+ insertBefore(parent, badge, null)
136
+ }
137
+
138
+ if (item.children && item.children.length > 0) {
139
+ const expand = createElement('span')
140
+ setAttribute(expand, 'class', 'vobs-kit-menu__expand')
141
+ setAttribute(expand, 'aria-hidden', 'true')
142
+ insertBefore(parent, expand, null)
143
+ }
144
+ }
145
+
146
+ function toggleExpanded(expanded: { value: ReadonlySet<string> }, key: string): void {
147
+ const next = new Set(expanded.value)
148
+ if (next.has(key)) next.delete(key)
149
+ else next.add(key)
150
+ expanded.value = next
151
+ }
152
+
153
+ function emitSelect(props: KitMenuProps, item: KitMenuItem): void {
154
+ const handler = readProp<unknown>(props, 'onSelect', undefined)
155
+ if (typeof handler === 'function') (handler as KitMenuProps['onSelect'])!(item.key, item)
156
+ }
157
+
158
+ function hasActiveDescendant(item: KitMenuItem, activeKey: string | undefined): boolean {
159
+ if (item.key === activeKey) return true
160
+ return item.children?.some(child => hasActiveDescendant(child, activeKey)) ?? false
161
+ }
package/src/sidebar.ts ADDED
@@ -0,0 +1,91 @@
1
+ import { effect } from '@vobs/reactivity'
2
+ import {
3
+ addEventListener,
4
+ createComponent,
5
+ createElement,
6
+ createText,
7
+ insertBefore,
8
+ insertDynamic,
9
+ setAttribute,
10
+ type VobsNode
11
+ } from '@vobs/vobs'
12
+ import { KitMenu } from './menu'
13
+ import {
14
+ bindClassList,
15
+ bindCommonAttributes,
16
+ bindTextContent,
17
+ bindUserStyle,
18
+ hasProp,
19
+ mountSlot,
20
+ readProp,
21
+ setOptionalAttribute
22
+ } from './utils'
23
+ import type { KitMenuItem, KitSidebarProps } from './types'
24
+
25
+ export function KitSidebar(props: KitSidebarProps = {}): VobsNode {
26
+ const root = createElement('aside')
27
+ const close = createElement('button')
28
+
29
+ bindClassList(root, props, () => [
30
+ 'vobs-kit-sidebar',
31
+ readProp(props, 'collapsed', false) ? 'is-collapsed' : undefined,
32
+ readProp(props, 'mobileOpen', false) ? 'is-mobile-open' : undefined,
33
+ readProp<KitSidebarProps['variant']>(props, 'variant', 'default') === 'centered' ? 'is-centered' : undefined
34
+ ])
35
+ bindCommonAttributes(root, props, [
36
+ 'items',
37
+ 'menu',
38
+ 'collapsed',
39
+ 'mobileOpen',
40
+ 'activeKey',
41
+ 'variant',
42
+ 'closeLabel',
43
+ 'footer',
44
+ 'onSelect',
45
+ 'onClose'
46
+ ])
47
+ bindUserStyle(root, props)
48
+ setAttribute(close, 'type', 'button')
49
+ setAttribute(close, 'class', 'vobs-kit-sidebar__close')
50
+ effect(() => {
51
+ setOptionalAttribute(close, 'aria-label', readProp(props, 'closeLabel', 'Close navigation'))
52
+ })
53
+ insertBefore(close, createCloseLabel(props), null)
54
+ addEventListener(close, 'click', () => {
55
+ const handler = readProp<unknown>(props, 'onClose', undefined)
56
+ if (typeof handler === 'function') (handler as KitSidebarProps['onClose'])!()
57
+ })
58
+ insertBefore(root, close, null)
59
+
60
+ insertDynamic(root, null, () => createDefaultMenu(props))
61
+ if (hasProp(props, 'footer')) {
62
+ const footer = createElement('div')
63
+ setAttribute(footer, 'class', 'vobs-kit-sidebar__footer')
64
+ mountSlot(footer, props, 'footer')
65
+ insertBefore(root, footer, null)
66
+ }
67
+ if (hasProp(props, 'children')) mountSlot(root, props, 'children')
68
+ return root
69
+ }
70
+
71
+ function createDefaultMenu(props: KitSidebarProps): VobsNode | null {
72
+ const items = readProp<readonly KitMenuItem[] | undefined>(props, 'items', undefined)
73
+ ?? readProp<readonly KitMenuItem[]>(props, 'menu', [])
74
+ if (items.length === 0 && hasProp(props, 'children')) return null
75
+
76
+ return createComponent(KitMenu, {
77
+ get items() { return items },
78
+ get collapsed() { return readProp(props, 'collapsed', false) },
79
+ get activeKey() { return readProp<string | undefined>(props, 'activeKey', undefined) },
80
+ get onSelect() { return readProp<KitSidebarProps['onSelect'] | undefined>(props, 'onSelect', undefined) }
81
+ })
82
+ }
83
+
84
+ function createCloseLabel(props: KitSidebarProps): VobsNode {
85
+ const text = createElement('span')
86
+ const content = createText('')
87
+ setAttribute(text, 'class', 'vobs-kit-sidebar__close-label')
88
+ bindTextContent(content, () => readProp(props, 'closeLabel', 'Close navigation'))
89
+ insertBefore(text, content, null)
90
+ return text
91
+ }