@maizzle/framework 6.0.0-rc.6 → 6.0.0-rc.8

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 (86) hide show
  1. package/dist/components/Body.vue +105 -36
  2. package/dist/components/Button.vue +4 -1
  3. package/dist/components/CodeBlock.vue +11 -18
  4. package/dist/components/CodeInline.vue +6 -1
  5. package/dist/components/Column.vue +30 -5
  6. package/dist/components/Container.vue +10 -2
  7. package/dist/components/Divider.vue +28 -0
  8. package/dist/components/Head.vue +22 -0
  9. package/dist/components/Heading.vue +28 -0
  10. package/dist/components/Html.vue +98 -47
  11. package/dist/components/Layout.vue +93 -0
  12. package/dist/components/Link.vue +26 -0
  13. package/dist/components/Markdown.vue +83 -0
  14. package/dist/components/Outlook.vue +36 -0
  15. package/dist/components/Overlap.vue +25 -5
  16. package/dist/components/{Preview.vue → Preheader.vue} +1 -1
  17. package/dist/components/Row.vue +16 -5
  18. package/dist/components/Section.vue +83 -0
  19. package/dist/components/Text.vue +29 -0
  20. package/dist/components/Vml.vue +165 -13
  21. package/dist/plugins/postcss/tailwindCleanup.mjs +22 -13
  22. package/dist/plugins/postcss/tailwindCleanup.mjs.map +1 -1
  23. package/dist/render/createRenderer.d.mts +2 -3
  24. package/dist/render/createRenderer.d.mts.map +1 -1
  25. package/dist/render/createRenderer.mjs +67 -4
  26. package/dist/render/createRenderer.mjs.map +1 -1
  27. package/dist/serve.d.mts.map +1 -1
  28. package/dist/serve.mjs +84 -4
  29. package/dist/serve.mjs.map +1 -1
  30. package/dist/server/compatibility.d.mts +1 -2
  31. package/dist/server/compatibility.d.mts.map +1 -1
  32. package/dist/server/compatibility.mjs +30 -16
  33. package/dist/server/compatibility.mjs.map +1 -1
  34. package/dist/server/email.d.mts +17 -0
  35. package/dist/server/email.d.mts.map +1 -0
  36. package/dist/server/email.mjs +41 -0
  37. package/dist/server/email.mjs.map +1 -0
  38. package/dist/server/linter.d.mts +1 -2
  39. package/dist/server/linter.d.mts.map +1 -1
  40. package/dist/server/linter.mjs +60 -71
  41. package/dist/server/linter.mjs.map +1 -1
  42. package/dist/server/ui/App.vue +205 -69
  43. package/dist/server/ui/components/ui/checkbox/Checkbox.vue +35 -0
  44. package/dist/server/ui/components/ui/checkbox/index.ts +1 -0
  45. package/dist/server/ui/components/ui/command/CommandDialog.vue +1 -1
  46. package/dist/server/ui/components/ui/command/CommandInput.vue +19 -1
  47. package/dist/server/ui/components/ui/command/CommandItem.vue +1 -1
  48. package/dist/server/ui/components/ui/command/CommandList.vue +1 -1
  49. package/dist/server/ui/components/ui/command/CommandShortcut.vue +1 -1
  50. package/dist/server/ui/components/ui/dialog/DialogOverlay.vue +9 -1
  51. package/dist/server/ui/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
  52. package/dist/server/ui/components/ui/scroll-area/ScrollBar.vue +1 -1
  53. package/dist/server/ui/components/ui/sheet/SheetContent.vue +1 -1
  54. package/dist/server/ui/components/ui/sheet/SheetOverlay.vue +9 -1
  55. package/dist/server/ui/components/ui/sidebar/Sidebar.vue +8 -1
  56. package/dist/server/ui/components/ui/sidebar/SidebarProvider.vue +1 -1
  57. package/dist/server/ui/components/ui/sidebar/SidebarTrigger.vue +5 -4
  58. package/dist/server/ui/components/ui/tags-input/TagsInput.vue +26 -0
  59. package/dist/server/ui/components/ui/tags-input/TagsInputInput.vue +17 -0
  60. package/dist/server/ui/components/ui/tags-input/TagsInputItem.vue +19 -0
  61. package/dist/server/ui/components/ui/tags-input/TagsInputItemDelete.vue +22 -0
  62. package/dist/server/ui/components/ui/tags-input/TagsInputItemText.vue +17 -0
  63. package/dist/server/ui/components/ui/tags-input/index.ts +5 -0
  64. package/dist/server/ui/components/ui/toggle/index.ts +3 -3
  65. package/dist/server/ui/components/ui/toggle-group/ToggleGroup.vue +1 -1
  66. package/dist/server/ui/components/ui/toggle-group/ToggleGroupItem.vue +2 -2
  67. package/dist/server/ui/main.css +20 -20
  68. package/dist/server/ui/pages/Home.vue +12 -5
  69. package/dist/server/ui/pages/Preview.vue +495 -211
  70. package/dist/transformers/inlineCSS.d.mts +1 -14
  71. package/dist/transformers/inlineCSS.d.mts.map +1 -1
  72. package/dist/transformers/inlineCSS.mjs +25 -34
  73. package/dist/transformers/inlineCSS.mjs.map +1 -1
  74. package/dist/transformers/purgeCSS.d.mts.map +1 -1
  75. package/dist/transformers/purgeCSS.mjs +67 -1
  76. package/dist/transformers/purgeCSS.mjs.map +1 -1
  77. package/dist/transformers/tailwindcss.mjs +3 -7
  78. package/dist/transformers/tailwindcss.mjs.map +1 -1
  79. package/dist/types/config.d.mts +47 -29
  80. package/dist/types/config.d.mts.map +1 -1
  81. package/dist/types/index.d.mts +2 -2
  82. package/package.json +7 -3
  83. package/dist/server/ui/components/ui/resizable/ResizableHandle.vue +0 -30
  84. package/dist/server/ui/components/ui/resizable/ResizablePanel.vue +0 -21
  85. package/dist/server/ui/components/ui/resizable/ResizablePanelGroup.vue +0 -25
  86. package/dist/server/ui/components/ui/resizable/index.ts +0 -3
@@ -9,20 +9,7 @@ import { ChildNode } from "domhandler";
9
9
  * This is important for email client compatibility (especially Outlook on Windows).
10
10
  *
11
11
  * Enabled when `css.inline` is set to `true` or an object with options.
12
- *
13
- * Options:
14
- * - removeStyleTags: Remove style tags after inlining (default: false)
15
- * - removeInlinedSelectors: Remove classes after they've been inlined (default: true)
16
- * - preferUnitlessValues: Convert 0px, 0em, etc. to 0 (default: true)
17
- * - safelist: Selectors that should not be removed after inlining
18
- * - styleToAttribute: Map CSS properties to HTML attributes (e.g., background-color -> bgcolor)
19
- * - applyWidthAttributes: Add width attributes based on inline CSS (default: true)
20
- * - applyHeightAttributes: Add height attributes based on inline CSS (default: true)
21
- * - widthElements: Elements that can receive width attributes (default: ['img', 'video'])
22
- * - heightElements: Elements that can receive height attributes (default: ['img', 'video'])
23
- * - excludedProperties: CSS properties to exclude from inlining
24
- * - codeBlocks: Fenced code blocks to ignore (default: { EJS: { start: '<%', end: '%>' }, HBS: { start: '{{', end: '}}' } })
25
- * - customCSS: Additional CSS to inline
12
+ * All Juice options are supported and passed through directly.
26
13
  */
27
14
  declare function inlineCSS(dom: ChildNode[], config?: CssConfig): ChildNode[];
28
15
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"inlineCSS.d.mts","names":[],"sources":["../../src/transformers/inlineCSS.ts"],"mappings":";;;;;;AA2CA;;;;;;;;;;;;;;;;;;;;iBAAgB,SAAA,CAAU,GAAA,EAAK,SAAA,IAAa,MAAA,GAAQ,SAAA,GAAiB,SAAA"}
1
+ {"version":3,"file":"inlineCSS.d.mts","names":[],"sources":["../../src/transformers/inlineCSS.ts"],"mappings":";;;;;;AAeA;;;;;;;iBAAgB,SAAA,CAAU,GAAA,EAAK,SAAA,IAAa,MAAA,GAAQ,SAAA,GAAiB,SAAA"}
@@ -12,32 +12,17 @@ import juice from "juice";
12
12
  * This is important for email client compatibility (especially Outlook on Windows).
13
13
  *
14
14
  * Enabled when `css.inline` is set to `true` or an object with options.
15
- *
16
- * Options:
17
- * - removeStyleTags: Remove style tags after inlining (default: false)
18
- * - removeInlinedSelectors: Remove classes after they've been inlined (default: true)
19
- * - preferUnitlessValues: Convert 0px, 0em, etc. to 0 (default: true)
20
- * - safelist: Selectors that should not be removed after inlining
21
- * - styleToAttribute: Map CSS properties to HTML attributes (e.g., background-color -> bgcolor)
22
- * - applyWidthAttributes: Add width attributes based on inline CSS (default: true)
23
- * - applyHeightAttributes: Add height attributes based on inline CSS (default: true)
24
- * - widthElements: Elements that can receive width attributes (default: ['img', 'video'])
25
- * - heightElements: Elements that can receive height attributes (default: ['img', 'video'])
26
- * - excludedProperties: CSS properties to exclude from inlining
27
- * - codeBlocks: Fenced code blocks to ignore (default: { EJS: { start: '<%', end: '%>' }, HBS: { start: '{{', end: '}}' } })
28
- * - customCSS: Additional CSS to inline
15
+ * All Juice options are supported and passed through directly.
29
16
  */
30
17
  function inlineCSS(dom, config = {}) {
31
18
  const inline = config.inline;
32
19
  if (!inline) return dom;
33
- const options = typeof inline === "object" ? inline : {};
34
- const removeStyleTags = options.removeStyleTags ?? false;
35
- const customCSS = options.customCSS ?? "";
36
- juice.styleToAttribute = options.styleToAttribute ?? {};
37
- juice.excludedProperties = ["--tw-shadow", ...options.excludedProperties ?? []];
38
- juice.widthElements = (options.widthElements ?? ["img", "video"]).map((i) => i.toUpperCase());
39
- juice.heightElements = (options.heightElements ?? ["img", "video"]).map((i) => i.toUpperCase());
40
- if (options.codeBlocks && typeof options.codeBlocks === "object") Object.entries(options.codeBlocks).forEach(([key, value]) => {
20
+ const { preferUnitlessValues = true, safelist, customCSS = "", styleToAttribute, excludedProperties, widthElements, heightElements, codeBlocks, ...juicePassthrough } = typeof inline === "object" ? inline : {};
21
+ juice.styleToAttribute = styleToAttribute ?? {};
22
+ juice.excludedProperties = ["--tw-shadow", ...excludedProperties ?? []];
23
+ juice.widthElements = (widthElements ?? ["img", "video"]).map((i) => i.toUpperCase());
24
+ juice.heightElements = (heightElements ?? ["img", "video"]).map((i) => i.toUpperCase());
25
+ if (codeBlocks && typeof codeBlocks === "object") Object.entries(codeBlocks).forEach(([key, value]) => {
41
26
  if (value.start && value.end) juice.codeBlocks[key] = value;
42
27
  });
43
28
  walk(dom, (node) => {
@@ -45,32 +30,38 @@ function inlineCSS(dom, config = {}) {
45
30
  if (el.name === "style" && el.attribs) {
46
31
  if (el.attribs.embed && !("data-embed" in el.attribs)) el.attribs["data-embed"] = "";
47
32
  if (el.attribs["data-embed"] && !("embed" in el.attribs)) el.attribs.embed = "";
33
+ if ("data-embed" in el.attribs) el.attribs["data-maizzle-embed"] = "";
48
34
  }
49
35
  });
50
36
  const serialized = serialize(dom);
51
37
  let inlinedHtml;
52
38
  try {
53
- const juiceOptions = {
54
- removeStyleTags,
55
- removeInlinedSelectors: options.removeInlinedSelectors ?? true,
56
- preservedSelectors: options.safelist ?? [],
57
- applyWidthAttributes: options.applyWidthAttributes ?? true,
58
- applyHeightAttributes: options.applyHeightAttributes ?? true
59
- };
60
- if (customCSS) inlinedHtml = juice(serialized, {
61
- ...juiceOptions,
62
- extraCss: customCSS
39
+ inlinedHtml = juice(serialized, {
40
+ removeStyleTags: juicePassthrough.removeStyleTags ?? false,
41
+ removeInlinedSelectors: juicePassthrough.removeInlinedSelectors ?? true,
42
+ applyWidthAttributes: juicePassthrough.applyWidthAttributes ?? true,
43
+ applyHeightAttributes: juicePassthrough.applyHeightAttributes ?? true,
44
+ preservedSelectors: safelist ?? [],
45
+ ...customCSS ? { extraCss: customCSS } : {},
46
+ inlineDuplicateProperties: juicePassthrough.inlineDuplicateProperties ?? true,
47
+ ...juicePassthrough
63
48
  });
64
- else inlinedHtml = juice(serialized, juiceOptions);
65
49
  } catch {
66
50
  return dom;
67
51
  }
68
- const preferUnitlessValues = options.preferUnitlessValues ?? true;
69
52
  const result = parse(inlinedHtml);
70
53
  if (preferUnitlessValues) walk(result, (node) => {
71
54
  const el = node;
72
55
  if (el.attribs?.style) el.attribs.style = el.attribs.style.replace(/\b0(px|rem|em|%|vh|vw|vmin|vmax|in|cm|mm|pt|pc|ex|ch)\b/g, "0");
73
56
  });
57
+ walk(result, (node) => {
58
+ const el = node;
59
+ if (el.name === "style" && el.attribs && "data-maizzle-embed" in el.attribs) {
60
+ el.attribs["data-embed"] = "";
61
+ el.attribs.embed = "";
62
+ delete el.attribs["data-maizzle-embed"];
63
+ }
64
+ });
74
65
  return result;
75
66
  }
76
67
 
@@ -1 +1 @@
1
- {"version":3,"file":"inlineCSS.mjs","names":[],"sources":["../../src/transformers/inlineCSS.ts"],"sourcesContent":["import juice from 'juice'\nimport { walk, parse, serialize } from '../utils/ast/index.ts'\nimport type { ChildNode, Element } from 'domhandler'\nimport type { Options as JuiceOptions } from 'juice'\nimport type { CssConfig } from '../types/config.ts'\n\ninterface InlineCssOptions {\n removeStyleTags?: boolean\n removeInlinedSelectors?: boolean\n preferUnitlessValues?: boolean\n safelist?: string[]\n styleToAttribute?: Record<string, string>\n applyWidthAttributes?: boolean\n applyHeightAttributes?: boolean\n widthElements?: string[]\n heightElements?: string[]\n excludedProperties?: string[]\n codeBlocks?: Record<string, { start: string; end: string }>\n customCSS?: string\n}\n\n/**\n * Inline CSS transformer.\n *\n * Inlines CSS from `<style>` tags into inline style attributes on HTML elements.\n * This is important for email client compatibility (especially Outlook on Windows).\n *\n * Enabled when `css.inline` is set to `true` or an object with options.\n *\n * Options:\n * - removeStyleTags: Remove style tags after inlining (default: false)\n * - removeInlinedSelectors: Remove classes after they've been inlined (default: true)\n * - preferUnitlessValues: Convert 0px, 0em, etc. to 0 (default: true)\n * - safelist: Selectors that should not be removed after inlining\n * - styleToAttribute: Map CSS properties to HTML attributes (e.g., background-color -> bgcolor)\n * - applyWidthAttributes: Add width attributes based on inline CSS (default: true)\n * - applyHeightAttributes: Add height attributes based on inline CSS (default: true)\n * - widthElements: Elements that can receive width attributes (default: ['img', 'video'])\n * - heightElements: Elements that can receive height attributes (default: ['img', 'video'])\n * - excludedProperties: CSS properties to exclude from inlining\n * - codeBlocks: Fenced code blocks to ignore (default: { EJS: { start: '<%', end: '%>' }, HBS: { start: '{{', end: '}}' } })\n * - customCSS: Additional CSS to inline\n */\nexport function inlineCSS(dom: ChildNode[], config: CssConfig = {}): ChildNode[] {\n const inline = config.inline\n\n // Disabled when inline is falsy or not an object/truthy\n if (!inline) {\n return dom\n }\n\n // Build options from config\n const options: InlineCssOptions = typeof inline === 'object' ? inline : {}\n\n const removeStyleTags = options.removeStyleTags ?? false\n const customCSS = options.customCSS ?? ''\n\n // Configure Juice static properties\n juice.styleToAttribute = options.styleToAttribute ?? {}\n juice.excludedProperties = ['--tw-shadow', ...(options.excludedProperties ?? [])]\n juice.widthElements = (options.widthElements ?? ['img', 'video']).map(i => i.toUpperCase()) as unknown as HTMLElement[]\n juice.heightElements = (options.heightElements ?? ['img', 'video']).map(i => i.toUpperCase()) as unknown as HTMLElement[]\n\n // Add custom code blocks\n if (options.codeBlocks && typeof options.codeBlocks === 'object') {\n Object.entries(options.codeBlocks).forEach(([key, value]) => {\n if (value.start && value.end) {\n juice.codeBlocks[key] = value\n }\n })\n }\n\n // Handle style tags with embed attributes\n walk(dom, (node) => {\n const el = node as Element\n if (el.name === 'style' && el.attribs) {\n // Add data-embed to style tags with embed attribute\n if (el.attribs.embed && !('data-embed' in el.attribs)) {\n el.attribs['data-embed'] = ''\n }\n // Add embed to style tags with data-embed attribute\n if (el.attribs['data-embed'] && !('embed' in el.attribs)) {\n el.attribs.embed = ''\n }\n }\n })\n\n // Serialize for juice (juice requires a string)\n const serialized = serialize(dom)\n\n let inlinedHtml: string\n\n try {\n const juiceOptions: JuiceOptions = {\n removeStyleTags,\n removeInlinedSelectors: options.removeInlinedSelectors ?? true,\n preservedSelectors: options.safelist ?? [],\n applyWidthAttributes: options.applyWidthAttributes ?? true,\n applyHeightAttributes: options.applyHeightAttributes ?? true,\n }\n\n if (customCSS) {\n inlinedHtml = juice(serialized, { ...juiceOptions, extraCss: customCSS })\n } else {\n inlinedHtml = juice(serialized, juiceOptions)\n }\n } catch {\n // If Juice fails, return the dom unchanged\n return dom\n }\n\n // Post-process for preferUnitlessValues\n const preferUnitlessValues = options.preferUnitlessValues ?? true\n const result = parse(inlinedHtml)\n\n if (preferUnitlessValues) {\n walk(result, (node) => {\n const el = node as Element\n if (el.attribs?.style) {\n el.attribs.style = el.attribs.style.replace(\n /\\b0(px|rem|em|%|vh|vw|vmin|vmax|in|cm|mm|pt|pc|ex|ch)\\b/g,\n '0'\n )\n }\n })\n }\n\n return result\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,SAAgB,UAAU,KAAkB,SAAoB,EAAE,EAAe;CAC/E,MAAM,SAAS,OAAO;AAGtB,KAAI,CAAC,OACH,QAAO;CAIT,MAAM,UAA4B,OAAO,WAAW,WAAW,SAAS,EAAE;CAE1E,MAAM,kBAAkB,QAAQ,mBAAmB;CACnD,MAAM,YAAY,QAAQ,aAAa;AAGvC,OAAM,mBAAmB,QAAQ,oBAAoB,EAAE;AACvD,OAAM,qBAAqB,CAAC,eAAe,GAAI,QAAQ,sBAAsB,EAAE,CAAE;AACjF,OAAM,iBAAiB,QAAQ,iBAAiB,CAAC,OAAO,QAAQ,EAAE,KAAI,MAAK,EAAE,aAAa,CAAC;AAC3F,OAAM,kBAAkB,QAAQ,kBAAkB,CAAC,OAAO,QAAQ,EAAE,KAAI,MAAK,EAAE,aAAa,CAAC;AAG7F,KAAI,QAAQ,cAAc,OAAO,QAAQ,eAAe,SACtD,QAAO,QAAQ,QAAQ,WAAW,CAAC,SAAS,CAAC,KAAK,WAAW;AAC3D,MAAI,MAAM,SAAS,MAAM,IACvB,OAAM,WAAW,OAAO;GAE1B;AAIJ,MAAK,MAAM,SAAS;EAClB,MAAM,KAAK;AACX,MAAI,GAAG,SAAS,WAAW,GAAG,SAAS;AAErC,OAAI,GAAG,QAAQ,SAAS,EAAE,gBAAgB,GAAG,SAC3C,IAAG,QAAQ,gBAAgB;AAG7B,OAAI,GAAG,QAAQ,iBAAiB,EAAE,WAAW,GAAG,SAC9C,IAAG,QAAQ,QAAQ;;GAGvB;CAGF,MAAM,aAAa,UAAU,IAAI;CAEjC,IAAI;AAEJ,KAAI;EACF,MAAM,eAA6B;GACjC;GACA,wBAAwB,QAAQ,0BAA0B;GAC1D,oBAAoB,QAAQ,YAAY,EAAE;GAC1C,sBAAsB,QAAQ,wBAAwB;GACtD,uBAAuB,QAAQ,yBAAyB;GACzD;AAED,MAAI,UACF,eAAc,MAAM,YAAY;GAAE,GAAG;GAAc,UAAU;GAAW,CAAC;MAEzE,eAAc,MAAM,YAAY,aAAa;SAEzC;AAEN,SAAO;;CAIT,MAAM,uBAAuB,QAAQ,wBAAwB;CAC7D,MAAM,SAAS,MAAM,YAAY;AAEjC,KAAI,qBACF,MAAK,SAAS,SAAS;EACrB,MAAM,KAAK;AACX,MAAI,GAAG,SAAS,MACd,IAAG,QAAQ,QAAQ,GAAG,QAAQ,MAAM,QAClC,4DACA,IACD;GAEH;AAGJ,QAAO"}
1
+ {"version":3,"file":"inlineCSS.mjs","names":[],"sources":["../../src/transformers/inlineCSS.ts"],"sourcesContent":["import juice from 'juice'\nimport { walk, parse, serialize } from '../utils/ast/index.ts'\nimport type { ChildNode, Element } from 'domhandler'\nimport type { Options as JuiceOptions } from 'juice'\nimport type { CssConfig } from '../types/config.ts'\n\n/**\n * Inline CSS transformer.\n *\n * Inlines CSS from `<style>` tags into inline style attributes on HTML elements.\n * This is important for email client compatibility (especially Outlook on Windows).\n *\n * Enabled when `css.inline` is set to `true` or an object with options.\n * All Juice options are supported and passed through directly.\n */\nexport function inlineCSS(dom: ChildNode[], config: CssConfig = {}): ChildNode[] {\n const inline = config.inline\n\n // Disabled when inline is falsy or not an object/truthy\n if (!inline) {\n return dom\n }\n\n // Build options from config\n const options = typeof inline === 'object' ? inline : {}\n\n // Separate Maizzle-specific options from Juice options\n const {\n preferUnitlessValues = true,\n safelist,\n customCSS = '',\n styleToAttribute,\n excludedProperties,\n widthElements,\n heightElements,\n codeBlocks,\n ...juicePassthrough\n } = options\n\n // Configure Juice static properties\n juice.styleToAttribute = styleToAttribute ?? {}\n juice.excludedProperties = ['--tw-shadow', ...(excludedProperties ?? [])]\n juice.widthElements = (widthElements ?? ['img', 'video']).map(i => i.toUpperCase()) as unknown as HTMLElement[]\n juice.heightElements = (heightElements ?? ['img', 'video']).map(i => i.toUpperCase()) as unknown as HTMLElement[]\n\n // Add custom code blocks\n if (codeBlocks && typeof codeBlocks === 'object') {\n Object.entries(codeBlocks).forEach(([key, value]) => {\n if (value.start && value.end) {\n juice.codeBlocks[key] = value\n }\n })\n }\n\n // Handle style tags with embed attributes.\n // We add a marker attribute that persists through the pipeline,\n // then restore data-embed from it after Juice runs.\n walk(dom, (node) => {\n const el = node as Element\n if (el.name === 'style' && el.attribs) {\n // Sync data-embed embed\n if (el.attribs.embed && !('data-embed' in el.attribs)) {\n el.attribs['data-embed'] = ''\n }\n if (el.attribs['data-embed'] && !('embed' in el.attribs)) {\n el.attribs.embed = ''\n }\n\n // Add marker that persists through the pipeline\n if ('data-embed' in el.attribs) {\n el.attribs['data-maizzle-embed'] = ''\n }\n }\n })\n\n // Serialize for juice (juice requires a string)\n const serialized = serialize(dom)\n\n let inlinedHtml: string\n\n try {\n const juiceOptions: JuiceOptions = {\n removeStyleTags: juicePassthrough.removeStyleTags ?? false,\n removeInlinedSelectors: juicePassthrough.removeInlinedSelectors ?? true,\n applyWidthAttributes: juicePassthrough.applyWidthAttributes ?? true,\n applyHeightAttributes: juicePassthrough.applyHeightAttributes ?? true,\n preservedSelectors: safelist ?? [],\n ...customCSS ? { extraCss: customCSS } : {},\n inlineDuplicateProperties: juicePassthrough.inlineDuplicateProperties ?? true,\n ...juicePassthrough,\n }\n\n inlinedHtml = juice(serialized, juiceOptions)\n } catch {\n // If Juice fails, return the dom unchanged\n return dom\n }\n\n // Post-process for preferUnitlessValues\n const result = parse(inlinedHtml)\n\n if (preferUnitlessValues) {\n walk(result, (node) => {\n const el = node as Element\n if (el.attribs?.style) {\n el.attribs.style = el.attribs.style.replace(\n /\\b0(px|rem|em|%|vh|vw|vmin|vmax|in|cm|mm|pt|pc|ex|ch)\\b/g,\n '0'\n )\n }\n })\n }\n\n // Restore data-embed from our marker, then remove the marker.\n // The purge step will handle final data-embed/embed removal.\n walk(result, (node) => {\n const el = node as Element\n if (el.name === 'style' && el.attribs && 'data-maizzle-embed' in el.attribs) {\n el.attribs['data-embed'] = ''\n el.attribs.embed = ''\n delete el.attribs['data-maizzle-embed']\n }\n })\n\n return result\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAeA,SAAgB,UAAU,KAAkB,SAAoB,EAAE,EAAe;CAC/E,MAAM,SAAS,OAAO;AAGtB,KAAI,CAAC,OACH,QAAO;CAOT,MAAM,EACJ,uBAAuB,MACvB,UACA,YAAY,IACZ,kBACA,oBACA,eACA,gBACA,YACA,GAAG,qBAZW,OAAO,WAAW,WAAW,SAAS,EAAE;AAgBxD,OAAM,mBAAmB,oBAAoB,EAAE;AAC/C,OAAM,qBAAqB,CAAC,eAAe,GAAI,sBAAsB,EAAE,CAAE;AACzE,OAAM,iBAAiB,iBAAiB,CAAC,OAAO,QAAQ,EAAE,KAAI,MAAK,EAAE,aAAa,CAAC;AACnF,OAAM,kBAAkB,kBAAkB,CAAC,OAAO,QAAQ,EAAE,KAAI,MAAK,EAAE,aAAa,CAAC;AAGrF,KAAI,cAAc,OAAO,eAAe,SACtC,QAAO,QAAQ,WAAW,CAAC,SAAS,CAAC,KAAK,WAAW;AACnD,MAAI,MAAM,SAAS,MAAM,IACvB,OAAM,WAAW,OAAO;GAE1B;AAMJ,MAAK,MAAM,SAAS;EAClB,MAAM,KAAK;AACX,MAAI,GAAG,SAAS,WAAW,GAAG,SAAS;AAErC,OAAI,GAAG,QAAQ,SAAS,EAAE,gBAAgB,GAAG,SAC3C,IAAG,QAAQ,gBAAgB;AAE7B,OAAI,GAAG,QAAQ,iBAAiB,EAAE,WAAW,GAAG,SAC9C,IAAG,QAAQ,QAAQ;AAIrB,OAAI,gBAAgB,GAAG,QACrB,IAAG,QAAQ,wBAAwB;;GAGvC;CAGF,MAAM,aAAa,UAAU,IAAI;CAEjC,IAAI;AAEJ,KAAI;AAYF,gBAAc,MAAM,YAXe;GACjC,iBAAiB,iBAAiB,mBAAmB;GACrD,wBAAwB,iBAAiB,0BAA0B;GACnE,sBAAsB,iBAAiB,wBAAwB;GAC/D,uBAAuB,iBAAiB,yBAAyB;GACjE,oBAAoB,YAAY,EAAE;GAClC,GAAG,YAAY,EAAE,UAAU,WAAW,GAAG,EAAE;GAC3C,2BAA2B,iBAAiB,6BAA6B;GACzE,GAAG;GACJ,CAE4C;SACvC;AAEN,SAAO;;CAIT,MAAM,SAAS,MAAM,YAAY;AAEjC,KAAI,qBACF,MAAK,SAAS,SAAS;EACrB,MAAM,KAAK;AACX,MAAI,GAAG,SAAS,MACd,IAAG,QAAQ,QAAQ,GAAG,QAAQ,MAAM,QAClC,4DACA,IACD;GAEH;AAKJ,MAAK,SAAS,SAAS;EACrB,MAAM,KAAK;AACX,MAAI,GAAG,SAAS,WAAW,GAAG,WAAW,wBAAwB,GAAG,SAAS;AAC3E,MAAG,QAAQ,gBAAgB;AAC3B,MAAG,QAAQ,QAAQ;AACnB,UAAO,GAAG,QAAQ;;GAEpB;AAEF,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"purgeCSS.d.mts","names":[],"sources":["../../src/transformers/purgeCSS.ts"],"mappings":";;;;;;AA+CA;;;;;;;;;;;;;iBAAgB,QAAA,CAAS,GAAA,EAAK,SAAA,IAAa,MAAA,GAAQ,SAAA,GAAiB,SAAA"}
1
+ {"version":3,"file":"purgeCSS.d.mts","names":[],"sources":["../../src/transformers/purgeCSS.ts"],"mappings":";;;;;;AAkDA;;;;;;;;;;;;;iBAAgB,QAAA,CAAS,GAAA,EAAK,SAAA,IAAa,MAAA,GAAQ,SAAA,GAAiB,SAAA"}
@@ -1,7 +1,11 @@
1
1
  import { parse } from "../utils/ast/parser.mjs";
2
+ import { walk } from "../utils/ast/walker.mjs";
2
3
  import { serialize } from "../utils/ast/serializer.mjs";
3
4
  import "../utils/ast/index.mjs";
4
5
  import { defu } from "defu";
6
+ import postcss from "postcss";
7
+ import safeParser from "postcss-safe-parser";
8
+ import { selectAll } from "css-select";
5
9
  import { comb } from "email-comb";
6
10
 
7
11
  //#region src/transformers/purgeCSS.ts
@@ -57,8 +61,70 @@ function purgeCSS(dom, config = {}) {
57
61
  ...restUserOptions,
58
62
  whitelist: [...DEFAULT_SAFELIST, ...userSafelist]
59
63
  }, DEFAULT_OPTIONS);
64
+ const safelist = [...DEFAULT_SAFELIST, ...userSafelist];
65
+ dom = deepPurge(dom, safelist);
60
66
  const { result } = comb(serialize(dom), options);
61
- return parse(result);
67
+ let purgedDom = parse(result);
68
+ walk(purgedDom, (node) => {
69
+ const el = node;
70
+ if (el.name === "style" && el.attribs) {
71
+ delete el.attribs["data-embed"];
72
+ delete el.attribs.embed;
73
+ }
74
+ });
75
+ return purgedDom;
76
+ }
77
+ /**
78
+ * Deep purge: uses PostCSS to parse CSS in non-embedded style tags,
79
+ * then checks each selector against the DOM with css-select.
80
+ * Removes rules where no selector matches any element.
81
+ */
82
+ function isSafelisted(selector, safelist) {
83
+ return safelist.some((pattern) => {
84
+ if (pattern.startsWith("*") && pattern.endsWith("*")) return selector.includes(pattern.slice(1, -1));
85
+ if (pattern.endsWith("*")) return selector.startsWith(pattern.slice(0, -1));
86
+ if (pattern.startsWith("*")) return selector.endsWith(pattern.slice(1));
87
+ return selector === pattern;
88
+ });
89
+ }
90
+ function deepPurge(dom, safelist) {
91
+ walk(dom, (node) => {
92
+ const el = node;
93
+ if (el.name !== "style" || !el.attribs) return;
94
+ if ("data-embed" in el.attribs) return;
95
+ const textNode = el.children?.find((c) => c.type === "text");
96
+ if (!textNode?.data?.trim()) return;
97
+ const root = postcss.parse(textNode.data, { parser: safeParser });
98
+ root.walkRules((rule) => {
99
+ if (rule.parent?.type === "atrule") return;
100
+ const selectors = rule.selectors ?? [rule.selector];
101
+ const matched = selectors.filter((sel) => {
102
+ if (isSafelisted(sel, safelist)) return true;
103
+ if (/::[\w-]/.test(sel)) return true;
104
+ if (/(?<!:):(?!not\b|is\b|where\b|has\b)[\w-]/.test(sel.replace(/\\./g, ""))) return true;
105
+ try {
106
+ return selectAll(sel, dom).length > 0;
107
+ } catch {
108
+ return true;
109
+ }
110
+ });
111
+ if (matched.length === 0) rule.remove();
112
+ else if (matched.length < selectors.length) rule.selectors = matched;
113
+ });
114
+ root.walkAtRules((atRule) => {
115
+ if (atRule.nodes?.length === 0) atRule.remove();
116
+ });
117
+ const purgedCss = root.toString();
118
+ if (purgedCss.trim()) textNode.data = purgedCss;
119
+ else {
120
+ const parent = el.parent;
121
+ if (parent && "children" in parent) {
122
+ const idx = parent.children.indexOf(el);
123
+ if (idx !== -1) parent.children.splice(idx, 1);
124
+ }
125
+ }
126
+ });
127
+ return dom;
62
128
  }
63
129
 
64
130
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"purgeCSS.mjs","names":["merge"],"sources":["../../src/transformers/purgeCSS.ts"],"sourcesContent":["import { comb } from 'email-comb'\nimport { defu as merge } from 'defu'\nimport type { ChildNode } from 'domhandler'\nimport { parse, serialize } from '../utils/ast/index.ts'\nimport type { CssConfig } from '../types/config.ts'\n\nconst DEFAULT_SAFELIST: string[] = [\n '*body*', // Gmail\n '.gmail*', // Gmail\n '.apple*', // Apple Mail\n '.ios*', // Mail on iOS\n '.ox-*', // Open-Xchange\n '.outlook*', // Outlook.com\n '[data-ogs*', // Outlook.com\n '.bloop_container', // Airmail\n '.Singleton', // Apple Mail 10\n '.unused', // Notes 8\n '.moz-text-html', // Thunderbird\n '.mail-detail-content', // Comcast, Libero webmail\n '*edo*', // Edison (all)\n '#*', // Freenet uses #msgBody\n '.lang*', // Fenced code blocks\n]\n\nconst DEFAULT_OPTIONS = {\n backend: [\n { heads: '{{', tails: '}}' },\n { heads: '{%', tails: '%}' },\n ],\n whitelist: [...DEFAULT_SAFELIST],\n}\n\n/**\n * Remove unused CSS transformer.\n *\n * Uses `email-comb` to strip CSS selectors and corresponding class/id\n * references that are not matched anywhere in the HTML body.\n *\n * Enable by setting `css.purge: true` (or passing options).\n * The user-supplied options are merged on top of the defaults, so\n * `safelist` values are **appended** to the built-in safelist rather\n * than replacing it.\n *\n * Accepts `ChildNode[]` as input, serializes internally before passing\n * to email-comb (which requires a raw HTML string), then parses the\n * result back to `ChildNode[]` so it fits in the DOM pipeline.\n */\nexport function purgeCSS(dom: ChildNode[], config: CssConfig = {}): ChildNode[] {\n const option = config.purge\n\n if (!option) return dom\n\n const userOptions = typeof option === 'object' ? option : {}\n\n // Merge user options on top of defaults.\n // defu merges objects deeply; for arrays it appends user values.\n // We want the user safelist appended to the default safelist,\n // so we build whitelist manually.\n const userSafelist = Array.isArray((userOptions as any).safelist)\n ? (userOptions as any).safelist as string[]\n : []\n\n const { safelist: _discard, ...restUserOptions } = userOptions as any\n\n const options = merge(\n { ...restUserOptions, whitelist: [...DEFAULT_SAFELIST, ...userSafelist] },\n DEFAULT_OPTIONS,\n )\n\n const { result } = comb(serialize(dom), options)\n\n return parse(result)\n}\n"],"mappings":";;;;;;;AAMA,MAAM,mBAA6B;CACjC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAED,MAAM,kBAAkB;CACtB,SAAS,CACP;EAAE,OAAO;EAAM,OAAO;EAAM,EAC5B;EAAE,OAAO;EAAM,OAAO;EAAM,CAC7B;CACD,WAAW,CAAC,GAAG,iBAAiB;CACjC;;;;;;;;;;;;;;;;AAiBD,SAAgB,SAAS,KAAkB,SAAoB,EAAE,EAAe;CAC9E,MAAM,SAAS,OAAO;AAEtB,KAAI,CAAC,OAAQ,QAAO;CAEpB,MAAM,cAAc,OAAO,WAAW,WAAW,SAAS,EAAE;CAM5D,MAAM,eAAe,MAAM,QAAS,YAAoB,SAAS,GAC5D,YAAoB,WACrB,EAAE;CAEN,MAAM,EAAE,UAAU,UAAU,GAAG,oBAAoB;CAEnD,MAAM,UAAUA,KACd;EAAE,GAAG;EAAiB,WAAW,CAAC,GAAG,kBAAkB,GAAG,aAAa;EAAE,EACzE,gBACD;CAED,MAAM,EAAE,WAAW,KAAK,UAAU,IAAI,EAAE,QAAQ;AAEhD,QAAO,MAAM,OAAO"}
1
+ {"version":3,"file":"purgeCSS.mjs","names":["merge"],"sources":["../../src/transformers/purgeCSS.ts"],"sourcesContent":["import { comb } from 'email-comb'\nimport { defu as merge } from 'defu'\nimport postcss from 'postcss'\nimport safeParser from 'postcss-safe-parser'\nimport { selectAll } from 'css-select'\nimport type { ChildNode, Element } from 'domhandler'\nimport { parse, serialize, walk } from '../utils/ast/index.ts'\nimport type { CssConfig } from '../types/config.ts'\n\nconst DEFAULT_SAFELIST: string[] = [\n '*body*', // Gmail\n '.gmail*', // Gmail\n '.apple*', // Apple Mail\n '.ios*', // Mail on iOS\n '.ox-*', // Open-Xchange\n '.outlook*', // Outlook.com\n '[data-ogs*', // Outlook.com\n '.bloop_container', // Airmail\n '.Singleton', // Apple Mail 10\n '.unused', // Notes 8\n '.moz-text-html', // Thunderbird\n '.mail-detail-content', // Comcast, Libero webmail\n '*edo*', // Edison (all)\n '#*', // Freenet uses #msgBody\n '.lang*', // Fenced code blocks\n]\n\nconst DEFAULT_OPTIONS = {\n backend: [\n { heads: '{{', tails: '}}' },\n { heads: '{%', tails: '%}' },\n ],\n whitelist: [...DEFAULT_SAFELIST],\n}\n\n/**\n * Remove unused CSS transformer.\n *\n * Uses `email-comb` to strip CSS selectors and corresponding class/id\n * references that are not matched anywhere in the HTML body.\n *\n * Enable by setting `css.purge: true` (or passing options).\n * The user-supplied options are merged on top of the defaults, so\n * `safelist` values are **appended** to the built-in safelist rather\n * than replacing it.\n *\n * Accepts `ChildNode[]` as input, serializes internally before passing\n * to email-comb (which requires a raw HTML string), then parses the\n * result back to `ChildNode[]` so it fits in the DOM pipeline.\n */\nexport function purgeCSS(dom: ChildNode[], config: CssConfig = {}): ChildNode[] {\n const option = config.purge\n\n if (!option) return dom\n\n const userOptions = typeof option === 'object' ? option : {}\n\n // Merge user options on top of defaults.\n // defu merges objects deeply; for arrays it appends user values.\n // We want the user safelist appended to the default safelist,\n // so we build whitelist manually.\n const userSafelist = Array.isArray((userOptions as any).safelist)\n ? (userOptions as any).safelist as string[]\n : []\n\n const { safelist: _discard, ...restUserOptions } = userOptions as any\n\n const options = merge(\n { ...restUserOptions, whitelist: [...DEFAULT_SAFELIST, ...userSafelist] },\n DEFAULT_OPTIONS,\n )\n\n // Deep purge first: DOM-aware selector removal using PostCSS + css-select.\n // Runs before email-comb so that email-comb can clean up orphaned classes\n // in HTML attributes left behind by removed CSS rules.\n const safelist = [...DEFAULT_SAFELIST, ...userSafelist]\n dom = deepPurge(dom, safelist)\n\n const { result } = comb(serialize(dom), options)\n\n let purgedDom = parse(result)\n\n // Clean up data-embed/embed attributes — no longer needed after purging\n walk(purgedDom, (node) => {\n const el = node as Element\n if (el.name === 'style' && el.attribs) {\n delete el.attribs['data-embed']\n delete el.attribs.embed\n }\n })\n\n return purgedDom\n}\n\n/**\n * Deep purge: uses PostCSS to parse CSS in non-embedded style tags,\n * then checks each selector against the DOM with css-select.\n * Removes rules where no selector matches any element.\n */\nfunction isSafelisted(selector: string, safelist: string[]): boolean {\n return safelist.some((pattern) => {\n if (pattern.startsWith('*') && pattern.endsWith('*')) {\n return selector.includes(pattern.slice(1, -1))\n }\n if (pattern.endsWith('*')) {\n return selector.startsWith(pattern.slice(0, -1))\n }\n if (pattern.startsWith('*')) {\n return selector.endsWith(pattern.slice(1))\n }\n return selector === pattern\n })\n}\n\nfunction deepPurge(dom: ChildNode[], safelist: string[]): ChildNode[] {\n walk(dom, (node) => {\n const el = node as Element\n\n if (el.name !== 'style' || !el.attribs) return\n if ('data-embed' in el.attribs) return\n\n const textNode = el.children?.find((c: any) => c.type === 'text') as any\n if (!textNode?.data?.trim()) return\n\n const root = postcss.parse(textNode.data, { parser: safeParser })\n\n root.walkRules((rule) => {\n // Skip rules inside @media or other at-rules — those may target\n // states we can't match statically (hover, responsive, etc.)\n if (rule.parent?.type === 'atrule') return\n\n const selectors = rule.selectors ?? [rule.selector]\n const matched = selectors.filter((sel) => {\n // Keep safelisted selectors\n if (isSafelisted(sel, safelist)) return true\n\n // Skip pseudo-classes/elements that can't be matched statically.\n // Functional pseudos like :not(), :is(), :where(), :has() are\n // matchable by css-select, so we only skip dynamic/state ones.\n if (/::[\\w-]/.test(sel)) return true\n if (/(?<!:):(?!not\\b|is\\b|where\\b|has\\b)[\\w-]/.test(sel.replace(/\\\\./g, ''))) return true\n\n try {\n return selectAll(sel, dom).length > 0\n } catch {\n // If css-select can't parse the selector, keep it\n return true\n }\n })\n\n if (matched.length === 0) {\n rule.remove()\n } else if (matched.length < selectors.length) {\n rule.selectors = matched\n }\n })\n\n // Remove empty at-rules\n root.walkAtRules((atRule) => {\n if (atRule.nodes?.length === 0) {\n atRule.remove()\n }\n })\n\n const purgedCss = root.toString()\n\n if (purgedCss.trim()) {\n textNode.data = purgedCss\n } else {\n // Remove the style tag entirely if empty\n const parent = el.parent\n if (parent && 'children' in parent) {\n const idx = parent.children.indexOf(el as any)\n if (idx !== -1) parent.children.splice(idx, 1)\n }\n }\n })\n\n return dom\n}\n"],"mappings":"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"}
@@ -115,15 +115,11 @@ async function tailwindcss(dom, config, filePath) {
115
115
  if (node.name !== "style") return;
116
116
  const el = node;
117
117
  const attrs = el.attribs || {};
118
- const markerAttr = [
119
- "raw",
120
- "embed",
121
- "data-embed"
122
- ].find((attr) => attr in attrs);
123
- if (markerAttr) {
124
- delete el.attribs[markerAttr];
118
+ if ("raw" in attrs) {
119
+ delete el.attribs.raw;
125
120
  return;
126
121
  }
122
+ if ("embed" in attrs || "data-embed" in attrs) return;
127
123
  const rawContent = el.children.filter((child) => child.type === "text").map((child) => child.data).join("");
128
124
  if (!rawContent.trim()) return;
129
125
  styleTags.push({
@@ -1 +1 @@
1
- {"version":3,"file":"tailwindcss.mjs","names":["pruneVars"],"sources":["../../src/transformers/tailwindcss.ts"],"sourcesContent":["import postcss from 'postcss'\nimport tailwindcssPostcss from '@tailwindcss/postcss'\nimport customProperties from 'postcss-custom-properties'\nimport postcssCalc from 'postcss-calc'\nimport pruneVars from '../plugins/postcss/pruneVars.ts'\nimport safeParser from 'postcss-safe-parser'\nimport { transform } from 'lightningcss'\nimport { resolve, dirname, relative } from 'node:path'\nimport type { ChildNode, Element } from 'domhandler'\nimport { walk } from '../utils/ast/index.ts'\nimport { tailwindCleanup } from '../plugins/postcss/tailwindCleanup.ts'\nimport { mergeMediaQueries } from '../plugins/postcss/mergeMediaQueries.ts'\nimport type { MaizzleConfig } from '../types/config.ts'\n\nfunction createProcessor(config: MaizzleConfig) {\n return postcss([\n tailwindcssPostcss({\n base: config.css?.base,\n transformAssetUrls: false,\n optimize: false, // we run Lightning CSS manually\n }),\n customProperties({\n preserve: false,\n }),\n postcssCalc({}),\n pruneVars(),\n ])\n}\n\n/**\n * Decode HTML entities that Vue SSR encodes inside <style> tags.\n *\n * Vue's renderToString HTML-encodes quotes and other characters\n * inside <style> tags within templates, breaking CSS like\n * `@import \"@maizzle/tailwindcss\"` → `@import &quot;...&quot;`\n */\nfunction decodeEntities(str: string): string {\n return str\n .replace(/&quot;/g, '\"')\n .replace(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&#39;/g, \"'\")\n .replace(/&apos;/g, \"'\")\n}\n\n/**\n * Check if CSS content uses Tailwind features that require source scanning.\n *\n * Only CSS that imports Tailwind (or @maizzle/tailwindcss) needs @source\n * directives. Plain CSS without Tailwind imports doesn't need scanning\n * and would pass through @source directives unconsumed.\n */\nfunction usesTailwind(css: string): boolean {\n return /(@import\\s+[\"'](tailwindcss|@maizzle\\/tailwindcss)|@tailwind\\s)/.test(css)\n}\n\n/**\n * Lower modern CSS syntax using lightningcss.\n *\n * Targets IE 1 to maximize syntax lowering — converts modern features\n * like nesting, oklch(), color-mix(), @property, etc. into simple CSS\n * that email clients can understand.\n */\nfunction lowerSyntax(css: string): string {\n const result = transform({\n filename: 'email.css',\n code: Buffer.from(css),\n minify: false,\n targets: {\n ie: 4 << 5,\n },\n })\n\n return result.code.toString()\n}\n\n/**\n * Run cleanup and media query merging on the compiled CSS.\n *\n * Removes unwanted selectors (:host, :lang) and at-rules (@layer, @property),\n * then sorts and merges media queries.\n */\nasync function optimizeCss(css: string, config: MaizzleConfig): Promise<string> {\n const plugins: postcss.Plugin[] = [...tailwindCleanup(config)]\n\n const mediaPlugin = mergeMediaQueries(config)\n if (mediaPlugin) plugins.push(mediaPlugin)\n\n const result = await postcss(plugins).process(css, { from: undefined })\n\n return result.css\n}\n\n/**\n * Build @source directives for Tailwind CSS scanning.\n *\n * Configures two types of sources:\n * 1. Exclusions for output dir and user-configured paths\n * 2. Inline source with all class attribute values from the rendered DOM,\n * capturing classes from all components (built-in + user), dynamic\n * expressions, and the template itself — Tailwind's scanner handles\n * the actual class extraction from these raw values\n */\nfunction buildSourceDirectives(dom: ChildNode[], config: MaizzleConfig, fromDir: string): string {\n const directives: string[] = []\n\n // Exclude output dir and user-configured paths\n const excludePaths = [\n resolve(config.output?.path ?? 'dist'),\n ...(config.css?.exclude ?? []).map(p => resolve(p)),\n ]\n\n for (const p of excludePaths) {\n directives.push(`@source not \"${relative(fromDir, resolve(p))}\";`)\n }\n\n // Inline source: collect all class attribute values from the rendered DOM.\n // After Vue SSR, the DOM contains every class from every component\n // (built-in framework components, user components, dynamic bindings).\n // We pass these raw values to Tailwind's scanner via @source inline().\n const classes: string[] = []\n walk(dom, (n) => {\n const cls = (n as Element).attribs?.class\n if (cls) classes.push(cls)\n })\n\n if (classes.length) {\n directives.push(`@source inline(\"${classes.join(' ')}\");`)\n }\n\n return directives.join('\\n')\n}\n\n/**\n * Tailwind CSS transformer.\n *\n * Compiles CSS inside <style> tags in the DOM using\n * @tailwindcss/postcss, then lowers modern CSS syntax with lightningcss.\n *\n * Configures Tailwind sources to scan:\n * - Rendered class attributes (via `@source inline`) for all classes from all components\n * - User project files (via Tailwind's auto-detection from base/from path)\n *\n * User `@source` and `@source not directives` in style tags are preserved.\n * Source directives are only added to style tags that import Tailwind.\n *\n * Runs as the first transformer in the pipeline so that subsequent\n * transformers (inliner, purge, etc.) work with fully compiled CSS.\n */\nexport async function tailwindcss(dom: ChildNode[], config: MaizzleConfig, filePath?: string): Promise<ChildNode[]> {\n const styleTags: { node: Element; cssContent: string }[] = []\n\n walk(dom, (node) => {\n if ((node as Element).name !== 'style') return\n\n const el = node as Element\n const attrs = el.attribs || {}\n\n // Skip marked style tags, but remove the marker attribute first\n const markerAttr = ['raw', 'embed', 'data-embed'].find(attr => attr in attrs)\n if (markerAttr) {\n delete el.attribs[markerAttr]\n return\n }\n\n // Get text content from children and decode HTML entities\n const rawContent = el.children\n .filter(child => child.type === 'text')\n .map(child => (child as any).data)\n .join('')\n\n if (!rawContent.trim()) return\n\n styleTags.push({ node: el, cssContent: decodeEntities(rawContent) })\n })\n\n if (!styleTags.length) return dom\n\n const fromPath = filePath ?? resolve(process.cwd(), 'template.vue')\n const fromDir = dirname(fromPath)\n\n // Only compute source directives if at least one style tag uses Tailwind\n const hasTailwindStyles = styleTags.some(({ cssContent }) => usesTailwind(cssContent))\n const sourceDirectives = hasTailwindStyles\n ? buildSourceDirectives(dom, config, fromDir)\n : ''\n\n // Create processor once — reused for all style tags in this template\n const processor = createProcessor(config)\n\n for (let i = 0; i < styleTags.length; i++) {\n const { node, cssContent } = styleTags[i]\n\n // Only add source directives to style tags that import Tailwind —\n // plain CSS doesn't need them and @tailwindcss/postcss would leave\n // the directives unconsumed in the output\n const fullCss = usesTailwind(cssContent)\n ? `${cssContent}\\n${sourceDirectives}`\n : cssContent\n\n try {\n const result = await processor.process(\n fullCss,\n {\n from: `${fromPath}?style=${i}`,\n parser: safeParser,\n }\n )\n\n const lowered = lowerSyntax(result.css)\n const optimized = await optimizeCss(lowered, config)\n\n // Replace the style tag's children with the compiled CSS\n node.children = [{\n type: 'text',\n data: optimized,\n parent: node,\n } as any]\n } catch {\n // If CSS processing fails, still replace with decoded content\n // so HTML entities don't break the CSS\n node.children = [{\n type: 'text',\n data: cssContent,\n parent: node,\n } as any]\n }\n }\n\n return dom\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tailwindcss.mjs","names":["pruneVars"],"sources":["../../src/transformers/tailwindcss.ts"],"sourcesContent":["import postcss from 'postcss'\nimport tailwindcssPostcss from '@tailwindcss/postcss'\nimport customProperties from 'postcss-custom-properties'\nimport postcssCalc from 'postcss-calc'\nimport pruneVars from '../plugins/postcss/pruneVars.ts'\nimport safeParser from 'postcss-safe-parser'\nimport { transform } from 'lightningcss'\nimport { resolve, dirname, relative } from 'node:path'\nimport type { ChildNode, Element } from 'domhandler'\nimport { walk } from '../utils/ast/index.ts'\nimport { tailwindCleanup } from '../plugins/postcss/tailwindCleanup.ts'\nimport { mergeMediaQueries } from '../plugins/postcss/mergeMediaQueries.ts'\nimport type { MaizzleConfig } from '../types/config.ts'\n\nfunction createProcessor(config: MaizzleConfig) {\n return postcss([\n tailwindcssPostcss({\n base: config.css?.base,\n transformAssetUrls: false,\n optimize: false, // we run Lightning CSS manually\n }),\n customProperties({\n preserve: false,\n }),\n postcssCalc({}),\n pruneVars(),\n ])\n}\n\n/**\n * Decode HTML entities that Vue SSR encodes inside <style> tags.\n *\n * Vue's renderToString HTML-encodes quotes and other characters\n * inside <style> tags within templates, breaking CSS like\n * `@import \"@maizzle/tailwindcss\"` → `@import &quot;...&quot;`\n */\nfunction decodeEntities(str: string): string {\n return str\n .replace(/&quot;/g, '\"')\n .replace(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&#39;/g, \"'\")\n .replace(/&apos;/g, \"'\")\n}\n\n/**\n * Check if CSS content uses Tailwind features that require source scanning.\n *\n * Only CSS that imports Tailwind (or @maizzle/tailwindcss) needs @source\n * directives. Plain CSS without Tailwind imports doesn't need scanning\n * and would pass through @source directives unconsumed.\n */\nfunction usesTailwind(css: string): boolean {\n return /(@import\\s+[\"'](tailwindcss|@maizzle\\/tailwindcss)|@tailwind\\s)/.test(css)\n}\n\n/**\n * Lower modern CSS syntax using lightningcss.\n *\n * Targets IE 1 to maximize syntax lowering — converts modern features\n * like nesting, oklch(), color-mix(), @property, etc. into simple CSS\n * that email clients can understand.\n */\nfunction lowerSyntax(css: string): string {\n const result = transform({\n filename: 'email.css',\n code: Buffer.from(css),\n minify: false,\n targets: {\n ie: 4 << 5,\n },\n })\n\n return result.code.toString()\n}\n\n/**\n * Run cleanup and media query merging on the compiled CSS.\n *\n * Removes unwanted selectors (:host, :lang) and at-rules (@layer, @property),\n * then sorts and merges media queries.\n */\nasync function optimizeCss(css: string, config: MaizzleConfig): Promise<string> {\n const plugins: postcss.Plugin[] = [...tailwindCleanup(config)]\n\n const mediaPlugin = mergeMediaQueries(config)\n if (mediaPlugin) plugins.push(mediaPlugin)\n\n const result = await postcss(plugins).process(css, { from: undefined })\n\n return result.css\n}\n\n/**\n * Build @source directives for Tailwind CSS scanning.\n *\n * Configures two types of sources:\n * 1. Exclusions for output dir and user-configured paths\n * 2. Inline source with all class attribute values from the rendered DOM,\n * capturing classes from all components (built-in + user), dynamic\n * expressions, and the template itself — Tailwind's scanner handles\n * the actual class extraction from these raw values\n */\nfunction buildSourceDirectives(dom: ChildNode[], config: MaizzleConfig, fromDir: string): string {\n const directives: string[] = []\n\n // Exclude output dir and user-configured paths\n const excludePaths = [\n resolve(config.output?.path ?? 'dist'),\n ...(config.css?.exclude ?? []).map(p => resolve(p)),\n ]\n\n for (const p of excludePaths) {\n directives.push(`@source not \"${relative(fromDir, resolve(p))}\";`)\n }\n\n // Inline source: collect all class attribute values from the rendered DOM.\n // After Vue SSR, the DOM contains every class from every component\n // (built-in framework components, user components, dynamic bindings).\n // We pass these raw values to Tailwind's scanner via @source inline().\n const classes: string[] = []\n walk(dom, (n) => {\n const cls = (n as Element).attribs?.class\n if (cls) classes.push(cls)\n })\n\n if (classes.length) {\n directives.push(`@source inline(\"${classes.join(' ')}\");`)\n }\n\n return directives.join('\\n')\n}\n\n/**\n * Tailwind CSS transformer.\n *\n * Compiles CSS inside <style> tags in the DOM using\n * @tailwindcss/postcss, then lowers modern CSS syntax with lightningcss.\n *\n * Configures Tailwind sources to scan:\n * - Rendered class attributes (via `@source inline`) for all classes from all components\n * - User project files (via Tailwind's auto-detection from base/from path)\n *\n * User `@source` and `@source not directives` in style tags are preserved.\n * Source directives are only added to style tags that import Tailwind.\n *\n * Runs as the first transformer in the pipeline so that subsequent\n * transformers (inliner, purge, etc.) work with fully compiled CSS.\n */\nexport async function tailwindcss(dom: ChildNode[], config: MaizzleConfig, filePath?: string): Promise<ChildNode[]> {\n const styleTags: { node: Element; cssContent: string }[] = []\n\n walk(dom, (node) => {\n if ((node as Element).name !== 'style') return\n\n const el = node as Element\n const attrs = el.attribs || {}\n\n // Skip marked style tags\n // Remove 'raw' marker but preserve 'embed'/'data-embed' for Juice\n if ('raw' in attrs) {\n delete el.attribs.raw\n return\n }\n\n if ('embed' in attrs || 'data-embed' in attrs) {\n return\n }\n\n // Get text content from children and decode HTML entities\n const rawContent = el.children\n .filter(child => child.type === 'text')\n .map(child => (child as any).data)\n .join('')\n\n if (!rawContent.trim()) return\n\n styleTags.push({ node: el, cssContent: decodeEntities(rawContent) })\n })\n\n if (!styleTags.length) return dom\n\n const fromPath = filePath ?? resolve(process.cwd(), 'template.vue')\n const fromDir = dirname(fromPath)\n\n // Only compute source directives if at least one style tag uses Tailwind\n const hasTailwindStyles = styleTags.some(({ cssContent }) => usesTailwind(cssContent))\n const sourceDirectives = hasTailwindStyles\n ? buildSourceDirectives(dom, config, fromDir)\n : ''\n\n // Create processor once — reused for all style tags in this template\n const processor = createProcessor(config)\n\n for (let i = 0; i < styleTags.length; i++) {\n const { node, cssContent } = styleTags[i]\n\n // Only add source directives to style tags that import Tailwind —\n // plain CSS doesn't need them and @tailwindcss/postcss would leave\n // the directives unconsumed in the output\n const fullCss = usesTailwind(cssContent)\n ? `${cssContent}\\n${sourceDirectives}`\n : cssContent\n\n try {\n const result = await processor.process(\n fullCss,\n {\n from: `${fromPath}?style=${i}`,\n parser: safeParser,\n }\n )\n\n const lowered = lowerSyntax(result.css)\n const optimized = await optimizeCss(lowered, config)\n\n // Replace the style tag's children with the compiled CSS\n node.children = [{\n type: 'text',\n data: optimized,\n parent: node,\n } as any]\n } catch {\n // If CSS processing fails, still replace with decoded content\n // so HTML entities don't break the CSS\n node.children = [{\n type: 'text',\n data: cssContent,\n parent: node,\n } as any]\n }\n }\n\n return dom\n}\n"],"mappings":"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"}
@@ -1,6 +1,8 @@
1
1
  import { RemoveValue } from "../plugins/postcss/removeDeclarations.mjs";
2
+ import { Options } from "juice";
2
3
  import * as oxfmt from "oxfmt";
3
- import * as unplugin_vue_markdown_types0 from "unplugin-vue-markdown/types";
4
+ import * as shiki from "shiki";
5
+ import { Options as Options$1 } from "unplugin-vue-markdown/types";
4
6
 
5
7
  //#region src/types/config.d.ts
6
8
  interface UrlQueryOptions {
@@ -91,7 +93,7 @@ interface CssConfig {
91
93
  * }
92
94
  * }
93
95
  */
94
- inline?: boolean | {
96
+ inline?: boolean | Options & {
95
97
  /**
96
98
  * Convert HTML attributes like `width`, `height`, `bgcolor`, and `valign`
97
99
  * to inline CSS styles. Set to `true` for all, or pass an array of attribute names.
@@ -99,18 +101,6 @@ interface CssConfig {
99
101
  * @default false
100
102
  */
101
103
  attributeToStyle?: boolean | string[];
102
- /**
103
- * Remove `<style>` tags after inlining.
104
- *
105
- * @default false
106
- */
107
- removeStyleTags?: boolean;
108
- /**
109
- * Remove selectors from `<style>` tags after they have been inlined.
110
- *
111
- * @default true
112
- */
113
- removeInlinedSelectors?: boolean;
114
104
  /**
115
105
  * Convert `0px`, `0em` etc. to `0` in inline styles.
116
106
  *
@@ -119,12 +109,14 @@ interface CssConfig {
119
109
  preferUnitlessValues?: boolean;
120
110
  /**
121
111
  * CSS selectors to preserve in `<style>` tags, even after inlining.
112
+ * Mapped to Juice's `preservedSelectors` option.
122
113
  *
123
114
  * @default []
124
115
  */
125
116
  safelist?: string[];
126
117
  /**
127
118
  * Duplicate CSS properties to HTML attributes.
119
+ * Mapped to Juice's static `styleToAttribute` property.
128
120
  *
129
121
  * @default {}
130
122
  *
@@ -134,38 +126,30 @@ interface CssConfig {
134
126
  * }
135
127
  */
136
128
  styleToAttribute?: Record<string, string>;
137
- /**
138
- * Add `width` HTML attributes based on inline CSS width values.
139
- *
140
- * @default true
141
- */
142
- applyWidthAttributes?: boolean;
143
- /**
144
- * Add `height` HTML attributes based on inline CSS height values.
145
- *
146
- * @default true
147
- */
148
- applyHeightAttributes?: boolean;
149
129
  /**
150
130
  * Elements that can receive `width` HTML attributes.
131
+ * Mapped to Juice's static `widthElements` property.
151
132
  *
152
133
  * @default ['img', 'video']
153
134
  */
154
135
  widthElements?: string[];
155
136
  /**
156
137
  * Elements that can receive `height` HTML attributes.
138
+ * Mapped to Juice's static `heightElements` property.
157
139
  *
158
140
  * @default ['img', 'video']
159
141
  */
160
142
  heightElements?: string[];
161
143
  /**
162
144
  * CSS properties to exclude from inlining.
145
+ * Mapped to Juice's static `excludedProperties` property.
163
146
  *
164
147
  * @default []
165
148
  */
166
149
  excludedProperties?: string[];
167
150
  /**
168
151
  * Template language code blocks to preserve during inlining.
152
+ * Mapped to Juice's static `codeBlocks` property.
169
153
  *
170
154
  * @default { EJS: { start: '<%', end: '%>' }, HBS: { start: '\{\{', end: '}}' } }
171
155
  */
@@ -175,6 +159,7 @@ interface CssConfig {
175
159
  }>;
176
160
  /**
177
161
  * Additional CSS string to inline alongside `<style>` tag contents.
162
+ * Mapped to Juice's `extraCss` option.
178
163
  */
179
164
  customCSS?: string;
180
165
  };
@@ -336,6 +321,14 @@ interface HtmlConfig {
336
321
  }
337
322
  type FilterFunction = (str: string, value: string) => string;
338
323
  type FiltersConfig = false | Record<string, FilterFunction>;
324
+ interface MarkdownConfig extends Options$1 {
325
+ /**
326
+ * The shiki theme to use for syntax highlighting in Markdown fenced code blocks.
327
+ *
328
+ * @default 'github-light'
329
+ */
330
+ shikiTheme?: shiki.BundledTheme;
331
+ }
339
332
  interface MaizzleConfig {
340
333
  /**
341
334
  * Root directory for the Maizzle email project.
@@ -352,8 +345,8 @@ interface MaizzleConfig {
352
345
  * })
353
346
  */
354
347
  root?: string;
355
- /** Options passed to `unplugin-vue-markdown` for Markdown template support. */
356
- markdown?: unplugin_vue_markdown_types0.Options;
348
+ /** Options for Markdown template support, extending `unplugin-vue-markdown`. */
349
+ markdown?: MarkdownConfig;
357
350
  /**
358
351
  * Glob patterns for email template files to process.
359
352
  *
@@ -431,6 +424,31 @@ interface MaizzleConfig {
431
424
  * }
432
425
  */
433
426
  watch?: string[];
427
+ /**
428
+ * Email sending configuration for the "Send test" feature in the dev UI.
429
+ *
430
+ * When not configured, falls back to Ethereal (free fake SMTP — emails
431
+ * are captured and viewable via a URL, never actually delivered).
432
+ *
433
+ * @example
434
+ * server: {
435
+ * email: {
436
+ * to: ['test@example.com'],
437
+ * from: 'dev@maizzle.test',
438
+ * transport: {
439
+ * host: 'smtp.mailtrap.io',
440
+ * port: 587,
441
+ * auth: { user: '...', pass: '...' },
442
+ * },
443
+ * },
444
+ * }
445
+ */
446
+ email?: {
447
+ /** Default recipient(s). */to?: string | string[]; /** Sender address. @default 'Maizzle <maizzle@ethereal.email>' */
448
+ from?: string; /** Default subject line. */
449
+ subject?: string; /** Nodemailer transport options (SMTP, SES, etc.). Omit to use Ethereal. */
450
+ transport?: Record<string, unknown>;
451
+ };
434
452
  };
435
453
  /** Tailwind CSS and email CSS optimization settings. */
436
454
  css?: CssConfig;
@@ -504,5 +522,5 @@ interface MaizzleConfig {
504
522
  [key: string]: any;
505
523
  }
506
524
  //#endregion
507
- export { AttributesConfig, CssConfig, EntitiesConfig, FilterFunction, FiltersConfig, HtmlConfig, MaizzleConfig, PostcssConfig, UrlConfig, UrlQuery, UrlQueryOptions };
525
+ export { AttributesConfig, CssConfig, EntitiesConfig, FilterFunction, FiltersConfig, HtmlConfig, MaizzleConfig, MarkdownConfig, PostcssConfig, UrlConfig, UrlQuery, UrlQueryOptions };
508
526
  //# sourceMappingURL=config.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.mts","names":[],"sources":["../../src/types/config.ts"],"mappings":"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"}
1
+ {"version":3,"file":"config.d.mts","names":[],"sources":["../../src/types/config.ts"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import { AttributesConfig, CssConfig, EntitiesConfig, FilterFunction, FiltersConfig, HtmlConfig, MaizzleConfig, PostcssConfig, UrlConfig, UrlQuery, UrlQueryOptions } from "./config.mjs";
2
- export { type AttributesConfig, type CssConfig, type EntitiesConfig, type FilterFunction, type FiltersConfig, type HtmlConfig, type MaizzleConfig, type PostcssConfig, type UrlConfig, type UrlQuery, type UrlQueryOptions };
1
+ import { AttributesConfig, CssConfig, EntitiesConfig, FilterFunction, FiltersConfig, HtmlConfig, MaizzleConfig, MarkdownConfig, PostcssConfig, UrlConfig, UrlQuery, UrlQueryOptions } from "./config.mjs";
2
+ export { type AttributesConfig, type CssConfig, type EntitiesConfig, type FilterFunction, type FiltersConfig, type HtmlConfig, type MaizzleConfig, type MarkdownConfig, type PostcssConfig, type UrlConfig, type UrlQuery, type UrlQueryOptions };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maizzle/framework",
3
- "version": "6.0.0-rc.6",
3
+ "version": "6.0.0-rc.8",
4
4
  "description": "Maizzle is a framework that helps you quickly build HTML emails with Tailwind CSS.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -33,7 +33,6 @@
33
33
  "maizzle"
34
34
  ],
35
35
  "dependencies": {
36
- "maizzle": "latest",
37
36
  "@tailwindcss/postcss": "^4.1.18",
38
37
  "@tailwindcss/vite": "^4.1.18",
39
38
  "@unhead/vue": "^2.1.4",
@@ -49,11 +48,15 @@
49
48
  "domhandler": "^5.0.3",
50
49
  "email-comb": "^7.1.3",
51
50
  "html-crush": "^6.1.3",
51
+ "html-to-image": "^1.11.13",
52
52
  "htmlparser2": "^10.1.0",
53
53
  "is-url-superb": "^6.1.0",
54
54
  "jiti": "^2.6.1",
55
55
  "juice": "^11.1.1",
56
56
  "lucide-vue-next": "^1.0.0",
57
+ "maizzle": "latest",
58
+ "markdown-exit": "^1.0.0-beta.9",
59
+ "nodemailer": "^8.0.5",
57
60
  "ora": "^9.3.0",
58
61
  "oxfmt": "^0.35.0",
59
62
  "postcss": "^8.5.6",
@@ -73,7 +76,7 @@
73
76
  "typescript": "^5.9.3",
74
77
  "unplugin-auto-import": "^21.0.0",
75
78
  "unplugin-vue-components": "^31.0.0",
76
- "unplugin-vue-markdown": "^29.2.0",
79
+ "unplugin-vue-markdown": "^30.0.0",
77
80
  "vite": "^7.3.1",
78
81
  "vue": "^3.5.28",
79
82
  "vue-router": "^5.0.2"
@@ -81,6 +84,7 @@
81
84
  "devDependencies": {
82
85
  "@types/js-beautify": "^1.14.3",
83
86
  "@types/node": "^25.2.3",
87
+ "@types/nodemailer": "^8.0.0",
84
88
  "@types/postcss-safe-parser": "^5.0.4",
85
89
  "@vue/test-utils": "^2.4.6",
86
90
  "happy-dom": "^20.6.3",
@@ -1,30 +0,0 @@
1
- <script setup lang="ts">
2
- import type { SplitterResizeHandleEmits, SplitterResizeHandleProps } from "reka-ui"
3
- import type { HTMLAttributes } from "vue"
4
- import { reactiveOmit } from "@vueuse/core"
5
- import { GripVertical } from "lucide-vue-next"
6
- import { SplitterResizeHandle, useForwardPropsEmits } from "reka-ui"
7
- import { cn } from "@/lib/utils"
8
-
9
- const props = defineProps<SplitterResizeHandleProps & { class?: HTMLAttributes["class"], withHandle?: boolean }>()
10
- const emits = defineEmits<SplitterResizeHandleEmits>()
11
-
12
- const delegatedProps = reactiveOmit(props, "class", "withHandle")
13
- const forwarded = useForwardPropsEmits(delegatedProps, emits)
14
- </script>
15
-
16
- <template>
17
- <SplitterResizeHandle
18
- data-slot="resizable-handle"
19
- v-bind="forwarded"
20
- :class="cn('bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[orientation=vertical]:h-px data-[orientation=vertical]:w-full data-[orientation=vertical]:after:left-0 data-[orientation=vertical]:after:h-1 data-[orientation=vertical]:after:w-full data-[orientation=vertical]:after:-translate-y-1/2 data-[orientation=vertical]:after:translate-x-0 [&[data-orientation=vertical]>div]:rotate-90', props.class)"
21
- >
22
- <template v-if="props.withHandle">
23
- <div class="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
24
- <slot>
25
- <GripVertical class="size-2.5" />
26
- </slot>
27
- </div>
28
- </template>
29
- </SplitterResizeHandle>
30
- </template>