@maizzle/framework 6.0.0-rc.13 → 6.0.0-rc.14

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 (58) hide show
  1. package/dist/components/Button.vue +2 -2
  2. package/dist/components/CodeBlock.vue +2 -1
  3. package/dist/components/Column.vue +28 -22
  4. package/dist/components/Container.vue +47 -9
  5. package/dist/components/Font.vue +96 -0
  6. package/dist/components/Layout.vue +9 -4
  7. package/dist/components/Overlap.vue +75 -18
  8. package/dist/components/Row.vue +40 -19
  9. package/dist/components/Section.vue +35 -8
  10. package/dist/components/utils.d.mts +14 -1
  11. package/dist/components/utils.d.mts.map +1 -1
  12. package/dist/components/utils.mjs +32 -1
  13. package/dist/components/utils.mjs.map +1 -1
  14. package/dist/components/utils.ts +39 -0
  15. package/dist/composables/renderContext.d.mts +8 -1
  16. package/dist/composables/renderContext.d.mts.map +1 -1
  17. package/dist/composables/renderContext.mjs.map +1 -1
  18. package/dist/composables/useFont.d.mts +50 -0
  19. package/dist/composables/useFont.d.mts.map +1 -0
  20. package/dist/composables/useFont.mjs +93 -0
  21. package/dist/composables/useFont.mjs.map +1 -0
  22. package/dist/index.d.mts +2 -1
  23. package/dist/index.mjs +2 -1
  24. package/dist/plugins/postcss/quoteFontFamilies.d.mts +13 -0
  25. package/dist/plugins/postcss/quoteFontFamilies.d.mts.map +1 -0
  26. package/dist/plugins/postcss/quoteFontFamilies.mjs +84 -0
  27. package/dist/plugins/postcss/quoteFontFamilies.mjs.map +1 -0
  28. package/dist/render/createRenderer.mjs +8 -2
  29. package/dist/render/createRenderer.mjs.map +1 -1
  30. package/dist/render/injectFonts.d.mts +15 -0
  31. package/dist/render/injectFonts.d.mts.map +1 -0
  32. package/dist/render/injectFonts.mjs +46 -0
  33. package/dist/render/injectFonts.mjs.map +1 -0
  34. package/dist/serve.d.mts.map +1 -1
  35. package/dist/serve.mjs +6 -2
  36. package/dist/serve.mjs.map +1 -1
  37. package/dist/server/ui/App.vue +25 -11
  38. package/dist/server/ui/lib/emulated-dark-mode.ts +131 -0
  39. package/dist/server/ui/pages/Preview.vue +24 -5
  40. package/dist/transformers/columnWidth.d.mts +31 -0
  41. package/dist/transformers/columnWidth.d.mts.map +1 -0
  42. package/dist/transformers/columnWidth.mjs +166 -0
  43. package/dist/transformers/columnWidth.mjs.map +1 -0
  44. package/dist/transformers/index.d.mts.map +1 -1
  45. package/dist/transformers/index.mjs +4 -0
  46. package/dist/transformers/index.mjs.map +1 -1
  47. package/dist/transformers/msoWidthFromClass.d.mts +19 -0
  48. package/dist/transformers/msoWidthFromClass.d.mts.map +1 -0
  49. package/dist/transformers/msoWidthFromClass.mjs +61 -0
  50. package/dist/transformers/msoWidthFromClass.mjs.map +1 -0
  51. package/dist/transformers/tailwindcss.d.mts.map +1 -1
  52. package/dist/transformers/tailwindcss.mjs +4 -12
  53. package/dist/transformers/tailwindcss.mjs.map +1 -1
  54. package/dist/utils/decodeStyleEntities.d.mts +15 -0
  55. package/dist/utils/decodeStyleEntities.d.mts.map +1 -0
  56. package/dist/utils/decodeStyleEntities.mjs +18 -0
  57. package/dist/utils/decodeStyleEntities.mjs.map +1 -0
  58. package/package.json +2 -1
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, createStaticVNode, useAttrs } from 'vue'
3
- import { normalizeToPixels } from './utils.ts'
3
+ import { hasWidthInStyle, hasWidthUtility, nextId, normalizeToPixels } from './utils.ts'
4
4
 
5
5
  defineOptions({ inheritAttrs: false })
6
6
 
@@ -12,11 +12,14 @@ const props = defineProps({
12
12
  *
13
13
  * Applied as `max-width` on the div and as `width` on the MSO table.
14
14
  *
15
- * @default '100%'
15
+ * When not set, the MSO table width is auto-derived from a width
16
+ * utility class (e.g. `max-w-md`) or inline style (`max-width`/
17
+ * `width`) on the component, after CSS inlining. Falls back to
18
+ * `100%` when no width source is provided.
16
19
  */
17
20
  width: {
18
21
  type: [String, Number],
19
- default: '100%'
22
+ default: null
20
23
  },
21
24
  /**
22
25
  * Inline CSS applied only to the MSO `<td>` element.
@@ -31,8 +34,6 @@ const props = defineProps({
31
34
  }
32
35
  })
33
36
 
34
- const hasCustomWidth = computed(() => props.width !== '100%')
35
-
36
37
  const userStyle = computed(() => {
37
38
  const s = attrs.style
38
39
  if (!s) return ''
@@ -41,9 +42,17 @@ const userStyle = computed(() => {
41
42
  : String(s)
42
43
  })
43
44
 
45
+ const userHasWidth = computed(() => {
46
+ const cls = (attrs.class as string) ?? ''
47
+ return hasWidthUtility(cls) || hasWidthInStyle(userStyle.value)
48
+ })
49
+
50
+ const useMarker = props.width == null && userHasWidth.value
51
+ const msoId = useMarker ? nextId('s') : null
52
+
44
53
  const divStyle = computed(() => {
45
54
  const parts: string[] = []
46
- if (hasCustomWidth.value) parts.push(`max-width: ${normalizeToPixels(props.width)}`)
55
+ if (props.width != null) parts.push(`max-width: ${normalizeToPixels(props.width)}`)
47
56
  if (userStyle.value) parts.push(userStyle.value)
48
57
  return parts.length ? parts.join('; ') : undefined
49
58
  })
@@ -60,10 +69,22 @@ const tdStyles = computed(() => {
60
69
  return parts.length ? parts.join('; ') : ''
61
70
  })
62
71
 
72
+ const msoWidth = computed(() => {
73
+ if (props.width != null) return normalizeToPixels(props.width)
74
+ if (useMarker) return `__MAIZZLE_MSOW_${msoId}__`
75
+ return '100%'
76
+ })
77
+
78
+ const colWidthSource = computed(() => {
79
+ if (props.width != null) return normalizeToPixels(props.width)
80
+ if (userHasWidth.value) return ''
81
+ return null
82
+ })
83
+
63
84
  const MsoBefore = () => {
64
85
  const tdStyle = tdStyles.value ? ` style="${tdStyles.value}"` : ''
65
86
  return createStaticVNode(
66
- `<!--[if mso]><table role="none" cellpadding="0" cellspacing="0" style="width: ${normalizeToPixels(props.width)}"><tr><td${tdStyle}><![endif]-->`,
87
+ `<!--[if mso]><table role="none" cellpadding="0" cellspacing="0" style="width: ${msoWidth.value}"><tr><td${tdStyle}><![endif]-->`,
67
88
  1
68
89
  )
69
90
  }
@@ -76,7 +97,13 @@ const MsoAfter = () => createStaticVNode(
76
97
 
77
98
  <template>
78
99
  <MsoBefore />
79
- <div v-bind="restAttrs" :style="divStyle">
100
+ <div
101
+ v-bind="restAttrs"
102
+ :style="divStyle"
103
+ :data-maizzle-msow-id="msoId"
104
+ :data-maizzle-msow-fallback="useMarker ? '100%' : null"
105
+ :data-maizzle-cw="colWidthSource"
106
+ >
80
107
  <slot />
81
108
  </div>
82
109
  <MsoAfter />
@@ -1,5 +1,18 @@
1
1
  //#region src/components/utils.d.ts
2
2
  declare function normalizeToPixels(value: string | number): string;
3
+ /**
4
+ * Module-scoped sequential ID generator. Used by components to mint
5
+ * unique marker ids (e.g. `c1`, `c2`) for the post-render transformer.
6
+ *
7
+ * Must live here (not inside `<script setup>`) because Vue compiles
8
+ * `<script setup>` into the component's `setup()` function — any
9
+ * `let counter = 0` there resets per instance, causing id collisions.
10
+ */
11
+ declare function nextId(prefix: string): string;
12
+ declare function hasWidthUtility(classStr: string): boolean;
13
+ declare function hasWidthInStyle(styleStr: string): boolean;
14
+ declare function hasHeightUtility(classStr: string): boolean;
15
+ declare function hasHeightInStyle(styleStr: string): boolean;
3
16
  //#endregion
4
- export { normalizeToPixels };
17
+ export { hasHeightInStyle, hasHeightUtility, hasWidthInStyle, hasWidthUtility, nextId, normalizeToPixels };
5
18
  //# sourceMappingURL=utils.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.mts","names":[],"sources":["../../src/components/utils.ts"],"mappings":";iBAAgB,iBAAA,CAAkB,KAAA"}
1
+ {"version":3,"file":"utils.d.mts","names":[],"sources":["../../src/components/utils.ts"],"mappings":";iBAAgB,iBAAA,CAAkB,KAAA;AAAlC;;;;;AAiBA;;;AAjBA,iBAiBgB,MAAA,CAAO,MAAA;AAAA,iBAKP,eAAA,CAAgB,QAAA;AAAA,iBAQhB,eAAA,CAAgB,QAAA;AAAA,iBAIhB,gBAAA,CAAiB,QAAA;AAAA,iBAQjB,gBAAA,CAAiB,QAAA"}
@@ -3,7 +3,38 @@ function normalizeToPixels(value) {
3
3
  if (typeof value === "number" || Number.isFinite(Number(value))) return `${value}px`;
4
4
  return value;
5
5
  }
6
+ const counters = {};
7
+ /**
8
+ * Module-scoped sequential ID generator. Used by components to mint
9
+ * unique marker ids (e.g. `c1`, `c2`) for the post-render transformer.
10
+ *
11
+ * Must live here (not inside `<script setup>`) because Vue compiles
12
+ * `<script setup>` into the component's `setup()` function — any
13
+ * `let counter = 0` there resets per instance, causing id collisions.
14
+ */
15
+ function nextId(prefix) {
16
+ counters[prefix] = (counters[prefix] ?? 0) + 1;
17
+ return `${prefix}${counters[prefix]}`;
18
+ }
19
+ function hasWidthUtility(classStr) {
20
+ return classStr.split(/\s+/).some((c) => {
21
+ const clean = (c.split(":").pop() ?? "").replace(/^!/, "");
22
+ return /^(w-|max-w-|min-w-)/.test(clean);
23
+ });
24
+ }
25
+ function hasWidthInStyle(styleStr) {
26
+ return /(?:^|;\s*)(?:max-width|width)\s*:/i.test(styleStr);
27
+ }
28
+ function hasHeightUtility(classStr) {
29
+ return classStr.split(/\s+/).some((c) => {
30
+ const clean = (c.split(":").pop() ?? "").replace(/^!/, "");
31
+ return /^(h-|max-h-|min-h-)/.test(clean);
32
+ });
33
+ }
34
+ function hasHeightInStyle(styleStr) {
35
+ return /(?:^|;\s*)(?:max-height|height)\s*:/i.test(styleStr);
36
+ }
6
37
 
7
38
  //#endregion
8
- export { normalizeToPixels };
39
+ export { hasHeightInStyle, hasHeightUtility, hasWidthInStyle, hasWidthUtility, nextId, normalizeToPixels };
9
40
  //# sourceMappingURL=utils.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.mjs","names":[],"sources":["../../src/components/utils.ts"],"sourcesContent":["export function normalizeToPixels(value: string | number): string {\n if (typeof value === 'number' || Number.isFinite(Number(value))) {\n return `${value}px`\n }\n return value\n}\n"],"mappings":";AAAA,SAAgB,kBAAkB,OAAgC;AAChE,KAAI,OAAO,UAAU,YAAY,OAAO,SAAS,OAAO,MAAM,CAAC,CAC7D,QAAO,GAAG,MAAM;AAElB,QAAO"}
1
+ {"version":3,"file":"utils.mjs","names":[],"sources":["../../src/components/utils.ts"],"sourcesContent":["export function normalizeToPixels(value: string | number): string {\n if (typeof value === 'number' || Number.isFinite(Number(value))) {\n return `${value}px`\n }\n return value\n}\n\nconst counters: Record<string, number> = {}\n\n/**\n * Module-scoped sequential ID generator. Used by components to mint\n * unique marker ids (e.g. `c1`, `c2`) for the post-render transformer.\n *\n * Must live here (not inside `<script setup>`) because Vue compiles\n * `<script setup>` into the component's `setup()` function — any\n * `let counter = 0` there resets per instance, causing id collisions.\n */\nexport function nextId(prefix: string): string {\n counters[prefix] = (counters[prefix] ?? 0) + 1\n return `${prefix}${counters[prefix]}`\n}\n\nexport function hasWidthUtility(classStr: string): boolean {\n return classStr.split(/\\s+/).some((c) => {\n const utility = c.split(':').pop() ?? ''\n const clean = utility.replace(/^!/, '')\n return /^(w-|max-w-|min-w-)/.test(clean)\n })\n}\n\nexport function hasWidthInStyle(styleStr: string): boolean {\n return /(?:^|;\\s*)(?:max-width|width)\\s*:/i.test(styleStr)\n}\n\nexport function hasHeightUtility(classStr: string): boolean {\n return classStr.split(/\\s+/).some((c) => {\n const utility = c.split(':').pop() ?? ''\n const clean = utility.replace(/^!/, '')\n return /^(h-|max-h-|min-h-)/.test(clean)\n })\n}\n\nexport function hasHeightInStyle(styleStr: string): boolean {\n return /(?:^|;\\s*)(?:max-height|height)\\s*:/i.test(styleStr)\n}\n"],"mappings":";AAAA,SAAgB,kBAAkB,OAAgC;AAChE,KAAI,OAAO,UAAU,YAAY,OAAO,SAAS,OAAO,MAAM,CAAC,CAC7D,QAAO,GAAG,MAAM;AAElB,QAAO;;AAGT,MAAM,WAAmC,EAAE;;;;;;;;;AAU3C,SAAgB,OAAO,QAAwB;AAC7C,UAAS,WAAW,SAAS,WAAW,KAAK;AAC7C,QAAO,GAAG,SAAS,SAAS;;AAG9B,SAAgB,gBAAgB,UAA2B;AACzD,QAAO,SAAS,MAAM,MAAM,CAAC,MAAM,MAAM;EAEvC,MAAM,SADU,EAAE,MAAM,IAAI,CAAC,KAAK,IAAI,IAChB,QAAQ,MAAM,GAAG;AACvC,SAAO,sBAAsB,KAAK,MAAM;GACxC;;AAGJ,SAAgB,gBAAgB,UAA2B;AACzD,QAAO,qCAAqC,KAAK,SAAS;;AAG5D,SAAgB,iBAAiB,UAA2B;AAC1D,QAAO,SAAS,MAAM,MAAM,CAAC,MAAM,MAAM;EAEvC,MAAM,SADU,EAAE,MAAM,IAAI,CAAC,KAAK,IAAI,IAChB,QAAQ,MAAM,GAAG;AACvC,SAAO,sBAAsB,KAAK,MAAM;GACxC;;AAGJ,SAAgB,iBAAiB,UAA2B;AAC1D,QAAO,uCAAuC,KAAK,SAAS"}
@@ -4,3 +4,42 @@ export function normalizeToPixels(value: string | number): string {
4
4
  }
5
5
  return value
6
6
  }
7
+
8
+ const counters: Record<string, number> = {}
9
+
10
+ /**
11
+ * Module-scoped sequential ID generator. Used by components to mint
12
+ * unique marker ids (e.g. `c1`, `c2`) for the post-render transformer.
13
+ *
14
+ * Must live here (not inside `<script setup>`) because Vue compiles
15
+ * `<script setup>` into the component's `setup()` function — any
16
+ * `let counter = 0` there resets per instance, causing id collisions.
17
+ */
18
+ export function nextId(prefix: string): string {
19
+ counters[prefix] = (counters[prefix] ?? 0) + 1
20
+ return `${prefix}${counters[prefix]}`
21
+ }
22
+
23
+ export function hasWidthUtility(classStr: string): boolean {
24
+ return classStr.split(/\s+/).some((c) => {
25
+ const utility = c.split(':').pop() ?? ''
26
+ const clean = utility.replace(/^!/, '')
27
+ return /^(w-|max-w-|min-w-)/.test(clean)
28
+ })
29
+ }
30
+
31
+ export function hasWidthInStyle(styleStr: string): boolean {
32
+ return /(?:^|;\s*)(?:max-width|width)\s*:/i.test(styleStr)
33
+ }
34
+
35
+ export function hasHeightUtility(classStr: string): boolean {
36
+ return classStr.split(/\s+/).some((c) => {
37
+ const utility = c.split(':').pop() ?? ''
38
+ const clean = utility.replace(/^!/, '')
39
+ return /^(h-|max-h-|min-h-)/.test(clean)
40
+ })
41
+ }
42
+
43
+ export function hasHeightInStyle(styleStr: string): boolean {
44
+ return /(?:^|;\s*)(?:max-height|height)\s*:/i.test(styleStr)
45
+ }
@@ -4,6 +4,12 @@ import { UsePlaintextOptions } from "./usePlaintext.mjs";
4
4
  import { InjectionKey } from "vue";
5
5
 
6
6
  //#region src/composables/renderContext.d.ts
7
+ interface FontRegistration {
8
+ family: string;
9
+ slug: string;
10
+ declaration: string;
11
+ url: string;
12
+ }
7
13
  interface RenderContext {
8
14
  doctype?: string;
9
15
  preheader?: {
@@ -17,8 +23,9 @@ interface RenderContext {
17
23
  handler: EventMap[EventName];
18
24
  }>;
19
25
  plaintext?: UsePlaintextOptions;
26
+ fonts?: FontRegistration[];
20
27
  }
21
28
  declare const RenderContextKey: InjectionKey<RenderContext>;
22
29
  //#endregion
23
- export { RenderContext, RenderContextKey };
30
+ export { FontRegistration, RenderContext, RenderContextKey };
24
31
  //# sourceMappingURL=renderContext.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"renderContext.d.mts","names":[],"sources":["../../src/composables/renderContext.ts"],"mappings":";;;;;;UAKiB,aAAA;EACf,OAAA;EACA,SAAA;IAAc,IAAA;IAAc,WAAA;IAAqB,QAAA;EAAA;EACjD,SAAA,GAAY,aAAA;EACZ,gBAAA,EAAkB,KAAA;IAAQ,IAAA,EAAM,SAAA;IAAW,OAAA,EAAS,QAAA,CAAS,SAAA;EAAA;EAC7D,SAAA,GAAY,mBAAA;AAAA;AAAA,cAGD,gBAAA,EAAkB,YAAA,CAAa,aAAA"}
1
+ {"version":3,"file":"renderContext.d.mts","names":[],"sources":["../../src/composables/renderContext.ts"],"mappings":";;;;;;UAKiB,gBAAA;EACf,MAAA;EACA,IAAA;EACA,WAAA;EACA,GAAA;AAAA;AAAA,UAGe,aAAA;EACf,OAAA;EACA,SAAA;IAAc,IAAA;IAAc,WAAA;IAAqB,QAAA;EAAA;EACjD,SAAA,GAAY,aAAA;EACZ,gBAAA,EAAkB,KAAA;IAAQ,IAAA,EAAM,SAAA;IAAW,OAAA,EAAS,QAAA,CAAS,SAAA;EAAA;EAC7D,SAAA,GAAY,mBAAA;EACZ,KAAA,GAAQ,gBAAA;AAAA;AAAA,cAGG,gBAAA,EAAkB,YAAA,CAAa,aAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"renderContext.mjs","names":[],"sources":["../../src/composables/renderContext.ts"],"sourcesContent":["import type { InjectionKey } from 'vue'\nimport type { MaizzleConfig } from '../types/index.ts'\nimport type { EventName, EventMap } from '../events/index.ts'\nimport type { UsePlaintextOptions } from './usePlaintext.ts'\n\nexport interface RenderContext {\n doctype?: string\n preheader?: { text: string; fillerCount: number; shyCount: number }\n sfcConfig?: MaizzleConfig\n sfcEventHandlers: Array<{ name: EventName; handler: EventMap[EventName] }>\n plaintext?: UsePlaintextOptions\n}\n\nexport const RenderContextKey: InjectionKey<RenderContext> = Symbol('RenderContext')\n"],"mappings":";AAaA,MAAa,mBAAgD,OAAO,gBAAgB"}
1
+ {"version":3,"file":"renderContext.mjs","names":[],"sources":["../../src/composables/renderContext.ts"],"sourcesContent":["import type { InjectionKey } from 'vue'\nimport type { MaizzleConfig } from '../types/index.ts'\nimport type { EventName, EventMap } from '../events/index.ts'\nimport type { UsePlaintextOptions } from './usePlaintext.ts'\n\nexport interface FontRegistration {\n family: string\n slug: string\n declaration: string\n url: string\n}\n\nexport interface RenderContext {\n doctype?: string\n preheader?: { text: string; fillerCount: number; shyCount: number }\n sfcConfig?: MaizzleConfig\n sfcEventHandlers: Array<{ name: EventName; handler: EventMap[EventName] }>\n plaintext?: UsePlaintextOptions\n fonts?: FontRegistration[]\n}\n\nexport const RenderContextKey: InjectionKey<RenderContext> = Symbol('RenderContext')\n"],"mappings":";AAqBA,MAAa,mBAAgD,OAAO,gBAAgB"}
@@ -0,0 +1,50 @@
1
+ //#region src/composables/useFont.d.ts
2
+ type FontProvider = 'google' | 'bunny';
3
+ interface UseFontOptions {
4
+ /**
5
+ * A single font family name, e.g. `"Roboto"` or `"Open Sans"`.
6
+ *
7
+ * For fallback fonts, use the `fallback` option instead of a
8
+ * comma-separated list here.
9
+ */
10
+ family: string;
11
+ /** CSS fallback list appended to the `font-family` declaration. */
12
+ fallback?: string;
13
+ /**
14
+ * Font provider used to build the stylesheet URL when `url` is omitted.
15
+ * Bunny Fonts is a drop-in, privacy-friendly Google Fonts mirror.
16
+ * @default 'google'
17
+ */
18
+ provider?: FontProvider;
19
+ /**
20
+ * Stylesheet URL. When provided, used as-is for the `<link href>`.
21
+ * When omitted, a URL is built from `provider`, `family`, `weights`,
22
+ * `display` and `styles`.
23
+ */
24
+ url?: string;
25
+ /** Font weights to load. Ignored when `url` is provided. */
26
+ weights?: number[];
27
+ /** `font-display` value. Ignored when `url` is provided. */
28
+ display?: 'auto' | 'block' | 'swap' | 'fallback' | 'optional';
29
+ /** Font styles to load. Ignored when `url` is provided. */
30
+ styles?: Array<'normal' | 'italic'>;
31
+ }
32
+ /**
33
+ * Register a font for the current email template.
34
+ *
35
+ * Builds a Google Fonts stylesheet URL from `family`/`weights`/`display`/`styles`
36
+ * (or uses `url` as-is). The renderer injects a `<link>` tag into `<head>`
37
+ * and merges `--font-{slug}` declarations into the template's existing
38
+ * `@import "tailwindcss"` style block so a `font-{slug}` utility class
39
+ * is generated. If no Tailwind import is found, falls back to a `:root`
40
+ * declaration so the CSS variable is still available.
41
+ *
42
+ * Usage in SFC <script setup>:
43
+ * ```ts
44
+ * useFont({ family: 'Roboto', fallback: 'Verdana, sans-serif', weights: [400, 600] })
45
+ * ```
46
+ */
47
+ declare function useFont(options: UseFontOptions): void;
48
+ //#endregion
49
+ export { FontProvider, UseFontOptions, useFont };
50
+ //# sourceMappingURL=useFont.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFont.d.mts","names":[],"sources":["../../src/composables/useFont.ts"],"mappings":";KA8CY,YAAA;AAAA,UAEK,cAAA;EAFO;;;;AAExB;;EAOE,MAAA;EAoBc;EAlBd,QAAA;EAAA;;;;;EAMA,QAAA,GAAW,YAAA;EAYX;;;;AA+CF;EArDE,GAAA;;EAEA,OAAA;EAmD6C;EAjD7C,OAAA;;EAEA,MAAA,GAAS,KAAA;AAAA;;;;;;;;;;;;;;;;iBA+CK,OAAA,CAAQ,OAAA,EAAS,cAAA"}
@@ -0,0 +1,93 @@
1
+ import { RenderContextKey } from "./renderContext.mjs";
2
+ import { inject } from "vue";
3
+
4
+ //#region src/composables/useFont.ts
5
+ const FAMILY_CATEGORIES = {
6
+ "Roboto": "sans",
7
+ "Open Sans": "sans",
8
+ "Inter": "sans",
9
+ "Lato": "sans",
10
+ "Montserrat": "sans",
11
+ "Merriweather": "serif",
12
+ "Playfair Display": "serif",
13
+ "Lora": "serif",
14
+ "PT Serif": "serif",
15
+ "Noto Serif": "serif",
16
+ "Oswald": "display",
17
+ "Bebas Neue": "display",
18
+ "Anton": "display",
19
+ "Lobster": "display",
20
+ "Pacifico": "display",
21
+ "Dancing Script": "handwriting",
22
+ "Caveat": "handwriting",
23
+ "Shadows Into Light": "handwriting",
24
+ "Satisfy": "handwriting",
25
+ "Great Vibes": "handwriting",
26
+ "Roboto Mono": "mono",
27
+ "Source Code Pro": "mono",
28
+ "JetBrains Mono": "mono",
29
+ "Fira Code": "mono",
30
+ "Inconsolata": "mono"
31
+ };
32
+ const DEFAULT_FALLBACKS = {
33
+ sans: "ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif",
34
+ serif: "ui-serif, Georgia, Cambria, \"Times New Roman\", Times, serif",
35
+ mono: "ui-monospace, Menlo, Consolas, monospace",
36
+ display: "Impact, \"Arial Black\", system-ui, sans-serif",
37
+ handwriting: "\"Segoe Script\", \"Brush Script MT\", cursive"
38
+ };
39
+ const PROVIDER_BASE_URL = {
40
+ google: "https://fonts.googleapis.com/css2",
41
+ bunny: "https://fonts.bunny.net/css2"
42
+ };
43
+ function slugify(family) {
44
+ return family.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
45
+ }
46
+ function buildProviderUrl(opts) {
47
+ const familyParam = opts.family.trim().replace(/\s+/g, "+");
48
+ const weights = [...opts.weights].sort((a, b) => a - b);
49
+ const hasItalic = opts.styles.includes("italic");
50
+ const hasNormal = opts.styles.includes("normal");
51
+ const axis = hasItalic ? `:ital,wght@${weights.flatMap((w) => [...hasNormal ? [`0,${w}`] : [], `1,${w}`]).join(";")}` : `:wght@${weights.join(";")}`;
52
+ return `${PROVIDER_BASE_URL[opts.provider]}?family=${familyParam}${axis}&display=${opts.display}`;
53
+ }
54
+ /**
55
+ * Register a font for the current email template.
56
+ *
57
+ * Builds a Google Fonts stylesheet URL from `family`/`weights`/`display`/`styles`
58
+ * (or uses `url` as-is). The renderer injects a `<link>` tag into `<head>`
59
+ * and merges `--font-{slug}` declarations into the template's existing
60
+ * `@import "tailwindcss"` style block so a `font-{slug}` utility class
61
+ * is generated. If no Tailwind import is found, falls back to a `:root`
62
+ * declaration so the CSS variable is still available.
63
+ *
64
+ * Usage in SFC <script setup>:
65
+ * ```ts
66
+ * useFont({ family: 'Roboto', fallback: 'Verdana, sans-serif', weights: [400, 600] })
67
+ * ```
68
+ */
69
+ function useFont(options) {
70
+ const ctx = inject(RenderContextKey);
71
+ if (!ctx) return;
72
+ ctx.fonts = ctx.fonts ?? [];
73
+ if (ctx.fonts.some((f) => f.family === options.family)) return;
74
+ const url = options.url ?? buildProviderUrl({
75
+ family: options.family,
76
+ provider: options.provider ?? "google",
77
+ weights: options.weights ?? [400],
78
+ display: options.display ?? "swap",
79
+ styles: options.styles ?? ["normal"]
80
+ });
81
+ const fallback = options.fallback ?? DEFAULT_FALLBACKS[FAMILY_CATEGORIES[options.family] ?? "sans"];
82
+ const declaration = `${/\s/.test(options.family) ? `"${options.family}"` : options.family}, ${fallback}`;
83
+ ctx.fonts.push({
84
+ family: options.family,
85
+ slug: slugify(options.family),
86
+ declaration,
87
+ url
88
+ });
89
+ }
90
+
91
+ //#endregion
92
+ export { useFont };
93
+ //# sourceMappingURL=useFont.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFont.mjs","names":[],"sources":["../../src/composables/useFont.ts"],"sourcesContent":["import { inject } from 'vue'\nimport { RenderContextKey } from './renderContext.ts'\n\ntype FontCategory = 'sans' | 'serif' | 'mono' | 'display' | 'handwriting'\n\nconst FAMILY_CATEGORIES: Record<string, FontCategory> = {\n // Sans-serif\n 'Roboto': 'sans',\n 'Open Sans': 'sans',\n 'Inter': 'sans',\n 'Lato': 'sans',\n 'Montserrat': 'sans',\n // Serif\n 'Merriweather': 'serif',\n 'Playfair Display': 'serif',\n 'Lora': 'serif',\n 'PT Serif': 'serif',\n 'Noto Serif': 'serif',\n // Display\n 'Oswald': 'display',\n 'Bebas Neue': 'display',\n 'Anton': 'display',\n 'Lobster': 'display',\n 'Pacifico': 'display',\n // Handwriting\n 'Dancing Script': 'handwriting',\n 'Caveat': 'handwriting',\n 'Shadows Into Light': 'handwriting',\n 'Satisfy': 'handwriting',\n 'Great Vibes': 'handwriting',\n // Monospace\n 'Roboto Mono': 'mono',\n 'Source Code Pro': 'mono',\n 'JetBrains Mono': 'mono',\n 'Fira Code': 'mono',\n 'Inconsolata': 'mono',\n}\n\nconst DEFAULT_FALLBACKS: Record<FontCategory, string> = {\n sans: 'ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif',\n serif: 'ui-serif, Georgia, Cambria, \"Times New Roman\", Times, serif',\n mono: 'ui-monospace, Menlo, Consolas, monospace',\n display: 'Impact, \"Arial Black\", system-ui, sans-serif',\n handwriting: '\"Segoe Script\", \"Brush Script MT\", cursive',\n}\n\nexport type FontProvider = 'google' | 'bunny'\n\nexport interface UseFontOptions {\n /**\n * A single font family name, e.g. `\"Roboto\"` or `\"Open Sans\"`.\n *\n * For fallback fonts, use the `fallback` option instead of a\n * comma-separated list here.\n */\n family: string\n /** CSS fallback list appended to the `font-family` declaration. */\n fallback?: string\n /**\n * Font provider used to build the stylesheet URL when `url` is omitted.\n * Bunny Fonts is a drop-in, privacy-friendly Google Fonts mirror.\n * @default 'google'\n */\n provider?: FontProvider\n /**\n * Stylesheet URL. When provided, used as-is for the `<link href>`.\n * When omitted, a URL is built from `provider`, `family`, `weights`,\n * `display` and `styles`.\n */\n url?: string\n /** Font weights to load. Ignored when `url` is provided. */\n weights?: number[]\n /** `font-display` value. Ignored when `url` is provided. */\n display?: 'auto' | 'block' | 'swap' | 'fallback' | 'optional'\n /** Font styles to load. Ignored when `url` is provided. */\n styles?: Array<'normal' | 'italic'>\n}\n\nconst PROVIDER_BASE_URL: Record<FontProvider, string> = {\n google: 'https://fonts.googleapis.com/css2',\n bunny: 'https://fonts.bunny.net/css2',\n}\n\nfunction slugify(family: string): string {\n return family\n .trim()\n .toLowerCase()\n .replace(/\\s+/g, '-')\n .replace(/[^a-z0-9-]/g, '')\n}\n\nfunction buildProviderUrl(opts: Required<Omit<UseFontOptions, 'url' | 'fallback'>>): string {\n const familyParam = opts.family.trim().replace(/\\s+/g, '+')\n const weights = [...opts.weights].sort((a, b) => a - b)\n const hasItalic = opts.styles.includes('italic')\n const hasNormal = opts.styles.includes('normal')\n\n const axis = hasItalic\n ? `:ital,wght@${weights.flatMap(w => [\n ...(hasNormal ? [`0,${w}`] : []),\n `1,${w}`,\n ]).join(';')}`\n : `:wght@${weights.join(';')}`\n\n return `${PROVIDER_BASE_URL[opts.provider]}?family=${familyParam}${axis}&display=${opts.display}`\n}\n\n/**\n * Register a font for the current email template.\n *\n * Builds a Google Fonts stylesheet URL from `family`/`weights`/`display`/`styles`\n * (or uses `url` as-is). The renderer injects a `<link>` tag into `<head>`\n * and merges `--font-{slug}` declarations into the template's existing\n * `@import \"tailwindcss\"` style block so a `font-{slug}` utility class\n * is generated. If no Tailwind import is found, falls back to a `:root`\n * declaration so the CSS variable is still available.\n *\n * Usage in SFC <script setup>:\n * ```ts\n * useFont({ family: 'Roboto', fallback: 'Verdana, sans-serif', weights: [400, 600] })\n * ```\n */\nexport function useFont(options: UseFontOptions): void {\n const ctx = inject(RenderContextKey)\n if (!ctx) return\n\n ctx.fonts = ctx.fonts ?? []\n if (ctx.fonts.some(f => f.family === options.family)) return\n\n const url = options.url ?? buildProviderUrl({\n family: options.family,\n provider: options.provider ?? 'google',\n weights: options.weights ?? [400],\n display: options.display ?? 'swap',\n styles: options.styles ?? ['normal'],\n })\n\n const fallback = options.fallback\n ?? DEFAULT_FALLBACKS[FAMILY_CATEGORIES[options.family] ?? 'sans']\n const quoted = /\\s/.test(options.family) ? `\"${options.family}\"` : options.family\n const declaration = `${quoted}, ${fallback}`\n\n ctx.fonts.push({\n family: options.family,\n slug: slugify(options.family),\n declaration,\n url,\n })\n}\n"],"mappings":";;;;AAKA,MAAM,oBAAkD;CAEtD,UAAU;CACV,aAAa;CACb,SAAS;CACT,QAAQ;CACR,cAAc;CAEd,gBAAgB;CAChB,oBAAoB;CACpB,QAAQ;CACR,YAAY;CACZ,cAAc;CAEd,UAAU;CACV,cAAc;CACd,SAAS;CACT,WAAW;CACX,YAAY;CAEZ,kBAAkB;CAClB,UAAU;CACV,sBAAsB;CACtB,WAAW;CACX,eAAe;CAEf,eAAe;CACf,mBAAmB;CACnB,kBAAkB;CAClB,aAAa;CACb,eAAe;CAChB;AAED,MAAM,oBAAkD;CACtD,MAAM;CACN,OAAO;CACP,MAAM;CACN,SAAS;CACT,aAAa;CACd;AAkCD,MAAM,oBAAkD;CACtD,QAAQ;CACR,OAAO;CACR;AAED,SAAS,QAAQ,QAAwB;AACvC,QAAO,OACJ,MAAM,CACN,aAAa,CACb,QAAQ,QAAQ,IAAI,CACpB,QAAQ,eAAe,GAAG;;AAG/B,SAAS,iBAAiB,MAAkE;CAC1F,MAAM,cAAc,KAAK,OAAO,MAAM,CAAC,QAAQ,QAAQ,IAAI;CAC3D,MAAM,UAAU,CAAC,GAAG,KAAK,QAAQ,CAAC,MAAM,GAAG,MAAM,IAAI,EAAE;CACvD,MAAM,YAAY,KAAK,OAAO,SAAS,SAAS;CAChD,MAAM,YAAY,KAAK,OAAO,SAAS,SAAS;CAEhD,MAAM,OAAO,YACT,cAAc,QAAQ,SAAQ,MAAK,CACjC,GAAI,YAAY,CAAC,KAAK,IAAI,GAAG,EAAE,EAC/B,KAAK,IACN,CAAC,CAAC,KAAK,IAAI,KACZ,SAAS,QAAQ,KAAK,IAAI;AAE9B,QAAO,GAAG,kBAAkB,KAAK,UAAU,UAAU,cAAc,KAAK,WAAW,KAAK;;;;;;;;;;;;;;;;;AAkB1F,SAAgB,QAAQ,SAA+B;CACrD,MAAM,MAAM,OAAO,iBAAiB;AACpC,KAAI,CAAC,IAAK;AAEV,KAAI,QAAQ,IAAI,SAAS,EAAE;AAC3B,KAAI,IAAI,MAAM,MAAK,MAAK,EAAE,WAAW,QAAQ,OAAO,CAAE;CAEtD,MAAM,MAAM,QAAQ,OAAO,iBAAiB;EAC1C,QAAQ,QAAQ;EAChB,UAAU,QAAQ,YAAY;EAC9B,SAAS,QAAQ,WAAW,CAAC,IAAI;EACjC,SAAS,QAAQ,WAAW;EAC5B,QAAQ,QAAQ,UAAU,CAAC,SAAS;EACrC,CAAC;CAEF,MAAM,WAAW,QAAQ,YACpB,kBAAkB,kBAAkB,QAAQ,WAAW;CAE5D,MAAM,cAAc,GADL,KAAK,KAAK,QAAQ,OAAO,GAAG,IAAI,QAAQ,OAAO,KAAK,QAAQ,OAC7C,IAAI;AAElC,KAAI,MAAM,KAAK;EACb,QAAQ,QAAQ;EAChB,MAAM,QAAQ,QAAQ,OAAO;EAC7B;EACA;EACD,CAAC"}
package/dist/index.d.mts CHANGED
@@ -5,6 +5,7 @@ import { usePlaintext } from "./composables/usePlaintext.mjs";
5
5
  import { useConfig } from "./composables/useConfig.mjs";
6
6
  import { useDoctype } from "./composables/useDoctype.mjs";
7
7
  import { useEvent } from "./composables/useEvent.mjs";
8
+ import { useFont } from "./composables/useFont.mjs";
8
9
  import { resolveConfig } from "./config/index.mjs";
9
10
  import { maizzle } from "./plugin.mjs";
10
11
  import { CreateRendererOptions, RenderedTemplate, Renderer, createRenderer } from "./render/createRenderer.mjs";
@@ -28,4 +29,4 @@ import { replaceStrings } from "./transformers/replaceStrings.mjs";
28
29
  import { format } from "./transformers/format.mjs";
29
30
  import { minify } from "./transformers/minify.mjs";
30
31
  import { useHead } from "@unhead/vue";
31
- export { type AttributesConfig, type CreateRendererOptions, type CssConfig, type EntitiesConfig, type FilterFunction, type FiltersConfig, type HtmlConfig, type MaizzleConfig, type RenderOptions, type RenderResult, type RenderedTemplate, type Renderer, type UrlConfig, type UrlQuery, type UrlQueryOptions, addAttributes, attributeToStyle, base, build, createPlaintext, createRenderer, defineConfig, entities, filters, format, inlineCSS, inlineLink, maizzle, minify, removeAttributes, purgeCSS as removeUnusedCSS, render, replaceStrings, resolveConfig, safeClassNames, serve, shorthandCSS, sixHex, urlQuery, useConfig, useDoctype, useEvent, useHead, usePlaintext };
32
+ export { type AttributesConfig, type CreateRendererOptions, type CssConfig, type EntitiesConfig, type FilterFunction, type FiltersConfig, type HtmlConfig, type MaizzleConfig, type RenderOptions, type RenderResult, type RenderedTemplate, type Renderer, type UrlConfig, type UrlQuery, type UrlQueryOptions, addAttributes, attributeToStyle, base, build, createPlaintext, createRenderer, defineConfig, entities, filters, format, inlineCSS, inlineLink, maizzle, minify, removeAttributes, purgeCSS as removeUnusedCSS, render, replaceStrings, resolveConfig, safeClassNames, serve, shorthandCSS, sixHex, urlQuery, useConfig, useDoctype, useEvent, useFont, useHead, usePlaintext };
package/dist/index.mjs CHANGED
@@ -25,7 +25,8 @@ import { render } from "./render/index.mjs";
25
25
  import { serve } from "./serve.mjs";
26
26
  import { useDoctype } from "./composables/useDoctype.mjs";
27
27
  import { useEvent } from "./composables/useEvent.mjs";
28
+ import { useFont } from "./composables/useFont.mjs";
28
29
  import { usePlaintext } from "./composables/usePlaintext.mjs";
29
30
  import { useHead } from "@unhead/vue";
30
31
 
31
- export { addAttributes, attributeToStyle, base, build, createPlaintext, createRenderer, defineConfig, entities, filters, format, inlineCSS, inlineLink, maizzle, minify, removeAttributes, purgeCSS as removeUnusedCSS, render, replaceStrings, resolveConfig, safeClassNames, serve, shorthandCSS, sixHex, urlQuery, useConfig, useDoctype, useEvent, useHead, usePlaintext };
32
+ export { addAttributes, attributeToStyle, base, build, createPlaintext, createRenderer, defineConfig, entities, filters, format, inlineCSS, inlineLink, maizzle, minify, removeAttributes, purgeCSS as removeUnusedCSS, render, replaceStrings, resolveConfig, safeClassNames, serve, shorthandCSS, sixHex, urlQuery, useConfig, useDoctype, useEvent, useFont, useHead, usePlaintext };
@@ -0,0 +1,13 @@
1
+ import { Plugin } from "postcss";
2
+
3
+ //#region src/plugins/postcss/quoteFontFamilies.d.ts
4
+ /**
5
+ * Re-quote multi-word font-family identifiers that lightningcss "optimised"
6
+ * by removing quotes. CSS allows space-separated identifiers as a family
7
+ * name, but Google Fonts (and most style guides) prescribe quoted form.
8
+ */
9
+ declare function quoteFontFamilies(): Plugin;
10
+ declare const postcss = true;
11
+ //#endregion
12
+ export { postcss, quoteFontFamilies };
13
+ //# sourceMappingURL=quoteFontFamilies.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quoteFontFamilies.d.mts","names":[],"sources":["../../../src/plugins/postcss/quoteFontFamilies.ts"],"mappings":";;;;;AA+CA;;;iBAAgB,iBAAA,CAAA,GAAqB,MAAA;AAAA,cA6BxB,OAAA"}
@@ -0,0 +1,84 @@
1
+ //#region src/plugins/postcss/quoteFontFamilies.ts
2
+ const GENERIC_KEYWORDS = new Set([
3
+ "serif",
4
+ "sans-serif",
5
+ "monospace",
6
+ "cursive",
7
+ "fantasy",
8
+ "system-ui",
9
+ "ui-serif",
10
+ "ui-sans-serif",
11
+ "ui-monospace",
12
+ "ui-rounded",
13
+ "emoji",
14
+ "math",
15
+ "fangsong",
16
+ "inherit",
17
+ "initial",
18
+ "unset",
19
+ "revert",
20
+ "revert-layer"
21
+ ]);
22
+ /**
23
+ * Split a font-family value on top-level commas only, preserving quoted
24
+ * strings and parenthesised groups like `var(...)`.
25
+ */
26
+ function splitFamilies(value) {
27
+ const parts = [];
28
+ let depth = 0;
29
+ let quote = null;
30
+ let start = 0;
31
+ for (let i = 0; i < value.length; i++) {
32
+ const ch = value[i];
33
+ if (quote) {
34
+ if (ch === "\\") {
35
+ i++;
36
+ continue;
37
+ }
38
+ if (ch === quote) quote = null;
39
+ continue;
40
+ }
41
+ if (ch === "\"" || ch === "'") {
42
+ quote = ch;
43
+ continue;
44
+ }
45
+ if (ch === "(") depth++;
46
+ else if (ch === ")") depth--;
47
+ else if (ch === "," && depth === 0) {
48
+ parts.push(value.slice(start, i).trim());
49
+ start = i + 1;
50
+ }
51
+ }
52
+ parts.push(value.slice(start).trim());
53
+ return parts.filter(Boolean);
54
+ }
55
+ /**
56
+ * Re-quote multi-word font-family identifiers that lightningcss "optimised"
57
+ * by removing quotes. CSS allows space-separated identifiers as a family
58
+ * name, but Google Fonts (and most style guides) prescribe quoted form.
59
+ */
60
+ function quoteFontFamilies() {
61
+ return {
62
+ postcssPlugin: "quote-font-families",
63
+ Declaration: { "font-family": (decl) => {
64
+ const value = decl.value;
65
+ if (!value || !/\s/.test(value)) return;
66
+ const families = splitFamilies(value);
67
+ let changed = false;
68
+ const fixed = families.map((token) => {
69
+ if (token.startsWith("\"") || token.startsWith("'")) return token;
70
+ if (token.startsWith("var(")) return token;
71
+ if (!token.includes(" ")) return token;
72
+ if (GENERIC_KEYWORDS.has(token.toLowerCase())) return token;
73
+ changed = true;
74
+ return `"${token}"`;
75
+ });
76
+ if (changed) decl.value = fixed.join(", ");
77
+ } }
78
+ };
79
+ }
80
+ const postcss = true;
81
+
82
+ //#endregion
83
+ export { postcss, quoteFontFamilies };
84
+ //# sourceMappingURL=quoteFontFamilies.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quoteFontFamilies.mjs","names":[],"sources":["../../../src/plugins/postcss/quoteFontFamilies.ts"],"sourcesContent":["import type { Plugin } from 'postcss'\n\nconst GENERIC_KEYWORDS = new Set([\n 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',\n 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',\n 'emoji', 'math', 'fangsong',\n 'inherit', 'initial', 'unset', 'revert', 'revert-layer',\n])\n\n/**\n * Split a font-family value on top-level commas only, preserving quoted\n * strings and parenthesised groups like `var(...)`.\n */\nfunction splitFamilies(value: string): string[] {\n const parts: string[] = []\n let depth = 0\n let quote: string | null = null\n let start = 0\n\n for (let i = 0; i < value.length; i++) {\n const ch = value[i]\n if (quote) {\n if (ch === '\\\\') { i++; continue }\n if (ch === quote) quote = null\n continue\n }\n if (ch === '\"' || ch === \"'\") {\n quote = ch\n continue\n }\n if (ch === '(') depth++\n else if (ch === ')') depth--\n else if (ch === ',' && depth === 0) {\n parts.push(value.slice(start, i).trim())\n start = i + 1\n }\n }\n\n parts.push(value.slice(start).trim())\n return parts.filter(Boolean)\n}\n\n/**\n * Re-quote multi-word font-family identifiers that lightningcss \"optimised\"\n * by removing quotes. CSS allows space-separated identifiers as a family\n * name, but Google Fonts (and most style guides) prescribe quoted form.\n */\nexport function quoteFontFamilies(): Plugin {\n return {\n postcssPlugin: 'quote-font-families',\n Declaration: {\n 'font-family': (decl) => {\n const value = decl.value\n if (!value || !/\\s/.test(value)) return\n\n const families = splitFamilies(value)\n let changed = false\n\n const fixed = families.map((token) => {\n if (token.startsWith('\"') || token.startsWith(\"'\")) return token\n if (token.startsWith('var(')) return token\n if (!token.includes(' ')) return token\n if (GENERIC_KEYWORDS.has(token.toLowerCase())) return token\n\n changed = true\n return `\"${token}\"`\n })\n\n if (changed) {\n decl.value = fixed.join(', ')\n }\n },\n },\n }\n}\n\nexport const postcss = true\n"],"mappings":";AAEA,MAAM,mBAAmB,IAAI,IAAI;CAC/B;CAAS;CAAc;CAAa;CAAW;CAC/C;CAAa;CAAY;CAAiB;CAAgB;CAC1D;CAAS;CAAQ;CACjB;CAAW;CAAW;CAAS;CAAU;CAC1C,CAAC;;;;;AAMF,SAAS,cAAc,OAAyB;CAC9C,MAAM,QAAkB,EAAE;CAC1B,IAAI,QAAQ;CACZ,IAAI,QAAuB;CAC3B,IAAI,QAAQ;AAEZ,MAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACrC,MAAM,KAAK,MAAM;AACjB,MAAI,OAAO;AACT,OAAI,OAAO,MAAM;AAAE;AAAK;;AACxB,OAAI,OAAO,MAAO,SAAQ;AAC1B;;AAEF,MAAI,OAAO,QAAO,OAAO,KAAK;AAC5B,WAAQ;AACR;;AAEF,MAAI,OAAO,IAAK;WACP,OAAO,IAAK;WACZ,OAAO,OAAO,UAAU,GAAG;AAClC,SAAM,KAAK,MAAM,MAAM,OAAO,EAAE,CAAC,MAAM,CAAC;AACxC,WAAQ,IAAI;;;AAIhB,OAAM,KAAK,MAAM,MAAM,MAAM,CAAC,MAAM,CAAC;AACrC,QAAO,MAAM,OAAO,QAAQ;;;;;;;AAQ9B,SAAgB,oBAA4B;AAC1C,QAAO;EACL,eAAe;EACf,aAAa,EACX,gBAAgB,SAAS;GACvB,MAAM,QAAQ,KAAK;AACnB,OAAI,CAAC,SAAS,CAAC,KAAK,KAAK,MAAM,CAAE;GAEjC,MAAM,WAAW,cAAc,MAAM;GACrC,IAAI,UAAU;GAEd,MAAM,QAAQ,SAAS,KAAK,UAAU;AACpC,QAAI,MAAM,WAAW,KAAI,IAAI,MAAM,WAAW,IAAI,CAAE,QAAO;AAC3D,QAAI,MAAM,WAAW,OAAO,CAAE,QAAO;AACrC,QAAI,CAAC,MAAM,SAAS,IAAI,CAAE,QAAO;AACjC,QAAI,iBAAiB,IAAI,MAAM,aAAa,CAAC,CAAE,QAAO;AAEtD,cAAU;AACV,WAAO,IAAI,MAAM;KACjB;AAEF,OAAI,QACF,MAAK,QAAQ,MAAM,KAAK,KAAK;KAGlC;EACF;;AAGH,MAAa,UAAU"}
@@ -236,10 +236,12 @@ async function createRenderer(options = {}) {
236
236
  if (bodyAttrs) html = html.replace(/<body([^>]*)>/, `<body$1 ${bodyAttrs}>`);
237
237
  if (bodyTagsOpen) html = html.replace(/<body([^>]*)>/, `<body$1>\n${bodyTagsOpen}`);
238
238
  if (bodyTags) html = html.replace("</body>", `${bodyTags}\n</body>`);
239
- if (ssrContext.teleports) {
239
+ const hasTeleports = ssrContext.teleports && Object.keys(ssrContext.teleports).length > 0;
240
+ const hasFonts = (renderContext.fonts?.length ?? 0) > 0;
241
+ if (hasTeleports || hasFonts) {
240
242
  const { parse: parseDom, serialize: serializeDom, walk } = await import("../utils/ast/index.mjs");
241
243
  let dom = parseDom(html);
242
- for (const [rawTarget, content] of Object.entries(ssrContext.teleports)) {
244
+ if (hasTeleports) for (const [rawTarget, content] of Object.entries(ssrContext.teleports)) {
243
245
  if (!content) continue;
244
246
  const prepend = rawTarget.endsWith(":start");
245
247
  const target = prepend ? rawTarget.slice(0, -6) : rawTarget;
@@ -253,6 +255,10 @@ async function createRenderer(options = {}) {
253
255
  }
254
256
  });
255
257
  }
258
+ if (hasFonts) {
259
+ const { injectFonts } = await import("./injectFonts.mjs");
260
+ injectFonts(dom, renderContext.fonts, parseDom, walk);
261
+ }
256
262
  html = serializeDom(dom);
257
263
  }
258
264
  if (renderContext.preheader) {