unframer 0.6.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/react.tsx ADDED
@@ -0,0 +1,229 @@
1
+ 'use client'
2
+ import { combinedCSSRules } from '../framer-fixed/dist/framer.js'
3
+
4
+ import {
5
+ ComponentPropsWithoutRef,
6
+ ComponentType,
7
+ ReactNode,
8
+ useEffect,
9
+ useSyncExternalStore,
10
+ } from 'react'
11
+
12
+ function getFonts(component) {
13
+ const fonts = component.fonts
14
+ return fonts || []
15
+ }
16
+
17
+ function classNames(...args) {
18
+ return args.filter(Boolean).join(' ')
19
+ }
20
+
21
+ const defaultBreakpoints = ['Desktop', 'Tablet', 'Mobile'] as const
22
+
23
+ type Breakpoint = (typeof defaultBreakpoints)[number]
24
+
25
+ let defaultMap: Record<Breakpoint, string> = Object.fromEntries(
26
+ defaultBreakpoints.map((x) => [x, x]),
27
+ ) as any
28
+
29
+ function getClassMap(breakpoints: Breakpoint[]): Record<Breakpoint, string> {
30
+ const classMap: Record<Breakpoint, string> = {
31
+ Desktop: '',
32
+ Tablet: '',
33
+ Mobile: '',
34
+ }
35
+
36
+ if (breakpoints.length === 1) {
37
+ classMap[breakpoints[0]] = 'FramerDesktop FramerTablet FramerMobile'
38
+ } else if (breakpoints.length === 2) {
39
+ if (breakpoints.includes('Desktop')) {
40
+ classMap.Desktop = 'Desktop'
41
+ classMap[breakpoints.find((b) => b !== 'Desktop')!] =
42
+ 'FramerTablet FramerMobile'
43
+ } else if (breakpoints.includes('Tablet')) {
44
+ classMap.Tablet = 'Tablet'
45
+ classMap[breakpoints.find((b) => b !== 'Tablet')!] =
46
+ 'FramerDesktop FramerMobile'
47
+ } else {
48
+ classMap.Mobile = 'Mobile'
49
+ classMap[breakpoints.find((b) => b !== 'Mobile')!] =
50
+ 'FramerDesktop FramerTablet'
51
+ }
52
+ } else if (breakpoints.length === 3) {
53
+ classMap.Desktop = 'FramerDesktop'
54
+ classMap.Tablet = 'FramerTablet'
55
+ classMap.Mobile = 'FramerMobile'
56
+ }
57
+
58
+ return classMap
59
+ }
60
+
61
+ function deduplicateByKey<T>(arr: T[], key: (k: T) => string) {
62
+ let map = new Map()
63
+ for (let item of arr) {
64
+ let value = item[key(item)]
65
+ if (map.has(value)) {
66
+ continue
67
+ }
68
+ map.set(value, item)
69
+ }
70
+ return Array.from(map.values())
71
+ }
72
+
73
+ export function getFontsStyles(Components) {
74
+ const allFonts = deduplicateByKey<{ family; url; style; weight }>(
75
+ Components.map(getFonts).flat(),
76
+ (x) => x.url,
77
+ ).filter((x) => x.url)
78
+
79
+ // console.log(JSON.stringify(fonts, null, 2))
80
+ let str = allFonts
81
+ .map((x) => {
82
+ let str = `@font-face { font-family: '${x.family}'; src: url(${x.url});`
83
+ if (x.style) {
84
+ str += ` font-style: ${x.style};`
85
+ }
86
+ if (x.weight) {
87
+ str += ` font-weight: ${x.weight};`
88
+ }
89
+ str += ` }`
90
+ return str
91
+ })
92
+ .join('\n')
93
+
94
+ return str
95
+ }
96
+
97
+ const breakpointSizes: Record<(typeof defaultBreakpoints)[number], number> = {
98
+ Desktop: 1024,
99
+ Tablet: 768,
100
+ Mobile: 0,
101
+ }
102
+
103
+ function getBreakpointNameFromWindowWidth(windowWidth: number) {
104
+ return defaultBreakpoints.find(
105
+ (name) => windowWidth >= breakpointSizes[name],
106
+ )
107
+ }
108
+
109
+ const breakpointsStyles = `
110
+
111
+ .FramerTablet,
112
+ .FramerMobile,
113
+ .FramerDesktop {
114
+ display: none;
115
+ }
116
+
117
+ @media (min-width: ${breakpointSizes.Desktop}px) {
118
+ .FramerDesktop {
119
+ display: contents;
120
+ }
121
+ }
122
+
123
+ @media (min-width: ${breakpointSizes.Tablet}px) and (max-width: ${breakpointSizes.Desktop}px) {
124
+ .FramerTablet {
125
+ display: contents;
126
+ }
127
+ }
128
+
129
+ @media (max-width: ${breakpointSizes.Tablet}px) {
130
+ .FramerMobile {
131
+ display: contents;
132
+ }
133
+ }
134
+
135
+ .contents {
136
+ display: contents;
137
+ }
138
+
139
+ `
140
+
141
+ export function FramerStyles({ Components = [] as any[] }) {
142
+ return (
143
+ <>
144
+ <style
145
+ dangerouslySetInnerHTML={{ __html: getFontsStyles(Components) }}
146
+ suppressHydrationWarning
147
+ hidden
148
+ />
149
+ <style
150
+ dangerouslySetInnerHTML={{
151
+ __html: combinedCSSRules.join('\n'),
152
+ }}
153
+ suppressHydrationWarning
154
+ hidden
155
+ />
156
+ <style
157
+ dangerouslySetInnerHTML={{ __html: breakpointsStyles }}
158
+ suppressHydrationWarning
159
+ hidden
160
+ />
161
+ </>
162
+ )
163
+ }
164
+
165
+ export function WithFramerBreakpoints<
166
+ T extends ComponentType<{ variant?: any; className?: string }>,
167
+ >({
168
+ Component,
169
+ variants: breakpointsMap = defaultMap,
170
+ ...rest
171
+ }: {
172
+ Component: T
173
+ variants?: Record<Breakpoint, ComponentPropsWithoutRef<T>['variant']>
174
+ } & Omit<ComponentPropsWithoutRef<T>, 'variant'>) {
175
+ const controls = Component['propertyControls']
176
+
177
+ const variantControls = controls?.['variant']
178
+ if (!variantControls) {
179
+ // @ts-expect-error
180
+ return <Component variant={undefined} {...rest} />
181
+ }
182
+
183
+ const options = variantControls?.optionTitles
184
+
185
+ const currentBreakpoint = useSyncExternalStore(
186
+ onResize,
187
+ () => {
188
+ // console.log('window.innerWidth', window.innerWidth)
189
+ const breakpoint = getBreakpointNameFromWindowWidth(
190
+ window.innerWidth,
191
+ )
192
+ return breakpoint
193
+ },
194
+ () => {
195
+ // on server
196
+ return ''
197
+ },
198
+ )
199
+ // console.log('currentBreakpoint', currentBreakpoint)
200
+
201
+ let parts: ReactNode[] = []
202
+ for (let breakpointName of defaultBreakpoints) {
203
+ if (currentBreakpoint && currentBreakpoint !== breakpointName) {
204
+ continue
205
+ }
206
+ let realVariant = breakpointsMap[breakpointName]
207
+ if (!realVariant) {
208
+ continue
209
+ }
210
+ let mapped = defaultBreakpoints.filter((x) => breakpointsMap[x])
211
+
212
+ let map = getClassMap(mapped)[breakpointName]
213
+ let className = classNames('', map)
214
+
215
+ parts.push(
216
+ <div key={breakpointName} className={className}>
217
+ {/* @ts-expect-error */}
218
+ <Component {...rest} variant={realVariant} />
219
+ </div>,
220
+ )
221
+ }
222
+
223
+ return parts
224
+ }
225
+
226
+ const onResize = (callback) => {
227
+ window.addEventListener('resize', callback)
228
+ return () => window.removeEventListener('resize', callback)
229
+ }