-
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathindex.ts
More file actions
214 lines (189 loc) · 5.74 KB
/
index.ts
File metadata and controls
214 lines (189 loc) · 5.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
// Breakpoint thresholds (by width)
// Array indices: mobile=0, sm=1, tablet=2, lg=3, pc=4
// Always 5 slots
export const BREAKPOINTS = {
mobile: 480, // index 0
sm: 768, // index 1
tablet: 992, // index 2
lg: 1280, // index 3
pc: Infinity, // index 4
} as const
export type BreakpointKey = keyof typeof BREAKPOINTS
// Breakpoint order (by index)
// [0: mobile, 1: sm, 2: tablet, 3: lg, 4: pc]
export const BREAKPOINT_ORDER: BreakpointKey[] = [
'mobile', // 0
'sm', // 1
'tablet', // 2
'lg', // 3
'pc', // 4
]
// Array index for each breakpoint
export const BREAKPOINT_INDEX: Record<BreakpointKey, number> = {
mobile: 0,
sm: 1,
tablet: 2,
lg: 3,
pc: 4,
}
/**
* Decide breakpoint by width.
*/
export function getBreakpointByWidth(width: number): BreakpointKey {
if (width <= BREAKPOINTS.mobile) return 'mobile'
if (width <= BREAKPOINTS.sm) return 'sm'
if (width <= BREAKPOINTS.tablet) return 'tablet'
if (width <= BREAKPOINTS.lg) return 'lg'
return 'pc'
}
/**
* Group Section children by width.
*/
export function groupChildrenByBreakpoint(
children: readonly SceneNode[],
): Map<BreakpointKey, SceneNode[]> {
const groups = new Map<BreakpointKey, SceneNode[]>()
for (const child of children) {
if ('width' in child) {
const breakpoint = getBreakpointByWidth(child.width)
const group = groups.get(breakpoint) || []
group.push(child)
groups.set(breakpoint, group)
}
}
return groups
}
type PropValue = boolean | string | number | undefined | null | object
type Props = Record<string, PropValue>
const SPECIAL_PROPS_WITH_INITIAL = new Set(['display', 'position', 'pos'])
/**
* Compare two prop values for equality.
*/
function isEqual(a: PropValue, b: PropValue): boolean {
if (a === b) return true
if (a === null || b === null) return a === b
if (typeof a !== typeof b) return false
if (typeof a === 'object' && typeof b === 'object') {
return JSON.stringify(a) === JSON.stringify(b)
}
return false
}
/**
* Optimize responsive array.
*
* Rules:
* 1. If only index 0 has a value and the rest are null, return single value.
* 2. Consecutive identical values keep the first, later ones become null.
* 3. Remove trailing nulls only.
*
* Examples:
* ["100px", null, null] -> "100px" (only first has value)
* ["100px", "100px", "100px"] -> "100px" (all same)
* ["200px", "200px", "100px"] -> ["200px", null, "100px"]
* [null, null, "none"] -> [null, null, "none"] (keeps leading nulls)
* [null, null, "none", null, null] -> [null, null, "none"] (trim trailing null)
* ["100px", "200px", "200px"] -> ["100px", "200px"] (trailing equal treated as trailing null)
*/
export function optimizeResponsiveValue(
arr: (PropValue | null)[],
): PropValue | (PropValue | null)[] {
const nonNullValues = arr.filter((v) => v !== null)
if (nonNullValues.length === 0) return null
// Collapse consecutive identical values after the first to null.
const optimized: (PropValue | null)[] = [...arr]
let lastValue: PropValue | null = null
for (let i = 0; i < optimized.length; i++) {
const current = optimized[i]
if (current !== null) {
if (isEqual(current, lastValue)) {
optimized[i] = null
} else {
lastValue = current
}
}
}
// Remove trailing nulls.
while (optimized.length > 0 && optimized[optimized.length - 1] === null) {
optimized.pop()
}
// If only index 0 has value, return single value.
if (optimized.length === 1 && optimized[0] !== null) {
return optimized[0]
}
return optimized
}
/**
* Merge props across breakpoints into responsive arrays.
* Always 5 slots: [mobile, sm, tablet, lg, pc]; trailing nulls trimmed.
*/
export function mergePropsToResponsive(
breakpointProps: Map<BreakpointKey, Props>,
): Props {
const result: Props = {}
// If only one breakpoint, return props as-is.
if (breakpointProps.size === 1) {
const onlyProps = [...breakpointProps.values()][0]
return onlyProps ? { ...onlyProps } : {}
}
// Collect all prop keys.
const allKeys = new Set<string>()
for (const props of breakpointProps.values()) {
for (const key of Object.keys(props)) {
allKeys.add(key)
}
}
for (const key of allKeys) {
// Collect values for 5 fixed slots.
const values: (PropValue | null)[] = BREAKPOINT_ORDER.map((bp) => {
const props = breakpointProps.get(bp)
if (!props) return null
const value = key in props ? props[key] : null
return value ?? null
})
// For display/position family, fill last slot with 'initial' if empty after a change.
if (SPECIAL_PROPS_WITH_INITIAL.has(key)) {
const lastNonNull = (() => {
for (let i = values.length - 1; i >= 0; i--) {
if (values[i] !== null) return i
}
return -1
})()
const lastIndex = values.length - 1
if (
lastNonNull >= 0 &&
lastNonNull < lastIndex &&
values[lastIndex] === null
) {
values[lastIndex] = 'initial'
}
}
// Optimize: single when all same, otherwise array.
const optimized = optimizeResponsiveValue(values)
if (optimized !== null) {
result[key] = optimized
}
}
return result
}
export interface ResponsiveNodeGroup {
breakpoint: BreakpointKey
node: SceneNode
props: Props
}
/**
* Group nodes with the same name for responsive matching.
*/
export function groupNodesByName(
breakpointNodes: Map<BreakpointKey, SceneNode[]>,
): Map<string, ResponsiveNodeGroup[]> {
const result = new Map<string, ResponsiveNodeGroup[]>()
for (const [breakpoint, nodes] of breakpointNodes) {
for (const node of nodes) {
const name = node.name
const group = result.get(name) || []
group.push({ breakpoint, node, props: {} })
result.set(name, group)
}
}
return result
}