@maizzle/framework 6.0.0-rc.2 → 6.0.0-rc.21

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 (551) hide show
  1. package/README.md +3 -3
  2. package/bin/maizzle.mjs +1 -1
  3. package/dist/build.d.ts +20 -0
  4. package/dist/build.d.ts.map +1 -0
  5. package/dist/build.js +163 -0
  6. package/dist/build.js.map +1 -0
  7. package/dist/components/Body.vue +128 -0
  8. package/dist/components/Button.vue +148 -52
  9. package/dist/components/CodeBlock.vue +69 -0
  10. package/dist/components/CodeInline.vue +49 -0
  11. package/dist/components/Column.vue +108 -0
  12. package/dist/components/Container.vue +123 -0
  13. package/dist/components/Font.vue +96 -0
  14. package/dist/components/Head.vue +30 -0
  15. package/dist/components/Heading.vue +28 -0
  16. package/dist/components/Hr.vue +33 -0
  17. package/dist/components/Html.vue +137 -0
  18. package/dist/components/Img.vue +70 -0
  19. package/dist/components/Layout.vue +143 -0
  20. package/dist/components/Link.vue +26 -0
  21. package/dist/components/Markdown.vue +89 -0
  22. package/dist/components/MarkdownLayout.vue +39 -0
  23. package/dist/components/NotPlaintext.vue +14 -0
  24. package/dist/components/Outlook.vue +74 -11
  25. package/dist/components/OutlookBg.vue +241 -0
  26. package/dist/components/Overlap.vue +156 -0
  27. package/dist/components/Plaintext.vue +14 -0
  28. package/dist/components/Preheader.vue +15 -0
  29. package/dist/components/QrCode.vue +157 -0
  30. package/dist/components/Raw.vue +28 -0
  31. package/dist/components/Row.vue +184 -0
  32. package/dist/components/Section.vue +124 -0
  33. package/dist/components/Spacer.vue +70 -21
  34. package/dist/components/Tailwind.vue +43 -0
  35. package/dist/components/Text.vue +29 -0
  36. package/dist/components/utils.d.ts +28 -0
  37. package/dist/components/utils.d.ts.map +1 -0
  38. package/dist/components/utils.js +50 -0
  39. package/dist/components/utils.js.map +1 -0
  40. package/dist/components/utils.ts +51 -0
  41. package/dist/composables/{defineConfig.d.mts → defineConfig.d.ts} +2 -2
  42. package/dist/composables/defineConfig.d.ts.map +1 -0
  43. package/dist/composables/{defineConfig.mjs → defineConfig.js} +4 -5
  44. package/dist/composables/defineConfig.js.map +1 -0
  45. package/dist/composables/renderContext.d.ts +37 -0
  46. package/dist/composables/renderContext.d.ts.map +1 -0
  47. package/dist/composables/{renderContext.mjs → renderContext.js} +2 -2
  48. package/dist/composables/renderContext.js.map +1 -0
  49. package/dist/composables/useBaseUrl.d.ts +19 -0
  50. package/dist/composables/useBaseUrl.d.ts.map +1 -0
  51. package/dist/composables/useBaseUrl.js +26 -0
  52. package/dist/composables/useBaseUrl.js.map +1 -0
  53. package/dist/composables/{useConfig.d.mts → useConfig.d.ts} +2 -2
  54. package/dist/composables/useConfig.d.ts.map +1 -0
  55. package/dist/composables/{useConfig.mjs → useConfig.js} +2 -3
  56. package/dist/composables/useConfig.js.map +1 -0
  57. package/dist/composables/useCurrentTemplate.d.ts +31 -0
  58. package/dist/composables/useCurrentTemplate.d.ts.map +1 -0
  59. package/dist/composables/useCurrentTemplate.js +35 -0
  60. package/dist/composables/useCurrentTemplate.js.map +1 -0
  61. package/dist/composables/{useDoctype.d.mts → useDoctype.d.ts} +1 -1
  62. package/dist/composables/useDoctype.d.ts.map +1 -0
  63. package/dist/composables/{useDoctype.mjs → useDoctype.js} +3 -4
  64. package/dist/composables/useDoctype.js.map +1 -0
  65. package/dist/composables/{useEvent.d.mts → useEvent.d.ts} +3 -3
  66. package/dist/composables/useEvent.d.ts.map +1 -0
  67. package/dist/composables/{useEvent.mjs → useEvent.js} +4 -5
  68. package/dist/composables/useEvent.js.map +1 -0
  69. package/dist/composables/useFont.d.ts +50 -0
  70. package/dist/composables/useFont.d.ts.map +1 -0
  71. package/dist/composables/useFont.js +92 -0
  72. package/dist/composables/useFont.js.map +1 -0
  73. package/dist/composables/useOutlookFallback.d.ts +21 -0
  74. package/dist/composables/useOutlookFallback.d.ts.map +1 -0
  75. package/dist/composables/useOutlookFallback.js +29 -0
  76. package/dist/composables/useOutlookFallback.js.map +1 -0
  77. package/dist/composables/{usePlaintext.d.mts → usePlaintext.d.ts} +3 -1
  78. package/dist/composables/usePlaintext.d.ts.map +1 -0
  79. package/dist/composables/{usePlaintext.mjs → usePlaintext.js} +4 -4
  80. package/dist/composables/usePlaintext.js.map +1 -0
  81. package/dist/composables/usePreheader.d.ts +24 -0
  82. package/dist/composables/usePreheader.d.ts.map +1 -0
  83. package/dist/composables/usePreheader.js +28 -0
  84. package/dist/composables/usePreheader.js.map +1 -0
  85. package/dist/composables/useTransformers.d.ts +34 -0
  86. package/dist/composables/useTransformers.d.ts.map +1 -0
  87. package/dist/composables/useTransformers.js +48 -0
  88. package/dist/composables/useTransformers.js.map +1 -0
  89. package/dist/composables/useUrlQuery.d.ts +19 -0
  90. package/dist/composables/useUrlQuery.d.ts.map +1 -0
  91. package/dist/composables/useUrlQuery.js +26 -0
  92. package/dist/composables/useUrlQuery.js.map +1 -0
  93. package/dist/config/{defaults.d.mts → defaults.d.ts} +2 -2
  94. package/dist/config/defaults.d.ts.map +1 -0
  95. package/dist/config/{defaults.mjs → defaults.js} +10 -6
  96. package/dist/config/defaults.js.map +1 -0
  97. package/dist/config/{index.d.mts → index.d.ts} +4 -4
  98. package/dist/config/index.d.ts.map +1 -0
  99. package/dist/config/{index.mjs → index.js} +12 -10
  100. package/dist/config/index.js.map +1 -0
  101. package/dist/events/{index.d.mts → index.d.ts} +30 -12
  102. package/dist/events/index.d.ts.map +1 -0
  103. package/dist/events/{index.mjs → index.js} +26 -13
  104. package/dist/events/index.js.map +1 -0
  105. package/dist/index.d.ts +39 -0
  106. package/dist/index.js +38 -0
  107. package/dist/{plaintext.d.mts → plaintext.d.ts} +1 -1
  108. package/dist/plaintext.d.ts.map +1 -0
  109. package/dist/{plaintext.mjs → plaintext.js} +4 -5
  110. package/dist/plaintext.js.map +1 -0
  111. package/dist/{plugin.d.mts → plugin.d.ts} +2 -2
  112. package/dist/plugin.d.ts.map +1 -0
  113. package/dist/plugin.js +57 -0
  114. package/dist/plugin.js.map +1 -0
  115. package/dist/plugins/postcss/{mergeMediaQueries.d.mts → mergeMediaQueries.d.ts} +2 -2
  116. package/dist/plugins/postcss/mergeMediaQueries.d.ts.map +1 -0
  117. package/dist/plugins/postcss/{mergeMediaQueries.mjs → mergeMediaQueries.js} +2 -3
  118. package/dist/plugins/postcss/mergeMediaQueries.js.map +1 -0
  119. package/dist/plugins/postcss/{pruneVars.d.mts → pruneVars.d.ts} +1 -1
  120. package/dist/plugins/postcss/pruneVars.d.ts.map +1 -0
  121. package/dist/plugins/postcss/{pruneVars.mjs → pruneVars.js} +2 -2
  122. package/dist/plugins/postcss/pruneVars.js.map +1 -0
  123. package/dist/plugins/postcss/quoteFontFamilies.d.ts +13 -0
  124. package/dist/plugins/postcss/quoteFontFamilies.d.ts.map +1 -0
  125. package/dist/plugins/postcss/quoteFontFamilies.js +84 -0
  126. package/dist/plugins/postcss/quoteFontFamilies.js.map +1 -0
  127. package/dist/plugins/postcss/{removeDeclarations.d.mts → removeDeclarations.d.ts} +1 -1
  128. package/dist/plugins/postcss/removeDeclarations.d.ts.map +1 -0
  129. package/dist/plugins/postcss/{removeDeclarations.mjs → removeDeclarations.js} +2 -2
  130. package/dist/plugins/postcss/removeDeclarations.js.map +1 -0
  131. package/dist/plugins/postcss/resolveMaizzleImports.d.ts +16 -0
  132. package/dist/plugins/postcss/resolveMaizzleImports.d.ts.map +1 -0
  133. package/dist/plugins/postcss/resolveMaizzleImports.js +39 -0
  134. package/dist/plugins/postcss/resolveMaizzleImports.js.map +1 -0
  135. package/dist/plugins/postcss/resolveProps.d.ts +8 -0
  136. package/dist/plugins/postcss/resolveProps.d.ts.map +1 -0
  137. package/dist/plugins/postcss/resolveProps.js +144 -0
  138. package/dist/plugins/postcss/resolveProps.js.map +1 -0
  139. package/dist/plugins/postcss/{tailwindCleanup.d.mts → tailwindCleanup.d.ts} +2 -2
  140. package/dist/plugins/postcss/tailwindCleanup.d.ts.map +1 -0
  141. package/dist/plugins/postcss/tailwindCleanup.js +68 -0
  142. package/dist/plugins/postcss/tailwindCleanup.js.map +1 -0
  143. package/dist/prepare.d.ts +17 -0
  144. package/dist/prepare.d.ts.map +1 -0
  145. package/dist/prepare.js +44 -0
  146. package/dist/prepare.js.map +1 -0
  147. package/dist/render/active.d.ts +8 -0
  148. package/dist/render/active.d.ts.map +1 -0
  149. package/dist/render/active.js +12 -0
  150. package/dist/render/active.js.map +1 -0
  151. package/dist/render/{createRenderer.d.mts → createRenderer.d.ts} +15 -7
  152. package/dist/render/createRenderer.d.ts.map +1 -0
  153. package/dist/render/createRenderer.js +320 -0
  154. package/dist/render/createRenderer.js.map +1 -0
  155. package/dist/render/index.d.ts +18 -0
  156. package/dist/render/index.d.ts.map +1 -0
  157. package/dist/render/index.js +53 -0
  158. package/dist/render/index.js.map +1 -0
  159. package/dist/render/injectFonts.d.ts +15 -0
  160. package/dist/render/injectFonts.d.ts.map +1 -0
  161. package/dist/render/injectFonts.js +45 -0
  162. package/dist/render/injectFonts.js.map +1 -0
  163. package/dist/render/plugins/codeBlockExtract.d.ts +14 -0
  164. package/dist/render/plugins/codeBlockExtract.d.ts.map +1 -0
  165. package/dist/render/plugins/codeBlockExtract.js +34 -0
  166. package/dist/render/plugins/codeBlockExtract.js.map +1 -0
  167. package/dist/render/plugins/markdownExtract.d.ts +12 -0
  168. package/dist/render/plugins/markdownExtract.d.ts.map +1 -0
  169. package/dist/render/plugins/markdownExtract.js +49 -0
  170. package/dist/render/plugins/markdownExtract.js.map +1 -0
  171. package/dist/render/plugins/rawExtract.d.ts +14 -0
  172. package/dist/render/plugins/rawExtract.d.ts.map +1 -0
  173. package/dist/render/plugins/rawExtract.js +34 -0
  174. package/dist/render/plugins/rawExtract.js.map +1 -0
  175. package/dist/render/plugins/rowSourceLocation.d.ts +18 -0
  176. package/dist/render/plugins/rowSourceLocation.d.ts.map +1 -0
  177. package/dist/render/plugins/rowSourceLocation.js +45 -0
  178. package/dist/render/plugins/rowSourceLocation.js.map +1 -0
  179. package/dist/{serve.d.mts → serve.d.ts} +4 -2
  180. package/dist/serve.d.ts.map +1 -0
  181. package/dist/{serve.mjs → serve.js} +203 -78
  182. package/dist/serve.js.map +1 -0
  183. package/dist/server/compatibility.d.ts +59 -0
  184. package/dist/server/compatibility.d.ts.map +1 -0
  185. package/dist/server/compatibility.js +911 -0
  186. package/dist/server/compatibility.js.map +1 -0
  187. package/dist/server/email.d.ts +17 -0
  188. package/dist/server/email.d.ts.map +1 -0
  189. package/dist/server/email.js +40 -0
  190. package/dist/server/email.js.map +1 -0
  191. package/dist/server/linter.d.ts +20 -0
  192. package/dist/server/linter.d.ts.map +1 -0
  193. package/dist/server/linter.js +339 -0
  194. package/dist/server/linter.js.map +1 -0
  195. package/dist/server/sfc-utils.d.ts +21 -0
  196. package/dist/server/sfc-utils.d.ts.map +1 -0
  197. package/dist/server/sfc-utils.js +198 -0
  198. package/dist/server/sfc-utils.js.map +1 -0
  199. package/dist/server/ui/App.vue +253 -77
  200. package/dist/server/ui/components/SidebarClose.vue +12 -0
  201. package/dist/server/ui/components/ui/checkbox/Checkbox.vue +35 -0
  202. package/dist/server/ui/components/ui/checkbox/index.ts +1 -0
  203. package/dist/server/ui/components/ui/command/Command.vue +5 -1
  204. package/dist/server/ui/components/ui/command/CommandDialog.vue +1 -1
  205. package/dist/server/ui/components/ui/command/CommandInput.vue +19 -1
  206. package/dist/server/ui/components/ui/command/CommandItem.vue +1 -1
  207. package/dist/server/ui/components/ui/command/CommandList.vue +1 -1
  208. package/dist/server/ui/components/ui/command/CommandShortcut.vue +1 -1
  209. package/dist/server/ui/components/ui/dialog/DialogOverlay.vue +9 -1
  210. package/dist/server/ui/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
  211. package/dist/server/ui/components/ui/input/Input.vue +1 -1
  212. package/dist/server/ui/components/ui/scroll-area/ScrollBar.vue +1 -1
  213. package/dist/server/ui/components/ui/sheet/SheetContent.vue +1 -1
  214. package/dist/server/ui/components/ui/sheet/SheetOverlay.vue +9 -1
  215. package/dist/server/ui/components/ui/sidebar/Sidebar.vue +8 -1
  216. package/dist/server/ui/components/ui/sidebar/SidebarProvider.vue +1 -1
  217. package/dist/server/ui/components/ui/sidebar/SidebarTrigger.vue +5 -4
  218. package/dist/server/ui/components/ui/tags-input/TagsInput.vue +26 -0
  219. package/dist/server/ui/components/ui/tags-input/TagsInputInput.vue +17 -0
  220. package/dist/server/ui/components/ui/tags-input/TagsInputItem.vue +19 -0
  221. package/dist/server/ui/components/ui/tags-input/TagsInputItemDelete.vue +22 -0
  222. package/dist/server/ui/components/ui/tags-input/TagsInputItemText.vue +17 -0
  223. package/dist/server/ui/components/ui/tags-input/index.ts +5 -0
  224. package/dist/server/ui/components/ui/toggle/index.ts +3 -3
  225. package/dist/server/ui/components/ui/toggle-group/ToggleGroup.vue +1 -1
  226. package/dist/server/ui/components/ui/toggle-group/ToggleGroupItem.vue +2 -2
  227. package/dist/server/ui/lib/emulated-dark-mode.ts +131 -0
  228. package/dist/server/ui/main.css +20 -20
  229. package/dist/server/ui/pages/Home.vue +12 -5
  230. package/dist/server/ui/pages/Preview.vue +716 -276
  231. package/dist/tests/render/_helpers.d.ts +6 -0
  232. package/dist/tests/render/_helpers.d.ts.map +1 -0
  233. package/dist/tests/render/_helpers.js +16 -0
  234. package/dist/tests/render/_helpers.js.map +1 -0
  235. package/dist/transformers/{addAttributes.d.mts → addAttributes.d.ts} +2 -2
  236. package/dist/transformers/addAttributes.d.ts.map +1 -0
  237. package/dist/transformers/{addAttributes.mjs → addAttributes.js} +16 -13
  238. package/dist/transformers/addAttributes.js.map +1 -0
  239. package/dist/transformers/attributeToStyle.d.ts +38 -0
  240. package/dist/transformers/attributeToStyle.d.ts.map +1 -0
  241. package/dist/transformers/attributeToStyle.js +94 -0
  242. package/dist/transformers/attributeToStyle.js.map +1 -0
  243. package/dist/transformers/base.d.ts +71 -0
  244. package/dist/transformers/base.d.ts.map +1 -0
  245. package/dist/transformers/{base.mjs → base.js} +56 -30
  246. package/dist/transformers/base.js.map +1 -0
  247. package/dist/transformers/columnWidth.d.ts +31 -0
  248. package/dist/transformers/columnWidth.d.ts.map +1 -0
  249. package/dist/transformers/columnWidth.js +546 -0
  250. package/dist/transformers/columnWidth.js.map +1 -0
  251. package/dist/transformers/entities.d.ts +37 -0
  252. package/dist/transformers/entities.d.ts.map +1 -0
  253. package/dist/transformers/entities.js +73 -0
  254. package/dist/transformers/entities.js.map +1 -0
  255. package/dist/transformers/filters/defaults.d.ts +6 -0
  256. package/dist/transformers/filters/defaults.d.ts.map +1 -0
  257. package/dist/transformers/filters/defaults.js +78 -0
  258. package/dist/transformers/filters/defaults.js.map +1 -0
  259. package/dist/transformers/filters/index.d.ts +43 -0
  260. package/dist/transformers/filters/index.d.ts.map +1 -0
  261. package/dist/transformers/filters/index.js +89 -0
  262. package/dist/transformers/filters/index.js.map +1 -0
  263. package/dist/transformers/format.d.ts +22 -0
  264. package/dist/transformers/format.d.ts.map +1 -0
  265. package/dist/transformers/format.js +30 -0
  266. package/dist/transformers/format.js.map +1 -0
  267. package/dist/transformers/{index.d.mts → index.d.ts} +14 -11
  268. package/dist/transformers/index.d.ts.map +1 -0
  269. package/dist/transformers/index.js +133 -0
  270. package/dist/transformers/index.js.map +1 -0
  271. package/dist/transformers/inlineCss.d.ts +84 -0
  272. package/dist/transformers/inlineCss.d.ts.map +1 -0
  273. package/dist/transformers/inlineCss.js +91 -0
  274. package/dist/transformers/inlineCss.js.map +1 -0
  275. package/dist/transformers/inlineLink.d.ts +35 -0
  276. package/dist/transformers/inlineLink.d.ts.map +1 -0
  277. package/dist/transformers/{inlineLink.mjs → inlineLink.js} +34 -10
  278. package/dist/transformers/inlineLink.js.map +1 -0
  279. package/dist/transformers/minify.d.ts +21 -0
  280. package/dist/transformers/minify.d.ts.map +1 -0
  281. package/dist/transformers/minify.js +25 -0
  282. package/dist/transformers/minify.js.map +1 -0
  283. package/dist/transformers/msoPlaceholders.d.ts +28 -0
  284. package/dist/transformers/msoPlaceholders.d.ts.map +1 -0
  285. package/dist/transformers/msoPlaceholders.js +88 -0
  286. package/dist/transformers/msoPlaceholders.js.map +1 -0
  287. package/dist/transformers/purgeCss.d.ts +43 -0
  288. package/dist/transformers/purgeCss.d.ts.map +1 -0
  289. package/dist/transformers/purgeCss.js +181 -0
  290. package/dist/transformers/purgeCss.js.map +1 -0
  291. package/dist/transformers/removeAttributes.d.ts +54 -0
  292. package/dist/transformers/removeAttributes.d.ts.map +1 -0
  293. package/dist/transformers/removeAttributes.js +70 -0
  294. package/dist/transformers/removeAttributes.js.map +1 -0
  295. package/dist/transformers/{replaceStrings.d.mts → replaceStrings.d.ts} +2 -2
  296. package/dist/transformers/replaceStrings.d.ts.map +1 -0
  297. package/dist/transformers/{replaceStrings.mjs → replaceStrings.js} +2 -2
  298. package/dist/transformers/replaceStrings.js.map +1 -0
  299. package/dist/transformers/{safeClassNames.d.mts → safeClassNames.d.ts} +2 -2
  300. package/dist/transformers/safeClassNames.d.ts.map +1 -0
  301. package/dist/transformers/{safeClassNames.mjs → safeClassNames.js} +4 -5
  302. package/dist/transformers/safeClassNames.js.map +1 -0
  303. package/dist/transformers/shorthandCss.d.ts +47 -0
  304. package/dist/transformers/shorthandCss.d.ts.map +1 -0
  305. package/dist/transformers/shorthandCss.js +61 -0
  306. package/dist/transformers/shorthandCss.js.map +1 -0
  307. package/dist/transformers/sixHex.d.ts +25 -0
  308. package/dist/transformers/sixHex.d.ts.map +1 -0
  309. package/dist/transformers/sixHex.js +42 -0
  310. package/dist/transformers/sixHex.js.map +1 -0
  311. package/dist/transformers/tailwindComponent.d.ts +16 -0
  312. package/dist/transformers/tailwindComponent.d.ts.map +1 -0
  313. package/dist/transformers/tailwindComponent.js +92 -0
  314. package/dist/transformers/tailwindComponent.js.map +1 -0
  315. package/dist/transformers/{tailwindcss.d.mts → tailwindcss.d.ts} +8 -4
  316. package/dist/transformers/tailwindcss.d.ts.map +1 -0
  317. package/dist/transformers/tailwindcss.js +97 -0
  318. package/dist/transformers/tailwindcss.js.map +1 -0
  319. package/dist/transformers/urlQuery.d.ts +36 -0
  320. package/dist/transformers/urlQuery.d.ts.map +1 -0
  321. package/dist/transformers/urlQuery.js +77 -0
  322. package/dist/transformers/urlQuery.js.map +1 -0
  323. package/dist/types/config.d.ts +737 -0
  324. package/dist/types/config.d.ts.map +1 -0
  325. package/dist/types/config.js +1 -0
  326. package/dist/types/index.d.ts +2 -0
  327. package/dist/types/index.js +1 -0
  328. package/dist/utils/ast/index.d.ts +4 -0
  329. package/dist/utils/ast/index.js +4 -0
  330. package/dist/utils/ast/{parser.d.mts → parser.d.ts} +1 -1
  331. package/dist/utils/ast/parser.d.ts.map +1 -0
  332. package/dist/utils/ast/{parser.mjs → parser.js} +2 -3
  333. package/dist/utils/ast/parser.js.map +1 -0
  334. package/dist/utils/ast/serializer.d.ts +8 -0
  335. package/dist/utils/ast/serializer.d.ts.map +1 -0
  336. package/dist/utils/ast/serializer.js +36 -0
  337. package/dist/utils/ast/serializer.js.map +1 -0
  338. package/dist/utils/ast/{walker.d.mts → walker.d.ts} +1 -1
  339. package/dist/utils/ast/walker.d.ts.map +1 -0
  340. package/dist/utils/ast/{walker.mjs → walker.js} +2 -2
  341. package/dist/utils/ast/walker.js.map +1 -0
  342. package/dist/utils/compileTailwindCss.d.ts +16 -0
  343. package/dist/utils/compileTailwindCss.d.ts.map +1 -0
  344. package/dist/utils/compileTailwindCss.js +54 -0
  345. package/dist/utils/compileTailwindCss.js.map +1 -0
  346. package/dist/utils/componentSources.d.ts +50 -0
  347. package/dist/utils/componentSources.d.ts.map +1 -0
  348. package/dist/utils/componentSources.js +50 -0
  349. package/dist/utils/componentSources.js.map +1 -0
  350. package/dist/utils/decodeStyleEntities.d.ts +15 -0
  351. package/dist/utils/decodeStyleEntities.d.ts.map +1 -0
  352. package/dist/utils/decodeStyleEntities.js +18 -0
  353. package/dist/utils/decodeStyleEntities.js.map +1 -0
  354. package/dist/utils/detect.d.ts +5 -0
  355. package/dist/utils/detect.d.ts.map +1 -0
  356. package/dist/utils/detect.js +10 -0
  357. package/dist/utils/detect.js.map +1 -0
  358. package/dist/utils/output-markers.d.ts +29 -0
  359. package/dist/utils/output-markers.d.ts.map +1 -0
  360. package/dist/utils/output-markers.js +68 -0
  361. package/dist/utils/output-markers.js.map +1 -0
  362. package/dist/utils/{url.d.mts → url.d.ts} +1 -1
  363. package/dist/utils/url.d.ts.map +1 -0
  364. package/dist/utils/{url.mjs → url.js} +2 -3
  365. package/dist/utils/url.js.map +1 -0
  366. package/dist/utils/watchPaths.d.ts +11 -0
  367. package/dist/utils/watchPaths.d.ts.map +1 -0
  368. package/dist/utils/watchPaths.js +19 -0
  369. package/dist/utils/watchPaths.js.map +1 -0
  370. package/node_modules/@clack/core/CHANGELOG.md +8 -0
  371. package/node_modules/@clack/core/dist/index.d.mts +18 -4
  372. package/node_modules/@clack/core/dist/index.mjs +16 -10
  373. package/node_modules/@clack/core/dist/index.mjs.map +1 -1
  374. package/node_modules/@clack/core/package.json +5 -2
  375. package/node_modules/@clack/prompts/CHANGELOG.md +15 -0
  376. package/node_modules/@clack/prompts/README.md +107 -2
  377. package/node_modules/@clack/prompts/dist/index.d.mts +16 -11
  378. package/node_modules/@clack/prompts/dist/index.mjs +114 -107
  379. package/node_modules/@clack/prompts/dist/index.mjs.map +1 -1
  380. package/node_modules/@clack/prompts/package.json +7 -4
  381. package/node_modules/fast-string-truncated-width/dist/index.js +36 -96
  382. package/node_modules/fast-string-truncated-width/dist/types.d.ts +0 -3
  383. package/node_modules/fast-string-truncated-width/dist/utils.d.ts +3 -3
  384. package/node_modules/fast-string-truncated-width/dist/utils.js +14 -9
  385. package/node_modules/fast-string-truncated-width/package.json +1 -1
  386. package/node_modules/fast-string-truncated-width/readme.md +2 -3
  387. package/node_modules/fast-string-width/package.json +2 -2
  388. package/node_modules/fast-string-width/readme.md +0 -3
  389. package/node_modules/fast-wrap-ansi/lib/main.js +4 -1
  390. package/node_modules/fast-wrap-ansi/lib/main.js.map +1 -1
  391. package/node_modules/fast-wrap-ansi/package.json +2 -2
  392. package/node_modules/maizzle/README.md +24 -0
  393. package/node_modules/maizzle/dist/commands/make/component.mjs +1 -1
  394. package/node_modules/maizzle/dist/commands/make/config.mjs +1 -1
  395. package/node_modules/maizzle/dist/commands/make/layout.mjs +3 -3
  396. package/node_modules/maizzle/dist/commands/make/scaffold.mjs +1 -1
  397. package/node_modules/maizzle/dist/commands/make/stubs/Layout.vue +146 -0
  398. package/node_modules/maizzle/dist/commands/make/stubs/component.vue +2 -4
  399. package/node_modules/maizzle/dist/commands/make/stubs/config.ts +1 -5
  400. package/node_modules/maizzle/dist/commands/make/template.mjs +1 -1
  401. package/node_modules/maizzle/dist/commands/new.mjs +32 -52
  402. package/node_modules/maizzle/dist/index.d.mts +1 -0
  403. package/node_modules/maizzle/dist/index.mjs +30 -7
  404. package/node_modules/maizzle/package.json +4 -3
  405. package/node_modules/nypm/dist/cli.mjs +28 -5
  406. package/node_modules/nypm/dist/index.d.mts +0 -8
  407. package/node_modules/nypm/dist/index.mjs +27 -4
  408. package/node_modules/nypm/package.json +12 -12
  409. package/node_modules/tinyexec/README.md +1 -1
  410. package/node_modules/tinyexec/dist/main.d.mts +6 -6
  411. package/node_modules/tinyexec/dist/main.mjs +126 -134
  412. package/node_modules/tinyexec/package.json +9 -9
  413. package/package.json +31 -21
  414. package/dist/build.d.mts +0 -19
  415. package/dist/build.d.mts.map +0 -1
  416. package/dist/build.mjs +0 -139
  417. package/dist/build.mjs.map +0 -1
  418. package/dist/components/Divider.vue +0 -105
  419. package/dist/components/Vml.vue +0 -89
  420. package/dist/components/utils.d.mts +0 -5
  421. package/dist/components/utils.d.mts.map +0 -1
  422. package/dist/components/utils.mjs +0 -9
  423. package/dist/components/utils.mjs.map +0 -1
  424. package/dist/composables/defineConfig.d.mts.map +0 -1
  425. package/dist/composables/defineConfig.mjs.map +0 -1
  426. package/dist/composables/renderContext.d.mts +0 -19
  427. package/dist/composables/renderContext.d.mts.map +0 -1
  428. package/dist/composables/renderContext.mjs.map +0 -1
  429. package/dist/composables/useConfig.d.mts.map +0 -1
  430. package/dist/composables/useConfig.mjs.map +0 -1
  431. package/dist/composables/useDoctype.d.mts.map +0 -1
  432. package/dist/composables/useDoctype.mjs.map +0 -1
  433. package/dist/composables/useEvent.d.mts.map +0 -1
  434. package/dist/composables/useEvent.mjs.map +0 -1
  435. package/dist/composables/usePlaintext.d.mts.map +0 -1
  436. package/dist/composables/usePlaintext.mjs.map +0 -1
  437. package/dist/config/defaults.d.mts.map +0 -1
  438. package/dist/config/defaults.mjs.map +0 -1
  439. package/dist/config/index.d.mts.map +0 -1
  440. package/dist/config/index.mjs.map +0 -1
  441. package/dist/events/index.d.mts.map +0 -1
  442. package/dist/events/index.mjs.map +0 -1
  443. package/dist/index.d.mts +0 -29
  444. package/dist/index.mjs +0 -29
  445. package/dist/plaintext.d.mts.map +0 -1
  446. package/dist/plaintext.mjs.map +0 -1
  447. package/dist/plugin.d.mts.map +0 -1
  448. package/dist/plugin.mjs +0 -41
  449. package/dist/plugin.mjs.map +0 -1
  450. package/dist/plugins/postcss/mergeMediaQueries.d.mts.map +0 -1
  451. package/dist/plugins/postcss/mergeMediaQueries.mjs.map +0 -1
  452. package/dist/plugins/postcss/pruneVars.d.mts.map +0 -1
  453. package/dist/plugins/postcss/pruneVars.mjs.map +0 -1
  454. package/dist/plugins/postcss/removeDeclarations.d.mts.map +0 -1
  455. package/dist/plugins/postcss/removeDeclarations.mjs.map +0 -1
  456. package/dist/plugins/postcss/tailwindCleanup.d.mts.map +0 -1
  457. package/dist/plugins/postcss/tailwindCleanup.mjs +0 -35
  458. package/dist/plugins/postcss/tailwindCleanup.mjs.map +0 -1
  459. package/dist/render/createRenderer.d.mts.map +0 -1
  460. package/dist/render/createRenderer.mjs +0 -155
  461. package/dist/render/createRenderer.mjs.map +0 -1
  462. package/dist/render/index.d.mts +0 -26
  463. package/dist/render/index.d.mts.map +0 -1
  464. package/dist/render/index.mjs +0 -44
  465. package/dist/render/index.mjs.map +0 -1
  466. package/dist/serve.d.mts.map +0 -1
  467. package/dist/serve.mjs.map +0 -1
  468. package/dist/server/compatibility.d.mts +0 -6
  469. package/dist/server/compatibility.d.mts.map +0 -1
  470. package/dist/server/compatibility.mjs +0 -83
  471. package/dist/server/compatibility.mjs.map +0 -1
  472. package/dist/server/linter.d.mts +0 -6
  473. package/dist/server/linter.d.mts.map +0 -1
  474. package/dist/server/linter.mjs +0 -200
  475. package/dist/server/linter.mjs.map +0 -1
  476. package/dist/server/ui/components/ui/resizable/ResizableHandle.vue +0 -30
  477. package/dist/server/ui/components/ui/resizable/ResizablePanel.vue +0 -21
  478. package/dist/server/ui/components/ui/resizable/ResizablePanelGroup.vue +0 -25
  479. package/dist/server/ui/components/ui/resizable/index.ts +0 -3
  480. package/dist/transformers/addAttributes.d.mts.map +0 -1
  481. package/dist/transformers/addAttributes.mjs.map +0 -1
  482. package/dist/transformers/attributeToStyle.d.mts +0 -25
  483. package/dist/transformers/attributeToStyle.d.mts.map +0 -1
  484. package/dist/transformers/attributeToStyle.mjs +0 -80
  485. package/dist/transformers/attributeToStyle.mjs.map +0 -1
  486. package/dist/transformers/base.d.mts +0 -8
  487. package/dist/transformers/base.d.mts.map +0 -1
  488. package/dist/transformers/base.mjs.map +0 -1
  489. package/dist/transformers/entities.d.mts +0 -8
  490. package/dist/transformers/entities.d.mts.map +0 -1
  491. package/dist/transformers/entities.mjs +0 -38
  492. package/dist/transformers/entities.mjs.map +0 -1
  493. package/dist/transformers/format.d.mts +0 -15
  494. package/dist/transformers/format.d.mts.map +0 -1
  495. package/dist/transformers/format.mjs +0 -26
  496. package/dist/transformers/format.mjs.map +0 -1
  497. package/dist/transformers/index.d.mts.map +0 -1
  498. package/dist/transformers/index.mjs +0 -73
  499. package/dist/transformers/index.mjs.map +0 -1
  500. package/dist/transformers/inlineCSS.d.mts +0 -30
  501. package/dist/transformers/inlineCSS.d.mts.map +0 -1
  502. package/dist/transformers/inlineCSS.mjs +0 -79
  503. package/dist/transformers/inlineCSS.mjs.map +0 -1
  504. package/dist/transformers/inlineLink.d.mts +0 -14
  505. package/dist/transformers/inlineLink.d.mts.map +0 -1
  506. package/dist/transformers/inlineLink.mjs.map +0 -1
  507. package/dist/transformers/minify.d.mts +0 -17
  508. package/dist/transformers/minify.d.mts.map +0 -1
  509. package/dist/transformers/minify.mjs +0 -24
  510. package/dist/transformers/minify.mjs.map +0 -1
  511. package/dist/transformers/purgeCSS.d.mts +0 -23
  512. package/dist/transformers/purgeCSS.d.mts.map +0 -1
  513. package/dist/transformers/purgeCSS.mjs +0 -66
  514. package/dist/transformers/purgeCSS.mjs.map +0 -1
  515. package/dist/transformers/removeAttributes.d.mts +0 -31
  516. package/dist/transformers/removeAttributes.d.mts.map +0 -1
  517. package/dist/transformers/removeAttributes.mjs +0 -63
  518. package/dist/transformers/removeAttributes.mjs.map +0 -1
  519. package/dist/transformers/replaceStrings.d.mts.map +0 -1
  520. package/dist/transformers/replaceStrings.mjs.map +0 -1
  521. package/dist/transformers/safeClassNames.d.mts.map +0 -1
  522. package/dist/transformers/safeClassNames.mjs.map +0 -1
  523. package/dist/transformers/shorthandCSS.d.mts +0 -24
  524. package/dist/transformers/shorthandCSS.d.mts.map +0 -1
  525. package/dist/transformers/shorthandCSS.mjs +0 -48
  526. package/dist/transformers/shorthandCSS.mjs.map +0 -1
  527. package/dist/transformers/tailwindcss.d.mts.map +0 -1
  528. package/dist/transformers/tailwindcss.mjs +0 -136
  529. package/dist/transformers/tailwindcss.mjs.map +0 -1
  530. package/dist/transformers/urlQuery.d.mts +0 -24
  531. package/dist/transformers/urlQuery.d.mts.map +0 -1
  532. package/dist/transformers/urlQuery.mjs +0 -65
  533. package/dist/transformers/urlQuery.mjs.map +0 -1
  534. package/dist/types/config.d.mts +0 -149
  535. package/dist/types/config.d.mts.map +0 -1
  536. package/dist/types/config.mjs +0 -1
  537. package/dist/types/index.d.mts +0 -2
  538. package/dist/types/index.mjs +0 -1
  539. package/dist/utils/ast/index.d.mts +0 -4
  540. package/dist/utils/ast/index.mjs +0 -5
  541. package/dist/utils/ast/parser.d.mts.map +0 -1
  542. package/dist/utils/ast/parser.mjs.map +0 -1
  543. package/dist/utils/ast/serializer.d.mts +0 -7
  544. package/dist/utils/ast/serializer.d.mts.map +0 -1
  545. package/dist/utils/ast/serializer.mjs +0 -13
  546. package/dist/utils/ast/serializer.mjs.map +0 -1
  547. package/dist/utils/ast/walker.d.mts.map +0 -1
  548. package/dist/utils/ast/walker.mjs.map +0 -1
  549. package/dist/utils/url.d.mts.map +0 -1
  550. package/dist/utils/url.mjs.map +0 -1
  551. package/node_modules/maizzle/dist/commands/make/stubs/layout.vue +0 -39
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
2
+ import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
3
3
  import { useRoute } from 'vue-router'
4
4
  import { ChevronUp, ChevronDown, Check } from 'lucide-vue-next'
5
5
  import {
@@ -8,15 +8,21 @@ import {
8
8
  DropdownMenuItem,
9
9
  DropdownMenuTrigger,
10
10
  } from '@/components/ui/dropdown-menu'
11
- import {
12
- ResizableHandle,
13
- ResizablePanel,
14
- ResizablePanelGroup,
15
- } from '@/components/ui/resizable'
16
11
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
17
12
  import { Button } from '@/components/ui/button'
13
+ import { Input } from '@/components/ui/input'
14
+ import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
15
+ import { Checkbox } from '@/components/ui/checkbox'
16
+ import {
17
+ TagsInput,
18
+ TagsInputInput,
19
+ TagsInputItem,
20
+ TagsInputItemDelete,
21
+ TagsInputItemText,
22
+ } from '@/components/ui/tags-input'
18
23
 
19
24
  import stripesUrl from '../stripes.svg'
25
+ import { applyColorInversion, undoColorInversion } from '@/lib/emulated-dark-mode'
20
26
 
21
27
  interface Device {
22
28
  name: string
@@ -24,12 +30,20 @@ interface Device {
24
30
  height: number
25
31
  }
26
32
 
33
+ interface Template {
34
+ name: string
35
+ path: string
36
+ href: string
37
+ }
38
+
27
39
  const props = defineProps<{
28
40
  device?: Device | null
29
41
  resetKey?: number
42
+ templates?: Template[]
30
43
  }>()
31
44
 
32
45
  const viewMode = defineModel<'preview' | 'source'>('viewMode', { default: 'preview' })
46
+ const darkMode = defineModel<boolean>('darkMode', { default: false })
33
47
 
34
48
  const route = useRoute()
35
49
  const srcdoc = ref('')
@@ -40,55 +54,100 @@ const sourceView = ref<'compiled' | 'vue' | 'plaintext'>('compiled')
40
54
  const copied = ref(false)
41
55
 
42
56
  const iframeEl = ref<HTMLIFrameElement>()
57
+ const compiledSourceEl = ref<HTMLElement>()
43
58
  const vueSourceEl = ref<HTMLElement>()
59
+ const plaintextEl = ref<HTMLElement>()
44
60
  const containerEl = ref<HTMLElement>()
45
- const previewEl = ref<InstanceType<typeof ResizablePanel>>()
46
- const leftPanel = ref<InstanceType<typeof ResizablePanel>>()
47
- const rightPanel = ref<InstanceType<typeof ResizablePanel>>()
48
- const topPanel = ref<InstanceType<typeof ResizablePanel>>()
49
- const bottomPanel = ref<InstanceType<typeof ResizablePanel>>()
61
+ const wrapperEl = ref<HTMLElement>()
50
62
 
51
63
  const panelWidth = defineModel<number>('panelWidth', { default: 0 })
52
64
  const panelHeight = defineModel<number>('panelHeight', { default: 0 })
65
+ /**
66
+ * Container's available area, exposed to the toolbar so size inputs can
67
+ * clamp typed values without paying a layout-recalc cost on
68
+ * every drag tick. Kept in sync via a ResizeObserver.
69
+ */
70
+ const maxIframeWidth = defineModel<number>('maxIframeWidth', { default: 0 })
71
+ const maxIframeHeight = defineModel<number>('maxIframeHeight', { default: 0 })
53
72
  const isDragging = defineModel<boolean>('isDragging', { default: false })
54
73
  const isFullSize = defineModel<boolean>('isFullSize', { default: true })
55
74
 
56
- const sideSizes = ref({ left: 0, right: 0, top: 0, bottom: 0 })
57
-
58
- function updateFullSize() {
59
- isFullSize.value = sideSizes.value.left < 0.5
60
- && sideSizes.value.right < 0.5
61
- && sideSizes.value.top < 0.5
62
- && sideSizes.value.bottom < 0.5
63
- }
64
-
65
- async function copySource() {
75
+ /**
76
+ * Custom resizable: width/height of the iframe wrapper (null = fill the
77
+ * container). Exposed as v-models so the toolbar's size indicator
78
+ * can drive these too, alongside the drag handles.
79
+ */
80
+ const iframeWidth = defineModel<number | null>('iframeWidth', { default: null })
81
+ const iframeHeight = defineModel<number | null>('iframeHeight', { default: null })
82
+ const iframeContentHeight = ref<number | null>(null)
83
+
84
+ function copySource() {
85
+ let text: string
66
86
  if (sourceView.value === 'compiled') {
67
- await navigator.clipboard.writeText(srcdoc.value)
87
+ // `renderedHtml` holds the raw compiled HTML (srcdoc is only populated
88
+ // for the initial iframe load; subsequent renders use doc.write).
89
+ text = renderedHtml || srcdoc.value
68
90
  } else if (sourceView.value === 'plaintext') {
69
- await navigator.clipboard.writeText(plaintextContent.value)
91
+ text = plaintextContent.value
70
92
  } else {
71
93
  const el = document.createElement('div')
72
94
  el.innerHTML = vueSourceHtml.value
73
- await navigator.clipboard.writeText(el.textContent || '')
95
+ text = el.textContent || ''
74
96
  }
75
- copied.value = true
76
- setTimeout(() => { copied.value = false }, 2000)
97
+
98
+ navigator.clipboard.writeText(text).then(() => {
99
+ copied.value = true
100
+ setTimeout(() => { copied.value = false }, 2000)
101
+ }).catch((err) => {
102
+ console.error('Copy failed:', err)
103
+ })
77
104
  }
78
105
 
79
- interface CompatibilityIssue {
80
- type: 'error' | 'warning'
106
+ interface CheckIssue {
107
+ kind: 'compat' | 'lint'
108
+ slug?: string
81
109
  title: string
82
- clients: Array<{ name: string, notes: string[] }>
83
110
  url?: string
111
+ category: string
84
112
  line?: number
113
+ file: string
114
+ // compat-only
115
+ supportLevel?: 'unsupported' | 'mitigated' | 'unknown'
116
+ supportLabel?: string
117
+ affectedClients?: string[]
118
+ // lint-only
119
+ severity?: 'error' | 'warning'
120
+ message?: string
85
121
  }
86
122
 
87
- interface LintIssue {
88
- type: 'error' | 'warning'
89
- title: string
90
- message: string
91
- line?: number
123
+ function supportPrefix(issue: CheckIssue): string {
124
+ if (issue.supportLevel === 'unsupported') return 'Not supported in'
125
+ if (issue.supportLevel === 'mitigated') return 'Partial support in'
126
+ return 'Support unknown in'
127
+ }
128
+
129
+ /**
130
+ * Split a message on backtick-delimited code spans. Returns alternating
131
+ * { text } and { code } segments so the template can render <code> inline
132
+ * without needing v-html.
133
+ */
134
+ function messageSegments(raw: string | undefined): Array<{ code: boolean, text: string }> {
135
+ if (!raw) return []
136
+ const out: Array<{ code: boolean, text: string }> = []
137
+ const parts = raw.split('`')
138
+ for (let i = 0; i < parts.length; i++) {
139
+ if (parts[i]) out.push({ code: i % 2 === 1, text: parts[i] })
140
+ }
141
+ return out
142
+ }
143
+
144
+ function issueColorClass(issue: CheckIssue): string {
145
+ if (issue.kind === 'lint') {
146
+ return issue.severity === 'error' ? 'text-rose-600' : 'text-amber-600'
147
+ }
148
+ if (issue.supportLevel === 'unsupported') return 'text-rose-600'
149
+ if (issue.supportLevel === 'mitigated') return 'text-amber-600'
150
+ return 'text-gray-500 dark:text-gray-400'
92
151
  }
93
152
 
94
153
  interface TemplateStats {
@@ -97,31 +156,179 @@ interface TemplateStats {
97
156
  links: number
98
157
  }
99
158
 
100
- const compatibilityIssues = ref<CompatibilityIssue[]>([])
159
+ const compatibilityIssues = ref<CheckIssue[]>([])
101
160
  const compatibilityLoading = ref(false)
102
- const lintIssues = ref<LintIssue[]>([])
103
- const lintLoading = ref(false)
161
+ const compatibilityError = ref('')
162
+ const compatibilityCategory = ref('')
163
+ // Injected by serveDevUI into index.html — synchronous, available before
164
+ // any HTTP calls, so the Checks tab never flashes in when disabled.
165
+ const checksConfig = (window as any).__MAIZZLE_CONFIG__?.checks
166
+ const compatibilityDisabled = ref(checksConfig === false)
167
+ const expandedIssueKeys = ref(new Set<string>())
168
+ const issueKey = (issue: CheckIssue, i: number): string => `${issue.file}|${issue.line ?? 0}|${issue.slug ?? issue.title}|${i}`
169
+ const compatibilityCategories = ['css', 'html', 'image', 'others'] as const
170
+ const activeCompatibilityCategories = computed(() =>
171
+ compatibilityCategories.filter(cat => compatibilityIssues.value.some(i => i.category === cat))
172
+ )
173
+ const filteredCompatibilityIssues = computed(() => {
174
+ if (!compatibilityCategory.value) return compatibilityIssues.value
175
+ return compatibilityIssues.value.filter(i => i.category === compatibilityCategory.value)
176
+ })
104
177
  const stats = ref<TemplateStats | null>(null)
105
178
  const statsLoading = ref(false)
106
179
 
180
+ // Email test state
181
+ const emailTo = ref<string[]>([])
182
+ const emailSubject = ref('')
183
+ const emailSending = ref(false)
184
+ const emailPreventThreading = ref(true)
185
+ const emailResult = ref<{ success: boolean; message: string; previewUrl?: string } | null>(null)
186
+
187
+ async function fetchEmailConfig() {
188
+ try {
189
+ const res = await fetch('/__maizzle/email-config')
190
+ const data = await res.json()
191
+ if (data.to?.length && !emailTo.value.length) emailTo.value = data.to
192
+ if (data.subject && !emailSubject.value) emailSubject.value = data.subject
193
+ } catch {}
194
+ }
195
+
196
+ async function sendTestEmail() {
197
+ if (!emailTo.value.length) return
198
+ emailSending.value = true
199
+ emailResult.value = null
200
+
201
+ try {
202
+ const res = await fetch(`/__maizzle/email/${route.params.template}`, {
203
+ method: 'POST',
204
+ headers: { 'Content-Type': 'application/json' },
205
+ body: JSON.stringify({
206
+ to: emailTo.value,
207
+ subject: (() => {
208
+ let subj = emailSubject.value || String(route.params.template)
209
+ if (emailPreventThreading.value) {
210
+ subj += ` | ${new Date().toISOString().slice(0, 19)}`
211
+ }
212
+ return subj
213
+ })(),
214
+ }),
215
+ })
216
+ emailResult.value = await res.json()
217
+ } catch (error: any) {
218
+ emailResult.value = { success: false, message: error.message }
219
+ } finally {
220
+ emailSending.value = false
221
+ }
222
+ }
223
+
224
+ let renderedHtml = ''
225
+
226
+ function updateIframeContentHeight() {
227
+ const iframe = iframeEl.value
228
+ const doc = iframe?.contentDocument
229
+ if (!iframe || !doc?.documentElement) return
230
+
231
+ // Hide iframe body overflow — scrolling is handled by the outer ScrollArea
232
+ if (doc.body) doc.body.style.overflow = 'hidden'
233
+
234
+ // Save scroll position of the ScrollArea viewport
235
+ const viewport = wrapperEl.value?.querySelector('[data-slot="scroll-area-viewport"]')
236
+ const scrollTop = viewport?.scrollTop ?? 0
237
+
238
+ // Temporarily collapse to measure true content height
239
+ iframe.style.height = '0'
240
+ iframeContentHeight.value = doc.documentElement.scrollHeight
241
+ iframe.style.height = `${iframeContentHeight.value}px`
242
+
243
+ // Restore scroll position
244
+ if (viewport) {
245
+ viewport.scrollTop = scrollTop
246
+ }
247
+ }
248
+
249
+ function onIframeLoad() {
250
+ updateIframeContentHeight()
251
+ const iframe = iframeEl.value
252
+ if (darkMode.value && iframe) applyColorInversion(iframe)
253
+ }
254
+
255
+ watch(darkMode, (on) => {
256
+ const iframe = iframeEl.value
257
+ if (!iframe) return
258
+ if (on) applyColorInversion(iframe)
259
+ else undoColorInversion(iframe)
260
+ })
261
+
107
262
  async function fetchTemplate() {
108
263
  const res = await fetch(`/__maizzle/render/${route.params.template}`)
109
- srcdoc.value = await res.text()
264
+ renderedHtml = await res.text()
265
+
266
+ const iframe = iframeEl.value
267
+ const doc = iframe?.contentDocument
268
+
269
+ // Write directly into the iframe document to avoid a full reload,
270
+ // which preserves scroll position natively.
271
+ if (doc) {
272
+ doc.open()
273
+ doc.write(renderedHtml)
274
+ doc.close()
275
+ // Hide iframe body overflow — scrolling is handled by the outer ScrollArea
276
+ if (doc.body) doc.body.style.overflow = 'hidden'
277
+ if (darkMode.value && iframe) applyColorInversion(iframe)
278
+ await nextTick()
279
+ updateIframeContentHeight()
280
+ } else {
281
+ // Fallback for initial load
282
+ srcdoc.value = renderedHtml
283
+ }
110
284
  }
111
285
 
286
+ const sourceLoading = ref(false)
287
+ const vueSourceLoading = ref(false)
288
+ const plaintextLoading = ref(false)
289
+
112
290
  async function fetchSource() {
113
- const res = await fetch(`/__maizzle/source/${route.params.template}`)
114
- sourceHtml.value = await res.text()
291
+ if (sourceLoading.value) return
292
+ sourceLoading.value = true
293
+ try {
294
+ const res = await fetch(`/__maizzle/source/${route.params.template}`)
295
+ sourceHtml.value = await res.text()
296
+ } finally {
297
+ sourceLoading.value = false
298
+ }
115
299
  }
116
300
 
117
301
  async function fetchVueSource() {
118
- const res = await fetch(`/__maizzle/vue-source/${route.params.template}`)
119
- vueSourceHtml.value = await res.text()
302
+ if (vueSourceLoading.value) return
303
+ vueSourceLoading.value = true
304
+ try {
305
+ const res = await fetch(`/__maizzle/vue-source/${route.params.template}`)
306
+ vueSourceHtml.value = await res.text()
307
+ } finally {
308
+ vueSourceLoading.value = false
309
+ }
120
310
  }
121
311
 
122
312
  async function fetchPlaintext() {
123
- const res = await fetch(`/__maizzle/plaintext/${route.params.template}`)
124
- plaintextContent.value = await res.text()
313
+ if (plaintextLoading.value) return
314
+ plaintextLoading.value = true
315
+ try {
316
+ const res = await fetch(`/__maizzle/plaintext/${route.params.template}`)
317
+ plaintextContent.value = await res.text()
318
+ } finally {
319
+ plaintextLoading.value = false
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Warm the three source views in the background so switching from the
325
+ * preview is instant. Single-flight guards above prevent duplication
326
+ * with any in-flight fetch from a view-switch watcher.
327
+ */
328
+ function prefetchSources() {
329
+ if (!sourceHtml.value) fetchSource()
330
+ if (!vueSourceHtml.value) fetchVueSource()
331
+ if (!plaintextContent.value) fetchPlaintext()
125
332
  }
126
333
 
127
334
  async function fetchStats() {
@@ -137,10 +344,31 @@ async function fetchStats() {
137
344
  }
138
345
 
139
346
  async function fetchCompatibility() {
347
+ if (compatibilityDisabled.value) return
348
+ const template = props.templates?.find(t => t.href === '/' + route.params.template)
349
+ if (!template) return
350
+
140
351
  compatibilityLoading.value = true
352
+ compatibilityError.value = ''
141
353
  try {
142
- const res = await fetch(`/__maizzle/compatibility/${route.params.template}`)
143
- compatibilityIssues.value = await res.json()
354
+ const res = await fetch(`/__maizzle/compatibility/${template.path}`)
355
+ const data = await res.json()
356
+ if (!Array.isArray(data) && data?.error) {
357
+ compatibilityError.value = data.error
358
+ compatibilityIssues.value = []
359
+ } else {
360
+ const issues: CheckIssue[] = Array.isArray(data) ? data : []
361
+ compatibilityIssues.value = issues
362
+ // Keep the current category if it still has issues; otherwise fall
363
+ // back to the first category that does. Prevents a "refresh" during
364
+ // edits from snapping back to CSS when the user is on HTML/Image.
365
+ const current = compatibilityCategory.value
366
+ const currentStillActive = current && issues.some((i) => i.category === current)
367
+ if (!currentStillActive) {
368
+ const firstCat = compatibilityCategories.find(cat => issues.some((i) => i.category === cat))
369
+ compatibilityCategory.value = firstCat || ''
370
+ }
371
+ }
144
372
  } catch {
145
373
  compatibilityIssues.value = []
146
374
  } finally {
@@ -148,16 +376,21 @@ async function fetchCompatibility() {
148
376
  }
149
377
  }
150
378
 
151
- async function fetchLint() {
152
- lintLoading.value = true
153
- try {
154
- const res = await fetch(`/__maizzle/lint/${route.params.template}`)
155
- lintIssues.value = await res.json()
156
- } catch {
157
- lintIssues.value = []
158
- } finally {
159
- lintLoading.value = false
160
- }
379
+ /** Check if an issue is from the currently viewed template file */
380
+ function isCurrentFile(issue: { file: string }): boolean {
381
+ const template = props.templates?.find(t => t.href === '/' + route.params.template)
382
+ if (!template) return true
383
+ return issue.file.endsWith(template.path)
384
+ }
385
+
386
+ /** Get a short display name for a component file path */
387
+ function componentName(filePath: string): string {
388
+ const parts = filePath.replace(/\\/g, '/').split('/')
389
+ return parts[parts.length - 1]?.replace(/\.vue$/, '') ?? filePath
390
+ }
391
+
392
+ function openInEditor(file: string, line: number) {
393
+ fetch(`/__open-in-editor?file=${encodeURIComponent(file + ':' + line)}`)
161
394
  }
162
395
 
163
396
  watch(() => route.params.template, () => {
@@ -165,18 +398,30 @@ watch(() => route.params.template, () => {
165
398
  vueSourceHtml.value = ''
166
399
  plaintextContent.value = ''
167
400
  compatibilityIssues.value = []
168
- lintIssues.value = []
401
+ compatibilityError.value = ''
169
402
  stats.value = null
403
+ emailResult.value = null
170
404
  sourceView.value = 'compiled'
171
- fetchTemplate()
405
+ fetchTemplate().then(prefetchSources)
172
406
  fetchCompatibility()
173
- fetchLint()
174
407
  fetchStats()
408
+ fetchEmailConfig()
175
409
  if (viewMode.value === 'source') fetchSource()
176
410
  }, { immediate: true })
177
411
 
412
+ // Templates list loads async from App.vue — re-trigger once available
413
+ watch(() => props.templates, (templates) => {
414
+ if (templates?.length && !compatibilityIssues.value.length && !compatibilityLoading.value) {
415
+ fetchCompatibility()
416
+ }
417
+ })
418
+
178
419
  watch(viewMode, (mode) => {
179
- if (mode === 'source' && !sourceHtml.value) fetchSource()
420
+ if (mode === 'source') {
421
+ if (sourceView.value === 'compiled' && !sourceHtml.value) fetchSource()
422
+ if (sourceView.value === 'vue' && !vueSourceHtml.value) fetchVueSource()
423
+ if (sourceView.value === 'plaintext' && !plaintextContent.value) fetchPlaintext()
424
+ }
180
425
  })
181
426
 
182
427
  watch(sourceView, (view) => {
@@ -185,45 +430,81 @@ watch(sourceView, (view) => {
185
430
  if (view === 'plaintext' && !plaintextContent.value) fetchPlaintext()
186
431
  })
187
432
 
433
+ /**
434
+ * Preserve scrollTop across in-place content updates (HMR refetch).
435
+ * Vue's default `flush: 'pre'` runs the watcher BEFORE the DOM is
436
+ * updated — so we read the current scrollTop, then restore it on the
437
+ * next tick after the new content has rendered. Skip the case where
438
+ * the value transitions from empty (first paint / route change) so a
439
+ * fresh template doesn't snap to a stale offset.
440
+ */
441
+ function viewportFor(el: HTMLElement | undefined): HTMLElement | null {
442
+ return (el?.closest('[data-slot="scroll-area-viewport"]') as HTMLElement | null) ?? null
443
+ }
444
+
445
+ function preserveScroll(getEl: () => HTMLElement | undefined) {
446
+ return async (newVal: string, oldVal: string) => {
447
+ if (!oldVal || !newVal) return
448
+ const vp = viewportFor(getEl())
449
+ if (!vp) return
450
+ const top = vp.scrollTop
451
+ await nextTick()
452
+ vp.scrollTop = top
453
+ }
454
+ }
455
+
456
+ watch(sourceHtml, preserveScroll(() => compiledSourceEl.value))
457
+ watch(vueSourceHtml, preserveScroll(() => vueSourceEl.value))
458
+ watch(plaintextContent, preserveScroll(() => plaintextEl.value))
459
+
188
460
  if ((import.meta as any).hot) {
189
461
  ;(import.meta as any).hot.on('maizzle:template-updated', () => {
190
- fetchTemplate()
191
462
  fetchCompatibility()
192
- fetchLint()
193
463
  fetchStats()
194
- // Clear non-active source views so they re-fetch when switched to
195
- if (sourceView.value !== 'compiled') sourceHtml.value = ''
196
- if (sourceView.value !== 'vue') vueSourceHtml.value = ''
197
- if (sourceView.value !== 'plaintext') plaintextContent.value = ''
198
-
199
- if (viewMode.value === 'source') {
200
- if (sourceView.value === 'compiled') fetchSource()
201
- if (sourceView.value === 'vue') fetchVueSource()
202
- if (sourceView.value === 'plaintext') fetchPlaintext()
464
+
465
+ // Refetch in place — don't clear the previous values first. v-html
466
+ // replaces the highlighted block atomically when the new content
467
+ // arrives, and the ScrollArea viewport keeps its scrollTop as long
468
+ // as the new content's height is similar. Plaintext interpolation
469
+ // updates a single text node, so scroll is naturally preserved.
470
+ fetchTemplate()
471
+ fetchSource()
472
+ fetchVueSource()
473
+ fetchPlaintext()
474
+ })
475
+
476
+ // Keep the UI in sync with live config edits. Payload is the same shape
477
+ // as the initial `window.__MAIZZLE_CONFIG__` inject — we replace it and
478
+ // derive per-feature flags from there.
479
+ ;(import.meta as any).hot.on('maizzle:config-updated', (data: Record<string, unknown>) => {
480
+ ;(window as any).__MAIZZLE_CONFIG__ = data
481
+ const wasDisabled = compatibilityDisabled.value
482
+ const nowDisabled = data?.checks === false
483
+ compatibilityDisabled.value = nowDisabled
484
+ if (nowDisabled) {
485
+ compatibilityIssues.value = []
486
+ if (activeTab.value === 'compatibility') activeTab.value = 'stats'
487
+ } else if (wasDisabled) {
488
+ fetchCompatibility()
203
489
  }
204
490
  })
205
491
  }
206
492
 
207
-
208
- async function goToLine(line: number) {
209
- // Switch to source view showing Vue source
493
+ async function goToCompiledLine(line: number) {
210
494
  viewMode.value = 'source'
211
- sourceView.value = 'vue'
495
+ sourceView.value = 'compiled'
212
496
 
213
- // Ensure vue source is loaded
214
- if (!vueSourceHtml.value) {
215
- await fetchVueSource()
497
+ if (!sourceHtml.value) {
498
+ await fetchSource()
216
499
  }
217
500
 
218
501
  await nextTick()
219
502
 
220
- const el = vueSourceEl.value
503
+ const el = compiledSourceEl.value
221
504
  if (!el) return
222
505
 
223
- // Remove previous highlight
224
506
  el.querySelectorAll('.shiki-highlight-line').forEach(l => l.classList.remove('shiki-highlight-line'))
225
507
 
226
- // Find and highlight the line
227
508
  const lineEl = el.querySelector(`[data-line="${line}"]`)
228
509
  if (lineEl) {
229
510
  lineEl.classList.add('shiki-highlight-line')
@@ -231,74 +512,94 @@ async function goToLine(line: number) {
231
512
  }
232
513
  }
233
514
 
234
- // Track which axis is being user-dragged so we can sync the opposite panel
235
- let hDragging = false
236
- let vDragging = false
237
-
238
515
  const emit = defineEmits<{ 'clear-device': [] }>()
239
516
 
240
- function onHDragStart() { hDragging = true; isDragging.value = true; emit('clear-device') }
241
- function onHDragEnd() { setTimeout(() => { hDragging = false }, 50); isDragging.value = false }
242
- function onVDragStart() { vDragging = true; isDragging.value = true; emit('clear-device') }
243
- function onVDragEnd() { setTimeout(() => { vDragging = false }, 50); isDragging.value = false }
517
+ type Edge = 'left' | 'right' | 'top' | 'bottom'
244
518
 
245
- function onHorizontalLayout(sizes: number[]) {
246
- if (!hDragging) return
519
+ function onEdgeDrag(e: MouseEvent | TouchEvent, edge: Edge) {
520
+ e.preventDefault()
521
+ isDragging.value = true
522
+ emit('clear-device')
523
+
524
+ const container = containerEl.value
525
+ if (!container) return
526
+
527
+ const isTouch = e.type === 'touchstart'
528
+ const startPoint = isTouch ? (e as TouchEvent).touches[0] : (e as MouseEvent)
529
+ const startX = startPoint.clientX
530
+ const startY = startPoint.clientY
531
+ const rect = container.getBoundingClientRect()
532
+ const gutter = 40 // 20px padding on each side
533
+ const maxW = rect.width - gutter
534
+ const maxH = rect.height - gutter
535
+ const startW = iframeWidth.value ?? maxW
536
+ const startH = iframeHeight.value ?? maxH
537
+
538
+ const isHorizontal = edge === 'left' || edge === 'right'
539
+ const sign = (edge === 'left' || edge === 'top') ? -1 : 1
540
+
541
+ document.documentElement.style.cursor = isHorizontal ? 'ew-resize' : 'ns-resize'
542
+
543
+ const onMove = (ev: MouseEvent | TouchEvent) => {
544
+ const point = ev.type === 'touchmove' ? (ev as TouchEvent).touches[0] : (ev as MouseEvent)
545
+ if (isHorizontal) {
546
+ // Symmetric: each side moves by the delta, so total change is 2x
547
+ const delta = (point.clientX - startX) * sign
548
+ iframeWidth.value = Math.max(200, Math.min(maxW, startW + delta * 2))
549
+ } else {
550
+ const delta = (point.clientY - startY) * sign
551
+ iframeHeight.value = Math.max(100, Math.min(maxH, startH + delta * 2))
552
+ }
553
+ }
247
554
 
248
- const [left, , right] = sizes
249
- if (Math.abs(left - right) < 0.5) return
555
+ const onUp = () => {
556
+ isDragging.value = false
557
+ document.documentElement.style.cursor = ''
558
+ updateFullSize()
559
+ document.removeEventListener('mousemove', onMove)
560
+ document.removeEventListener('mouseup', onUp)
561
+ document.removeEventListener('touchmove', onMove)
562
+ document.removeEventListener('touchend', onUp)
563
+ }
250
564
 
251
- hDragging = false
252
- const side = Math.max(left, right)
253
- if (left < side) leftPanel.value?.resize(side)
254
- if (right < side) rightPanel.value?.resize(side)
565
+ document.addEventListener('mousemove', onMove)
566
+ document.addEventListener('mouseup', onUp)
567
+ document.addEventListener('touchmove', onMove, { passive: false })
568
+ document.addEventListener('touchend', onUp)
255
569
  }
256
570
 
257
- function onVerticalLayout(sizes: number[]) {
258
- if (!vDragging) return
259
-
260
- const [top, , bottom] = sizes
261
- if (Math.abs(top - bottom) < 0.5) return
262
-
263
- vDragging = false
264
- const side = Math.max(top, bottom)
265
- if (top < side) topPanel.value?.resize(side)
266
- if (bottom < side) bottomPanel.value?.resize(side)
571
+ function updateFullSize() {
572
+ const container = containerEl.value
573
+ if (!container) return
574
+ const rect = container.getBoundingClientRect()
575
+ const gutter = 40
576
+ isFullSize.value = (iframeWidth.value === null || iframeWidth.value >= rect.width - gutter - 2)
577
+ && (iframeHeight.value === null || iframeHeight.value >= rect.height - gutter - 2)
267
578
  }
268
579
 
269
- function applyDeviceSize(device: Device | null | undefined) {
270
- const el = containerEl.value
271
- if (!el) return
272
580
 
581
+ function applyDeviceSize(device: Device | null | undefined) {
273
582
  if (!device) {
274
- if (!hDragging && !vDragging) {
275
- leftPanel.value?.resize(0)
276
- rightPanel.value?.resize(0)
277
- topPanel.value?.resize(0)
278
- bottomPanel.value?.resize(0)
279
- }
583
+ iframeWidth.value = null
584
+ iframeHeight.value = null
585
+ updateFullSize()
280
586
  return
281
587
  }
282
588
 
283
- const rect = el.getBoundingClientRect()
284
- if (!rect.width || !rect.height) return
285
-
286
- const handleSize = 16
287
- const hPanelSpace = rect.width - handleSize * 2
288
- const vPanelSpace = rect.height - handleSize * 2
589
+ const container = containerEl.value
590
+ if (!container) return
591
+ const rect = container.getBoundingClientRect()
592
+ const gutter = 40
289
593
 
290
- const hSide = Math.max(0, ((hPanelSpace - device.width) / 2) / hPanelSpace * 100)
291
- const vSide = Math.max(0, ((vPanelSpace - device.height) / 2) / vPanelSpace * 100)
292
-
293
- leftPanel.value?.resize(hSide)
294
- rightPanel.value?.resize(hSide)
295
- topPanel.value?.resize(vSide)
296
- bottomPanel.value?.resize(vSide)
594
+ iframeWidth.value = Math.min(device.width, rect.width - gutter)
595
+ iframeHeight.value = Math.min(device.height, rect.height - gutter)
596
+ updateFullSize()
297
597
  }
298
598
 
299
599
  watch(() => props.device, (device) => {
300
600
  if (viewMode.value === 'source') return
301
- applyDeviceSize(device)
601
+ // Only apply when a device is selected, not when cleared (drag start clears device)
602
+ if (device) applyDeviceSize(device)
302
603
  })
303
604
 
304
605
  watch(() => props.resetKey, () => {
@@ -313,6 +614,7 @@ watch(viewMode, async (mode) => {
313
614
  })
314
615
 
315
616
  let observer: ResizeObserver | null = null
617
+ let containerObserver: ResizeObserver | null = null
316
618
 
317
619
  function forwardIframeKeys(iframe: HTMLIFrameElement) {
318
620
  try {
@@ -327,15 +629,16 @@ function forwardIframeKeys(iframe: HTMLIFrameElement) {
327
629
  metaKey: e.metaKey,
328
630
  shiftKey: e.shiftKey,
329
631
  altKey: e.altKey,
632
+ bubbles: true,
330
633
  }))
331
634
  })
332
635
  } catch {}
333
636
  }
334
637
 
335
638
  onMounted(() => {
336
- const el = iframeEl.value
337
- if (el) {
338
- const rect = el.getBoundingClientRect()
639
+ const wrapper = wrapperEl.value
640
+ if (wrapper) {
641
+ const rect = wrapper.getBoundingClientRect()
339
642
  panelWidth.value = Math.round(rect.width)
340
643
  panelHeight.value = Math.round(rect.height)
341
644
  observer = new ResizeObserver((entries) => {
@@ -343,25 +646,48 @@ onMounted(() => {
343
646
  panelWidth.value = Math.round(entry.contentRect.width)
344
647
  panelHeight.value = Math.round(entry.contentRect.height)
345
648
  }
649
+ updateIframeContentHeight()
346
650
  })
347
- observer.observe(el)
651
+ observer.observe(wrapper)
652
+ }
653
+
654
+ const container = containerEl.value
655
+ if (container) {
656
+ const gutter = 40
657
+ const rect = container.getBoundingClientRect()
658
+ maxIframeWidth.value = Math.max(0, Math.round(rect.width - gutter))
659
+ maxIframeHeight.value = Math.max(0, Math.round(rect.height - gutter))
660
+ containerObserver = new ResizeObserver((entries) => {
661
+ for (const entry of entries) {
662
+ maxIframeWidth.value = Math.max(0, Math.round(entry.contentRect.width - gutter))
663
+ maxIframeHeight.value = Math.max(0, Math.round(entry.contentRect.height - gutter))
664
+ }
665
+ })
666
+ containerObserver.observe(container)
667
+ }
668
+
669
+ const el = iframeEl.value
670
+ if (el) {
348
671
  el.addEventListener('load', () => forwardIframeKeys(el))
349
672
  }
350
673
  })
351
674
 
352
675
  onUnmounted(() => {
353
676
  observer?.disconnect()
677
+ containerObserver?.disconnect()
354
678
  })
355
679
 
356
680
  const bottomPanelOpen = ref(false)
357
681
  const tabsPanelHeight = ref(40)
358
682
  const activeTab = ref<string | undefined>(undefined)
359
683
 
684
+ const defaultTab = () => compatibilityDisabled.value ? 'stats' : 'compatibility'
685
+
360
686
  function toggleBottomPanel() {
361
687
  bottomPanelOpen.value = !bottomPanelOpen.value
362
688
  if (bottomPanelOpen.value) {
363
- tabsPanelHeight.value = 200
364
- if (!activeTab.value) activeTab.value = 'compatibility'
689
+ tabsPanelHeight.value = 300
690
+ if (!activeTab.value) activeTab.value = defaultTab()
365
691
  } else {
366
692
  tabsPanelHeight.value = 40
367
693
  activeTab.value = undefined
@@ -378,38 +704,45 @@ function onTabClick(tab: string) {
378
704
  activeTab.value = tab
379
705
  if (!bottomPanelOpen.value) {
380
706
  bottomPanelOpen.value = true
381
- tabsPanelHeight.value = 200
707
+ tabsPanelHeight.value = 300
382
708
  }
383
709
  }
384
710
 
385
711
  const tabsDragging = ref(false)
386
712
 
387
- function onTabsDragStart(e: MouseEvent) {
713
+ function onTabsDragStart(e: MouseEvent | TouchEvent) {
388
714
  e.preventDefault()
389
715
  tabsDragging.value = true
390
- const startY = e.clientY
716
+ const isTouch = e.type === 'touchstart'
717
+ const startY = isTouch ? (e as TouchEvent).touches[0].clientY : (e as MouseEvent).clientY
391
718
  const startHeight = tabsPanelHeight.value
392
719
 
393
- const onMouseMove = (e: MouseEvent) => {
394
- const newHeight = Math.max(40, startHeight + startY - e.clientY)
720
+ const rootEl = containerEl.value?.closest('.relative.h-full') as HTMLElement | null
721
+ const maxHeight = rootEl ? rootEl.getBoundingClientRect().height : Infinity
722
+
723
+ const onMove = (e: MouseEvent | TouchEvent) => {
724
+ const clientY = e.type === 'touchmove' ? (e as TouchEvent).touches[0].clientY : (e as MouseEvent).clientY
725
+ const newHeight = Math.max(40, Math.min(maxHeight, startHeight + startY - clientY))
395
726
  tabsPanelHeight.value = newHeight
396
727
  bottomPanelOpen.value = newHeight > 40
397
728
 
398
729
  if (!bottomPanelOpen.value) {
399
730
  activeTab.value = undefined
400
731
  } else if (!activeTab.value) {
401
- activeTab.value = 'compatibility'
732
+ activeTab.value = defaultTab()
402
733
  }
403
734
  }
404
735
 
405
- const onMouseUp = () => {
736
+ const onEnd = () => {
406
737
  tabsDragging.value = false
407
- document.removeEventListener('mousemove', onMouseMove)
408
- document.removeEventListener('mouseup', onMouseUp)
738
+ document.removeEventListener('mousemove', onMove)
739
+ document.removeEventListener('mouseup', onEnd)
740
+ document.removeEventListener('touchmove', onMove)
741
+ document.removeEventListener('touchend', onEnd)
409
742
  }
410
743
 
411
- document.addEventListener('mousemove', onMouseMove)
412
- document.addEventListener('mouseup', onMouseUp)
744
+ document.addEventListener(isTouch ? 'touchmove' : 'mousemove', onMove)
745
+ document.addEventListener(isTouch ? 'touchend' : 'mouseup', onEnd)
413
746
  }
414
747
 
415
748
  const stripeBg = {
@@ -420,187 +753,294 @@ const stripeBg = {
420
753
  </script>
421
754
 
422
755
  <template>
423
- <div class="flex flex-col h-full">
424
- <div class="relative flex-1 min-h-0">
756
+ <div class="relative h-full">
757
+ <div class="absolute inset-0 bottom-10 overflow-hidden">
425
758
  <!-- Source code view -->
426
759
  <div v-show="viewMode === 'source'" class="absolute inset-0 min-w-0 overflow-hidden">
427
760
  <div class="absolute top-3 left-6 z-10">
428
761
  <DropdownMenu :modal="false">
429
- <DropdownMenuTrigger class="inline-flex items-center gap-1 rounded-md bg-white/10 px-2.5 h-7 text-xs font-medium text-gray-300 hover:bg-white/15 transition-colors">
762
+ <DropdownMenuTrigger class="inline-flex items-center gap-1 rounded-md bg-[#27212e]/80 dark:bg-gray-950/80 backdrop-blur-md border border-white/10 px-2.5 h-7 text-xs font-medium text-gray-300 hover:bg-[#27212e] dark:hover:bg-gray-950 transition-colors">
430
763
  {{ sourceView === 'compiled' ? 'HTML' : sourceView === 'vue' ? 'Source' : 'Plaintext' }}
431
764
  <ChevronDown class="size-3 opacity-50" />
432
765
  </DropdownMenuTrigger>
433
- <DropdownMenuContent align="start" class="min-w-0 bg-white/10 backdrop-blur-md border-white/10">
434
- <DropdownMenuItem class="text-xs font-medium text-gray-300 hover:text-white focus:bg-white/10 focus:text-white" @click="sourceView = 'vue'">
435
- <Check v-if="sourceView === 'vue'" class="size-3.5" />
436
- <span :class="sourceView === 'vue' ? '' : 'pl-5.5'">Source</span>
766
+ <DropdownMenuContent align="start" class="min-w-32 bg-[#27212e]/80 dark:bg-gray-950/80 backdrop-blur-md border-white/10">
767
+ <DropdownMenuItem class="text-xs font-medium text-gray-400 focus:text-gray-200 focus:bg-white/10" @click="sourceView = 'vue'">
768
+ <Check v-if="sourceView === 'vue'" class="size-3 text-gray-200" />
769
+ <span :class="[sourceView === 'vue' ? 'text-gray-200' : 'pl-5']">Source</span>
437
770
  </DropdownMenuItem>
438
- <DropdownMenuItem class="text-xs font-medium text-gray-300 hover:text-white focus:bg-white/10 focus:text-white" @click="sourceView = 'compiled'">
439
- <Check v-if="sourceView === 'compiled'" class="size-3.5" />
440
- <span :class="sourceView === 'compiled' ? '' : 'pl-5.5'">HTML</span>
771
+ <DropdownMenuItem class="text-xs font-medium text-gray-400 focus:text-gray-200 focus:bg-white/10" @click="sourceView = 'compiled'">
772
+ <Check v-if="sourceView === 'compiled'" class="size-3 text-gray-200" />
773
+ <span :class="[sourceView === 'compiled' ? 'text-gray-200' : 'pl-5']">HTML</span>
441
774
  </DropdownMenuItem>
442
- <DropdownMenuItem class="text-xs font-medium text-gray-300 hover:text-white focus:bg-white/10 focus:text-white" @click="sourceView = 'plaintext'">
443
- <Check v-if="sourceView === 'plaintext'" class="size-3.5" />
444
- <span :class="sourceView === 'plaintext' ? '' : 'pl-5.5'">Plaintext</span>
775
+ <DropdownMenuItem class="text-xs font-medium text-gray-400 focus:text-gray-200 focus:bg-white/10" @click="sourceView = 'plaintext'">
776
+ <Check v-if="sourceView === 'plaintext'" class="size-3 text-gray-200" />
777
+ <span :class="[sourceView === 'plaintext' ? 'text-gray-200' : 'pl-5']">Plaintext</span>
445
778
  </DropdownMenuItem>
446
779
  </DropdownMenuContent>
447
780
  </DropdownMenu>
448
781
  </div>
449
782
  <button
450
- class="absolute top-3 right-6 z-10 inline-flex items-center justify-center rounded-md px-2.5 h-8 bg-transparent hover:bg-transparent group disabled:opacity-50 disabled:cursor-not-allowed transition-all"
783
+ class="absolute top-3 right-[26px] z-10 inline-flex items-center justify-center rounded-md size-7 bg-[#27212e]/80 dark:bg-gray-950/80 backdrop-blur-md border border-white/10 hover:bg-[#27212e] dark:hover:bg-gray-950 group disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
451
784
  :disabled="copied"
452
785
  @click="copySource"
453
786
  >
454
- <svg v-if="!copied" class="size-5 text-gray-400 group-hover:text-gray-300" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M14.25 5.25H7.25C6.14543 5.25 5.25 6.14543 5.25 7.25V14.25C5.25 15.3546 6.14543 16.25 7.25 16.25H14.25C15.3546 16.25 16.25 15.3546 16.25 14.25V7.25C16.25 6.14543 15.3546 5.25 14.25 5.25Z" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M2.80103 11.998L1.77203 5.07397C1.61003 3.98097 2.36403 2.96397 3.45603 2.80197L10.38 1.77297C11.313 1.63397 12.19 2.16297 12.528 3.00097" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /></svg>
455
- <svg v-else class="size-5 text-emerald-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
787
+ <svg v-if="!copied" class="size-3.5 text-gray-400 group-hover:text-gray-300" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M14.25 5.25H7.25C6.14543 5.25 5.25 6.14543 5.25 7.25V14.25C5.25 15.3546 6.14543 16.25 7.25 16.25H14.25C15.3546 16.25 16.25 15.3546 16.25 14.25V7.25C16.25 6.14543 15.3546 5.25 14.25 5.25Z" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M2.80103 11.998L1.77203 5.07397C1.61003 3.98097 2.36403 2.96397 3.45603 2.80197L10.38 1.77297C11.313 1.63397 12.19 2.16297 12.528 3.00097" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /></svg>
788
+ <svg v-else class="size-3.5 text-emerald-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
456
789
  </button>
457
- <div
458
- v-show="sourceView === 'compiled'"
459
- class="shiki-line-numbers h-full overflow-auto [&_pre]:p-6 [&_pre]:pt-14 [&_pre]:text-base [&_pre]:leading-6 [&_pre]:min-h-full [&_pre]:overflow-x-auto"
460
- v-html="sourceHtml"
461
- />
462
- <div
463
- ref="vueSourceEl"
464
- v-show="sourceView === 'vue'"
465
- class="shiki-line-numbers h-full overflow-auto [&_pre]:p-6 [&_pre]:pt-14 [&_pre]:text-base [&_pre]:leading-6 [&_pre]:min-h-full [&_pre]:overflow-x-auto"
466
- v-html="vueSourceHtml"
467
- />
468
- <pre
469
- v-show="sourceView === 'plaintext'"
470
- class="h-full overflow-auto p-6 pt-14 text-sm leading-6 min-h-full text-gray-300 bg-[#27212e] whitespace-pre-wrap break-words"
471
- >{{ plaintextContent }}</pre>
790
+ <ScrollArea v-show="sourceView === 'compiled'" class="h-full [&_[data-slot=scroll-area-viewport]>div]:flex [&_[data-slot=scroll-area-viewport]>div]:flex-col [&_[data-slot=scroll-area-viewport]>div]:min-h-full">
791
+ <div
792
+ ref="compiledSourceEl"
793
+ class="flex-1 bg-[#27212e] dark:bg-gray-950 shiki-line-numbers [&_pre]:p-6 [&_pre]:pt-14 [&_pre]:text-base [&_pre]:leading-6 [&_pre]:min-h-full dark:[&_pre]:bg-gray-950!"
794
+ v-html="sourceHtml"
795
+ />
796
+ <ScrollBar orientation="horizontal" />
797
+ </ScrollArea>
798
+ <ScrollArea v-show="sourceView === 'vue'" class="h-full [&_[data-slot=scroll-area-viewport]>div]:flex [&_[data-slot=scroll-area-viewport]>div]:flex-col [&_[data-slot=scroll-area-viewport]>div]:min-h-full">
799
+ <div
800
+ ref="vueSourceEl"
801
+ class="flex-1 bg-[#27212e] dark:bg-gray-950 shiki-line-numbers [&_pre]:p-6 [&_pre]:pt-14 [&_pre]:text-base [&_pre]:leading-6 [&_pre]:min-h-full dark:[&_pre]:bg-gray-950!"
802
+ v-html="vueSourceHtml"
803
+ />
804
+ <ScrollBar orientation="horizontal" />
805
+ </ScrollArea>
806
+ <ScrollArea v-show="sourceView === 'plaintext'" class="h-full [&_[data-slot=scroll-area-viewport]>div]:flex [&_[data-slot=scroll-area-viewport]>div]:flex-col [&_[data-slot=scroll-area-viewport]>div]:min-h-full">
807
+ <pre
808
+ ref="plaintextEl"
809
+ class="p-6 pt-14 text-sm leading-6 flex-1 text-gray-300 bg-[#27212e] dark:bg-gray-950 whitespace-pre-wrap break-words"
810
+ >{{ plaintextContent }}</pre>
811
+ </ScrollArea>
472
812
  </div>
473
813
 
814
+ <!-- Blocks iframe from stealing pointer events while dragging tabs -->
815
+ <div v-if="tabsDragging" class="fixed inset-0 z-50" />
816
+
474
817
  <!-- Preview view -->
475
818
  <div v-show="viewMode !== 'source'" class="absolute inset-0">
476
819
  <div class="relative h-full opacity-5" :style="stripeBg" />
477
820
  </div>
478
821
 
479
- <div v-show="viewMode !== 'source'" ref="containerEl" class="absolute inset-0 z-10 flex flex-col">
480
- <div class="flex-1 min-h-0">
481
- <ResizablePanelGroup direction="vertical" class="h-full" @layout="onVerticalLayout">
482
- <ResizablePanel ref="topPanel" :default-size="0" @resize="(s: number) => { sideSizes.top = s; updateFullSize() }" />
483
- <ResizableHandle class="h-4! bg-gray-50 hover:bg-gray-100 dark:bg-white/5 dark:hover:bg-white/10 transition-colors after:hidden!" @dragging="(v: boolean) => v ? onVDragStart() : onVDragEnd()" />
484
- <ResizablePanel :default-size="100" :min-size="20">
485
- <ResizablePanelGroup direction="horizontal" class="h-full" @layout="onHorizontalLayout">
486
- <ResizablePanel ref="leftPanel" :default-size="0" @resize="(s: number) => { sideSizes.left = s; updateFullSize() }" />
487
- <ResizableHandle class="w-4 bg-gray-50 hover:bg-gray-100 dark:bg-white/5 dark:hover:bg-white/10 transition-colors after:hidden!" @dragging="(v: boolean) => v ? onHDragStart() : onHDragEnd()" />
488
- <ResizablePanel ref="previewEl" :default-size="100" :min-size="20">
489
- <iframe
490
- ref="iframeEl"
491
- :srcdoc="srcdoc"
492
- class="h-full w-full border-0 bg-white"
493
- />
494
- </ResizablePanel>
495
- <ResizableHandle class="w-4 bg-gray-50 hover:bg-gray-100 dark:bg-white/5 dark:hover:bg-white/10 transition-colors after:hidden!" @dragging="(v: boolean) => v ? onHDragStart() : onHDragEnd()" />
496
- <ResizablePanel ref="rightPanel" :default-size="0" @resize="(s: number) => { sideSizes.right = s; updateFullSize() }" />
497
- </ResizablePanelGroup>
498
- </ResizablePanel>
499
- <ResizableHandle class="h-4! bg-gray-50 hover:bg-gray-100 dark:bg-white/5 dark:hover:bg-white/10 transition-colors after:hidden!" @dragging="(v: boolean) => v ? onVDragStart() : onVDragEnd()" />
500
- <ResizablePanel ref="bottomPanel" :default-size="0" @resize="(s: number) => { sideSizes.bottom = s; updateFullSize() }" />
501
- </ResizablePanelGroup>
822
+ <div v-show="viewMode !== 'source'" ref="containerEl" class="absolute inset-0 z-10 flex items-center justify-center">
823
+ <!-- Blocks iframe from stealing pointer events while dragging -->
824
+ <div v-if="isDragging" class="absolute inset-0 z-20" />
825
+ <div
826
+ class="relative"
827
+ :style="{
828
+ width: iframeWidth != null ? `${iframeWidth + 40}px` : '100%',
829
+ height: iframeHeight != null ? `${iframeHeight + 40}px` : '100%',
830
+ transition: isDragging ? 'none' : 'width 0.2s ease, height 0.2s ease',
831
+ }"
832
+ >
833
+ <!-- Top handle -->
834
+ <div class="group hidden min-[430px]:flex absolute top-0 left-5 right-5 h-5 items-center justify-center cursor-ns-resize" @mousedown="onEdgeDrag($event, 'top')" @touchstart.prevent="onEdgeDrag($event, 'top')">
835
+ <div class="h-1 w-12 rounded-full bg-gray-300 dark:bg-gray-600 group-hover:bg-gray-400 group-active:bg-gray-500 dark:group-hover:bg-gray-500 dark:group-active:bg-gray-400 transition-colors" />
836
+ </div>
837
+ <!-- Bottom handle -->
838
+ <div class="group hidden min-[430px]:flex absolute bottom-0 left-5 right-5 h-5 items-center justify-center cursor-ns-resize" @mousedown="onEdgeDrag($event, 'bottom')" @touchstart.prevent="onEdgeDrag($event, 'bottom')">
839
+ <div class="h-1 w-12 rounded-full bg-gray-300 dark:bg-gray-600 group-hover:bg-gray-400 group-active:bg-gray-500 dark:group-hover:bg-gray-500 dark:group-active:bg-gray-400 transition-colors" />
840
+ </div>
841
+ <!-- Left handle -->
842
+ <div class="group hidden min-[430px]:flex absolute left-0 top-5 bottom-5 w-5 items-center justify-center cursor-ew-resize" @mousedown="onEdgeDrag($event, 'left')" @touchstart.prevent="onEdgeDrag($event, 'left')">
843
+ <div class="w-1 h-12 rounded-full bg-gray-300 dark:bg-gray-600 group-hover:bg-gray-400 group-active:bg-gray-500 dark:group-hover:bg-gray-500 dark:group-active:bg-gray-400 transition-colors" />
844
+ </div>
845
+ <!-- Right handle -->
846
+ <div class="group hidden min-[430px]:flex absolute right-0 top-5 bottom-5 w-5 items-center justify-center cursor-ew-resize" @mousedown="onEdgeDrag($event, 'right')" @touchstart.prevent="onEdgeDrag($event, 'right')">
847
+ <div class="w-1 h-12 rounded-full bg-gray-300 dark:bg-gray-600 group-hover:bg-gray-400 group-active:bg-gray-500 dark:group-hover:bg-gray-500 dark:group-active:bg-gray-400 transition-colors" />
848
+ </div>
849
+ <!-- Iframe -->
850
+ <div ref="wrapperEl" class="absolute inset-0 min-[430px]:inset-5 border border-gray-200 dark:border-gray-800">
851
+ <ScrollArea class="h-full w-full bg-white dark:bg-gray-950">
852
+ <iframe
853
+ ref="iframeEl"
854
+ :srcdoc="srcdoc"
855
+ @load="onIframeLoad"
856
+ class="w-full border-0 bg-white dark:bg-gray-950"
857
+ :style="{ height: iframeContentHeight ? `${iframeContentHeight}px` : '100%' }"
858
+ />
859
+ </ScrollArea>
860
+ </div>
502
861
  </div>
503
862
  </div>
504
863
  </div>
505
864
 
506
- <!-- Tabs panel (always visible) -->
865
+ <!-- Tabs panel (overlay) -->
507
866
  <div
508
- class="shrink-0 bg-white dark:bg-gray-950 overflow-hidden"
509
- :class="!tabsDragging ? 'transition-[height] duration-200 ease-in-out' : ''"
867
+ class="absolute bottom-0 left-0 right-0 z-20 overflow-hidden border-t border-gray-200 dark:border-gray-800/50"
868
+ :class="[
869
+ !tabsDragging ? 'transition-[height] duration-200 ease-in-out' : '',
870
+ 'bg-white dark:bg-gray-950',
871
+ ]"
510
872
  :style="{ height: `${tabsPanelHeight}px` }"
511
873
  >
512
874
  <div
513
- class="relative h-px bg-gray-200 dark:bg-gray-800 cursor-row-resize before:absolute before:-top-2 before:left-0 before:right-0 before:h-5 before:content-['']"
875
+ class="relative h-0 cursor-row-resize before:absolute before:top-0 before:left-0 before:right-0 before:h-3.25 before:content-['']"
514
876
  @mousedown="onTabsDragStart"
877
+ @touchstart.prevent="onTabsDragStart"
515
878
  />
516
879
  <Tabs :model-value="activeTab" class="flex flex-col min-h-0 h-full">
517
- <div class="flex items-center justify-between min-h-10 px-4 shrink-0" :class="bottomPanelOpen ? 'border-b' : ''">
880
+ <div class="flex items-center justify-between min-h-10 pl-2 pr-3 shrink-0" :class="bottomPanelOpen ? 'border-b' : ''">
518
881
  <TabsList class="h-full bg-transparent! rounded-none! p-0 gap-1">
519
- <TabsTrigger value="compatibility" class="text-xs px-3 h-full rounded-none! border-0! shadow-none! border-b! border-transparent data-[state=active]:border-gray-400 data-[state=active]:dark:border-gray-600 data-[state=active]:bg-transparent data-[state=inactive]:bg-transparent" @click="onTabClick('compatibility')">
520
- Compatibility
882
+ <TabsTrigger v-if="!compatibilityDisabled" value="compatibility" class="text-xs font-normal px-3 h-full rounded-none! border-0! shadow-none! border-b! border-transparent select-none data-[state=active]:border-gray-400 data-[state=active]:dark:border-gray-600 data-[state=active]:bg-transparent data-[state=inactive]:bg-transparent dark:bg-transparent! dark:hover:bg-transparent!" @click="onTabClick('compatibility')">
883
+ Checks
521
884
  </TabsTrigger>
522
- <TabsTrigger value="lint" class="text-xs px-3 h-full rounded-none! border-0! shadow-none! border-b! border-transparent data-[state=active]:border-gray-400 data-[state=active]:dark:border-gray-600 data-[state=active]:bg-transparent data-[state=inactive]:bg-transparent" @click="onTabClick('lint')">
523
- Linter
524
- </TabsTrigger>
525
- <TabsTrigger value="stats" class="text-xs px-3 h-full rounded-none! border-0! shadow-none! border-b! border-transparent data-[state=active]:border-gray-400 data-[state=active]:dark:border-gray-600 data-[state=active]:bg-transparent data-[state=inactive]:bg-transparent" @click="onTabClick('stats')">
885
+ <TabsTrigger value="stats" class="text-xs font-normal px-3 h-full rounded-none! border-0! shadow-none! border-b! border-transparent select-none data-[state=active]:border-gray-400 data-[state=active]:dark:border-gray-600 data-[state=active]:bg-transparent data-[state=inactive]:bg-transparent dark:bg-transparent! dark:hover:bg-transparent!" @click="onTabClick('stats')">
526
886
  Stats
527
887
  </TabsTrigger>
888
+ <TabsTrigger value="test" class="text-xs font-normal px-3 h-full rounded-none! border-0! shadow-none! border-b! border-transparent select-none data-[state=active]:border-gray-400 data-[state=active]:dark:border-gray-600 data-[state=active]:bg-transparent data-[state=inactive]:bg-transparent dark:bg-transparent! dark:hover:bg-transparent!" @click="onTabClick('test')">
889
+ Test
890
+ </TabsTrigger>
528
891
  </TabsList>
529
892
  <Button variant="ghost" size="icon" class="h-7 w-7 hover:bg-transparent!" @click="toggleBottomPanel">
530
- <ChevronUp v-if="!bottomPanelOpen" class="size-4" />
531
- <ChevronDown v-else class="size-4" />
893
+ <ChevronUp v-if="!bottomPanelOpen" class="size-4 dark:text-gray-400" :stroke-width="1" />
894
+ <ChevronDown v-else class="size-4 dark:text-gray-400" :stroke-width="1" />
532
895
  </Button>
533
896
  </div>
534
- <div class="flex-1 overflow-auto">
535
- <TabsContent value="compatibility" class="mt-0">
536
- <p v-if="compatibilityLoading" class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">Checking compatibility...</p>
537
- <p v-else-if="compatibilityIssues.length === 0" class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">No compatibility issues found.</p>
538
- <ul v-else class="text-xs divide-y">
539
- <li
540
- v-for="(issue, i) in compatibilityIssues"
541
- :key="i"
542
- class="px-4 py-2 hover:bg-gray-50 dark:hover:bg-white/5"
897
+ <div class="flex-1 min-h-0">
898
+ <TabsContent value="compatibility" class="mt-0 h-full flex flex-col"><div v-if="!compatibilityLoading && !compatibilityError && compatibilityIssues.length > 0" class="flex gap-1 pl-3 pr-4 py-2 border-b border-gray-200 dark:border-white/10 shrink-0">
899
+ <button
900
+ v-for="cat in activeCompatibilityCategories"
901
+ :key="cat"
902
+ class="px-2 py-0.5 text-[11px] rounded-full cursor-default transition-colors"
903
+ :class="compatibilityCategory === cat
904
+ ? 'bg-gray-900 text-white dark:bg-gray-600 dark:text-gray-100'
905
+ : 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-white/10'"
906
+ @click="compatibilityCategory = cat"
543
907
  >
544
- <div class="flex items-start justify-between gap-4">
545
- <div>
546
- <a v-if="issue.url" :href="issue.url" target="_blank" rel="noopener" class="font-medium hover:underline" :class="issue.type === 'error' ? 'text-red-600' : 'text-amber-600'">
547
- {{ issue.title }}
548
- </a>
549
- <span v-else class="font-medium" :class="issue.type === 'error' ? 'text-red-600' : 'text-amber-600'">
550
- {{ issue.title }}
551
- </span>
552
- <div class="text-gray-500 dark:text-gray-400 mt-1 space-y-0.5">
553
- <div v-for="client in issue.clients" :key="client.name">
554
- <span class="text-gray-700 dark:text-gray-300">{{ client.name }}</span><span v-if="client.notes.length">: {{ client.notes.join('. ') }}</span>
908
+ {{ cat === 'css' ? 'CSS' : cat === 'html' ? 'HTML' : cat.charAt(0).toUpperCase() + cat.slice(1) }}
909
+ <span class="ml-0.5 tabular-nums">{{ compatibilityIssues.filter(i => i.category === cat).length }}</span>
910
+ </button>
911
+ </div>
912
+ <ScrollArea class="h-full flex-1 min-h-0 pl-5">
913
+ <p v-if="compatibilityLoading" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">Running checks...</p>
914
+ <p v-else-if="compatibilityError" class="pr-4 py-3 text-xs text-red-500 dark:text-red-400">{{ compatibilityError }}</p>
915
+ <p v-else-if="compatibilityIssues.length === 0" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">No issues found.</p>
916
+ <ul v-else class="text-xs divide-y">
917
+ <li
918
+ v-for="(issue, i) in filteredCompatibilityIssues"
919
+ :key="i"
920
+ class="pr-4 py-2"
921
+ >
922
+ <div class="flex items-center justify-between gap-4">
923
+ <div>
924
+ <a v-if="issue.url" :href="issue.url" target="_blank" rel="noopener" class="font-medium hover:underline" :class="issueColorClass(issue)">
925
+ {{ issue.title }}
926
+ </a>
927
+ <span v-else class="font-medium" :class="issueColorClass(issue)">
928
+ {{ issue.title }}
929
+ </span>
930
+ <div class="text-gray-500 dark:text-gray-400 mt-0.5">
931
+ <template v-if="issue.kind === 'lint'">
932
+ <template v-for="(seg, j) in messageSegments(issue.message)" :key="j">
933
+ <code v-if="seg.code" class="px-1 py-0.5 rounded bg-gray-100 dark:bg-white/10 font-mono text-[11px]">{{ seg.text }}</code>
934
+ <template v-else>{{ seg.text }}</template>
935
+ </template>
936
+ </template>
937
+ <template v-else>
938
+ {{ supportPrefix(issue) }}
939
+ <template v-if="(issue.affectedClients?.length ?? 0) <= 4 || expandedIssueKeys.has(issueKey(issue, i))">
940
+ {{ (issue.affectedClients ?? []).join(', ') }}
941
+ </template>
942
+ <template v-else>
943
+ {{ issue.affectedClients!.slice(0, 4).join(', ') }}
944
+ <button class="underline cursor-pointer hover:text-gray-700 dark:hover:text-gray-200" @click="expandedIssueKeys.add(issueKey(issue, i)); expandedIssueKeys = new Set(expandedIssueKeys)">
945
+ + {{ issue.affectedClients!.length - 4 }} others
946
+ </button>
947
+ </template>
948
+ </template>
555
949
  </div>
556
950
  </div>
951
+ <button v-if="issue.line" class="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 cursor-pointer tabular-nums shrink-0" @click="openInEditor(issue.file, issue.line!)">{{ isCurrentFile(issue) ? `L${issue.line}` : `${componentName(issue.file)}:${issue.line}` }}</button>
557
952
  </div>
558
- <button v-if="issue.line" class="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 cursor-pointer tabular-nums shrink-0" @click="goToLine(issue.line!)">L{{ issue.line }}</button>
559
- </div>
560
- </li>
561
- </ul>
953
+ </li>
954
+ </ul>
955
+ </ScrollArea>
562
956
  </TabsContent>
563
- <TabsContent value="lint" class="mt-0">
564
- <p v-if="lintLoading" class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">Linting...</p>
565
- <p v-else-if="lintIssues.length === 0" class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">No issues found.</p>
566
- <ul v-else class="text-xs divide-y">
567
- <li
568
- v-for="(issue, i) in lintIssues"
569
- :key="i"
570
- class="px-4 py-2 hover:bg-gray-50 dark:hover:bg-white/5"
571
- >
572
- <div class="flex items-start justify-between gap-4">
573
- <div>
574
- <span class="font-medium" :class="issue.type === 'error' ? 'text-red-600' : 'text-amber-600'">
575
- {{ issue.title }}
576
- </span>
577
- <div class="text-gray-500 dark:text-gray-400 mt-0.5">{{ issue.message }}</div>
957
+ <TabsContent value="stats" class="mt-0 h-full">
958
+ <ScrollArea class="h-full pl-5">
959
+ <p v-if="statsLoading" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">Loading stats...</p>
960
+ <p v-else-if="!stats" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">No stats available.</p>
961
+ <ul v-else class="text-xs divide-y divide-gray-200 dark:divide-white/10">
962
+ <li class="pr-4 py-2">
963
+ <div class="flex items-center justify-between gap-4">
964
+ <div>
965
+ <span class="font-medium" :class="stats.size.bytes > 102400 ? 'text-red-600' : stats.size.bytes > 51200 ? 'text-amber-600' : 'text-gray-900 dark:text-gray-300'">Size</span>
966
+ <div class="text-gray-500 dark:text-gray-400 mt-0.5">Compiled HTML size. Gmail clips emails larger than ~100KB.</div>
967
+ </div>
968
+ <span class="font-medium tabular-nums shrink-0" :class="stats.size.bytes > 102400 ? 'text-red-600' : stats.size.bytes > 51200 ? 'text-amber-600' : 'text-gray-900 dark:text-gray-300'">{{ stats.size.formatted }}</span>
578
969
  </div>
579
- <button v-if="issue.line" class="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 cursor-pointer tabular-nums shrink-0" @click="goToLine(issue.line!)">L{{ issue.line }}</button>
580
- </div>
581
- </li>
582
- </ul>
970
+ </li>
971
+ <li class="pr-4 py-2">
972
+ <div class="flex items-center justify-between gap-4">
973
+ <div>
974
+ <span class="font-medium text-gray-900 dark:text-gray-300">Images</span>
975
+ <div class="text-gray-500 dark:text-gray-400 mt-0.5">Total from &lt;img&gt; tags and CSS background images.</div>
976
+ </div>
977
+ <span class="font-medium tabular-nums shrink-0">{{ stats.images }}</span>
978
+ </div>
979
+ </li>
980
+ <li class="pr-4 py-2">
981
+ <div class="flex items-center justify-between gap-4">
982
+ <div>
983
+ <span class="font-medium text-gray-900 dark:text-gray-300">Links</span>
984
+ <div class="text-gray-500 dark:text-gray-400 mt-0.5">Total &lt;a&gt; tags with an href attribute.</div>
985
+ </div>
986
+ <span class="font-medium tabular-nums shrink-0">{{ stats.links }}</span>
987
+ </div>
988
+ </li>
989
+ </ul>
990
+ </ScrollArea>
583
991
  </TabsContent>
584
- <TabsContent value="stats" class="mt-0">
585
- <p v-if="statsLoading" class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">Loading stats...</p>
586
- <p v-else-if="!stats" class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">No stats available.</p>
587
- <div v-else class="px-4 py-3 flex items-center gap-6 text-xs">
588
- <div class="flex items-center gap-1.5">
589
- <span class="text-gray-500 dark:text-gray-400">Size</span>
590
- <span
591
- class="font-medium tabular-nums"
592
- :class="stats.size.bytes > 102400 ? 'text-red-600' : stats.size.bytes > 51200 ? 'text-amber-600' : 'text-gray-900 dark:text-gray-100'"
593
- >{{ stats.size.formatted }}</span>
594
- </div>
595
- <div class="flex items-center gap-1.5">
596
- <span class="text-gray-500 dark:text-gray-400">Images</span>
597
- <span class="font-medium tabular-nums">{{ stats.images }}</span>
598
- </div>
599
- <div class="flex items-center gap-1.5">
600
- <span class="text-gray-500 dark:text-gray-400">Links</span>
601
- <span class="font-medium tabular-nums">{{ stats.links }}</span>
992
+ <TabsContent value="test" class="mt-0 h-full">
993
+ <ScrollArea class="h-full pl-5">
994
+ <div class="pr-4 py-3 max-w-md">
995
+ <div class="space-y-2">
996
+ <div class="flex items-center gap-2">
997
+ <label for="email-to" class="text-xs text-gray-500 dark:text-gray-400 w-12 shrink-0 cursor-pointer">To</label>
998
+ <TagsInput v-model="emailTo" delimiter=" " add-on-paste add-on-blur class="flex-1 min-h-7 gap-1 px-2 py-1">
999
+ <TagsInputItem v-for="item in emailTo" :key="item" :value="item" class="h-5 text-xs rounded">
1000
+ <TagsInputItemText class="px-1.5 py-0 text-xs" />
1001
+ <TagsInputItemDelete class="size-3.5" />
1002
+ </TagsInputItem>
1003
+ <TagsInputInput id="email-to" class="text-xs min-h-5 px-0.5" placeholder="Add emails..." />
1004
+ </TagsInput>
1005
+ </div>
1006
+ <div class="flex items-center gap-2">
1007
+ <label for="email-subject" class="text-xs text-gray-500 dark:text-gray-400 w-12 shrink-0 cursor-pointer">Subject</label>
1008
+ <div class="flex-1 flex items-center gap-3">
1009
+ <Input id="email-subject" v-model="emailSubject" :placeholder="String(route.params.template)" class="flex-1 h-7 text-xs! px-2" />
1010
+ <label class="flex items-center gap-1.5 cursor-pointer select-none shrink-0">
1011
+ <Checkbox v-model="emailPreventThreading" :default-checked="true" class="size-3.5" />
1012
+ <span class="text-xs text-gray-500 dark:text-gray-400">Prevent threading</span>
1013
+ </label>
1014
+ </div>
1015
+ </div>
1016
+ </div>
1017
+ <div class="flex items-center gap-3 mt-3">
1018
+ <Button
1019
+ size="sm"
1020
+ class="h-7 text-xs px-3"
1021
+ :disabled="!emailTo.length || emailSending"
1022
+ @click="sendTestEmail"
1023
+ >
1024
+ <svg v-if="emailSending" class="size-3.5 animate-spin [animation-duration:0.6s]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" /><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" /></svg>
1025
+ {{ emailSending ? 'Sending' : 'Send' }}
1026
+ </Button>
1027
+ </div>
1028
+ <div v-if="emailResult" class="mt-2">
1029
+ <p class="text-xs" :class="emailResult.success ? 'text-gray-950 dark:text-white' : 'text-red-600'">
1030
+ {{ emailResult.message }}
1031
+ <a
1032
+ v-if="emailResult.previewUrl"
1033
+ :href="emailResult.previewUrl"
1034
+ target="_blank"
1035
+ rel="noopener"
1036
+ class="text-gray-500 dark:text-gray-400 hover:underline"
1037
+ >
1038
+ (view)
1039
+ </a>
1040
+ </p>
1041
+ </div>
602
1042
  </div>
603
- </div>
1043
+ </ScrollArea>
604
1044
  </TabsContent>
605
1045
  </div>
606
1046
  </Tabs>