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.
Files changed (162) hide show
  1. package/README.md +5 -5
  2. package/dist/babel-jsx.d.ts +9 -0
  3. package/dist/babel-jsx.d.ts.map +1 -1
  4. package/dist/babel-jsx.js +72 -0
  5. package/dist/babel-jsx.js.map +1 -1
  6. package/dist/cli.d.ts +2 -2
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +262 -42
  9. package/dist/cli.js.map +1 -1
  10. package/dist/css-core.d.ts +50 -0
  11. package/dist/css-core.d.ts.map +1 -0
  12. package/dist/css-core.js +231 -0
  13. package/dist/css-core.js.map +1 -0
  14. package/dist/css.d.ts +9 -43
  15. package/dist/css.d.ts.map +1 -1
  16. package/dist/css.js +9 -232
  17. package/dist/css.js.map +1 -1
  18. package/dist/exporter.d.ts +1 -1
  19. package/dist/exporter.d.ts.map +1 -1
  20. package/dist/exporter.js +72 -11
  21. package/dist/exporter.js.map +1 -1
  22. package/dist/exporter.test.js +78 -0
  23. package/dist/exporter.test.js.map +1 -1
  24. package/dist/framer-chunks/{SqliteDatabase-VAKIICSG-R7ZS6CHH.js → SqliteDatabase-VAKIICSG-OPV4MG3Y.js} +5 -4
  25. package/dist/framer-chunks/{chunk-2DZGP7C2.js → chunk-7ZLQTYXQ.js} +5 -24
  26. package/dist/framer-chunks/chunk-IKQSD2QC.js +10 -0
  27. package/dist/framer-chunks/default-blog-sqlite-7ZHEY3GT-TVGUYU6H.js +7 -0
  28. package/dist/framer-chunks/fontshare-4THNDPMZ-FLLTWCDO.js +7 -0
  29. package/dist/framer-chunks/fontshare-B2QLD7YB-5V3XZJAH.js +7 -0
  30. package/dist/framer-chunks/fontshare-O22OBJ3D-FIG3CRN3.js +7 -0
  31. package/dist/framer-chunks/framer-font-45AI7UCZ-CKGC4MJC.js +7 -0
  32. package/dist/framer-chunks/google-3FCAKCAC-AM34UPJK.js +7 -0
  33. package/dist/framer-chunks/google-3SZHWBC6-2VTQEQ7J.js +7 -0
  34. package/dist/framer-chunks/google-GXDJLGJB-JNEJGCGD.js +7 -0
  35. package/dist/framer-chunks/sqlite-wasm-FGP37EAY-NO5QVZDI.js +7 -0
  36. package/dist/framer-chunks/sqlite3-SISQ6ENZ-5WRKGWTY.js +7 -0
  37. package/dist/framer.d.ts +128 -0
  38. package/dist/framer.js +820 -428
  39. package/dist/plugin-mcp-dist/lib/framer.d.ts +1 -7
  40. package/dist/plugin-mcp-dist/lib/framer.d.ts.map +1 -1
  41. package/dist/plugin-mcp-dist/lib/framer.js +90 -23
  42. package/dist/plugin-mcp-dist/lib/framer.js.map +1 -1
  43. package/dist/plugin-mcp-dist/lib/framer.test.d.ts +2 -0
  44. package/dist/plugin-mcp-dist/lib/framer.test.d.ts.map +1 -0
  45. package/dist/plugin-mcp-dist/lib/framer.test.js +214 -0
  46. package/dist/plugin-mcp-dist/lib/framer.test.js.map +1 -0
  47. package/dist/plugin-mcp-dist/lib/mcp-handlers.d.ts.map +1 -1
  48. package/dist/plugin-mcp-dist/lib/mcp-handlers.js +69 -11
  49. package/dist/plugin-mcp-dist/lib/mcp-handlers.js.map +1 -1
  50. package/dist/plugin-mcp-dist/lib/mcp.test.js +340 -364
  51. package/dist/plugin-mcp-dist/lib/mcp.test.js.map +1 -1
  52. package/dist/plugin-mcp-dist/lib/plugin-websocket.d.ts.map +1 -1
  53. package/dist/plugin-mcp-dist/lib/plugin-websocket.js +0 -3
  54. package/dist/plugin-mcp-dist/lib/plugin-websocket.js.map +1 -1
  55. package/dist/plugin-mcp-dist/lib/react-export.d.ts.map +1 -1
  56. package/dist/plugin-mcp-dist/lib/react-export.js +79 -61
  57. package/dist/plugin-mcp-dist/lib/react-export.js.map +1 -1
  58. package/dist/plugin-mcp-dist/lib/schema.d.ts +8 -0
  59. package/dist/plugin-mcp-dist/lib/schema.d.ts.map +1 -1
  60. package/dist/plugin-mcp-dist/lib/schema.js +18 -1
  61. package/dist/plugin-mcp-dist/lib/schema.js.map +1 -1
  62. package/dist/plugin-mcp-dist/lib/store.d.ts +1 -1
  63. package/dist/plugin-mcp-dist/lib/store.d.ts.map +1 -1
  64. package/dist/plugin-mcp-dist/lib/tunnel-integration.test.d.ts +2 -0
  65. package/dist/plugin-mcp-dist/lib/tunnel-integration.test.d.ts.map +1 -0
  66. package/dist/plugin-mcp-dist/lib/tunnel-integration.test.js +147 -0
  67. package/dist/plugin-mcp-dist/lib/tunnel-integration.test.js.map +1 -0
  68. package/dist/plugin-mcp-dist/lib/tunnel.d.ts +46 -0
  69. package/dist/plugin-mcp-dist/lib/tunnel.d.ts.map +1 -0
  70. package/dist/plugin-mcp-dist/lib/tunnel.js +117 -0
  71. package/dist/plugin-mcp-dist/lib/tunnel.js.map +1 -0
  72. package/dist/plugin-mcp-dist/lib/upstream-socket.d.ts +13 -0
  73. package/dist/plugin-mcp-dist/lib/upstream-socket.d.ts.map +1 -0
  74. package/dist/plugin-mcp-dist/lib/upstream-socket.js +56 -0
  75. package/dist/plugin-mcp-dist/lib/upstream-socket.js.map +1 -0
  76. package/dist/plugin-mcp-dist/lib/upstream-socket.test.d.ts +2 -0
  77. package/dist/plugin-mcp-dist/lib/upstream-socket.test.d.ts.map +1 -0
  78. package/dist/plugin-mcp-dist/lib/upstream-socket.test.js +212 -0
  79. package/dist/plugin-mcp-dist/lib/upstream-socket.test.js.map +1 -0
  80. package/dist/plugin-mcp-dist/lib/utils.d.ts +1 -9
  81. package/dist/plugin-mcp-dist/lib/utils.d.ts.map +1 -1
  82. package/dist/plugin-mcp-dist/lib/utils.js +13 -9
  83. package/dist/plugin-mcp-dist/lib/utils.js.map +1 -1
  84. package/dist/react.d.ts +1 -1
  85. package/dist/react.d.ts.map +1 -1
  86. package/dist/react.js +7 -13
  87. package/dist/react.js.map +1 -1
  88. package/dist/version.d.ts +1 -1
  89. package/dist/version.js +1 -1
  90. package/package.json +7 -8
  91. package/src/babel-jsx.ts +99 -0
  92. package/src/cli.ts +295 -42
  93. package/src/css-core.ts +277 -0
  94. package/src/css.tsx +10 -276
  95. package/src/exporter.test.ts +82 -0
  96. package/src/exporter.ts +92 -11
  97. package/src/framer.d.ts +128 -0
  98. package/src/framer.js +820 -428
  99. package/src/plugin-mcp-dist/lib/framer.d.ts +2 -5
  100. package/src/plugin-mcp-dist/lib/framer.d.ts.map +1 -1
  101. package/src/plugin-mcp-dist/lib/framer.js +90 -23
  102. package/src/plugin-mcp-dist/lib/framer.js.map +1 -1
  103. package/src/plugin-mcp-dist/lib/framer.test.d.ts +2 -0
  104. package/src/plugin-mcp-dist/lib/framer.test.d.ts.map +1 -0
  105. package/src/plugin-mcp-dist/lib/framer.test.js +213 -0
  106. package/src/plugin-mcp-dist/lib/framer.test.js.map +1 -0
  107. package/src/plugin-mcp-dist/lib/mcp-handlers.d.ts +2 -2
  108. package/src/plugin-mcp-dist/lib/mcp-handlers.d.ts.map +1 -1
  109. package/src/plugin-mcp-dist/lib/mcp-handlers.js +69 -11
  110. package/src/plugin-mcp-dist/lib/mcp-handlers.js.map +1 -1
  111. package/src/plugin-mcp-dist/lib/mcp.test.js +340 -364
  112. package/src/plugin-mcp-dist/lib/mcp.test.js.map +1 -1
  113. package/src/plugin-mcp-dist/lib/plugin-websocket.d.ts.map +1 -1
  114. package/src/plugin-mcp-dist/lib/plugin-websocket.js +0 -3
  115. package/src/plugin-mcp-dist/lib/plugin-websocket.js.map +1 -1
  116. package/src/plugin-mcp-dist/lib/react-export.d.ts +1 -1
  117. package/src/plugin-mcp-dist/lib/react-export.d.ts.map +1 -1
  118. package/src/plugin-mcp-dist/lib/react-export.js +79 -61
  119. package/src/plugin-mcp-dist/lib/react-export.js.map +1 -1
  120. package/src/plugin-mcp-dist/lib/schema.d.ts +11 -3
  121. package/src/plugin-mcp-dist/lib/schema.d.ts.map +1 -1
  122. package/src/plugin-mcp-dist/lib/schema.js +18 -1
  123. package/src/plugin-mcp-dist/lib/schema.js.map +1 -1
  124. package/src/plugin-mcp-dist/lib/tunnel-integration.test.d.ts +2 -0
  125. package/src/plugin-mcp-dist/lib/tunnel-integration.test.d.ts.map +1 -0
  126. package/src/plugin-mcp-dist/lib/tunnel-integration.test.js +146 -0
  127. package/src/plugin-mcp-dist/lib/tunnel-integration.test.js.map +1 -0
  128. package/src/plugin-mcp-dist/lib/tunnel.d.ts +55 -0
  129. package/src/plugin-mcp-dist/lib/tunnel.d.ts.map +1 -0
  130. package/src/plugin-mcp-dist/lib/tunnel.js +116 -0
  131. package/src/plugin-mcp-dist/lib/tunnel.js.map +1 -0
  132. package/src/plugin-mcp-dist/lib/upstream-socket.d.ts +28 -0
  133. package/src/plugin-mcp-dist/lib/upstream-socket.d.ts.map +1 -0
  134. package/src/plugin-mcp-dist/lib/upstream-socket.js +55 -0
  135. package/src/plugin-mcp-dist/lib/upstream-socket.js.map +1 -0
  136. package/src/plugin-mcp-dist/lib/upstream-socket.test.d.ts +5 -0
  137. package/src/plugin-mcp-dist/lib/upstream-socket.test.d.ts.map +1 -0
  138. package/src/plugin-mcp-dist/lib/upstream-socket.test.js +211 -0
  139. package/src/plugin-mcp-dist/lib/upstream-socket.test.js.map +1 -0
  140. package/src/plugin-mcp-dist/lib/utils.d.ts +2 -2
  141. package/src/plugin-mcp-dist/lib/utils.d.ts.map +1 -1
  142. package/src/plugin-mcp-dist/lib/utils.js +13 -9
  143. package/src/plugin-mcp-dist/lib/utils.js.map +1 -1
  144. package/src/react.tsx +7 -19
  145. package/src/styles/framer.css +34 -64
  146. package/src/version.ts +1 -1
  147. package/dist/framer-chunks/default-blog-sqlite-7ZHEY3GT-27R5KAAW.js +0 -9
  148. package/dist/framer-chunks/fontshare-4THNDPMZ-BJQGNHXN.js +0 -27099
  149. package/dist/framer-chunks/fontshare-B2QLD7YB-4BZEAA37.js +0 -119
  150. package/dist/framer-chunks/fontshare-O22OBJ3D-ALBQLFE5.js +0 -825
  151. package/dist/framer-chunks/framer-font-45AI7UCZ-LU7DEIDM.js +0 -7
  152. package/dist/framer-chunks/google-3FCAKCAC-P5EL6KGL.js +0 -623876
  153. package/dist/framer-chunks/google-3SZHWBC6-OBXS3UIH.js +0 -1627
  154. package/dist/framer-chunks/google-GXDJLGJB-HHIXFE4M.js +0 -6885
  155. package/dist/framer-chunks/sqlite-wasm-FGP37EAY-HR6PIAJQ.js +0 -11479
  156. package/dist/framer-chunks/sqlite3-SISQ6ENZ-KMXYXSSV.js +0 -9
  157. package/dist/generated/api-client.d.ts +0 -21
  158. package/dist/generated/api-client.d.ts.map +0 -1
  159. package/dist/generated/api-client.js +0 -27
  160. package/dist/generated/api-client.js.map +0 -1
  161. package/src/generated/api-client.d.ts +0 -1238
  162. package/src/generated/api-client.js +0 -26
@@ -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 { ComponentFont } from './framer.js'
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
- function sortByKey<T>(arr: T[], key: (x: T) => string) {
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
  }
@@ -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 = [