unframer 4.1.8 → 4.2.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/README.md +5 -5
- package/dist/babel-jsx.d.ts +9 -0
- package/dist/babel-jsx.d.ts.map +1 -1
- package/dist/babel-jsx.js +72 -0
- package/dist/babel-jsx.js.map +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +262 -42
- package/dist/cli.js.map +1 -1
- package/dist/css-core.d.ts +50 -0
- package/dist/css-core.d.ts.map +1 -0
- package/dist/css-core.js +231 -0
- package/dist/css-core.js.map +1 -0
- package/dist/css.d.ts +9 -43
- package/dist/css.d.ts.map +1 -1
- package/dist/css.js +9 -232
- package/dist/css.js.map +1 -1
- package/dist/exporter.d.ts +1 -1
- package/dist/exporter.d.ts.map +1 -1
- package/dist/exporter.js +72 -11
- package/dist/exporter.js.map +1 -1
- package/dist/exporter.test.js +78 -0
- package/dist/exporter.test.js.map +1 -1
- package/dist/framer-chunks/{SqliteDatabase-VAKIICSG-R7ZS6CHH.js → SqliteDatabase-VAKIICSG-OPV4MG3Y.js} +5 -4
- package/dist/framer-chunks/{chunk-2DZGP7C2.js → chunk-7ZLQTYXQ.js} +5 -24
- package/dist/framer-chunks/chunk-IKQSD2QC.js +10 -0
- package/dist/framer-chunks/default-blog-sqlite-7ZHEY3GT-TVGUYU6H.js +7 -0
- package/dist/framer-chunks/fontshare-4THNDPMZ-FLLTWCDO.js +7 -0
- package/dist/framer-chunks/fontshare-B2QLD7YB-5V3XZJAH.js +7 -0
- package/dist/framer-chunks/fontshare-O22OBJ3D-FIG3CRN3.js +7 -0
- package/dist/framer-chunks/framer-font-45AI7UCZ-CKGC4MJC.js +7 -0
- package/dist/framer-chunks/google-3FCAKCAC-AM34UPJK.js +7 -0
- package/dist/framer-chunks/google-3SZHWBC6-2VTQEQ7J.js +7 -0
- package/dist/framer-chunks/google-GXDJLGJB-JNEJGCGD.js +7 -0
- package/dist/framer-chunks/sqlite-wasm-FGP37EAY-NO5QVZDI.js +7 -0
- package/dist/framer-chunks/sqlite3-SISQ6ENZ-5WRKGWTY.js +7 -0
- package/dist/framer.d.ts +128 -0
- package/dist/framer.js +820 -428
- package/dist/plugin-mcp-dist/lib/framer.d.ts +1 -7
- package/dist/plugin-mcp-dist/lib/framer.d.ts.map +1 -1
- package/dist/plugin-mcp-dist/lib/framer.js +90 -23
- package/dist/plugin-mcp-dist/lib/framer.js.map +1 -1
- package/dist/plugin-mcp-dist/lib/framer.test.d.ts +2 -0
- package/dist/plugin-mcp-dist/lib/framer.test.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/framer.test.js +214 -0
- package/dist/plugin-mcp-dist/lib/framer.test.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/mcp-handlers.d.ts.map +1 -1
- package/dist/plugin-mcp-dist/lib/mcp-handlers.js +69 -11
- package/dist/plugin-mcp-dist/lib/mcp-handlers.js.map +1 -1
- package/dist/plugin-mcp-dist/lib/mcp.test.js +340 -364
- package/dist/plugin-mcp-dist/lib/mcp.test.js.map +1 -1
- package/dist/plugin-mcp-dist/lib/plugin-websocket.d.ts.map +1 -1
- package/dist/plugin-mcp-dist/lib/plugin-websocket.js +0 -3
- package/dist/plugin-mcp-dist/lib/plugin-websocket.js.map +1 -1
- package/dist/plugin-mcp-dist/lib/react-export.d.ts.map +1 -1
- package/dist/plugin-mcp-dist/lib/react-export.js +79 -61
- package/dist/plugin-mcp-dist/lib/react-export.js.map +1 -1
- package/dist/plugin-mcp-dist/lib/schema.d.ts +8 -0
- package/dist/plugin-mcp-dist/lib/schema.d.ts.map +1 -1
- package/dist/plugin-mcp-dist/lib/schema.js +18 -1
- package/dist/plugin-mcp-dist/lib/schema.js.map +1 -1
- package/dist/plugin-mcp-dist/lib/store.d.ts +1 -1
- package/dist/plugin-mcp-dist/lib/store.d.ts.map +1 -1
- package/dist/plugin-mcp-dist/lib/tunnel-integration.test.d.ts +2 -0
- package/dist/plugin-mcp-dist/lib/tunnel-integration.test.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/tunnel-integration.test.js +147 -0
- package/dist/plugin-mcp-dist/lib/tunnel-integration.test.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/tunnel.d.ts +46 -0
- package/dist/plugin-mcp-dist/lib/tunnel.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/tunnel.js +117 -0
- package/dist/plugin-mcp-dist/lib/tunnel.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/upstream-socket.d.ts +13 -0
- package/dist/plugin-mcp-dist/lib/upstream-socket.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/upstream-socket.js +56 -0
- package/dist/plugin-mcp-dist/lib/upstream-socket.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/upstream-socket.test.d.ts +2 -0
- package/dist/plugin-mcp-dist/lib/upstream-socket.test.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/upstream-socket.test.js +212 -0
- package/dist/plugin-mcp-dist/lib/upstream-socket.test.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/utils.d.ts +1 -9
- package/dist/plugin-mcp-dist/lib/utils.d.ts.map +1 -1
- package/dist/plugin-mcp-dist/lib/utils.js +13 -9
- package/dist/plugin-mcp-dist/lib/utils.js.map +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +7 -13
- package/dist/react.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -8
- package/src/babel-jsx.ts +99 -0
- package/src/cli.ts +295 -42
- package/src/css-core.ts +277 -0
- package/src/css.tsx +10 -276
- package/src/exporter.test.ts +82 -0
- package/src/exporter.ts +92 -11
- package/src/framer.d.ts +128 -0
- package/src/framer.js +820 -428
- package/src/plugin-mcp-dist/lib/framer.d.ts +2 -5
- package/src/plugin-mcp-dist/lib/framer.d.ts.map +1 -1
- package/src/plugin-mcp-dist/lib/framer.js +90 -23
- package/src/plugin-mcp-dist/lib/framer.js.map +1 -1
- package/src/plugin-mcp-dist/lib/framer.test.d.ts +2 -0
- package/src/plugin-mcp-dist/lib/framer.test.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/framer.test.js +213 -0
- package/src/plugin-mcp-dist/lib/framer.test.js.map +1 -0
- package/src/plugin-mcp-dist/lib/mcp-handlers.d.ts +2 -2
- package/src/plugin-mcp-dist/lib/mcp-handlers.d.ts.map +1 -1
- package/src/plugin-mcp-dist/lib/mcp-handlers.js +69 -11
- package/src/plugin-mcp-dist/lib/mcp-handlers.js.map +1 -1
- package/src/plugin-mcp-dist/lib/mcp.test.js +340 -364
- package/src/plugin-mcp-dist/lib/mcp.test.js.map +1 -1
- package/src/plugin-mcp-dist/lib/plugin-websocket.d.ts.map +1 -1
- package/src/plugin-mcp-dist/lib/plugin-websocket.js +0 -3
- package/src/plugin-mcp-dist/lib/plugin-websocket.js.map +1 -1
- package/src/plugin-mcp-dist/lib/react-export.d.ts +1 -1
- package/src/plugin-mcp-dist/lib/react-export.d.ts.map +1 -1
- package/src/plugin-mcp-dist/lib/react-export.js +79 -61
- package/src/plugin-mcp-dist/lib/react-export.js.map +1 -1
- package/src/plugin-mcp-dist/lib/schema.d.ts +11 -3
- package/src/plugin-mcp-dist/lib/schema.d.ts.map +1 -1
- package/src/plugin-mcp-dist/lib/schema.js +18 -1
- package/src/plugin-mcp-dist/lib/schema.js.map +1 -1
- package/src/plugin-mcp-dist/lib/tunnel-integration.test.d.ts +2 -0
- package/src/plugin-mcp-dist/lib/tunnel-integration.test.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/tunnel-integration.test.js +146 -0
- package/src/plugin-mcp-dist/lib/tunnel-integration.test.js.map +1 -0
- package/src/plugin-mcp-dist/lib/tunnel.d.ts +55 -0
- package/src/plugin-mcp-dist/lib/tunnel.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/tunnel.js +116 -0
- package/src/plugin-mcp-dist/lib/tunnel.js.map +1 -0
- package/src/plugin-mcp-dist/lib/upstream-socket.d.ts +28 -0
- package/src/plugin-mcp-dist/lib/upstream-socket.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/upstream-socket.js +55 -0
- package/src/plugin-mcp-dist/lib/upstream-socket.js.map +1 -0
- package/src/plugin-mcp-dist/lib/upstream-socket.test.d.ts +5 -0
- package/src/plugin-mcp-dist/lib/upstream-socket.test.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/upstream-socket.test.js +211 -0
- package/src/plugin-mcp-dist/lib/upstream-socket.test.js.map +1 -0
- package/src/plugin-mcp-dist/lib/utils.d.ts +2 -2
- package/src/plugin-mcp-dist/lib/utils.d.ts.map +1 -1
- package/src/plugin-mcp-dist/lib/utils.js +13 -9
- package/src/plugin-mcp-dist/lib/utils.js.map +1 -1
- package/src/react.tsx +7 -19
- package/src/styles/framer.css +34 -64
- package/src/version.ts +1 -1
- package/dist/framer-chunks/default-blog-sqlite-7ZHEY3GT-27R5KAAW.js +0 -9
- package/dist/framer-chunks/fontshare-4THNDPMZ-BJQGNHXN.js +0 -27099
- package/dist/framer-chunks/fontshare-B2QLD7YB-4BZEAA37.js +0 -119
- package/dist/framer-chunks/fontshare-O22OBJ3D-ALBQLFE5.js +0 -825
- package/dist/framer-chunks/framer-font-45AI7UCZ-LU7DEIDM.js +0 -7
- package/dist/framer-chunks/google-3FCAKCAC-P5EL6KGL.js +0 -623876
- package/dist/framer-chunks/google-3SZHWBC6-OBXS3UIH.js +0 -1627
- package/dist/framer-chunks/google-GXDJLGJB-HHIXFE4M.js +0 -6885
- package/dist/framer-chunks/sqlite-wasm-FGP37EAY-HR6PIAJQ.js +0 -11479
- package/dist/framer-chunks/sqlite3-SISQ6ENZ-KMXYXSSV.js +0 -9
- package/dist/generated/api-client.d.ts +0 -21
- package/dist/generated/api-client.d.ts.map +0 -1
- package/dist/generated/api-client.js +0 -27
- package/dist/generated/api-client.js.map +0 -1
- package/src/generated/api-client.d.ts +0 -1238
- package/src/generated/api-client.js +0 -26
package/src/css-core.ts
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure CSS generation helpers for exported Framer components.
|
|
3
|
+
* This file must not import React or framer.js so server-only tools can use it
|
|
4
|
+
* without loading the generated Framer runtime in React Server Components.
|
|
5
|
+
*/
|
|
6
|
+
import dedent from 'string-dedent'
|
|
7
|
+
|
|
8
|
+
export interface ComponentFont {
|
|
9
|
+
cssFamilyName?: string
|
|
10
|
+
family?: string
|
|
11
|
+
url: string
|
|
12
|
+
style?: string
|
|
13
|
+
weight?: string | number
|
|
14
|
+
unicodeRange?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getFontFamilyName(font: ComponentFont): string | undefined {
|
|
18
|
+
return font.cssFamilyName || font.family
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function deduplicateByKey<T>(arr: T[], key: (k: T) => string): T[] {
|
|
22
|
+
const map = new Map<string, T>()
|
|
23
|
+
for (const item of arr) {
|
|
24
|
+
const value = key(item)
|
|
25
|
+
if (map.has(value)) {
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
map.set(value, item)
|
|
29
|
+
}
|
|
30
|
+
return Array.from(map.values())
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ComponentFontBundle = {
|
|
34
|
+
/**
|
|
35
|
+
* This flag specifies whether the font bundle includes the specific font
|
|
36
|
+
* weights of Framer’s Inter font that the component uses. New smart
|
|
37
|
+
* components do that (which means we can emit CSS only for Inter fonts that
|
|
38
|
+
* are actually used); older smart components don’t (which means some places
|
|
39
|
+
* will emit CSS for all Inter fonts if this flag is not set).
|
|
40
|
+
* https://www.notion.so/framer/RFC-ComponentFont-v2-d5fd3e822fb049ffb6971554ab0e4e42
|
|
41
|
+
*/
|
|
42
|
+
explicitInter: boolean
|
|
43
|
+
fileName?: string
|
|
44
|
+
fonts: ComponentFont[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function logFontsUsage(fontsBundles: ComponentFontBundle[]) {
|
|
48
|
+
if (!fontsBundles.length) {
|
|
49
|
+
return ''
|
|
50
|
+
}
|
|
51
|
+
const familyToFilenames = new Map<string, Set<string>>()
|
|
52
|
+
for (const fontDefBundle of fontsBundles) {
|
|
53
|
+
const filename = fontDefBundle.fileName
|
|
54
|
+
for (const font of fontDefBundle.fonts) {
|
|
55
|
+
const familyName = getFontFamilyName(font)
|
|
56
|
+
if (!familyName) continue
|
|
57
|
+
if (familyToFilenames.has(familyName)) {
|
|
58
|
+
familyToFilenames.get(familyName)!.add(filename!)
|
|
59
|
+
} else {
|
|
60
|
+
familyToFilenames.set(familyName, new Set([filename!]))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
let str = `There are ${familyToFilenames.size} fonts used:\n`
|
|
65
|
+
for (const [family, filenames] of familyToFilenames.entries()) {
|
|
66
|
+
str += `${JSON.stringify(family)}, used by\n`
|
|
67
|
+
for (const filename of filenames) {
|
|
68
|
+
str += ` - ${filename}\n`
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return str
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getFontsStyles(_fontsDefs: ComponentFontBundle[]) {
|
|
75
|
+
if (!_fontsDefs.length) {
|
|
76
|
+
return ''
|
|
77
|
+
}
|
|
78
|
+
const urlToFilenames = new Map<string, Set<string>>()
|
|
79
|
+
|
|
80
|
+
for (const fontDefBundle of _fontsDefs) {
|
|
81
|
+
const filename = fontDefBundle.fileName
|
|
82
|
+
for (const font of fontDefBundle.fonts) {
|
|
83
|
+
if (urlToFilenames.has(font.url)) {
|
|
84
|
+
urlToFilenames.get(font.url)!.add(filename!)
|
|
85
|
+
} else {
|
|
86
|
+
urlToFilenames.set(font.url, new Set([filename!]))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const allFonts = deduplicateByKey(
|
|
91
|
+
_fontsDefs.flatMap((x) => x.fonts),
|
|
92
|
+
(x) => x?.url,
|
|
93
|
+
)
|
|
94
|
+
.filter((x) => {
|
|
95
|
+
if (!x.url) return false
|
|
96
|
+
const familyName = getFontFamilyName(x)
|
|
97
|
+
if (!familyName) {
|
|
98
|
+
console.log('Font missing family field:', JSON.stringify(x, null, 2))
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
return true
|
|
102
|
+
})
|
|
103
|
+
.sort((a, b) => a.url.localeCompare(b.url))
|
|
104
|
+
|
|
105
|
+
const grouped = groupBy(allFonts, (x) => {
|
|
106
|
+
return [...(urlToFilenames.get(x.url) || [])].sort().join(', ')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
let str = '\n\n'
|
|
110
|
+
for (let [groupComment, fonts] of grouped.entries()) {
|
|
111
|
+
fonts = sortByKey(fonts, (x) => x.url)
|
|
112
|
+
str += `/* used by ${groupComment} */\n`
|
|
113
|
+
str +=
|
|
114
|
+
'\n' +
|
|
115
|
+
fonts
|
|
116
|
+
.map((x) => {
|
|
117
|
+
let str = ''
|
|
118
|
+
const familyName = getFontFamilyName(x)!
|
|
119
|
+
str += dedent`
|
|
120
|
+
@font-face {
|
|
121
|
+
font-family: "${familyName}";
|
|
122
|
+
src: url('${x.url}');\n
|
|
123
|
+
`
|
|
124
|
+
if (x.style) {
|
|
125
|
+
str += ` font-style: ${x.style};\n`
|
|
126
|
+
}
|
|
127
|
+
if (x.weight) {
|
|
128
|
+
str += ` font-weight: ${x.weight};\n`
|
|
129
|
+
}
|
|
130
|
+
if (x.unicodeRange) {
|
|
131
|
+
str += ` unicode-range: ${x.unicodeRange};\n`
|
|
132
|
+
}
|
|
133
|
+
str += `}\n`
|
|
134
|
+
return str
|
|
135
|
+
})
|
|
136
|
+
.join('\n') +
|
|
137
|
+
'\n'
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return str
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const defaultBreakpointSizes = {
|
|
144
|
+
base: 0,
|
|
145
|
+
sm: 320,
|
|
146
|
+
md: 768,
|
|
147
|
+
lg: 960,
|
|
148
|
+
xl: 1200,
|
|
149
|
+
'2xl': 1536,
|
|
150
|
+
} as const
|
|
151
|
+
|
|
152
|
+
export type BreakpointSizes = typeof defaultBreakpointSizes
|
|
153
|
+
|
|
154
|
+
export const breakpointsStylesLegacy = (breakpointSizes?: BreakpointSizes) => {
|
|
155
|
+
breakpointSizes = { ...defaultBreakpointSizes, ...breakpointSizes }
|
|
156
|
+
return /* css */ `
|
|
157
|
+
/* Base */
|
|
158
|
+
@media (min-width: ${breakpointSizes.base}px) and (max-width: ${
|
|
159
|
+
breakpointSizes.sm - 1
|
|
160
|
+
}px) {
|
|
161
|
+
.unframer-hidden.unframer-base {
|
|
162
|
+
display: contents;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* Small */
|
|
167
|
+
@media (min-width: ${breakpointSizes.sm}px) and (max-width: ${
|
|
168
|
+
breakpointSizes.md - 1
|
|
169
|
+
}px) {
|
|
170
|
+
.unframer-hidden.unframer-sm {
|
|
171
|
+
display: contents;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* Medium */
|
|
176
|
+
@media (min-width: ${breakpointSizes.md}px) and (max-width: ${
|
|
177
|
+
breakpointSizes.lg - 1
|
|
178
|
+
}px) {
|
|
179
|
+
.unframer-hidden.unframer-md {
|
|
180
|
+
display: contents;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* Large */
|
|
185
|
+
@media (min-width: ${breakpointSizes.lg}px) and (max-width: ${
|
|
186
|
+
breakpointSizes.xl - 1
|
|
187
|
+
}px) {
|
|
188
|
+
.unframer-hidden.unframer-lg {
|
|
189
|
+
display: contents;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* Extra Large */
|
|
194
|
+
@media (min-width: ${breakpointSizes.xl}px) and (max-width: ${
|
|
195
|
+
breakpointSizes['2xl'] - 1
|
|
196
|
+
}px) {
|
|
197
|
+
.unframer-hidden.unframer-xl {
|
|
198
|
+
display: contents;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* 2 Extra Large */
|
|
203
|
+
@media (min-width: ${breakpointSizes['2xl']}px) {
|
|
204
|
+
.unframer-hidden.unframer-2xl {
|
|
205
|
+
display: contents;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.unframer-hidden {
|
|
210
|
+
display: none;
|
|
211
|
+
}
|
|
212
|
+
`
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export const breakpointsStyles = (breakpointSizes?: BreakpointSizes) => {
|
|
216
|
+
breakpointSizes = { ...defaultBreakpointSizes, ...breakpointSizes }
|
|
217
|
+
return /* css */ `
|
|
218
|
+
|
|
219
|
+
/* Base */
|
|
220
|
+
@media (min-width: ${breakpointSizes.base}px) and (max-width: ${breakpointSizes.sm - 1}px) {
|
|
221
|
+
.unframer-hidden:not(.unframer-base) {
|
|
222
|
+
display: none !important;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* Small */
|
|
227
|
+
@media (min-width: ${breakpointSizes.sm}px) and (max-width: ${breakpointSizes.md - 1}px) {
|
|
228
|
+
.unframer-hidden:not(.unframer-sm) {
|
|
229
|
+
display: none !important;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* Medium */
|
|
234
|
+
@media (min-width: ${breakpointSizes.md}px) and (max-width: ${breakpointSizes.lg - 1}px) {
|
|
235
|
+
.unframer-hidden:not(.unframer-md) {
|
|
236
|
+
display: none !important;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* Large */
|
|
241
|
+
@media (min-width: ${breakpointSizes.lg}px) and (max-width: ${breakpointSizes.xl - 1}px) {
|
|
242
|
+
.unframer-hidden:not(.unframer-lg) {
|
|
243
|
+
display: none !important;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* Extra Large */
|
|
248
|
+
@media (min-width: ${breakpointSizes.xl}px) and (max-width: ${breakpointSizes['2xl'] - 1}px) {
|
|
249
|
+
.unframer-hidden:not(.unframer-xl) {
|
|
250
|
+
display: none !important;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* 2 Extra Large */
|
|
255
|
+
@media (min-width: ${breakpointSizes['2xl']}px) {
|
|
256
|
+
.unframer-hidden:not(.unframer-2xl) {
|
|
257
|
+
display: none !important;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
`
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function groupBy<T>(arr: T[], key: (x: T) => string) {
|
|
264
|
+
const map = new Map<string, T[]>()
|
|
265
|
+
for (const item of arr) {
|
|
266
|
+
const k = key(item)
|
|
267
|
+
if (!map.has(k)) {
|
|
268
|
+
map.set(k, [])
|
|
269
|
+
}
|
|
270
|
+
map.get(k)?.push(item)
|
|
271
|
+
}
|
|
272
|
+
return map
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function sortByKey<T>(arr: T[], key: (x: T) => string) {
|
|
276
|
+
return arr.slice().sort((a, b) => key(a).localeCompare(key(b)))
|
|
277
|
+
}
|
package/src/css.tsx
CHANGED
|
@@ -1,281 +1,18 @@
|
|
|
1
|
-
import dedent from 'string-dedent'
|
|
2
|
-
import { withCSS as withCSSOriginal } from './framer.js'
|
|
3
1
|
import React from 'react'
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
interface ComponentFontV1 {
|
|
7
|
-
family: string
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
type AnyComponentFont = ComponentFont | ComponentFontV1
|
|
11
|
-
|
|
12
|
-
function getFontFamilyName(font: AnyComponentFont): string | undefined {
|
|
13
|
-
// New fonts have cssFamilyName, old fonts have family
|
|
14
|
-
return (font as ComponentFont).cssFamilyName || (font as ComponentFontV1).family
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function deduplicateByKey<T>(arr: T[], key: (k: T) => string): T[] {
|
|
18
|
-
let map = new Map()
|
|
19
|
-
for (let item of arr) {
|
|
20
|
-
let value = key(item)
|
|
21
|
-
if (map.has(value)) {
|
|
22
|
-
continue
|
|
23
|
-
}
|
|
24
|
-
map.set(value, item)
|
|
25
|
-
}
|
|
26
|
-
return Array.from(map.values())
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type ComponentFontBundle = {
|
|
30
|
-
/**
|
|
31
|
-
* This flag specifies whether the font bundle includes the specific font
|
|
32
|
-
* weights of Framer’s Inter font that the component uses. New smart
|
|
33
|
-
* components do that (which means we can emit CSS only for Inter fonts that
|
|
34
|
-
* are actually used); older smart components don’t (which means some places
|
|
35
|
-
* will emit CSS for all Inter fonts if this flag is not set).
|
|
36
|
-
* https://www.notion.so/framer/RFC-ComponentFont-v2-d5fd3e822fb049ffb6971554ab0e4e42
|
|
37
|
-
*/
|
|
38
|
-
explicitInter: boolean
|
|
39
|
-
fileName?: string
|
|
40
|
-
fonts: ComponentFont[]
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function logFontsUsage(fontsBundles: ComponentFontBundle[]) {
|
|
44
|
-
if (!fontsBundles.length) {
|
|
45
|
-
return ''
|
|
46
|
-
}
|
|
47
|
-
let familyToFilenames = new Map<string, Set<string>>()
|
|
48
|
-
for (let fontDefBundle of fontsBundles) {
|
|
49
|
-
let filename = fontDefBundle.fileName
|
|
50
|
-
for (let font of fontDefBundle.fonts) {
|
|
51
|
-
const familyName = getFontFamilyName(font)
|
|
52
|
-
if (!familyName) continue
|
|
53
|
-
if (familyToFilenames.has(familyName)) {
|
|
54
|
-
familyToFilenames.get(familyName)!.add(filename!)
|
|
55
|
-
} else {
|
|
56
|
-
familyToFilenames.set(familyName, new Set([filename!]))
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
let str = `There are ${familyToFilenames.size} fonts used:\n`
|
|
61
|
-
for (let [family, filenames] of familyToFilenames.entries()) {
|
|
62
|
-
str += `${JSON.stringify(family)}, used by\n`
|
|
63
|
-
for (let filename of filenames) {
|
|
64
|
-
str += ` - ${filename}\n`
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return str
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function getFontsStyles(_fontsDefs: ComponentFontBundle[]) {
|
|
71
|
-
if (!_fontsDefs.length) {
|
|
72
|
-
return ''
|
|
73
|
-
}
|
|
74
|
-
let urlToFilenames = new Map<string, Set<string>>()
|
|
75
|
-
|
|
76
|
-
for (let fontDefBundle of _fontsDefs) {
|
|
77
|
-
let filename = fontDefBundle.fileName
|
|
78
|
-
for (let font of fontDefBundle.fonts) {
|
|
79
|
-
if (urlToFilenames.has(font.url)) {
|
|
80
|
-
urlToFilenames.get(font.url)!.add(filename!)
|
|
81
|
-
} else {
|
|
82
|
-
urlToFilenames.set(font.url, new Set([filename!]))
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
const allFonts = deduplicateByKey(
|
|
87
|
-
_fontsDefs.flatMap((x) => x.fonts),
|
|
88
|
-
(x) => x?.url,
|
|
89
|
-
)
|
|
90
|
-
.filter((x) => {
|
|
91
|
-
if (!x.url) return false
|
|
92
|
-
const familyName = getFontFamilyName(x)
|
|
93
|
-
if (!familyName) {
|
|
94
|
-
console.log('Font missing family field:', JSON.stringify(x, null, 2))
|
|
95
|
-
return false
|
|
96
|
-
}
|
|
97
|
-
return true
|
|
98
|
-
})
|
|
99
|
-
.sort((a, b) => a.url.localeCompare(b.url))
|
|
100
|
-
|
|
101
|
-
// group fonts by the filenames users
|
|
102
|
-
const grouped = groupBy(allFonts, (x) => {
|
|
103
|
-
return [...(urlToFilenames.get(x.url) || [])].sort().join(', ')
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
let str = '\n\n'
|
|
107
|
-
for (let [groupComment, fonts] of grouped.entries()) {
|
|
108
|
-
fonts = sortByKey(fonts, (x) => x.url)
|
|
109
|
-
str += `/* used by ${groupComment} */\n`
|
|
110
|
-
str +=
|
|
111
|
-
'\n' +
|
|
112
|
-
fonts
|
|
113
|
-
.map((x) => {
|
|
114
|
-
let str = ''
|
|
115
|
-
const familyName = getFontFamilyName(x)!
|
|
116
|
-
str += dedent`
|
|
117
|
-
@font-face {
|
|
118
|
-
font-family: "${familyName}";
|
|
119
|
-
src: url('${x.url}');\n
|
|
120
|
-
`
|
|
121
|
-
if (x.style) {
|
|
122
|
-
str += ` font-style: ${x.style};\n`
|
|
123
|
-
}
|
|
124
|
-
if (x.weight) {
|
|
125
|
-
str += ` font-weight: ${x.weight};\n`
|
|
126
|
-
}
|
|
127
|
-
if (x.unicodeRange) {
|
|
128
|
-
str += ` unicode-range: ${x.unicodeRange};\n`
|
|
129
|
-
}
|
|
130
|
-
str += `}\n`
|
|
131
|
-
return str
|
|
132
|
-
})
|
|
133
|
-
.join('\n') +
|
|
134
|
-
'\n'
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return str
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export const defaultBreakpointSizes = {
|
|
141
|
-
base: 0,
|
|
142
|
-
sm: 320,
|
|
143
|
-
md: 768,
|
|
144
|
-
lg: 960,
|
|
145
|
-
xl: 1200,
|
|
146
|
-
'2xl': 1536,
|
|
147
|
-
} as const
|
|
148
|
-
|
|
149
|
-
export type BreakpointSizes = typeof defaultBreakpointSizes
|
|
150
|
-
|
|
151
|
-
// @deprecated TODO remove this in future releases
|
|
152
|
-
export const breakpointsStylesLegacy = (breakpointSizes?: BreakpointSizes) => {
|
|
153
|
-
breakpointSizes = { ...defaultBreakpointSizes, ...breakpointSizes }
|
|
154
|
-
return /* css */ `
|
|
155
|
-
/* Base */
|
|
156
|
-
@media (min-width: ${breakpointSizes.base}px) and (max-width: ${
|
|
157
|
-
breakpointSizes.sm - 1
|
|
158
|
-
}px) {
|
|
159
|
-
.unframer-hidden.unframer-base {
|
|
160
|
-
display: contents;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/* Small */
|
|
165
|
-
@media (min-width: ${breakpointSizes.sm}px) and (max-width: ${
|
|
166
|
-
breakpointSizes.md - 1
|
|
167
|
-
}px) {
|
|
168
|
-
.unframer-hidden.unframer-sm {
|
|
169
|
-
display: contents;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/* Medium */
|
|
174
|
-
@media (min-width: ${breakpointSizes.md}px) and (max-width: ${
|
|
175
|
-
breakpointSizes.lg - 1
|
|
176
|
-
}px) {
|
|
177
|
-
.unframer-hidden.unframer-md {
|
|
178
|
-
display: contents;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/* Large */
|
|
183
|
-
@media (min-width: ${breakpointSizes.lg}px) and (max-width: ${
|
|
184
|
-
breakpointSizes.xl - 1
|
|
185
|
-
}px) {
|
|
186
|
-
.unframer-hidden.unframer-lg {
|
|
187
|
-
display: contents;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/* Extra Large */
|
|
192
|
-
@media (min-width: ${breakpointSizes.xl}px) and (max-width: ${
|
|
193
|
-
breakpointSizes['2xl'] - 1
|
|
194
|
-
}px) {
|
|
195
|
-
.unframer-hidden.unframer-xl {
|
|
196
|
-
display: contents;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/* 2 Extra Large */
|
|
201
|
-
@media (min-width: ${breakpointSizes['2xl']}px) {
|
|
202
|
-
.unframer-hidden.unframer-2xl {
|
|
203
|
-
display: contents;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
.unframer-hidden {
|
|
208
|
-
display: none;
|
|
209
|
-
}
|
|
210
|
-
`
|
|
211
|
-
}
|
|
212
|
-
export const breakpointsStyles = (breakpointSizes?: BreakpointSizes) => {
|
|
213
|
-
breakpointSizes = { ...defaultBreakpointSizes, ...breakpointSizes }
|
|
214
|
-
return /* css */ `
|
|
215
|
-
|
|
216
|
-
/* Base */
|
|
217
|
-
@media (min-width: ${breakpointSizes.base}px) and (max-width: ${breakpointSizes.sm - 1}px) {
|
|
218
|
-
.unframer-hidden:not(.unframer-base) {
|
|
219
|
-
display: none !important;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/* Small */
|
|
224
|
-
@media (min-width: ${breakpointSizes.sm}px) and (max-width: ${breakpointSizes.md - 1}px) {
|
|
225
|
-
.unframer-hidden:not(.unframer-sm) {
|
|
226
|
-
display: none !important;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/* Medium */
|
|
231
|
-
@media (min-width: ${breakpointSizes.md}px) and (max-width: ${breakpointSizes.lg - 1}px) {
|
|
232
|
-
.unframer-hidden:not(.unframer-md) {
|
|
233
|
-
display: none !important;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/* Large */
|
|
238
|
-
@media (min-width: ${breakpointSizes.lg}px) and (max-width: ${breakpointSizes.xl - 1}px) {
|
|
239
|
-
.unframer-hidden:not(.unframer-lg) {
|
|
240
|
-
display: none !important;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/* Extra Large */
|
|
245
|
-
@media (min-width: ${breakpointSizes.xl}px) and (max-width: ${breakpointSizes['2xl'] - 1}px) {
|
|
246
|
-
.unframer-hidden:not(.unframer-xl) {
|
|
247
|
-
display: none !important;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/* 2 Extra Large */
|
|
252
|
-
@media (min-width: ${breakpointSizes['2xl']}px) {
|
|
253
|
-
.unframer-hidden:not(.unframer-2xl) {
|
|
254
|
-
display: none !important;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
`
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export function groupBy<T>(arr: T[], key: (x: T) => string) {
|
|
261
|
-
const map = new Map<string, T[]>()
|
|
262
|
-
for (let item of arr) {
|
|
263
|
-
const k = key(item)
|
|
264
|
-
if (!map.has(k)) {
|
|
265
|
-
map.set(k, [])
|
|
266
|
-
}
|
|
267
|
-
map.get(k)?.push(item)
|
|
268
|
-
}
|
|
269
|
-
return map
|
|
270
|
-
}
|
|
2
|
+
import { withCSS as withCSSOriginal } from './framer.js'
|
|
271
3
|
|
|
272
|
-
|
|
273
|
-
return arr.slice().sort((a, b) => key(a).localeCompare(key(b)))
|
|
274
|
-
}
|
|
4
|
+
export * from './css-core.js'
|
|
275
5
|
|
|
276
6
|
/**
|
|
277
7
|
* Custom withCSS function that restores the previous behavior
|
|
278
|
-
* of rendering inline style tags instead of using cssCollector
|
|
8
|
+
* of rendering inline style tags instead of using cssCollector.
|
|
9
|
+
*
|
|
10
|
+
* On the client we return withCSSOriginal so the component tree structure
|
|
11
|
+
* matches what framer.js internal components produce (just <Component/>,
|
|
12
|
+
* no Fragment wrapper). On the server we render <style> + <Component>
|
|
13
|
+
* inside a Fragment to inject CSS into the SSR HTML. The tree differs
|
|
14
|
+
* between server and client but suppressHydrationWarning on the Component
|
|
15
|
+
* handles the mismatch.
|
|
279
16
|
*/
|
|
280
17
|
export function withCSS(
|
|
281
18
|
Component: any,
|
|
@@ -288,13 +25,11 @@ export function withCSS(
|
|
|
288
25
|
return withCSSOriginal(Component, escapedCSS, componentSerializationId || '')
|
|
289
26
|
|
|
290
27
|
return (props: any) => {
|
|
291
|
-
// Check if we're in SSR mode
|
|
292
28
|
const isBrowser =
|
|
293
29
|
typeof window !== 'undefined' &&
|
|
294
30
|
typeof window.document !== 'undefined'
|
|
295
31
|
|
|
296
32
|
if (!isBrowser) {
|
|
297
|
-
// Server-side: render style tags like the old behavior
|
|
298
33
|
const id = componentSerializationId
|
|
299
34
|
const cssContent =
|
|
300
35
|
typeof escapedCSS === 'function'
|
|
@@ -315,7 +50,6 @@ export function withCSS(
|
|
|
315
50
|
)
|
|
316
51
|
}
|
|
317
52
|
|
|
318
|
-
// Client-side: just render the component
|
|
319
53
|
return <Component {...props} />
|
|
320
54
|
}
|
|
321
55
|
}
|
package/src/exporter.test.ts
CHANGED
|
@@ -1,7 +1,89 @@
|
|
|
1
1
|
import { describe, test, expect } from 'vitest'
|
|
2
2
|
import { replaceWebPageIds } from './esbuild.js'
|
|
3
|
+
import { getStyleTokensCss } from './exporter.js'
|
|
4
|
+
import { transform } from 'lightningcss'
|
|
3
5
|
import path from 'path'
|
|
4
6
|
|
|
7
|
+
// Regression for https://github.com/unframer/website-c50b9 CI failure:
|
|
8
|
+
// a Framer color style whose color value contained CSS-structural chars (a `}`
|
|
9
|
+
// followed by grammar-like text from a code-highlighting component) closed the
|
|
10
|
+
// `:root {}` block early. The leaked text became a top-level class rule like
|
|
11
|
+
// `.[-:=] { -: =; }`. Browsers tolerate this but lightningcss (used by Vite's
|
|
12
|
+
// css minifier) rejects it with "Expected identifier in class selector".
|
|
13
|
+
// getStyleTokensCss must always emit CSS that lightningcss can minify.
|
|
14
|
+
describe('getStyleTokensCss', () => {
|
|
15
|
+
function assertMinifiable(css: string) {
|
|
16
|
+
// throws if lightningcss cannot parse/minify the css
|
|
17
|
+
transform({ filename: 'styles.css', code: Buffer.from(css), minify: true })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test('emits valid CSS for normal tokens', () => {
|
|
21
|
+
const css = getStyleTokensCss([
|
|
22
|
+
{ id: 'abc', name: 'Primary', lightColor: '#fff', darkColor: '#000' },
|
|
23
|
+
])
|
|
24
|
+
assertMinifiable(css)
|
|
25
|
+
expect(css).toMatchInlineSnapshot(`
|
|
26
|
+
":root {
|
|
27
|
+
--unframer-primary: #fff;
|
|
28
|
+
|
|
29
|
+
--token-abc: #fff;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.dark {
|
|
33
|
+
--unframer-primary: #000;
|
|
34
|
+
|
|
35
|
+
--token-abc: #000;
|
|
36
|
+
}"
|
|
37
|
+
`)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('skips tokens whose color value breaks out of the block', () => {
|
|
41
|
+
const css = getStyleTokensCss([
|
|
42
|
+
{
|
|
43
|
+
id: 'a',
|
|
44
|
+
name: 'Evil',
|
|
45
|
+
lightColor: 'red } .[-:=] { -: =',
|
|
46
|
+
darkColor: 'blue',
|
|
47
|
+
},
|
|
48
|
+
{ id: 'b', name: 'Good', lightColor: '#fff', darkColor: '#000' },
|
|
49
|
+
])
|
|
50
|
+
assertMinifiable(css)
|
|
51
|
+
expect(css).toMatchInlineSnapshot(`
|
|
52
|
+
":root {
|
|
53
|
+
--unframer-good: #fff;
|
|
54
|
+
|
|
55
|
+
--token-b: #fff;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.dark {
|
|
59
|
+
--unframer-good: #000;
|
|
60
|
+
|
|
61
|
+
--token-b: #000;
|
|
62
|
+
}"
|
|
63
|
+
`)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('skips tokens whose name produces an empty css variable', () => {
|
|
67
|
+
const css = getStyleTokensCss([
|
|
68
|
+
{ id: 'a', name: '![a-zA-Z:_]', lightColor: '#111', darkColor: '#222' },
|
|
69
|
+
])
|
|
70
|
+
assertMinifiable(css)
|
|
71
|
+
expect(css).toMatchInlineSnapshot(`
|
|
72
|
+
":root {
|
|
73
|
+
--unframer-a-z-a-z: #111;
|
|
74
|
+
|
|
75
|
+
--token-a: #111;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.dark {
|
|
79
|
+
--unframer-a-z-a-z: #222;
|
|
80
|
+
|
|
81
|
+
--token-a: #222;
|
|
82
|
+
}"
|
|
83
|
+
`)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
5
87
|
describe('replaceWebPageIds', () => {
|
|
6
88
|
test('replaces webPageIds with paths', () => {
|
|
7
89
|
const elements = [
|