@akinon/pz-theme 2.0.98-rc.0 → 2.0.99-beta.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/CHANGELOG.md +16 -33
- package/jest/setup.ts +18 -7
- package/package.json +4 -4
- package/src/__tests__/history-restore-recovery.test.tsx +452 -0
- package/src/chrome/__tests__/theme-fonts-css.test.ts +109 -0
- package/src/chrome/theme-fonts-css.ts +52 -0
- package/src/chrome/theme-fonts-link.tsx +44 -0
- package/src/chrome/theme-restore-guard.tsx +148 -0
- package/src/index.ts +18 -2
- package/src/theme-placeholder-wrapper.tsx +171 -79
- package/src/theme-placeholder.tsx +2 -0
- package/src/utils/custom-fonts.ts +59 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { neutralizeStyleTagBreakout } from './css-safety';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The designer's uploaded faces as real CSS, emitted from the same render that
|
|
5
|
+
* produced the markup.
|
|
6
|
+
*
|
|
7
|
+
* They used to exist only as `<style>` tags an effect appended on mount, which
|
|
8
|
+
* made the brand's typography depend on hydration having run. It does not
|
|
9
|
+
* always run: a history navigation (back, then forward) can hand the browser a
|
|
10
|
+
* cached copy of the document whose React tree never hydrates, and the page
|
|
11
|
+
* then paints every word in the fallback face — the Jost stack behind
|
|
12
|
+
* `GantModernV2` — with no error anywhere (ZERO-5004). Emitted with the markup,
|
|
13
|
+
* the faces are part of the document itself and survive any restore, and the
|
|
14
|
+
* first paint no longer swaps fonts a frame later.
|
|
15
|
+
*
|
|
16
|
+
* The effect below still runs: it is what applies a font the editor uploads
|
|
17
|
+
* mid-session, and it skips faces this CSS already covers.
|
|
18
|
+
*/
|
|
19
|
+
export const buildCustomFontFaceCss = (
|
|
20
|
+
settings: Record<string, unknown> | null
|
|
21
|
+
): string => {
|
|
22
|
+
const fonts = settings?.customFonts;
|
|
23
|
+
|
|
24
|
+
if (!Array.isArray(fonts)) return '';
|
|
25
|
+
|
|
26
|
+
return fonts
|
|
27
|
+
.map((font) => {
|
|
28
|
+
const name = (font as { name?: unknown })?.name;
|
|
29
|
+
const sources = (font as { sources?: unknown })?.sources;
|
|
30
|
+
|
|
31
|
+
if (typeof name !== 'string' || !name.trim() || !Array.isArray(sources)) {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const src = sources
|
|
36
|
+
.map((source) => {
|
|
37
|
+
const dataUrl = (source as { dataUrl?: unknown })?.dataUrl;
|
|
38
|
+
const format = (source as { format?: unknown })?.format;
|
|
39
|
+
|
|
40
|
+
if (typeof dataUrl !== 'string' || !dataUrl) return '';
|
|
41
|
+
|
|
42
|
+
return typeof format === 'string' && format
|
|
43
|
+
? `url('${dataUrl}') format('${format}')`
|
|
44
|
+
: `url('${dataUrl}')`;
|
|
45
|
+
})
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.join(', ');
|
|
48
|
+
|
|
49
|
+
if (!src) return '';
|
|
50
|
+
|
|
51
|
+
// Editor-controlled values on their way into a <style> tag — the same
|
|
52
|
+
// breakout neutralization the generated section CSS goes through.
|
|
53
|
+
return `@font-face{font-family:'${neutralizeStyleTagBreakout(
|
|
54
|
+
name.trim()
|
|
55
|
+
)}';src:${neutralizeStyleTagBreakout(src)};font-display:swap;}`;
|
|
56
|
+
})
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.join('');
|
|
59
|
+
};
|