@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/LICENSE +21 -0
- package/README.md +49 -0
- package/package.json +38 -0
- package/src/breadcrumb.ts +92 -0
- package/src/context.ts +17 -0
- package/src/header.ts +92 -0
- package/src/index.ts +32 -0
- package/src/layout.test.ts +271 -0
- package/src/layout.ts +294 -0
- package/src/menu.ts +161 -0
- package/src/sidebar.ts +91 -0
- package/src/styles/layout.css +421 -0
- package/src/styles/styles.css +1 -0
- package/src/tabs.ts +127 -0
- package/src/types.ts +150 -0
- package/src/utils.ts +149 -0
- package/src/viewport.ts +54 -0
|
@@ -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
|
+
}
|