@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
package/src/utils.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { effect } from '@vobs/reactivity'
|
|
2
|
+
import {
|
|
3
|
+
createFragment,
|
|
4
|
+
createText,
|
|
5
|
+
insertBefore,
|
|
6
|
+
insertDynamic,
|
|
7
|
+
setAttribute,
|
|
8
|
+
setProperty,
|
|
9
|
+
setTextContent,
|
|
10
|
+
type VobsNode
|
|
11
|
+
} from '@vobs/vobs'
|
|
12
|
+
import type { LayoutAttributeValue, LayoutCommonProps } from './types'
|
|
13
|
+
|
|
14
|
+
export function readProp<T>(props: object, name: string, fallback: T): T {
|
|
15
|
+
const value = Reflect.get(props, name)
|
|
16
|
+
return (value === undefined ? fallback : value) as T
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function hasProp(props: object, name: string): boolean {
|
|
20
|
+
return name in props
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function classNames(...values: readonly (string | false | null | undefined)[]): string {
|
|
24
|
+
return values
|
|
25
|
+
.flatMap(value => typeof value === 'string' ? value.trim().split(/\s+/u) : [])
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.join(' ')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function bindClassList(
|
|
31
|
+
root: Element,
|
|
32
|
+
props: object,
|
|
33
|
+
readBaseClasses: () => readonly (string | undefined)[]
|
|
34
|
+
): void {
|
|
35
|
+
effect(() => {
|
|
36
|
+
setAttribute(root, 'class', classNames(
|
|
37
|
+
...readBaseClasses(),
|
|
38
|
+
readString(props, 'class'),
|
|
39
|
+
readString(props, 'className')
|
|
40
|
+
))
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function bindCommonAttributes(
|
|
45
|
+
root: Element,
|
|
46
|
+
props: object,
|
|
47
|
+
skip: readonly string[] = []
|
|
48
|
+
): void {
|
|
49
|
+
const ignored = new Set(skip)
|
|
50
|
+
ignored.add('class')
|
|
51
|
+
ignored.add('className')
|
|
52
|
+
ignored.add('children')
|
|
53
|
+
ignored.add('style')
|
|
54
|
+
|
|
55
|
+
effect(() => {
|
|
56
|
+
const next = new Map<string, string>()
|
|
57
|
+
for (const name of Object.keys(props)) {
|
|
58
|
+
if (ignored.has(name) || !isCommonAttribute(name)) continue
|
|
59
|
+
const normalized = normalizeAttribute(name, Reflect.get(props, name))
|
|
60
|
+
if (normalized) next.set(normalized.name, normalized.value)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const managed = managedAttributes.get(root) ?? new Set<string>()
|
|
64
|
+
for (const name of managed) {
|
|
65
|
+
if (!next.has(name)) removeAttribute(root, name)
|
|
66
|
+
}
|
|
67
|
+
for (const [name, value] of next) setAttribute(root, name, value)
|
|
68
|
+
managedAttributes.set(root, new Set(next.keys()))
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function bindUserStyle(root: Element, props: object, internal?: () => string): void {
|
|
73
|
+
effect(() => {
|
|
74
|
+
const values = [internal?.() ?? '', readString(props, 'style') ?? ''].filter(Boolean)
|
|
75
|
+
if (values.length > 0) setAttribute(root, 'style', values.join('; '))
|
|
76
|
+
else removeAttribute(root, 'style')
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function bindTextContent(node: Text, read: () => unknown): void {
|
|
81
|
+
effect(() => {
|
|
82
|
+
setTextContent(node, String(read() ?? ''))
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function setOptionalAttribute(node: Element, name: string, value: unknown): void {
|
|
87
|
+
if (value === undefined || value === null || value === false || value === '') {
|
|
88
|
+
removeAttribute(node, name)
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
setAttribute(node, name, String(value))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function setOptionalProperty(node: Element, name: string, value: unknown): void {
|
|
95
|
+
if (value === undefined || value === null) return
|
|
96
|
+
setProperty(node, name, value)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function mountSlot(parent: Node, props: object, name: string): void {
|
|
100
|
+
insertDynamic(parent, null, () => resolveSlot(Reflect.get(props, name)))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function resolveSlot(value: unknown): VobsNode | null {
|
|
104
|
+
const resolved = typeof value === 'function' ? value() : value
|
|
105
|
+
if (resolved === undefined || resolved === null || resolved === false) return null
|
|
106
|
+
if (typeof resolved === 'string' || typeof resolved === 'number') return createText(String(resolved))
|
|
107
|
+
if (Array.isArray(resolved)) {
|
|
108
|
+
return createFragment((parent, anchor) => {
|
|
109
|
+
for (const child of resolved) {
|
|
110
|
+
const node = resolveSlot(child)
|
|
111
|
+
if (node) insertBefore(parent, node, anchor)
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
return resolved as VobsNode
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function readString(props: object, name: string): string | undefined {
|
|
119
|
+
const value = Reflect.get(props, name)
|
|
120
|
+
return typeof value === 'string' ? value : undefined
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isCommonAttribute(name: string): boolean {
|
|
124
|
+
return name === 'id'
|
|
125
|
+
|| name === 'title'
|
|
126
|
+
|| name === 'role'
|
|
127
|
+
|| name === 'tabIndex'
|
|
128
|
+
|| name.startsWith('aria-')
|
|
129
|
+
|| name.startsWith('data-')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizeAttribute(name: string, value: unknown): { name: string; value: string } | undefined {
|
|
133
|
+
if (value === undefined || value === null) return undefined
|
|
134
|
+
if (value === false && !name.startsWith('aria-') && !name.startsWith('data-')) return undefined
|
|
135
|
+
return {
|
|
136
|
+
name: name === 'tabIndex' ? 'tabindex' : name,
|
|
137
|
+
value: String(value)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function removeAttribute(node: Element, name: string): void {
|
|
142
|
+
const candidate = node as Element & { removeAttribute?: (attribute: string) => void }
|
|
143
|
+
candidate.removeAttribute?.(name)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const managedAttributes = new WeakMap<object, Set<string>>()
|
|
147
|
+
|
|
148
|
+
export type LayoutProps = LayoutCommonProps
|
|
149
|
+
export type { LayoutAttributeValue }
|
package/src/viewport.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { getCurrentOwner, memo, onDispose, state } from '@vobs/reactivity'
|
|
2
|
+
import type { KitViewport } from './types'
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_MOBILE_BREAKPOINT = 768
|
|
5
|
+
|
|
6
|
+
export function createKitViewport(breakpoint = DEFAULT_MOBILE_BREAKPOINT): KitViewport {
|
|
7
|
+
const normalizedBreakpoint = normalizeBreakpoint(breakpoint)
|
|
8
|
+
const width = state(readWidth(), 'layout.viewport.width')
|
|
9
|
+
const height = state(readHeight(), 'layout.viewport.height')
|
|
10
|
+
const isMobile = memo(() => width.value < normalizedBreakpoint)
|
|
11
|
+
let disposed = false
|
|
12
|
+
|
|
13
|
+
const onResize = (): void => {
|
|
14
|
+
if (disposed) return
|
|
15
|
+
width.value = readWidth()
|
|
16
|
+
height.value = readHeight()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (typeof window !== 'undefined') {
|
|
20
|
+
window.addEventListener('resize', onResize)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const viewport: KitViewport = {
|
|
24
|
+
width,
|
|
25
|
+
height,
|
|
26
|
+
isMobile,
|
|
27
|
+
dispose(): void {
|
|
28
|
+
if (disposed) return
|
|
29
|
+
disposed = true
|
|
30
|
+
if (typeof window !== 'undefined') window.removeEventListener('resize', onResize)
|
|
31
|
+
isMobile.dispose()
|
|
32
|
+
width.dispose()
|
|
33
|
+
height.dispose()
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (getCurrentOwner()) onDispose(viewport.dispose)
|
|
38
|
+
return viewport
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function normalizeBreakpoint(value: number): number {
|
|
42
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
43
|
+
throw new Error('VOBS_KIT002: mobileBreakpoint 必须是大于 0 的有限数字')
|
|
44
|
+
}
|
|
45
|
+
return Math.round(value)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readWidth(): number {
|
|
49
|
+
return typeof window === 'undefined' ? DEFAULT_MOBILE_BREAKPOINT + 256 : window.innerWidth
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readHeight(): number {
|
|
53
|
+
return typeof window === 'undefined' ? 768 : window.innerHeight
|
|
54
|
+
}
|