@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.
@@ -0,0 +1,421 @@
1
+ .vobs-kit-layout {
2
+ --vobs-kit-sidebar-width: 180px;
3
+ --vobs-kit-sidebar-collapsed-width: 48px;
4
+ display: flex;
5
+ flex-direction: column;
6
+ height: 100%;
7
+ min-height: 0;
8
+ overflow: hidden;
9
+ color: var(--text-default, #1f2937);
10
+ background: var(--bg-base-default, #ffffff);
11
+ }
12
+
13
+ .vobs-kit-layout__header,
14
+ .vobs-kit-layout__footer {
15
+ flex: 0 0 auto;
16
+ }
17
+
18
+ .vobs-kit-layout__body {
19
+ display: grid;
20
+ grid-template-columns: var(--vobs-kit-sidebar-width) minmax(0, 1fr);
21
+ flex: 1 1 auto;
22
+ min-height: 0;
23
+ overflow: hidden;
24
+ }
25
+
26
+ .vobs-kit-layout--sidebar-collapsed .vobs-kit-layout__body {
27
+ grid-template-columns: var(--vobs-kit-sidebar-collapsed-width) minmax(0, 1fr);
28
+ }
29
+
30
+ .vobs-kit-layout__sidebar {
31
+ min-width: 0;
32
+ min-height: 0;
33
+ overflow: auto;
34
+ border-right: 1px solid var(--border-neutral-l1, #e5e7eb);
35
+ background: var(--bg-base-secondary, #f9fafb);
36
+ }
37
+
38
+ .vobs-kit-layout__sidebar--disabled,
39
+ .vobs-kit-layout__sidebar[hidden] {
40
+ display: none;
41
+ }
42
+
43
+ .vobs-kit-layout__main {
44
+ display: flex;
45
+ flex-direction: column;
46
+ min-width: 0;
47
+ min-height: 0;
48
+ overflow: hidden;
49
+ }
50
+
51
+ .vobs-kit-layout__breadcrumb,
52
+ .vobs-kit-layout__tabs {
53
+ flex: 0 0 auto;
54
+ padding-inline: 24px;
55
+ }
56
+
57
+ .vobs-kit-layout__content {
58
+ display: flex;
59
+ flex: 1 1 auto;
60
+ flex-direction: column;
61
+ gap: 24px;
62
+ min-width: 0;
63
+ min-height: 0;
64
+ overflow: auto;
65
+ overscroll-behavior: contain;
66
+ padding: 24px;
67
+ }
68
+
69
+ .vobs-kit-layout__backdrop {
70
+ display: none;
71
+ }
72
+
73
+ .vobs-kit-layout__backdrop[hidden] {
74
+ display: none;
75
+ }
76
+
77
+ .vobs-kit-layout__footer {
78
+ border-top: 1px solid var(--border-neutral-l1, #e5e7eb);
79
+ }
80
+
81
+ .vobs-kit-header {
82
+ display: flex;
83
+ align-items: center;
84
+ gap: 16px;
85
+ min-height: 56px;
86
+ padding: 8px 16px;
87
+ border-bottom: 1px solid var(--border-neutral-l1, #e5e7eb);
88
+ background: var(--bg-base-secondary, #f9fafb);
89
+ }
90
+
91
+ .vobs-kit-header__toggle,
92
+ .vobs-kit-header__brand,
93
+ .vobs-kit-header__actions {
94
+ display: inline-flex;
95
+ align-items: center;
96
+ gap: 8px;
97
+ }
98
+
99
+ .vobs-kit-header__brand {
100
+ min-width: 0;
101
+ flex: 1 1 auto;
102
+ }
103
+
104
+ .vobs-kit-header__logo {
105
+ display: inline-flex;
106
+ align-items: center;
107
+ justify-content: center;
108
+ flex: 0 0 auto;
109
+ }
110
+
111
+ .vobs-kit-header__title {
112
+ min-width: 0;
113
+ margin: 0;
114
+ overflow: hidden;
115
+ font-size: 16px;
116
+ font-weight: 600;
117
+ text-overflow: ellipsis;
118
+ white-space: nowrap;
119
+ }
120
+
121
+ .vobs-kit-header__toggle-button,
122
+ .vobs-kit-sidebar__close {
123
+ min-height: 32px;
124
+ padding: 4px 10px;
125
+ border: 1px solid var(--border-neutral-l2, #d1d5db);
126
+ border-radius: 4px;
127
+ color: inherit;
128
+ background: transparent;
129
+ font: inherit;
130
+ cursor: pointer;
131
+ }
132
+
133
+ .vobs-kit-sidebar {
134
+ display: flex;
135
+ flex-direction: column;
136
+ min-height: 100%;
137
+ padding: 10px 6px;
138
+ }
139
+
140
+ .vobs-kit-sidebar__close {
141
+ display: none;
142
+ align-self: flex-end;
143
+ margin-bottom: 8px;
144
+ }
145
+
146
+ .vobs-kit-sidebar__close-label {
147
+ display: inline;
148
+ }
149
+
150
+ .vobs-kit-sidebar.is-centered .vobs-kit-menu {
151
+ margin-block: auto;
152
+ }
153
+
154
+ .vobs-kit-menu,
155
+ .vobs-kit-menu__children {
156
+ display: flex;
157
+ flex-direction: column;
158
+ gap: 2px;
159
+ margin: 0;
160
+ padding: 0;
161
+ list-style: none;
162
+ }
163
+
164
+ .vobs-kit-menu__children {
165
+ padding-left: 16px;
166
+ }
167
+
168
+ .vobs-kit-menu__link {
169
+ position: relative;
170
+ display: flex;
171
+ align-items: center;
172
+ width: 100%;
173
+ min-height: 36px;
174
+ gap: 8px;
175
+ padding: 6px 10px;
176
+ border: 0;
177
+ border-radius: 4px;
178
+ color: var(--text-secondary, #4b5563);
179
+ background: transparent;
180
+ font: inherit;
181
+ text-align: left;
182
+ text-decoration: none;
183
+ cursor: pointer;
184
+ }
185
+
186
+ .vobs-kit-menu__item.is-active > .vobs-kit-menu__link::before {
187
+ content: '';
188
+ position: absolute;
189
+ left: -6px;
190
+ top: 50%;
191
+ width: 3px;
192
+ height: 20px;
193
+ border-radius: var(--radius-full, 999px);
194
+ background: var(--bg-brand, #32f08c);
195
+ transform: translateY(-50%);
196
+ }
197
+
198
+ .vobs-kit-menu__link:hover,
199
+ .vobs-kit-menu__item.is-active > .vobs-kit-menu__link {
200
+ color: var(--text-default, #111827);
201
+ background: var(--bg-overlay-l2, #e5e7eb);
202
+ }
203
+
204
+ .vobs-kit-menu__icon,
205
+ .vobs-kit-menu__expand {
206
+ display: inline-flex;
207
+ align-items: center;
208
+ justify-content: center;
209
+ flex: 0 0 20px;
210
+ }
211
+
212
+ .vobs-kit-menu__label {
213
+ min-width: 0;
214
+ flex: 1 1 auto;
215
+ overflow: hidden;
216
+ text-overflow: ellipsis;
217
+ white-space: nowrap;
218
+ }
219
+
220
+ .vobs-kit-menu__badge {
221
+ flex: 0 0 auto;
222
+ color: var(--text-tertiary, #6b7280);
223
+ font-size: 12px;
224
+ }
225
+
226
+ .vobs-kit-menu__expand::before {
227
+ content: '+';
228
+ }
229
+
230
+ .vobs-kit-sidebar.is-collapsed .vobs-kit-menu__label,
231
+ .vobs-kit-sidebar.is-collapsed .vobs-kit-menu__badge,
232
+ .vobs-kit-sidebar.is-collapsed .vobs-kit-menu__expand {
233
+ display: none;
234
+ }
235
+
236
+ .vobs-kit-sidebar.is-collapsed .vobs-kit-menu__link {
237
+ justify-content: center;
238
+ padding-inline: 6px;
239
+ }
240
+
241
+ .vobs-kit-sidebar__footer {
242
+ margin-top: auto;
243
+ padding-top: 6px;
244
+ }
245
+
246
+ .vobs-kit-breadcrumb {
247
+ overflow: auto;
248
+ color: var(--text-secondary, #4b5563);
249
+ }
250
+
251
+ .vobs-kit-breadcrumb__list {
252
+ display: flex;
253
+ align-items: center;
254
+ min-width: max-content;
255
+ gap: 8px;
256
+ margin: 0;
257
+ padding: 12px 0;
258
+ list-style: none;
259
+ }
260
+
261
+ .vobs-kit-breadcrumb__item {
262
+ display: inline-flex;
263
+ align-items: center;
264
+ gap: 8px;
265
+ font-size: 13px;
266
+ }
267
+
268
+ .vobs-kit-breadcrumb__separator {
269
+ color: var(--text-tertiary, #9ca3af);
270
+ }
271
+
272
+ .vobs-kit-breadcrumb__label {
273
+ color: inherit;
274
+ text-decoration: none;
275
+ }
276
+
277
+ .vobs-kit-breadcrumb__item.is-current .vobs-kit-breadcrumb__label {
278
+ color: var(--text-default, #111827);
279
+ font-weight: 600;
280
+ }
281
+
282
+ .vobs-kit-tabs {
283
+ display: flex;
284
+ align-items: center;
285
+ gap: 4px;
286
+ overflow: auto;
287
+ border-bottom: 1px solid var(--border-neutral-l1, #e5e7eb);
288
+ }
289
+
290
+ .vobs-kit-tabs__tab {
291
+ display: inline-flex;
292
+ align-items: center;
293
+ flex: 0 0 auto;
294
+ min-height: 36px;
295
+ gap: 6px;
296
+ padding: 6px 10px;
297
+ border: 0;
298
+ border-bottom: 2px solid transparent;
299
+ color: var(--text-secondary, #4b5563);
300
+ background: transparent;
301
+ font: inherit;
302
+ text-decoration: none;
303
+ cursor: pointer;
304
+ }
305
+
306
+ .vobs-kit-tabs__tab:hover,
307
+ .vobs-kit-tabs__tab.is-active {
308
+ color: var(--text-default, #111827);
309
+ }
310
+
311
+ .vobs-kit-tabs__tab.is-active {
312
+ border-bottom-color: var(--text-default, #111827);
313
+ }
314
+
315
+ .vobs-kit-tabs__icon {
316
+ display: inline-flex;
317
+ }
318
+
319
+ .vobs-kit-tabs__label {
320
+ white-space: nowrap;
321
+ }
322
+
323
+ .vobs-kit-tabs__close {
324
+ display: inline-flex;
325
+ align-items: center;
326
+ justify-content: center;
327
+ width: 20px;
328
+ height: 20px;
329
+ padding: 0;
330
+ border: 0;
331
+ color: inherit;
332
+ background: transparent;
333
+ cursor: pointer;
334
+ }
335
+
336
+ @media (max-width: 767px) {
337
+ .vobs-kit-layout__body {
338
+ display: block;
339
+ }
340
+
341
+ .vobs-kit-layout__main {
342
+ height: 100%;
343
+ }
344
+
345
+ .vobs-kit-layout__sidebar {
346
+ position: fixed;
347
+ inset: 0 auto 0 0;
348
+ z-index: 20;
349
+ width: var(--vobs-kit-sidebar-width);
350
+ min-height: 100vh;
351
+ border-right: 1px solid var(--border-neutral-l1, #e5e7eb);
352
+ box-shadow: 8px 0 24px rgb(0 0 0 / 16%);
353
+ transform: translateX(-100%);
354
+ transition: transform 160ms ease;
355
+ }
356
+
357
+ .vobs-kit-layout--sidebar-collapsed .vobs-kit-layout__sidebar {
358
+ width: var(--vobs-kit-sidebar-width);
359
+ }
360
+
361
+ .vobs-kit-layout--mobile-open .vobs-kit-layout__sidebar {
362
+ transform: translateX(0);
363
+ }
364
+
365
+ .vobs-kit-layout__backdrop {
366
+ position: fixed;
367
+ inset: 0;
368
+ z-index: 10;
369
+ display: block;
370
+ background: rgb(17 24 39 / 42%);
371
+ }
372
+
373
+ .vobs-kit-layout__backdrop-button {
374
+ display: block;
375
+ width: 100%;
376
+ height: 100%;
377
+ padding: 0;
378
+ border: 0;
379
+ background: transparent;
380
+ cursor: pointer;
381
+ }
382
+
383
+ .vobs-kit-sidebar__close {
384
+ display: inline-flex;
385
+ }
386
+
387
+ .vobs-kit-layout__breadcrumb,
388
+ .vobs-kit-layout__tabs {
389
+ padding-inline: 16px;
390
+ }
391
+
392
+ .vobs-kit-layout__content {
393
+ padding: 16px;
394
+ }
395
+ }
396
+
397
+ .vobs-kit-layout--mobile .vobs-kit-layout__body {
398
+ display: block;
399
+ }
400
+
401
+ .vobs-kit-layout--mobile .vobs-kit-layout__sidebar {
402
+ position: fixed;
403
+ inset: 0 auto 0 0;
404
+ z-index: 20;
405
+ width: var(--vobs-kit-sidebar-width);
406
+ min-height: 100vh;
407
+ box-shadow: 8px 0 24px rgb(0 0 0 / 16%);
408
+ transform: translateX(-100%);
409
+ }
410
+
411
+ .vobs-kit-layout--mobile.vobs-kit-layout--mobile-open .vobs-kit-layout__sidebar {
412
+ transform: translateX(0);
413
+ }
414
+
415
+ .vobs-kit-layout--mobile .vobs-kit-layout__backdrop:not([hidden]) {
416
+ position: fixed;
417
+ inset: 0;
418
+ z-index: 10;
419
+ display: block;
420
+ background: rgb(17 24 39 / 42%);
421
+ }
@@ -0,0 +1 @@
1
+ @import './layout.css';
package/src/tabs.ts ADDED
@@ -0,0 +1,127 @@
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 { KitTabItem, KitTabsProps } from './types'
22
+
23
+ export function KitTabs(props: KitTabsProps = {}): VobsNode {
24
+ const root = createElement('nav')
25
+ const active = state<string | undefined>(undefined)
26
+
27
+ bindClassList(root, props, () => ['vobs-kit-tabs'])
28
+ bindCommonAttributes(root, props, [
29
+ 'tabs',
30
+ 'items',
31
+ 'value',
32
+ 'activeKey',
33
+ 'closable',
34
+ 'onSelect',
35
+ 'onChange',
36
+ 'onClose'
37
+ ])
38
+ if (!hasProp(props, 'aria-label')) setAttribute(root, 'aria-label', 'Tabs')
39
+ if (!hasProp(props, 'role')) setAttribute(root, 'role', 'tablist')
40
+ insertDynamic(root, null, () => createTabItems(props, active))
41
+ if (hasProp(props, 'children')) mountSlot(root, props, 'children')
42
+ return root
43
+ }
44
+
45
+ function createTabItems(
46
+ props: KitTabsProps,
47
+ internal: { value: string | undefined }
48
+ ): VobsNode {
49
+ const tabs = readProp<readonly KitTabItem[]>(props, 'tabs', readProp(props, 'items', []))
50
+ const selected = readProp<string | undefined>(
51
+ props,
52
+ 'activeKey',
53
+ readProp<string | undefined>(props, 'value', undefined)
54
+ ) ?? internal.value ?? tabs.find(tab => tab.disabled !== true)?.id
55
+
56
+ return createFragment((parent, anchor) => {
57
+ for (const tab of tabs) {
58
+ insertBefore(parent, createTab(tab, props, selected, internal), anchor)
59
+ }
60
+ })
61
+ }
62
+
63
+ function createTab(
64
+ tab: KitTabItem,
65
+ props: KitTabsProps,
66
+ selected: string | undefined,
67
+ internal: { value: string | undefined }
68
+ ): VobsNode {
69
+ const active = tab.id === selected
70
+ const element = createElement(tab.href === undefined ? 'button' : 'a')
71
+ setAttribute(element, 'class', `vobs-kit-tabs__tab${active ? ' is-active' : ''}`)
72
+ setAttribute(element, 'role', 'tab')
73
+ setAttribute(element, 'aria-selected', active ? 'true' : 'false')
74
+ setOptionalAttribute(element, 'data-tab-id', tab.id)
75
+ if (tab.href === undefined) setAttribute(element, 'type', 'button')
76
+ else setAttribute(element, 'href', tab.href)
77
+ if (tab.disabled === true) {
78
+ setAttribute(element, 'aria-disabled', 'true')
79
+ setProperty(element, 'tabIndex', -1)
80
+ if (tab.href === undefined) setProperty(element, 'disabled', true)
81
+ }
82
+
83
+ if (tab.icon !== undefined) {
84
+ const icon = createElement('span')
85
+ setAttribute(icon, 'class', 'vobs-kit-tabs__icon')
86
+ mountSlot(icon, tab, 'icon')
87
+ insertBefore(element, icon, null)
88
+ }
89
+
90
+ const label = createElement('span')
91
+ setAttribute(label, 'class', 'vobs-kit-tabs__label')
92
+ insertBefore(label, createText(tab.label), null)
93
+ insertBefore(element, label, null)
94
+
95
+ const closable = readProp(props, 'closable', false) || tab.closable === true
96
+ if (closable) {
97
+ const close = createElement('button')
98
+ setAttribute(close, 'class', 'vobs-kit-tabs__close')
99
+ setAttribute(close, 'type', 'button')
100
+ setAttribute(close, 'aria-label', `Close ${tab.label}`)
101
+ insertBefore(close, createText('x'), null)
102
+ addEventListener(close, 'click', event => {
103
+ event.stopPropagation?.()
104
+ const handler = readProp<unknown>(props, 'onClose', undefined)
105
+ if (typeof handler === 'function') (handler as KitTabsProps['onClose'])!(tab)
106
+ })
107
+ insertBefore(element, close, null)
108
+ }
109
+
110
+ addEventListener(element, 'click', event => {
111
+ if (tab.disabled === true) {
112
+ event.preventDefault?.()
113
+ return
114
+ }
115
+ if (!isControlled(props)) internal.value = tab.id
116
+ const onSelect = readProp<unknown>(props, 'onSelect', undefined)
117
+ if (typeof onSelect === 'function') (onSelect as KitTabsProps['onSelect'])!(tab)
118
+ const onChange = readProp<unknown>(props, 'onChange', undefined)
119
+ if (typeof onChange === 'function') (onChange as KitTabsProps['onChange'])!(tab.id, tab)
120
+ })
121
+ return element
122
+ }
123
+
124
+ function isControlled(props: KitTabsProps): boolean {
125
+ return readProp<string | undefined>(props, 'activeKey', undefined) !== undefined
126
+ || readProp<string | undefined>(props, 'value', undefined) !== undefined
127
+ }
package/src/types.ts ADDED
@@ -0,0 +1,150 @@
1
+ import type { Signal } from '@vobs/reactivity'
2
+ import type { VobsNode } from '@vobs/vobs'
3
+
4
+ export type LayoutAttributeValue = string | number | boolean | undefined
5
+
6
+ export type LayoutChild = VobsNode | string | number | null | undefined | false
7
+
8
+ export type LayoutChildren =
9
+ | LayoutChild
10
+ | readonly LayoutChildren[]
11
+ | (() => LayoutChildren)
12
+
13
+ export interface LayoutCommonProps {
14
+ readonly class?: string
15
+ readonly className?: string
16
+ readonly style?: string
17
+ readonly id?: string
18
+ readonly title?: string
19
+ readonly role?: string
20
+ readonly tabIndex?: number
21
+ readonly children?: LayoutChildren
22
+ readonly [name: `aria-${string}`]: LayoutAttributeValue
23
+ readonly [name: `data-${string}`]: LayoutAttributeValue
24
+ }
25
+
26
+ export interface KitMenuItem {
27
+ readonly key: string
28
+ readonly label: string
29
+ readonly icon?: LayoutChildren
30
+ readonly badge?: LayoutChildren
31
+ readonly href?: string
32
+ readonly target?: string
33
+ readonly rel?: string
34
+ readonly permission?: string
35
+ readonly disabled?: boolean
36
+ readonly children?: readonly KitMenuItem[]
37
+ }
38
+
39
+ export type KitSidebarVariant = 'default' | 'centered'
40
+
41
+ export interface KitLayoutProps extends LayoutCommonProps {
42
+ readonly header?: LayoutChildren
43
+ readonly sidebar?: LayoutChildren
44
+ readonly breadcrumb?: LayoutChildren
45
+ readonly tabs?: LayoutChildren
46
+ readonly footer?: LayoutChildren
47
+ readonly mobileBackdrop?: LayoutChildren
48
+ readonly menu?: readonly KitMenuItem[]
49
+ readonly logo?: LayoutChildren
50
+ readonly title?: string
51
+ readonly userMenu?: LayoutChildren
52
+ readonly headerActions?: LayoutChildren
53
+ readonly activeKey?: string
54
+ /** Whether the sidebar starts expanded when no controlled collapsed state is provided. */
55
+ readonly sidebarExpanded?: boolean
56
+ readonly sidebarCollapsed?: boolean
57
+ readonly mobileOpen?: boolean
58
+ readonly sidebarWidth?: number
59
+ readonly mobileBreakpoint?: number
60
+ readonly sidebarVariant?: KitSidebarVariant
61
+ readonly onSidebarCollapsedChange?: (collapsed: boolean) => void
62
+ readonly onMobileOpenChange?: (open: boolean) => void
63
+ readonly onToggleSidebar?: () => void
64
+ readonly onCloseSidebar?: () => void
65
+ readonly onMenuSelect?: (key: string, item: KitMenuItem) => void
66
+ }
67
+
68
+ export interface KitHeaderProps extends LayoutCommonProps {
69
+ readonly logo?: LayoutChildren
70
+ readonly title?: string
71
+ readonly sidebarToggle?: LayoutChildren
72
+ readonly userMenu?: LayoutChildren
73
+ readonly headerActions?: LayoutChildren
74
+ readonly toggleLabel?: string
75
+ readonly onToggleSidebar?: () => void
76
+ }
77
+
78
+ export interface KitSidebarProps extends LayoutCommonProps {
79
+ readonly items?: readonly KitMenuItem[]
80
+ readonly menu?: readonly KitMenuItem[]
81
+ readonly collapsed?: boolean
82
+ readonly mobileOpen?: boolean
83
+ readonly activeKey?: string
84
+ readonly variant?: KitSidebarVariant
85
+ readonly closeLabel?: string
86
+ readonly footer?: LayoutChildren
87
+ readonly onSelect?: (key: string, item: KitMenuItem) => void
88
+ readonly onClose?: () => void
89
+ }
90
+
91
+ export interface KitMenuProps extends LayoutCommonProps {
92
+ readonly items: readonly KitMenuItem[]
93
+ readonly collapsed?: boolean
94
+ readonly activeKey?: string
95
+ readonly onSelect?: (key: string, item: KitMenuItem) => void
96
+ }
97
+
98
+ export interface KitBreadcrumbItem {
99
+ readonly key?: string
100
+ readonly label: string
101
+ readonly href?: string
102
+ readonly disabled?: boolean
103
+ }
104
+
105
+ export interface KitBreadcrumbProps extends LayoutCommonProps {
106
+ readonly items: readonly KitBreadcrumbItem[]
107
+ readonly separator?: LayoutChildren
108
+ readonly onNavigate?: (item: KitBreadcrumbItem, index: number) => void
109
+ }
110
+
111
+ export interface KitTabItem {
112
+ readonly id: string
113
+ readonly label: string
114
+ readonly href?: string
115
+ readonly icon?: LayoutChildren
116
+ readonly disabled?: boolean
117
+ readonly closable?: boolean
118
+ }
119
+
120
+ export interface KitTabsProps extends LayoutCommonProps {
121
+ readonly tabs?: readonly KitTabItem[]
122
+ readonly items?: readonly KitTabItem[]
123
+ readonly value?: string
124
+ readonly activeKey?: string
125
+ readonly closable?: boolean
126
+ readonly onSelect?: (tab: KitTabItem) => void
127
+ readonly onChange?: (id: string, tab: KitTabItem) => void
128
+ readonly onClose?: (tab: KitTabItem) => void
129
+ }
130
+
131
+ export interface KitReadonlySignal<T> {
132
+ readonly value: T
133
+ }
134
+
135
+ export interface KitLayoutContext {
136
+ readonly sidebarCollapsed: KitReadonlySignal<boolean>
137
+ readonly mobileOpen: KitReadonlySignal<boolean>
138
+ readonly isMobile: KitReadonlySignal<boolean>
139
+ readonly breakpoint: number
140
+ toggleSidebar(): void
141
+ setSidebarCollapsed(value: boolean): void
142
+ setMobileOpen(value: boolean): void
143
+ }
144
+
145
+ export interface KitViewport {
146
+ readonly width: Signal<number>
147
+ readonly height: Signal<number>
148
+ readonly isMobile: Signal<boolean>
149
+ dispose(): void
150
+ }