@maizzle/framework 6.0.0-rc.1 → 6.0.0-rc.11

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 (347) hide show
  1. package/bin/maizzle.mjs +1 -1
  2. package/dist/_virtual/_rolldown/runtime.mjs +32 -0
  3. package/dist/build.mjs +6 -3
  4. package/dist/build.mjs.map +1 -1
  5. package/dist/components/Body.vue +111 -0
  6. package/dist/components/Button.vue +68 -14
  7. package/dist/components/CodeBlock.vue +68 -0
  8. package/dist/components/CodeInline.vue +49 -0
  9. package/dist/components/Column.vue +78 -0
  10. package/dist/components/Container.vue +53 -0
  11. package/dist/components/Divider.vue +28 -0
  12. package/dist/components/Head.vue +30 -0
  13. package/dist/components/Heading.vue +28 -0
  14. package/dist/components/Html.vue +104 -0
  15. package/dist/components/Img.vue +70 -0
  16. package/dist/components/Layout.vue +93 -0
  17. package/dist/components/Link.vue +26 -0
  18. package/dist/components/Markdown.vue +89 -0
  19. package/dist/components/Outlook.vue +36 -0
  20. package/dist/components/Overlap.vue +84 -0
  21. package/dist/components/Preheader.vue +20 -0
  22. package/dist/components/Row.vue +91 -0
  23. package/dist/components/Section.vue +83 -0
  24. package/dist/components/Spacer.vue +50 -7
  25. package/dist/components/Text.vue +29 -0
  26. package/dist/components/Vml.vue +165 -13
  27. package/dist/composables/renderContext.d.mts +5 -0
  28. package/dist/composables/renderContext.d.mts.map +1 -1
  29. package/dist/composables/renderContext.mjs.map +1 -1
  30. package/dist/composables/usePreheader.d.mts +24 -0
  31. package/dist/composables/usePreheader.d.mts.map +1 -0
  32. package/dist/composables/usePreheader.mjs +29 -0
  33. package/dist/composables/usePreheader.mjs.map +1 -0
  34. package/dist/config/defaults.mjs +1 -3
  35. package/dist/config/defaults.mjs.map +1 -1
  36. package/dist/config/index.mjs +7 -0
  37. package/dist/config/index.mjs.map +1 -1
  38. package/dist/index.d.mts +4 -2
  39. package/dist/index.mjs +3 -1
  40. package/dist/node_modules/picomatch/index.mjs +13 -0
  41. package/dist/node_modules/picomatch/index.mjs.map +1 -0
  42. package/dist/node_modules/picomatch/lib/constants.mjs +174 -0
  43. package/dist/node_modules/picomatch/lib/constants.mjs.map +1 -0
  44. package/dist/node_modules/picomatch/lib/parse.mjs +1067 -0
  45. package/dist/node_modules/picomatch/lib/parse.mjs.map +1 -0
  46. package/dist/node_modules/picomatch/lib/picomatch.mjs +304 -0
  47. package/dist/node_modules/picomatch/lib/picomatch.mjs.map +1 -0
  48. package/dist/node_modules/picomatch/lib/scan.mjs +296 -0
  49. package/dist/node_modules/picomatch/lib/scan.mjs.map +1 -0
  50. package/dist/node_modules/picomatch/lib/utils.mjs +53 -0
  51. package/dist/node_modules/picomatch/lib/utils.mjs.map +1 -0
  52. package/dist/plugin.d.mts.map +1 -1
  53. package/dist/plugin.mjs +24 -7
  54. package/dist/plugin.mjs.map +1 -1
  55. package/dist/plugins/postcss/resolveProps.d.mts +8 -0
  56. package/dist/plugins/postcss/resolveProps.d.mts.map +1 -0
  57. package/dist/plugins/postcss/resolveProps.mjs +144 -0
  58. package/dist/plugins/postcss/resolveProps.mjs.map +1 -0
  59. package/dist/plugins/postcss/tailwindCleanup.d.mts.map +1 -1
  60. package/dist/plugins/postcss/tailwindCleanup.mjs +46 -13
  61. package/dist/plugins/postcss/tailwindCleanup.mjs.map +1 -1
  62. package/dist/render/createRenderer.d.mts +8 -3
  63. package/dist/render/createRenderer.d.mts.map +1 -1
  64. package/dist/render/createRenderer.mjs +146 -10
  65. package/dist/render/createRenderer.mjs.map +1 -1
  66. package/dist/render/index.mjs +6 -3
  67. package/dist/render/index.mjs.map +1 -1
  68. package/dist/serve.d.mts.map +1 -1
  69. package/dist/serve.mjs +157 -63
  70. package/dist/serve.mjs.map +1 -1
  71. package/dist/server/compatibility.d.mts +1 -2
  72. package/dist/server/compatibility.d.mts.map +1 -1
  73. package/dist/server/compatibility.mjs +30 -16
  74. package/dist/server/compatibility.mjs.map +1 -1
  75. package/dist/server/email.d.mts +17 -0
  76. package/dist/server/email.d.mts.map +1 -0
  77. package/dist/server/email.mjs +41 -0
  78. package/dist/server/email.mjs.map +1 -0
  79. package/dist/server/linter.d.mts +1 -2
  80. package/dist/server/linter.d.mts.map +1 -1
  81. package/dist/server/linter.mjs +60 -71
  82. package/dist/server/linter.mjs.map +1 -1
  83. package/dist/server/ui/App.vue +206 -70
  84. package/dist/server/ui/components/ui/checkbox/Checkbox.vue +35 -0
  85. package/dist/server/ui/components/ui/checkbox/index.ts +1 -0
  86. package/dist/server/ui/components/ui/command/CommandDialog.vue +1 -1
  87. package/dist/server/ui/components/ui/command/CommandInput.vue +19 -1
  88. package/dist/server/ui/components/ui/command/CommandItem.vue +1 -1
  89. package/dist/server/ui/components/ui/command/CommandList.vue +1 -1
  90. package/dist/server/ui/components/ui/command/CommandShortcut.vue +1 -1
  91. package/dist/server/ui/components/ui/dialog/DialogOverlay.vue +9 -1
  92. package/dist/server/ui/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
  93. package/dist/server/ui/components/ui/scroll-area/ScrollBar.vue +1 -1
  94. package/dist/server/ui/components/ui/sheet/SheetContent.vue +1 -1
  95. package/dist/server/ui/components/ui/sheet/SheetOverlay.vue +9 -1
  96. package/dist/server/ui/components/ui/sidebar/Sidebar.vue +8 -1
  97. package/dist/server/ui/components/ui/sidebar/SidebarProvider.vue +1 -1
  98. package/dist/server/ui/components/ui/sidebar/SidebarTrigger.vue +5 -4
  99. package/dist/server/ui/components/ui/tags-input/TagsInput.vue +26 -0
  100. package/dist/server/ui/components/ui/tags-input/TagsInputInput.vue +17 -0
  101. package/dist/server/ui/components/ui/tags-input/TagsInputItem.vue +19 -0
  102. package/dist/server/ui/components/ui/tags-input/TagsInputItemDelete.vue +22 -0
  103. package/dist/server/ui/components/ui/tags-input/TagsInputItemText.vue +17 -0
  104. package/dist/server/ui/components/ui/tags-input/index.ts +5 -0
  105. package/dist/server/ui/components/ui/toggle/index.ts +3 -3
  106. package/dist/server/ui/components/ui/toggle-group/ToggleGroup.vue +1 -1
  107. package/dist/server/ui/components/ui/toggle-group/ToggleGroupItem.vue +2 -2
  108. package/dist/server/ui/main.css +20 -20
  109. package/dist/server/ui/pages/Home.vue +12 -5
  110. package/dist/server/ui/pages/Preview.vue +506 -216
  111. package/dist/transformers/entities.d.mts.map +1 -1
  112. package/dist/transformers/entities.mjs +3 -0
  113. package/dist/transformers/entities.mjs.map +1 -1
  114. package/dist/transformers/filters/defaults.d.mts +6 -0
  115. package/dist/transformers/filters/defaults.d.mts.map +1 -0
  116. package/dist/transformers/filters/defaults.mjs +78 -0
  117. package/dist/transformers/filters/defaults.mjs.map +1 -0
  118. package/dist/transformers/filters/index.d.mts +22 -0
  119. package/dist/transformers/filters/index.d.mts.map +1 -0
  120. package/dist/transformers/filters/index.mjs +67 -0
  121. package/dist/transformers/filters/index.mjs.map +1 -0
  122. package/dist/transformers/index.d.mts +11 -9
  123. package/dist/transformers/index.d.mts.map +1 -1
  124. package/dist/transformers/index.mjs +19 -11
  125. package/dist/transformers/index.mjs.map +1 -1
  126. package/dist/transformers/inlineCSS.d.mts +1 -14
  127. package/dist/transformers/inlineCSS.d.mts.map +1 -1
  128. package/dist/transformers/inlineCSS.mjs +32 -36
  129. package/dist/transformers/inlineCSS.mjs.map +1 -1
  130. package/dist/transformers/purgeCSS.d.mts.map +1 -1
  131. package/dist/transformers/purgeCSS.mjs +67 -1
  132. package/dist/transformers/purgeCSS.mjs.map +1 -1
  133. package/dist/transformers/sixHex.d.mts +16 -0
  134. package/dist/transformers/sixHex.d.mts.map +1 -0
  135. package/dist/transformers/sixHex.mjs +30 -0
  136. package/dist/transformers/sixHex.mjs.map +1 -0
  137. package/dist/transformers/tailwindcss.d.mts +6 -2
  138. package/dist/transformers/tailwindcss.d.mts.map +1 -1
  139. package/dist/transformers/tailwindcss.mjs +54 -30
  140. package/dist/transformers/tailwindcss.mjs.map +1 -1
  141. package/dist/types/config.d.mts +436 -23
  142. package/dist/types/config.d.mts.map +1 -1
  143. package/dist/types/index.d.mts +2 -2
  144. package/dist/utils/ast/serializer.d.mts +3 -2
  145. package/dist/utils/ast/serializer.d.mts.map +1 -1
  146. package/dist/utils/ast/serializer.mjs +24 -0
  147. package/dist/utils/ast/serializer.mjs.map +1 -1
  148. package/dist/utils/detect.d.mts +5 -0
  149. package/dist/utils/detect.d.mts.map +1 -0
  150. package/dist/utils/detect.mjs +11 -0
  151. package/dist/utils/detect.mjs.map +1 -0
  152. package/node_modules/@clack/core/CHANGELOG.md +87 -4
  153. package/node_modules/@clack/core/README.md +1 -1
  154. package/node_modules/@clack/core/dist/index.d.mts +186 -48
  155. package/node_modules/@clack/core/dist/index.mjs +10 -14
  156. package/node_modules/@clack/core/dist/index.mjs.map +1 -1
  157. package/node_modules/@clack/core/package.json +7 -9
  158. package/node_modules/@clack/prompts/CHANGELOG.md +171 -7
  159. package/node_modules/@clack/prompts/README.md +66 -3
  160. package/node_modules/@clack/prompts/dist/index.d.mts +302 -76
  161. package/node_modules/@clack/prompts/dist/index.mjs +134 -84
  162. package/node_modules/@clack/prompts/dist/index.mjs.map +1 -1
  163. package/node_modules/@clack/prompts/package.json +14 -10
  164. package/node_modules/citty/LICENSE +0 -15
  165. package/node_modules/citty/README.md +166 -69
  166. package/node_modules/citty/dist/index.d.mts +88 -56
  167. package/node_modules/citty/dist/index.mjs +399 -437
  168. package/node_modules/citty/package.json +28 -35
  169. package/node_modules/giget/README.md +59 -11
  170. package/node_modules/giget/dist/THIRD-PARTY-LICENSES.md +205 -0
  171. package/node_modules/giget/dist/_chunks/giget.mjs +508 -0
  172. package/node_modules/giget/dist/_chunks/libs/citty.mjs +269 -0
  173. package/node_modules/giget/dist/_chunks/libs/nypm.d.mts +1 -0
  174. package/node_modules/giget/dist/_chunks/libs/nypm.mjs +669 -0
  175. package/node_modules/giget/dist/_chunks/libs/tar.mjs +2931 -0
  176. package/node_modules/giget/dist/_chunks/rolldown-runtime.mjs +14 -0
  177. package/node_modules/giget/dist/cli.d.mts +1 -0
  178. package/node_modules/giget/dist/cli.mjs +89 -111
  179. package/node_modules/giget/dist/index.d.mts +46 -35
  180. package/node_modules/giget/dist/index.mjs +2 -22
  181. package/node_modules/giget/package.json +32 -45
  182. package/node_modules/maizzle/README.md +140 -0
  183. package/node_modules/maizzle/bin/maizzle.mjs +5 -0
  184. package/node_modules/maizzle/dist/commands/new.d.mts +7 -0
  185. package/node_modules/maizzle/dist/commands/new.mjs +253 -0
  186. package/node_modules/{@maizzle/cli → maizzle}/dist/index.d.mts +1 -1
  187. package/node_modules/maizzle/dist/index.mjs +44 -0
  188. package/node_modules/{commander → maizzle/node_modules/commander}/Readme.md +94 -67
  189. package/node_modules/{commander → maizzle/node_modules/commander}/lib/argument.js +5 -4
  190. package/node_modules/{commander → maizzle/node_modules/commander}/lib/command.js +154 -39
  191. package/node_modules/{commander → maizzle/node_modules/commander}/lib/help.js +77 -39
  192. package/node_modules/{commander → maizzle/node_modules/commander}/lib/option.js +16 -3
  193. package/node_modules/{commander → maizzle/node_modules/commander}/package-support.json +4 -1
  194. package/node_modules/{commander → maizzle/node_modules/commander}/package.json +8 -8
  195. package/node_modules/{commander → maizzle/node_modules/commander}/typings/index.d.ts +71 -3
  196. package/node_modules/{@maizzle/cli → maizzle}/package.json +14 -12
  197. package/node_modules/tinyexec/README.md +49 -3
  198. package/node_modules/tinyexec/dist/main.d.mts +25 -14
  199. package/node_modules/tinyexec/dist/main.mjs +148 -100
  200. package/node_modules/tinyexec/package.json +9 -8
  201. package/package.json +17 -13
  202. package/dist/server/ui/components/ui/resizable/ResizableHandle.vue +0 -30
  203. package/dist/server/ui/components/ui/resizable/ResizablePanel.vue +0 -21
  204. package/dist/server/ui/components/ui/resizable/ResizablePanelGroup.vue +0 -25
  205. package/dist/server/ui/components/ui/resizable/index.ts +0 -3
  206. package/node_modules/@clack/core/dist/index.cjs +0 -15
  207. package/node_modules/@clack/core/dist/index.cjs.map +0 -1
  208. package/node_modules/@clack/core/dist/index.d.cts +0 -211
  209. package/node_modules/@clack/core/dist/index.d.ts +0 -211
  210. package/node_modules/@clack/prompts/dist/index.cjs +0 -87
  211. package/node_modules/@clack/prompts/dist/index.cjs.map +0 -1
  212. package/node_modules/@clack/prompts/dist/index.d.cts +0 -165
  213. package/node_modules/@clack/prompts/dist/index.d.ts +0 -165
  214. package/node_modules/@maizzle/cli/README.md +0 -58
  215. package/node_modules/@maizzle/cli/dist/index.mjs +0 -42
  216. package/node_modules/citty/dist/index.cjs +0 -475
  217. package/node_modules/citty/dist/index.d.cts +0 -80
  218. package/node_modules/citty/dist/index.d.ts +0 -80
  219. package/node_modules/consola/LICENSE +0 -47
  220. package/node_modules/consola/README.md +0 -352
  221. package/node_modules/consola/basic.d.ts +0 -1
  222. package/node_modules/consola/browser.d.ts +0 -1
  223. package/node_modules/consola/core.d.ts +0 -1
  224. package/node_modules/consola/dist/basic.cjs +0 -32
  225. package/node_modules/consola/dist/basic.d.cts +0 -23
  226. package/node_modules/consola/dist/basic.d.mts +0 -21
  227. package/node_modules/consola/dist/basic.d.ts +0 -23
  228. package/node_modules/consola/dist/basic.mjs +0 -24
  229. package/node_modules/consola/dist/browser.cjs +0 -84
  230. package/node_modules/consola/dist/browser.d.cts +0 -23
  231. package/node_modules/consola/dist/browser.d.mts +0 -21
  232. package/node_modules/consola/dist/browser.d.ts +0 -23
  233. package/node_modules/consola/dist/browser.mjs +0 -76
  234. package/node_modules/consola/dist/chunks/prompt.cjs +0 -288
  235. package/node_modules/consola/dist/chunks/prompt.mjs +0 -280
  236. package/node_modules/consola/dist/core.cjs +0 -517
  237. package/node_modules/consola/dist/core.d.cts +0 -459
  238. package/node_modules/consola/dist/core.d.mts +0 -459
  239. package/node_modules/consola/dist/core.d.ts +0 -459
  240. package/node_modules/consola/dist/core.mjs +0 -512
  241. package/node_modules/consola/dist/index.cjs +0 -663
  242. package/node_modules/consola/dist/index.d.cts +0 -24
  243. package/node_modules/consola/dist/index.d.mts +0 -22
  244. package/node_modules/consola/dist/index.d.ts +0 -24
  245. package/node_modules/consola/dist/index.mjs +0 -651
  246. package/node_modules/consola/dist/shared/consola.DCGIlDNP.cjs +0 -75
  247. package/node_modules/consola/dist/shared/consola.DRwqZj3T.mjs +0 -72
  248. package/node_modules/consola/dist/shared/consola.DXBYu-KD.mjs +0 -288
  249. package/node_modules/consola/dist/shared/consola.DwRq1yyg.cjs +0 -312
  250. package/node_modules/consola/dist/utils.cjs +0 -64
  251. package/node_modules/consola/dist/utils.d.cts +0 -286
  252. package/node_modules/consola/dist/utils.d.mts +0 -286
  253. package/node_modules/consola/dist/utils.d.ts +0 -286
  254. package/node_modules/consola/dist/utils.mjs +0 -54
  255. package/node_modules/consola/lib/index.cjs +0 -10
  256. package/node_modules/consola/package.json +0 -136
  257. package/node_modules/consola/utils.d.ts +0 -1
  258. package/node_modules/create-maizzle/README.md +0 -86
  259. package/node_modules/create-maizzle/bin/create-maizzle.mjs +0 -4
  260. package/node_modules/create-maizzle/node_modules/@clack/core/CHANGELOG.md +0 -340
  261. package/node_modules/create-maizzle/node_modules/@clack/core/LICENSE +0 -9
  262. package/node_modules/create-maizzle/node_modules/@clack/core/README.md +0 -22
  263. package/node_modules/create-maizzle/node_modules/@clack/core/dist/index.d.mts +0 -349
  264. package/node_modules/create-maizzle/node_modules/@clack/core/dist/index.mjs +0 -11
  265. package/node_modules/create-maizzle/node_modules/@clack/core/dist/index.mjs.map +0 -1
  266. package/node_modules/create-maizzle/node_modules/@clack/core/package.json +0 -60
  267. package/node_modules/create-maizzle/node_modules/@clack/prompts/CHANGELOG.md +0 -576
  268. package/node_modules/create-maizzle/node_modules/@clack/prompts/LICENSE +0 -9
  269. package/node_modules/create-maizzle/node_modules/@clack/prompts/README.md +0 -270
  270. package/node_modules/create-maizzle/node_modules/@clack/prompts/dist/index.d.mts +0 -391
  271. package/node_modules/create-maizzle/node_modules/@clack/prompts/dist/index.mjs +0 -137
  272. package/node_modules/create-maizzle/node_modules/@clack/prompts/dist/index.mjs.map +0 -1
  273. package/node_modules/create-maizzle/node_modules/@clack/prompts/package.json +0 -65
  274. package/node_modules/create-maizzle/package.json +0 -47
  275. package/node_modules/create-maizzle/src/index.js +0 -242
  276. package/node_modules/defu/LICENSE +0 -21
  277. package/node_modules/defu/README.md +0 -171
  278. package/node_modules/defu/dist/defu.cjs +0 -77
  279. package/node_modules/defu/dist/defu.d.cts +0 -31
  280. package/node_modules/defu/dist/defu.d.mts +0 -29
  281. package/node_modules/defu/dist/defu.d.ts +0 -31
  282. package/node_modules/defu/dist/defu.mjs +0 -69
  283. package/node_modules/defu/lib/defu.cjs +0 -10
  284. package/node_modules/defu/lib/defu.d.cts +0 -12
  285. package/node_modules/defu/package.json +0 -48
  286. package/node_modules/giget/dist/shared/giget.OCaTp9b-.mjs +0 -468
  287. package/node_modules/node-fetch-native/LICENSE +0 -114
  288. package/node_modules/node-fetch-native/README.md +0 -225
  289. package/node_modules/node-fetch-native/dist/chunks/multipart-parser.cjs +0 -2
  290. package/node_modules/node-fetch-native/dist/chunks/multipart-parser.mjs +0 -2
  291. package/node_modules/node-fetch-native/dist/index.cjs +0 -1
  292. package/node_modules/node-fetch-native/dist/index.mjs +0 -1
  293. package/node_modules/node-fetch-native/dist/native.cjs +0 -1
  294. package/node_modules/node-fetch-native/dist/native.mjs +0 -1
  295. package/node_modules/node-fetch-native/dist/node.cjs +0 -19
  296. package/node_modules/node-fetch-native/dist/node.mjs +0 -19
  297. package/node_modules/node-fetch-native/dist/polyfill.cjs +0 -1
  298. package/node_modules/node-fetch-native/dist/polyfill.mjs +0 -1
  299. package/node_modules/node-fetch-native/dist/proxy-stub.cjs +0 -1
  300. package/node_modules/node-fetch-native/dist/proxy-stub.mjs +0 -1
  301. package/node_modules/node-fetch-native/dist/proxy.cjs +0 -58
  302. package/node_modules/node-fetch-native/dist/shared/node-fetch-native.DfbY2q-x.mjs +0 -1
  303. package/node_modules/node-fetch-native/dist/shared/node-fetch-native.DhEqb06g.cjs +0 -1
  304. package/node_modules/node-fetch-native/index.d.ts +0 -1
  305. package/node_modules/node-fetch-native/lib/empty.cjs +0 -0
  306. package/node_modules/node-fetch-native/lib/empty.mjs +0 -0
  307. package/node_modules/node-fetch-native/lib/index.cjs +0 -11
  308. package/node_modules/node-fetch-native/lib/index.d.cts +0 -10
  309. package/node_modules/node-fetch-native/lib/index.d.mts +0 -10
  310. package/node_modules/node-fetch-native/lib/index.d.ts +0 -10
  311. package/node_modules/node-fetch-native/lib/native.cjs +0 -11
  312. package/node_modules/node-fetch-native/lib/polyfill.d.cts +0 -1
  313. package/node_modules/node-fetch-native/lib/polyfill.d.mts +0 -1
  314. package/node_modules/node-fetch-native/lib/polyfill.d.ts +0 -1
  315. package/node_modules/node-fetch-native/lib/proxy.d.ts +0 -32
  316. package/node_modules/node-fetch-native/node.d.ts +0 -1
  317. package/node_modules/node-fetch-native/package.json +0 -138
  318. package/node_modules/node-fetch-native/polyfill.d.ts +0 -1
  319. package/node_modules/node-fetch-native/proxy.d.ts +0 -1
  320. package/node_modules/nypm/node_modules/citty/LICENSE +0 -21
  321. package/node_modules/nypm/node_modules/citty/README.md +0 -231
  322. package/node_modules/nypm/node_modules/citty/dist/index.d.mts +0 -112
  323. package/node_modules/nypm/node_modules/citty/dist/index.mjs +0 -425
  324. package/node_modules/nypm/node_modules/citty/package.json +0 -42
  325. /package/node_modules/{nypm/node_modules/citty → citty}/dist/THIRD-PARTY-LICENSES.md +0 -0
  326. /package/node_modules/{nypm/node_modules/citty → citty}/dist/_chunks/libs/scule.mjs +0 -0
  327. /package/node_modules/{@maizzle/cli → maizzle}/LICENSE +0 -0
  328. /package/node_modules/{@maizzle/cli → maizzle}/dist/commands/make/component.d.mts +0 -0
  329. /package/node_modules/{@maizzle/cli → maizzle}/dist/commands/make/component.mjs +0 -0
  330. /package/node_modules/{@maizzle/cli → maizzle}/dist/commands/make/config.d.mts +0 -0
  331. /package/node_modules/{@maizzle/cli → maizzle}/dist/commands/make/config.mjs +0 -0
  332. /package/node_modules/{@maizzle/cli → maizzle}/dist/commands/make/layout.d.mts +0 -0
  333. /package/node_modules/{@maizzle/cli → maizzle}/dist/commands/make/layout.mjs +0 -0
  334. /package/node_modules/{@maizzle/cli → maizzle}/dist/commands/make/scaffold.d.mts +0 -0
  335. /package/node_modules/{@maizzle/cli → maizzle}/dist/commands/make/scaffold.mjs +0 -0
  336. /package/node_modules/{@maizzle/cli → maizzle}/dist/commands/make/stubs/component.vue +0 -0
  337. /package/node_modules/{@maizzle/cli → maizzle}/dist/commands/make/stubs/config.ts +0 -0
  338. /package/node_modules/{@maizzle/cli → maizzle}/dist/commands/make/stubs/layout.vue +0 -0
  339. /package/node_modules/{@maizzle/cli → maizzle}/dist/commands/make/stubs/template.vue +0 -0
  340. /package/node_modules/{@maizzle/cli → maizzle}/dist/commands/make/template.d.mts +0 -0
  341. /package/node_modules/{@maizzle/cli → maizzle}/dist/commands/make/template.mjs +0 -0
  342. /package/node_modules/{commander → maizzle/node_modules/commander}/LICENSE +0 -0
  343. /package/node_modules/{commander → maizzle/node_modules/commander}/esm.mjs +0 -0
  344. /package/node_modules/{commander → maizzle/node_modules/commander}/index.js +0 -0
  345. /package/node_modules/{commander → maizzle/node_modules/commander}/lib/error.js +0 -0
  346. /package/node_modules/{commander → maizzle/node_modules/commander}/lib/suggestSimilar.js +0 -0
  347. /package/node_modules/{commander → maizzle/node_modules/commander}/typings/esm.d.mts +0 -0
@@ -1,20 +1,26 @@
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
- import { ChevronUp, ChevronDown, Check } from 'lucide-vue-next'
4
+ import { ChevronUp, ChevronDown, Check, Info } from 'lucide-vue-next'
5
5
  import {
6
6
  DropdownMenu,
7
7
  DropdownMenuContent,
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'
23
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
18
24
 
19
25
  import stripesUrl from '../stripes.svg'
20
26
 
@@ -24,9 +30,16 @@ 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' })
@@ -40,45 +53,45 @@ const sourceView = ref<'compiled' | 'vue' | 'plaintext'>('compiled')
40
53
  const copied = ref(false)
41
54
 
42
55
  const iframeEl = ref<HTMLIFrameElement>()
56
+ const compiledSourceEl = ref<HTMLElement>()
43
57
  const vueSourceEl = ref<HTMLElement>()
44
58
  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>>()
59
+ const wrapperEl = ref<HTMLElement>()
50
60
 
51
61
  const panelWidth = defineModel<number>('panelWidth', { default: 0 })
52
62
  const panelHeight = defineModel<number>('panelHeight', { default: 0 })
53
63
  const isDragging = defineModel<boolean>('isDragging', { default: false })
54
64
  const isFullSize = defineModel<boolean>('isFullSize', { default: true })
55
65
 
56
- const sideSizes = ref({ left: 0, right: 0, top: 0, bottom: 0 })
66
+ // Custom resizable: width/height of the iframe wrapper (null = fill container)
67
+ const iframeWidth = ref<number | null>(null)
68
+ const iframeHeight = ref<number | null>(null)
69
+ const iframeContentHeight = ref<number | null>(null)
57
70
 
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() {
71
+ function copySource() {
72
+ let text: string
66
73
  if (sourceView.value === 'compiled') {
67
- await navigator.clipboard.writeText(srcdoc.value)
74
+ text = srcdoc.value
68
75
  } else if (sourceView.value === 'plaintext') {
69
- await navigator.clipboard.writeText(plaintextContent.value)
76
+ text = plaintextContent.value
70
77
  } else {
71
78
  const el = document.createElement('div')
72
79
  el.innerHTML = vueSourceHtml.value
73
- await navigator.clipboard.writeText(el.textContent || '')
80
+ text = el.textContent || ''
74
81
  }
75
- copied.value = true
76
- setTimeout(() => { copied.value = false }, 2000)
82
+
83
+ const blob = new Blob([text], { type: 'text/plain' })
84
+ const item = new ClipboardItem({ 'text/plain': blob })
85
+ navigator.clipboard.write([item]).then(() => {
86
+ copied.value = true
87
+ setTimeout(() => { copied.value = false }, 2000)
88
+ })
77
89
  }
78
90
 
79
91
  interface CompatibilityIssue {
80
92
  type: 'error' | 'warning'
81
93
  title: string
94
+ category: string
82
95
  clients: Array<{ name: string, notes: string[] }>
83
96
  url?: string
84
97
  line?: number
@@ -99,14 +112,111 @@ interface TemplateStats {
99
112
 
100
113
  const compatibilityIssues = ref<CompatibilityIssue[]>([])
101
114
  const compatibilityLoading = ref(false)
115
+ const compatibilityError = ref('')
116
+ const compatibilityCategory = ref('')
117
+ const compatibilityCategories = ['css', 'html', 'image', 'others'] as const
118
+ const activeCompatibilityCategories = computed(() =>
119
+ compatibilityCategories.filter(cat => compatibilityIssues.value.some(i => i.category === cat))
120
+ )
121
+ const filteredCompatibilityIssues = computed(() => {
122
+ if (!compatibilityCategory.value) return compatibilityIssues.value
123
+ return compatibilityIssues.value.filter(i => i.category === compatibilityCategory.value)
124
+ })
102
125
  const lintIssues = ref<LintIssue[]>([])
103
126
  const lintLoading = ref(false)
104
127
  const stats = ref<TemplateStats | null>(null)
105
128
  const statsLoading = ref(false)
106
129
 
130
+ // Email test state
131
+ const emailTo = ref<string[]>([])
132
+ const emailSubject = ref('')
133
+ const emailSending = ref(false)
134
+ const emailPreventThreading = ref(true)
135
+ const emailResult = ref<{ success: boolean; message: string; previewUrl?: string } | null>(null)
136
+
137
+ async function fetchEmailConfig() {
138
+ try {
139
+ const res = await fetch('/__maizzle/email-config')
140
+ const data = await res.json()
141
+ if (data.to?.length && !emailTo.value.length) emailTo.value = data.to
142
+ if (data.subject && !emailSubject.value) emailSubject.value = data.subject
143
+ } catch {}
144
+ }
145
+
146
+ async function sendTestEmail() {
147
+ if (!emailTo.value.length) return
148
+ emailSending.value = true
149
+ emailResult.value = null
150
+
151
+ try {
152
+ const res = await fetch(`/__maizzle/email/${route.params.template}`, {
153
+ method: 'POST',
154
+ headers: { 'Content-Type': 'application/json' },
155
+ body: JSON.stringify({
156
+ to: emailTo.value,
157
+ subject: (() => {
158
+ let subj = emailSubject.value || String(route.params.template)
159
+ if (emailPreventThreading.value) {
160
+ subj += ` | ${new Date().toISOString().slice(0, 19)}`
161
+ }
162
+ return subj
163
+ })(),
164
+ }),
165
+ })
166
+ emailResult.value = await res.json()
167
+ } catch (error: any) {
168
+ emailResult.value = { success: false, message: error.message }
169
+ } finally {
170
+ emailSending.value = false
171
+ }
172
+ }
173
+
174
+ let renderedHtml = ''
175
+
176
+ function updateIframeContentHeight() {
177
+ const iframe = iframeEl.value
178
+ const doc = iframe?.contentDocument
179
+ if (!iframe || !doc?.documentElement) return
180
+
181
+ // Hide iframe body overflow — scrolling is handled by the outer ScrollArea
182
+ if (doc.body) doc.body.style.overflow = 'hidden'
183
+
184
+ // Save scroll position of the ScrollArea viewport
185
+ const viewport = wrapperEl.value?.querySelector('[data-slot="scroll-area-viewport"]')
186
+ const scrollTop = viewport?.scrollTop ?? 0
187
+
188
+ // Temporarily collapse to measure true content height
189
+ iframe.style.height = '0'
190
+ iframeContentHeight.value = doc.documentElement.scrollHeight
191
+ iframe.style.height = `${iframeContentHeight.value}px`
192
+
193
+ // Restore scroll position
194
+ if (viewport) {
195
+ viewport.scrollTop = scrollTop
196
+ }
197
+ }
198
+
107
199
  async function fetchTemplate() {
108
200
  const res = await fetch(`/__maizzle/render/${route.params.template}`)
109
- srcdoc.value = await res.text()
201
+ renderedHtml = await res.text()
202
+
203
+ const iframe = iframeEl.value
204
+ const doc = iframe?.contentDocument
205
+
206
+ // Write directly into the iframe document to avoid a full reload,
207
+ // which preserves scroll position natively.
208
+ if (doc) {
209
+ doc.open()
210
+ doc.write(renderedHtml)
211
+ doc.close()
212
+ // Hide iframe body overflow — scrolling is handled by the outer ScrollArea
213
+ if (doc.body) doc.body.style.overflow = 'hidden'
214
+ await nextTick()
215
+ updateIframeContentHeight()
216
+ } else {
217
+ // Fallback for initial load
218
+ srcdoc.value = renderedHtml
219
+ }
110
220
  }
111
221
 
112
222
  async function fetchSource() {
@@ -138,9 +248,22 @@ async function fetchStats() {
138
248
 
139
249
  async function fetchCompatibility() {
140
250
  compatibilityLoading.value = true
251
+ compatibilityError.value = ''
141
252
  try {
142
- const res = await fetch(`/__maizzle/compatibility/${route.params.template}`)
143
- compatibilityIssues.value = await res.json()
253
+ const res = await fetch('/__maizzle/compatibility', {
254
+ method: 'POST',
255
+ body: renderedHtml,
256
+ })
257
+ const data = await res.json()
258
+ if (data?.error) {
259
+ compatibilityError.value = data.error
260
+ compatibilityIssues.value = []
261
+ } else {
262
+ compatibilityIssues.value = data
263
+ // Default to first category that has issues
264
+ const firstCat = compatibilityCategories.find(cat => data.some((i: CompatibilityIssue) => i.category === cat))
265
+ compatibilityCategory.value = firstCat || ''
266
+ }
144
267
  } catch {
145
268
  compatibilityIssues.value = []
146
269
  } finally {
@@ -151,8 +274,11 @@ async function fetchCompatibility() {
151
274
  async function fetchLint() {
152
275
  lintLoading.value = true
153
276
  try {
154
- const res = await fetch(`/__maizzle/lint/${route.params.template}`)
155
- lintIssues.value = await res.json()
277
+ const template = props.templates?.find(t => t.href === '/' + route.params.template)
278
+ const filePath = template?.path ?? route.params.template
279
+ const res = await fetch(`/__maizzle/lint/${filePath}`)
280
+ const data = await res.json()
281
+ lintIssues.value = Array.isArray(data) ? data.filter((i: LintIssue) => i.title) : []
156
282
  } catch {
157
283
  lintIssues.value = []
158
284
  } finally {
@@ -165,18 +291,24 @@ watch(() => route.params.template, () => {
165
291
  vueSourceHtml.value = ''
166
292
  plaintextContent.value = ''
167
293
  compatibilityIssues.value = []
294
+ compatibilityError.value = ''
168
295
  lintIssues.value = []
169
296
  stats.value = null
297
+ emailResult.value = null
170
298
  sourceView.value = 'compiled'
171
- fetchTemplate()
172
- fetchCompatibility()
299
+ fetchTemplate().then(fetchCompatibility)
173
300
  fetchLint()
174
301
  fetchStats()
302
+ fetchEmailConfig()
175
303
  if (viewMode.value === 'source') fetchSource()
176
304
  }, { immediate: true })
177
305
 
178
306
  watch(viewMode, (mode) => {
179
- if (mode === 'source' && !sourceHtml.value) fetchSource()
307
+ if (mode === 'source') {
308
+ if (sourceView.value === 'compiled' && !sourceHtml.value) fetchSource()
309
+ if (sourceView.value === 'vue' && !vueSourceHtml.value) fetchVueSource()
310
+ if (sourceView.value === 'plaintext' && !plaintextContent.value) fetchPlaintext()
311
+ }
180
312
  })
181
313
 
182
314
  watch(sourceView, (view) => {
@@ -187,15 +319,16 @@ watch(sourceView, (view) => {
187
319
 
188
320
  if ((import.meta as any).hot) {
189
321
  ;(import.meta as any).hot.on('maizzle:template-updated', () => {
190
- fetchTemplate()
191
- fetchCompatibility()
322
+ fetchTemplate().then(fetchCompatibility)
192
323
  fetchLint()
193
324
  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
325
 
326
+ // Always clear all source views so they re-fetch when switched to
327
+ sourceHtml.value = ''
328
+ vueSourceHtml.value = ''
329
+ plaintextContent.value = ''
330
+
331
+ // Re-fetch the active source view immediately if currently visible
199
332
  if (viewMode.value === 'source') {
200
333
  if (sourceView.value === 'compiled') fetchSource()
201
334
  if (sourceView.value === 'vue') fetchVueSource()
@@ -231,74 +364,112 @@ async function goToLine(line: number) {
231
364
  }
232
365
  }
233
366
 
234
- // Track which axis is being user-dragged so we can sync the opposite panel
235
- let hDragging = false
236
- let vDragging = false
367
+ async function goToCompiledLine(line: number) {
368
+ viewMode.value = 'source'
369
+ sourceView.value = 'compiled'
237
370
 
238
- const emit = defineEmits<{ 'clear-device': [] }>()
371
+ if (!sourceHtml.value) {
372
+ await fetchSource()
373
+ }
239
374
 
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 }
375
+ await nextTick()
244
376
 
245
- function onHorizontalLayout(sizes: number[]) {
246
- if (!hDragging) return
377
+ const el = compiledSourceEl.value
378
+ if (!el) return
247
379
 
248
- const [left, , right] = sizes
249
- if (Math.abs(left - right) < 0.5) return
380
+ el.querySelectorAll('.shiki-highlight-line').forEach(l => l.classList.remove('shiki-highlight-line'))
250
381
 
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)
382
+ const lineEl = el.querySelector(`[data-line="${line}"]`)
383
+ if (lineEl) {
384
+ lineEl.classList.add('shiki-highlight-line')
385
+ lineEl.scrollIntoView({ block: 'center', behavior: 'smooth' })
386
+ }
255
387
  }
256
388
 
257
- function onVerticalLayout(sizes: number[]) {
258
- if (!vDragging) return
389
+ const emit = defineEmits<{ 'clear-device': [] }>()
390
+
391
+ type Edge = 'left' | 'right' | 'top' | 'bottom'
259
392
 
260
- const [top, , bottom] = sizes
261
- if (Math.abs(top - bottom) < 0.5) return
393
+ function onEdgeDrag(e: MouseEvent | TouchEvent, edge: Edge) {
394
+ e.preventDefault()
395
+ isDragging.value = true
396
+ emit('clear-device')
397
+
398
+ const container = containerEl.value
399
+ if (!container) return
400
+
401
+ const isTouch = e.type === 'touchstart'
402
+ const startPoint = isTouch ? (e as TouchEvent).touches[0] : (e as MouseEvent)
403
+ const startX = startPoint.clientX
404
+ const startY = startPoint.clientY
405
+ const rect = container.getBoundingClientRect()
406
+ const gutter = 40 // 20px padding on each side
407
+ const maxW = rect.width - gutter
408
+ const maxH = rect.height - gutter
409
+ const startW = iframeWidth.value ?? maxW
410
+ const startH = iframeHeight.value ?? maxH
411
+
412
+ const isHorizontal = edge === 'left' || edge === 'right'
413
+ const sign = (edge === 'left' || edge === 'top') ? -1 : 1
414
+
415
+ const onMove = (ev: MouseEvent | TouchEvent) => {
416
+ const point = ev.type === 'touchmove' ? (ev as TouchEvent).touches[0] : (ev as MouseEvent)
417
+ if (isHorizontal) {
418
+ // Symmetric: each side moves by the delta, so total change is 2x
419
+ const delta = (point.clientX - startX) * sign
420
+ iframeWidth.value = Math.max(200, Math.min(maxW, startW + delta * 2))
421
+ } else {
422
+ const delta = (point.clientY - startY) * sign
423
+ iframeHeight.value = Math.max(100, Math.min(maxH, startH + delta * 2))
424
+ }
425
+ }
262
426
 
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)
427
+ const onUp = () => {
428
+ isDragging.value = false
429
+ updateFullSize()
430
+ document.removeEventListener('mousemove', onMove)
431
+ document.removeEventListener('mouseup', onUp)
432
+ document.removeEventListener('touchmove', onMove)
433
+ document.removeEventListener('touchend', onUp)
434
+ }
435
+
436
+ document.addEventListener('mousemove', onMove)
437
+ document.addEventListener('mouseup', onUp)
438
+ document.addEventListener('touchmove', onMove, { passive: false })
439
+ document.addEventListener('touchend', onUp)
267
440
  }
268
441
 
269
- function applyDeviceSize(device: Device | null | undefined) {
270
- const el = containerEl.value
271
- if (!el) return
442
+ function updateFullSize() {
443
+ const container = containerEl.value
444
+ if (!container) return
445
+ const rect = container.getBoundingClientRect()
446
+ const gutter = 40
447
+ isFullSize.value = (iframeWidth.value === null || iframeWidth.value >= rect.width - gutter - 2)
448
+ && (iframeHeight.value === null || iframeHeight.value >= rect.height - gutter - 2)
449
+ }
272
450
 
451
+ function applyDeviceSize(device: Device | null | undefined) {
273
452
  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
- }
453
+ iframeWidth.value = null
454
+ iframeHeight.value = null
455
+ updateFullSize()
280
456
  return
281
457
  }
282
458
 
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
289
-
290
- const hSide = Math.max(0, ((hPanelSpace - device.width) / 2) / hPanelSpace * 100)
291
- const vSide = Math.max(0, ((vPanelSpace - device.height) / 2) / vPanelSpace * 100)
459
+ const container = containerEl.value
460
+ if (!container) return
461
+ const rect = container.getBoundingClientRect()
462
+ const gutter = 40
292
463
 
293
- leftPanel.value?.resize(hSide)
294
- rightPanel.value?.resize(hSide)
295
- topPanel.value?.resize(vSide)
296
- bottomPanel.value?.resize(vSide)
464
+ iframeWidth.value = Math.min(device.width, rect.width - gutter)
465
+ iframeHeight.value = Math.min(device.height, rect.height - gutter)
466
+ updateFullSize()
297
467
  }
298
468
 
299
469
  watch(() => props.device, (device) => {
300
470
  if (viewMode.value === 'source') return
301
- applyDeviceSize(device)
471
+ // Only apply when a device is selected, not when cleared (drag start clears device)
472
+ if (device) applyDeviceSize(device)
302
473
  })
303
474
 
304
475
  watch(() => props.resetKey, () => {
@@ -333,9 +504,9 @@ function forwardIframeKeys(iframe: HTMLIFrameElement) {
333
504
  }
334
505
 
335
506
  onMounted(() => {
336
- const el = iframeEl.value
337
- if (el) {
338
- const rect = el.getBoundingClientRect()
507
+ const wrapper = wrapperEl.value
508
+ if (wrapper) {
509
+ const rect = wrapper.getBoundingClientRect()
339
510
  panelWidth.value = Math.round(rect.width)
340
511
  panelHeight.value = Math.round(rect.height)
341
512
  observer = new ResizeObserver((entries) => {
@@ -343,8 +514,13 @@ onMounted(() => {
343
514
  panelWidth.value = Math.round(entry.contentRect.width)
344
515
  panelHeight.value = Math.round(entry.contentRect.height)
345
516
  }
517
+ updateIframeContentHeight()
346
518
  })
347
- observer.observe(el)
519
+ observer.observe(wrapper)
520
+ }
521
+
522
+ const el = iframeEl.value
523
+ if (el) {
348
524
  el.addEventListener('load', () => forwardIframeKeys(el))
349
525
  }
350
526
  })
@@ -360,7 +536,7 @@ const activeTab = ref<string | undefined>(undefined)
360
536
  function toggleBottomPanel() {
361
537
  bottomPanelOpen.value = !bottomPanelOpen.value
362
538
  if (bottomPanelOpen.value) {
363
- tabsPanelHeight.value = 200
539
+ tabsPanelHeight.value = 300
364
540
  if (!activeTab.value) activeTab.value = 'compatibility'
365
541
  } else {
366
542
  tabsPanelHeight.value = 40
@@ -378,7 +554,7 @@ function onTabClick(tab: string) {
378
554
  activeTab.value = tab
379
555
  if (!bottomPanelOpen.value) {
380
556
  bottomPanelOpen.value = true
381
- tabsPanelHeight.value = 200
557
+ tabsPanelHeight.value = 300
382
558
  }
383
559
  }
384
560
 
@@ -390,8 +566,11 @@ function onTabsDragStart(e: MouseEvent) {
390
566
  const startY = e.clientY
391
567
  const startHeight = tabsPanelHeight.value
392
568
 
569
+ const rootEl = containerEl.value?.closest('.relative.h-full') as HTMLElement | null
570
+ const maxHeight = rootEl ? rootEl.getBoundingClientRect().height : Infinity
571
+
393
572
  const onMouseMove = (e: MouseEvent) => {
394
- const newHeight = Math.max(40, startHeight + startY - e.clientY)
573
+ const newHeight = Math.max(40, Math.min(maxHeight, startHeight + startY - e.clientY))
395
574
  tabsPanelHeight.value = newHeight
396
575
  bottomPanelOpen.value = newHeight > 40
397
576
 
@@ -420,187 +599,298 @@ const stripeBg = {
420
599
  </script>
421
600
 
422
601
  <template>
423
- <div class="flex flex-col h-full">
424
- <div class="relative flex-1 min-h-0">
602
+ <div class="relative h-full">
603
+ <div class="absolute inset-0 bottom-10 overflow-hidden">
425
604
  <!-- Source code view -->
426
605
  <div v-show="viewMode === 'source'" class="absolute inset-0 min-w-0 overflow-hidden">
427
606
  <div class="absolute top-3 left-6 z-10">
428
607
  <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">
608
+ <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
609
  {{ sourceView === 'compiled' ? 'HTML' : sourceView === 'vue' ? 'Source' : 'Plaintext' }}
431
610
  <ChevronDown class="size-3 opacity-50" />
432
611
  </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>
612
+ <DropdownMenuContent align="start" class="min-w-32 bg-[#27212e]/80 dark:bg-gray-950/80 backdrop-blur-md border-white/10">
613
+ <DropdownMenuItem class="text-xs font-medium text-gray-400 focus:text-gray-200 focus:bg-white/10" @click="sourceView = 'vue'">
614
+ <Check v-if="sourceView === 'vue'" class="size-3 text-gray-200" />
615
+ <span :class="[sourceView === 'vue' ? 'text-gray-200' : 'pl-5']">Source</span>
437
616
  </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>
617
+ <DropdownMenuItem class="text-xs font-medium text-gray-400 focus:text-gray-200 focus:bg-white/10" @click="sourceView = 'compiled'">
618
+ <Check v-if="sourceView === 'compiled'" class="size-3 text-gray-200" />
619
+ <span :class="[sourceView === 'compiled' ? 'text-gray-200' : 'pl-5']">HTML</span>
441
620
  </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>
621
+ <DropdownMenuItem class="text-xs font-medium text-gray-400 focus:text-gray-200 focus:bg-white/10" @click="sourceView = 'plaintext'">
622
+ <Check v-if="sourceView === 'plaintext'" class="size-3 text-gray-200" />
623
+ <span :class="[sourceView === 'plaintext' ? 'text-gray-200' : 'pl-5']">Plaintext</span>
445
624
  </DropdownMenuItem>
446
625
  </DropdownMenuContent>
447
626
  </DropdownMenu>
448
627
  </div>
449
628
  <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"
629
+ 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
630
  :disabled="copied"
452
631
  @click="copySource"
453
632
  >
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>
633
+ <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>
634
+ <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
635
  </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>
636
+ <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">
637
+ <div
638
+ ref="compiledSourceEl"
639
+ 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!"
640
+ v-html="sourceHtml"
641
+ />
642
+ <ScrollBar orientation="horizontal" />
643
+ </ScrollArea>
644
+ <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">
645
+ <div
646
+ ref="vueSourceEl"
647
+ 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!"
648
+ v-html="vueSourceHtml"
649
+ />
650
+ <ScrollBar orientation="horizontal" />
651
+ </ScrollArea>
652
+ <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">
653
+ <pre
654
+ 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"
655
+ >{{ plaintextContent }}</pre>
656
+ </ScrollArea>
472
657
  </div>
473
658
 
659
+ <!-- Blocks iframe from stealing pointer events while dragging tabs -->
660
+ <div v-if="tabsDragging" class="fixed inset-0 z-50" />
661
+
474
662
  <!-- Preview view -->
475
663
  <div v-show="viewMode !== 'source'" class="absolute inset-0">
476
664
  <div class="relative h-full opacity-5" :style="stripeBg" />
477
665
  </div>
478
666
 
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>
667
+ <div v-show="viewMode !== 'source'" ref="containerEl" class="absolute inset-0 z-10 flex items-center justify-center">
668
+ <!-- Blocks iframe from stealing pointer events while dragging -->
669
+ <div v-if="isDragging" class="absolute inset-0 z-20" />
670
+ <div
671
+ class="relative"
672
+ :style="{
673
+ width: iframeWidth != null ? `${iframeWidth + 40}px` : '100%',
674
+ height: iframeHeight != null ? `${iframeHeight + 40}px` : '100%',
675
+ transition: isDragging ? 'none' : 'width 0.2s ease, height 0.2s ease',
676
+ }"
677
+ >
678
+ <!-- Top handle -->
679
+ <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')">
680
+ <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" />
681
+ </div>
682
+ <!-- Bottom handle -->
683
+ <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')">
684
+ <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" />
685
+ </div>
686
+ <!-- Left handle -->
687
+ <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')">
688
+ <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" />
689
+ </div>
690
+ <!-- Right handle -->
691
+ <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')">
692
+ <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" />
693
+ </div>
694
+ <!-- Iframe -->
695
+ <div ref="wrapperEl" class="absolute inset-0 min-[430px]:inset-5 border border-gray-200 dark:border-gray-800">
696
+ <ScrollArea class="h-full w-full bg-white dark:bg-gray-950">
697
+ <iframe
698
+ ref="iframeEl"
699
+ :srcdoc="srcdoc"
700
+ @load="updateIframeContentHeight"
701
+ class="w-full border-0 bg-white dark:bg-gray-950"
702
+ :style="{ height: iframeContentHeight ? `${iframeContentHeight}px` : '100%' }"
703
+ />
704
+ </ScrollArea>
705
+ </div>
502
706
  </div>
503
707
  </div>
504
708
  </div>
505
709
 
506
- <!-- Tabs panel (always visible) -->
710
+ <!-- Tabs panel (overlay) -->
507
711
  <div
508
- class="shrink-0 bg-white dark:bg-gray-950 overflow-hidden"
509
- :class="!tabsDragging ? 'transition-[height] duration-200 ease-in-out' : ''"
712
+ class="absolute bottom-0 left-0 right-0 z-20 overflow-hidden border-t border-gray-200 dark:border-gray-800/50"
713
+ :class="[
714
+ !tabsDragging ? 'transition-[height] duration-200 ease-in-out' : '',
715
+ 'bg-white dark:bg-gray-950',
716
+ ]"
510
717
  :style="{ height: `${tabsPanelHeight}px` }"
511
718
  >
512
719
  <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-['']"
720
+ 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
721
  @mousedown="onTabsDragStart"
515
722
  />
516
723
  <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' : ''">
724
+ <div class="flex items-center justify-between min-h-10 pl-2 pr-3 shrink-0" :class="bottomPanelOpen ? 'border-b' : ''">
518
725
  <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')">
726
+ <TabsTrigger 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" @click="onTabClick('compatibility')">
520
727
  Compatibility
521
728
  </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')">
729
+ <TabsTrigger value="lint" 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" @click="onTabClick('lint')">
523
730
  Linter
524
731
  </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')">
732
+ <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" @click="onTabClick('stats')">
526
733
  Stats
527
734
  </TabsTrigger>
735
+ <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" @click="onTabClick('test')">
736
+ Test
737
+ </TabsTrigger>
528
738
  </TabsList>
529
739
  <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" />
740
+ <ChevronUp v-if="!bottomPanelOpen" class="size-4 dark:text-gray-400" :stroke-width="1" />
741
+ <ChevronDown v-else class="size-4 dark:text-gray-400" :stroke-width="1" />
532
742
  </Button>
533
743
  </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"
744
+ <div class="flex-1 min-h-0">
745
+ <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">
746
+ <button
747
+ v-for="cat in activeCompatibilityCategories"
748
+ :key="cat"
749
+ class="px-2 py-0.5 text-[11px] rounded-full cursor-pointer transition-colors"
750
+ :class="compatibilityCategory === cat
751
+ ? 'bg-gray-900 text-white dark:bg-gray-600 dark:text-gray-100'
752
+ : 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-white/10'"
753
+ @click="compatibilityCategory = cat"
543
754
  >
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'">
755
+ {{ cat === 'css' ? 'CSS' : cat === 'html' ? 'HTML' : cat.charAt(0).toUpperCase() + cat.slice(1) }}
756
+ <span class="ml-0.5 tabular-nums">{{ compatibilityIssues.filter(i => i.category === cat).length }}</span>
757
+ </button>
758
+ </div>
759
+ <ScrollArea class="h-full flex-1 min-h-0 pl-5">
760
+ <p v-if="compatibilityLoading" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">Checking compatibility...</p>
761
+ <p v-else-if="compatibilityError" class="pr-4 py-3 text-xs text-red-500 dark:text-red-400">{{ compatibilityError }}</p>
762
+ <p v-else-if="compatibilityIssues.length === 0" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">No compatibility issues found.</p>
763
+ <ul v-else class="text-xs divide-y">
764
+ <li
765
+ v-for="(issue, i) in filteredCompatibilityIssues"
766
+ :key="i"
767
+ class="pr-4 py-1.5 hover:bg-gray-50 dark:hover:bg-white/5"
768
+ >
769
+ <div class="flex items-center gap-2">
770
+ <a v-if="issue.url" :href="issue.url" target="_blank" rel="noopener" class="font-medium hover:underline shrink-0" :class="issue.type === 'error' ? 'text-red-600' : 'text-amber-600'">
547
771
  {{ issue.title }}
548
772
  </a>
549
- <span v-else class="font-medium" :class="issue.type === 'error' ? 'text-red-600' : 'text-amber-600'">
773
+ <span v-else class="font-medium shrink-0" :class="issue.type === 'error' ? 'text-red-600' : 'text-amber-600'">
550
774
  {{ issue.title }}
551
775
  </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>
555
- </div>
776
+ <span class="text-gray-400 dark:text-gray-600 shrink-0">&middot;</span>
777
+ <span class="text-gray-500 dark:text-gray-400 truncate">{{ issue.type === 'error' ? 'Not supported' : 'Partial support' }} in {{ issue.clients.map((c: any) => c.name).join(', ') }}</span>
778
+ <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 ml-auto" @click="goToCompiledLine(issue.line!)">L{{ issue.line }}</button>
779
+ </div>
780
+ </li>
781
+ </ul>
782
+ </ScrollArea>
783
+ </TabsContent>
784
+ <TabsContent value="lint" class="mt-0 h-full">
785
+ <ScrollArea class="h-full pl-5">
786
+ <p v-if="lintLoading" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">Linting...</p>
787
+ <p v-else-if="lintIssues.length === 0" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">No issues found.</p>
788
+ <ul v-else class="text-xs divide-y">
789
+ <li
790
+ v-for="(issue, i) in lintIssues"
791
+ :key="i"
792
+ class="pr-4 py-2 hover:bg-gray-50 dark:hover:bg-white/5"
793
+ >
794
+ <div class="flex items-start justify-between gap-4">
795
+ <div>
796
+ <span class="font-medium" :class="issue.type === 'error' ? 'text-red-600' : 'text-amber-600'">
797
+ {{ issue.title }}
798
+ </span>
799
+ <div class="text-gray-500 dark:text-gray-400 mt-0.5">{{ issue.message }}</div>
556
800
  </div>
801
+ <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>
557
802
  </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>
803
+ </li>
804
+ </ul>
805
+ </ScrollArea>
806
+ </TabsContent>
807
+ <TabsContent value="stats" class="mt-0 h-full">
808
+ <ScrollArea class="h-full pl-5">
809
+ <p v-if="statsLoading" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">Loading stats...</p>
810
+ <p v-else-if="!stats" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">No stats available.</p>
811
+ <div v-else class="pr-4 py-3 flex items-center gap-6 text-xs">
812
+ <div class="flex items-center gap-1.5">
813
+ <span class="text-gray-500 dark:text-gray-400">Size</span>
814
+ <span
815
+ class="font-medium tabular-nums"
816
+ :class="stats.size.bytes > 102400 ? 'text-red-600' : stats.size.bytes > 51200 ? 'text-amber-600' : 'text-gray-900 dark:text-gray-300'"
817
+ >{{ stats.size.formatted }}</span>
818
+ <TooltipProvider :delay-duration="0">
819
+ <Tooltip>
820
+ <TooltipTrigger as-child>
821
+ <button type="button">
822
+ <Info class="size-3 text-gray-400 dark:text-gray-500" />
823
+ </button>
824
+ </TooltipTrigger>
825
+ <TooltipContent class="max-w-60">
826
+ Compiled HTML size, excludes image files. Gmail clips content at ~100KB.
827
+ </TooltipContent>
828
+ </Tooltip>
829
+ </TooltipProvider>
830
+ </div>
831
+ <div class="flex items-center gap-1.5">
832
+ <span class="text-gray-500 dark:text-gray-400">Images</span>
833
+ <span class="font-medium tabular-nums">{{ stats.images }}</span>
834
+ </div>
835
+ <div class="flex items-center gap-1.5">
836
+ <span class="text-gray-500 dark:text-gray-400">Links</span>
837
+ <span class="font-medium tabular-nums">{{ stats.links }}</span>
559
838
  </div>
560
- </li>
561
- </ul>
839
+ </div>
840
+ </ScrollArea>
562
841
  </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>
842
+ <TabsContent value="test" class="mt-0 h-full">
843
+ <ScrollArea class="h-full pl-5">
844
+ <div class="pr-4 py-3 max-w-md">
845
+ <div class="space-y-2">
846
+ <div class="flex items-center gap-2">
847
+ <label class="text-xs text-gray-500 dark:text-gray-400 w-12 shrink-0">To</label>
848
+ <TagsInput v-model="emailTo" delimiter=" " add-on-paste add-on-blur class="flex-1 min-h-7 gap-1 px-2 py-1">
849
+ <TagsInputItem v-for="item in emailTo" :key="item" :value="item" class="h-5 text-xs rounded">
850
+ <TagsInputItemText class="px-1.5 py-0 text-xs" />
851
+ <TagsInputItemDelete class="size-3.5" />
852
+ </TagsInputItem>
853
+ <TagsInputInput class="text-xs min-h-5 px-0.5" placeholder="Add emails..." />
854
+ </TagsInput>
855
+ </div>
856
+ <div class="flex items-center gap-2">
857
+ <label class="text-xs text-gray-500 dark:text-gray-400 w-12 shrink-0">Subject</label>
858
+ <div class="flex-1 flex items-center gap-3">
859
+ <Input v-model="emailSubject" :placeholder="String(route.params.template)" class="flex-1 h-7 text-xs! px-2" />
860
+ <label class="flex items-center gap-1.5 cursor-pointer select-none shrink-0">
861
+ <Checkbox v-model="emailPreventThreading" :default-checked="true" class="size-3.5" />
862
+ <span class="text-xs text-gray-500 dark:text-gray-400">Prevent threading</span>
863
+ </label>
864
+ </div>
578
865
  </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
866
  </div>
581
- </li>
582
- </ul>
583
- </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>
867
+ <div class="flex items-center gap-3 mt-3">
868
+ <Button
869
+ size="sm"
870
+ class="h-7 text-xs px-3"
871
+ :disabled="!emailTo.length || emailSending"
872
+ @click="sendTestEmail"
873
+ >
874
+ <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>
875
+ {{ emailSending ? 'Sending' : 'Send' }}
876
+ </Button>
877
+ </div>
878
+ <div v-if="emailResult" class="mt-2">
879
+ <p class="text-xs" :class="emailResult.success ? 'text-gray-950 dark:text-white' : 'text-red-600'">
880
+ {{ emailResult.message }}
881
+ <a
882
+ v-if="emailResult.previewUrl"
883
+ :href="emailResult.previewUrl"
884
+ target="_blank"
885
+ rel="noopener"
886
+ class="text-gray-500 dark:text-gray-400 hover:underline"
887
+ >
888
+ (view)
889
+ </a>
890
+ </p>
891
+ </div>
602
892
  </div>
603
- </div>
893
+ </ScrollArea>
604
894
  </TabsContent>
605
895
  </div>
606
896
  </Tabs>