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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/build.mjs +4 -1
  2. package/dist/build.mjs.map +1 -1
  3. package/dist/components/Button.vue +2 -2
  4. package/dist/components/CodeBlock.vue +2 -1
  5. package/dist/components/Column.vue +28 -22
  6. package/dist/components/Container.vue +47 -9
  7. package/dist/components/Font.vue +96 -0
  8. package/dist/components/Layout.vue +9 -4
  9. package/dist/components/Overlap.vue +75 -18
  10. package/dist/components/Row.vue +40 -19
  11. package/dist/components/Section.vue +35 -8
  12. package/dist/components/utils.d.mts +14 -1
  13. package/dist/components/utils.d.mts.map +1 -1
  14. package/dist/components/utils.mjs +32 -1
  15. package/dist/components/utils.mjs.map +1 -1
  16. package/dist/components/utils.ts +39 -0
  17. package/dist/composables/renderContext.d.mts +8 -1
  18. package/dist/composables/renderContext.d.mts.map +1 -1
  19. package/dist/composables/renderContext.mjs.map +1 -1
  20. package/dist/composables/useFont.d.mts +50 -0
  21. package/dist/composables/useFont.d.mts.map +1 -0
  22. package/dist/composables/useFont.mjs +93 -0
  23. package/dist/composables/useFont.mjs.map +1 -0
  24. package/dist/index.d.mts +2 -1
  25. package/dist/index.mjs +2 -1
  26. package/dist/plugins/postcss/quoteFontFamilies.d.mts +13 -0
  27. package/dist/plugins/postcss/quoteFontFamilies.d.mts.map +1 -0
  28. package/dist/plugins/postcss/quoteFontFamilies.mjs +84 -0
  29. package/dist/plugins/postcss/quoteFontFamilies.mjs.map +1 -0
  30. package/dist/render/createRenderer.mjs +8 -2
  31. package/dist/render/createRenderer.mjs.map +1 -1
  32. package/dist/render/injectFonts.d.mts +15 -0
  33. package/dist/render/injectFonts.d.mts.map +1 -0
  34. package/dist/render/injectFonts.mjs +46 -0
  35. package/dist/render/injectFonts.mjs.map +1 -0
  36. package/dist/serve.d.mts.map +1 -1
  37. package/dist/serve.mjs +28 -12
  38. package/dist/serve.mjs.map +1 -1
  39. package/dist/server/compatibility.d.mts +54 -2
  40. package/dist/server/compatibility.d.mts.map +1 -1
  41. package/dist/server/compatibility.mjs +890 -76
  42. package/dist/server/compatibility.mjs.map +1 -1
  43. package/dist/server/linter.d.mts +15 -2
  44. package/dist/server/linter.d.mts.map +1 -1
  45. package/dist/server/linter.mjs +194 -43
  46. package/dist/server/linter.mjs.map +1 -1
  47. package/dist/server/sfc-utils.d.mts +18 -0
  48. package/dist/server/sfc-utils.d.mts.map +1 -0
  49. package/dist/server/sfc-utils.mjs +184 -0
  50. package/dist/server/sfc-utils.mjs.map +1 -0
  51. package/dist/server/ui/App.vue +27 -50
  52. package/dist/server/ui/components/SidebarClose.vue +12 -0
  53. package/dist/server/ui/components/ui/command/Command.vue +1 -0
  54. package/dist/server/ui/components/ui/input/Input.vue +1 -1
  55. package/dist/server/ui/components/ui/sidebar/SidebarTrigger.vue +1 -1
  56. package/dist/server/ui/components/ui/tags-input/TagsInputInput.vue +1 -1
  57. package/dist/server/ui/lib/emulated-dark-mode.ts +131 -0
  58. package/dist/server/ui/pages/Preview.vue +215 -156
  59. package/dist/transformers/addAttributes.mjs +10 -6
  60. package/dist/transformers/addAttributes.mjs.map +1 -1
  61. package/dist/transformers/columnWidth.d.mts +31 -0
  62. package/dist/transformers/columnWidth.d.mts.map +1 -0
  63. package/dist/transformers/columnWidth.mjs +166 -0
  64. package/dist/transformers/columnWidth.mjs.map +1 -0
  65. package/dist/transformers/index.d.mts.map +1 -1
  66. package/dist/transformers/index.mjs +4 -0
  67. package/dist/transformers/index.mjs.map +1 -1
  68. package/dist/transformers/inlineCSS.mjs +2 -2
  69. package/dist/transformers/inlineCSS.mjs.map +1 -1
  70. package/dist/transformers/msoWidthFromClass.d.mts +19 -0
  71. package/dist/transformers/msoWidthFromClass.d.mts.map +1 -0
  72. package/dist/transformers/msoWidthFromClass.mjs +61 -0
  73. package/dist/transformers/msoWidthFromClass.mjs.map +1 -0
  74. package/dist/transformers/purgeCSS.mjs +1 -1
  75. package/dist/transformers/purgeCSS.mjs.map +1 -1
  76. package/dist/transformers/tailwindcss.d.mts.map +1 -1
  77. package/dist/transformers/tailwindcss.mjs +6 -16
  78. package/dist/transformers/tailwindcss.mjs.map +1 -1
  79. package/dist/types/config.d.mts +42 -2
  80. package/dist/types/config.d.mts.map +1 -1
  81. package/dist/types/index.d.mts +2 -2
  82. package/dist/utils/decodeStyleEntities.d.mts +15 -0
  83. package/dist/utils/decodeStyleEntities.d.mts.map +1 -0
  84. package/dist/utils/decodeStyleEntities.mjs +18 -0
  85. package/dist/utils/decodeStyleEntities.mjs.map +1 -0
  86. package/package.json +2 -3
  87. package/dist/_virtual/_rolldown/runtime.mjs +0 -32
  88. package/dist/node_modules/picomatch/index.mjs +0 -13
  89. package/dist/node_modules/picomatch/index.mjs.map +0 -1
  90. package/dist/node_modules/picomatch/lib/constants.mjs +0 -174
  91. package/dist/node_modules/picomatch/lib/constants.mjs.map +0 -1
  92. package/dist/node_modules/picomatch/lib/parse.mjs +0 -1067
  93. package/dist/node_modules/picomatch/lib/parse.mjs.map +0 -1
  94. package/dist/node_modules/picomatch/lib/picomatch.mjs +0 -304
  95. package/dist/node_modules/picomatch/lib/picomatch.mjs.map +0 -1
  96. package/dist/node_modules/picomatch/lib/scan.mjs +0 -296
  97. package/dist/node_modules/picomatch/lib/scan.mjs.map +0 -1
  98. package/dist/node_modules/picomatch/lib/utils.mjs +0 -53
  99. package/dist/node_modules/picomatch/lib/utils.mjs.map +0 -1
@@ -0,0 +1,184 @@
1
+ import { existsSync } from "node:fs";
2
+ import { basename, dirname, resolve } from "node:path";
3
+ import { glob } from "tinyglobby";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ //#region src/server/sfc-utils.ts
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ function parseSfcBlocks(source) {
9
+ let template = null;
10
+ const styles = [];
11
+ const templateMatch = source.match(/<template\b[^>]*>([\s\S]*)<\/template>/);
12
+ if (templateMatch) {
13
+ const contentStart = source.indexOf(templateMatch[0]) + templateMatch[0].indexOf(templateMatch[1]);
14
+ const offset = source.slice(0, contentStart).split("\n").length - 1;
15
+ template = {
16
+ content: templateMatch[1],
17
+ offset
18
+ };
19
+ }
20
+ const styleRe = /<style\b([^>]*)>([\s\S]*?)<\/style>/g;
21
+ let m;
22
+ while ((m = styleRe.exec(source)) !== null) {
23
+ if (/\blang\s*=\s*["'](?!css)/i.test(m[1])) continue;
24
+ const contentStart = m.index + m[0].indexOf(m[2]);
25
+ const offset = source.slice(0, contentStart).split("\n").length - 1;
26
+ styles.push({
27
+ content: m[2],
28
+ offset
29
+ });
30
+ }
31
+ return {
32
+ template,
33
+ styles
34
+ };
35
+ }
36
+ /**
37
+ * Standard HTML elements — anything not in this set is treated as a component.
38
+ */
39
+ const HTML_ELEMENTS = new Set([
40
+ "a",
41
+ "abbr",
42
+ "address",
43
+ "area",
44
+ "article",
45
+ "aside",
46
+ "audio",
47
+ "b",
48
+ "base",
49
+ "bdi",
50
+ "bdo",
51
+ "blockquote",
52
+ "body",
53
+ "br",
54
+ "button",
55
+ "canvas",
56
+ "caption",
57
+ "cite",
58
+ "code",
59
+ "col",
60
+ "colgroup",
61
+ "data",
62
+ "datalist",
63
+ "dd",
64
+ "del",
65
+ "details",
66
+ "dfn",
67
+ "dialog",
68
+ "div",
69
+ "dl",
70
+ "dt",
71
+ "em",
72
+ "embed",
73
+ "fieldset",
74
+ "figcaption",
75
+ "figure",
76
+ "footer",
77
+ "form",
78
+ "h1",
79
+ "h2",
80
+ "h3",
81
+ "h4",
82
+ "h5",
83
+ "h6",
84
+ "head",
85
+ "header",
86
+ "hgroup",
87
+ "hr",
88
+ "html",
89
+ "i",
90
+ "iframe",
91
+ "img",
92
+ "input",
93
+ "ins",
94
+ "kbd",
95
+ "label",
96
+ "legend",
97
+ "li",
98
+ "link",
99
+ "main",
100
+ "map",
101
+ "mark",
102
+ "menu",
103
+ "meta",
104
+ "meter",
105
+ "nav",
106
+ "noscript",
107
+ "object",
108
+ "ol",
109
+ "optgroup",
110
+ "option",
111
+ "output",
112
+ "p",
113
+ "picture",
114
+ "pre",
115
+ "progress",
116
+ "q",
117
+ "rp",
118
+ "rt",
119
+ "ruby",
120
+ "s",
121
+ "samp",
122
+ "script",
123
+ "search",
124
+ "section",
125
+ "select",
126
+ "slot",
127
+ "small",
128
+ "source",
129
+ "span",
130
+ "strong",
131
+ "style",
132
+ "sub",
133
+ "summary",
134
+ "sup",
135
+ "table",
136
+ "tbody",
137
+ "td",
138
+ "template",
139
+ "textarea",
140
+ "tfoot",
141
+ "th",
142
+ "thead",
143
+ "time",
144
+ "title",
145
+ "tr",
146
+ "track",
147
+ "u",
148
+ "ul",
149
+ "var",
150
+ "video",
151
+ "wbr"
152
+ ]);
153
+ function findComponentTags(templateContent) {
154
+ const tags = /* @__PURE__ */ new Set();
155
+ const pascalRe = /<([A-Z][a-zA-Z0-9]*)\b/g;
156
+ let m;
157
+ while ((m = pascalRe.exec(templateContent)) !== null) tags.add(m[1]);
158
+ const kebabRe = /<([a-z][a-z0-9]*(?:-[a-z0-9]+)+)\b/g;
159
+ while ((m = kebabRe.exec(templateContent)) !== null) if (!HTML_ELEMENTS.has(m[1])) tags.add(m[1]);
160
+ return [...tags];
161
+ }
162
+ async function buildComponentMap(root, componentDirs) {
163
+ const map = /* @__PURE__ */ new Map();
164
+ const dirs = [
165
+ resolve(__dirname, "../components"),
166
+ resolve(root, "components"),
167
+ ...componentDirs
168
+ ].filter((d) => existsSync(d));
169
+ for (const dir of dirs) {
170
+ const files = await glob(["**/*.vue"], {
171
+ cwd: dir,
172
+ absolute: true
173
+ });
174
+ for (const file of files) {
175
+ const name = basename(file, ".vue");
176
+ map.set(name.toLowerCase(), file);
177
+ }
178
+ }
179
+ return map;
180
+ }
181
+
182
+ //#endregion
183
+ export { HTML_ELEMENTS, buildComponentMap, findComponentTags, parseSfcBlocks };
184
+ //# sourceMappingURL=sfc-utils.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sfc-utils.mjs","names":[],"sources":["../../src/server/sfc-utils.ts"],"sourcesContent":["import { existsSync } from 'node:fs'\nimport { resolve, dirname, basename } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { glob } from 'tinyglobby'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\nexport interface SfcBlock {\n content: string\n offset: number\n}\n\nexport function parseSfcBlocks(source: string): { template: SfcBlock | null, styles: SfcBlock[] } {\n let template: SfcBlock | null = null\n const styles: SfcBlock[] = []\n\n const templateMatch = source.match(/<template\\b[^>]*>([\\s\\S]*)<\\/template>/)\n if (templateMatch) {\n const contentStart = source.indexOf(templateMatch[0]) + templateMatch[0].indexOf(templateMatch[1])\n const offset = source.slice(0, contentStart).split('\\n').length - 1\n template = { content: templateMatch[1], offset }\n }\n\n const styleRe = /<style\\b([^>]*)>([\\s\\S]*?)<\\/style>/g\n let m\n while ((m = styleRe.exec(source)) !== null) {\n // Skip preprocessor styles (scss, less, etc.) — caniemail only parses plain CSS\n if (/\\blang\\s*=\\s*[\"'](?!css)/i.test(m[1])) continue\n\n const contentStart = m.index + m[0].indexOf(m[2])\n const offset = source.slice(0, contentStart).split('\\n').length - 1\n styles.push({ content: m[2], offset })\n }\n\n return { template, styles }\n}\n\n/**\n * Standard HTML elements — anything not in this set is treated as a component.\n */\nexport const HTML_ELEMENTS = new Set([\n 'a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base',\n 'bdi', 'bdo', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption',\n 'cite', 'code', 'col', 'colgroup', 'data', 'datalist', 'dd', 'del',\n 'details', 'dfn', 'dialog', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset',\n 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5',\n 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'img',\n 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link', 'main', 'map',\n 'mark', 'menu', 'meta', 'meter', 'nav', 'noscript', 'object', 'ol',\n 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q',\n 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'search', 'section', 'select',\n 'slot', 'small', 'source', 'span', 'strong', 'style', 'sub', 'summary',\n 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th',\n 'thead', 'time', 'title', 'tr', 'track', 'u', 'ul', 'var', 'video', 'wbr',\n])\n\nexport function findComponentTags(templateContent: string): string[] {\n const tags = new Set<string>()\n\n // PascalCase tags like <Section>, <Button>\n const pascalRe = /<([A-Z][a-zA-Z0-9]*)\\b/g\n let m\n while ((m = pascalRe.exec(templateContent)) !== null) {\n tags.add(m[1])\n }\n\n // kebab-case tags like <my-component>\n const kebabRe = /<([a-z][a-z0-9]*(?:-[a-z0-9]+)+)\\b/g\n while ((m = kebabRe.exec(templateContent)) !== null) {\n if (!HTML_ELEMENTS.has(m[1])) {\n tags.add(m[1])\n }\n }\n\n return [...tags]\n}\n\nexport async function buildComponentMap(root: string, componentDirs: string[]): Promise<Map<string, string>> {\n const map = new Map<string, string>()\n\n const dirs = [\n resolve(__dirname, '../components'),\n resolve(root, 'components'),\n ...componentDirs,\n ].filter(d => existsSync(d))\n\n for (const dir of dirs) {\n const files = await glob(['**/*.vue'], { cwd: dir, absolute: true })\n for (const file of files) {\n const name = basename(file, '.vue')\n // Store lowercased for case-insensitive matching\n map.set(name.toLowerCase(), file)\n }\n }\n\n return map\n}\n"],"mappings":";;;;;;AAKA,MAAM,YAAY,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;AAOzD,SAAgB,eAAe,QAAmE;CAChG,IAAI,WAA4B;CAChC,MAAM,SAAqB,EAAE;CAE7B,MAAM,gBAAgB,OAAO,MAAM,yCAAyC;AAC5E,KAAI,eAAe;EACjB,MAAM,eAAe,OAAO,QAAQ,cAAc,GAAG,GAAG,cAAc,GAAG,QAAQ,cAAc,GAAG;EAClG,MAAM,SAAS,OAAO,MAAM,GAAG,aAAa,CAAC,MAAM,KAAK,CAAC,SAAS;AAClE,aAAW;GAAE,SAAS,cAAc;GAAI;GAAQ;;CAGlD,MAAM,UAAU;CAChB,IAAI;AACJ,SAAQ,IAAI,QAAQ,KAAK,OAAO,MAAM,MAAM;AAE1C,MAAI,4BAA4B,KAAK,EAAE,GAAG,CAAE;EAE5C,MAAM,eAAe,EAAE,QAAQ,EAAE,GAAG,QAAQ,EAAE,GAAG;EACjD,MAAM,SAAS,OAAO,MAAM,GAAG,aAAa,CAAC,MAAM,KAAK,CAAC,SAAS;AAClE,SAAO,KAAK;GAAE,SAAS,EAAE;GAAI;GAAQ,CAAC;;AAGxC,QAAO;EAAE;EAAU;EAAQ;;;;;AAM7B,MAAa,gBAAgB,IAAI,IAAI;CACnC;CAAK;CAAQ;CAAW;CAAQ;CAAW;CAAS;CAAS;CAAK;CAClE;CAAO;CAAO;CAAc;CAAQ;CAAM;CAAU;CAAU;CAC9D;CAAQ;CAAQ;CAAO;CAAY;CAAQ;CAAY;CAAM;CAC7D;CAAW;CAAO;CAAU;CAAO;CAAM;CAAM;CAAM;CAAS;CAC9D;CAAc;CAAU;CAAU;CAAQ;CAAM;CAAM;CAAM;CAAM;CAClE;CAAM;CAAQ;CAAU;CAAU;CAAM;CAAQ;CAAK;CAAU;CAC/D;CAAS;CAAO;CAAO;CAAS;CAAU;CAAM;CAAQ;CAAQ;CAChE;CAAQ;CAAQ;CAAQ;CAAS;CAAO;CAAY;CAAU;CAC9D;CAAY;CAAU;CAAU;CAAK;CAAW;CAAO;CAAY;CACnE;CAAM;CAAM;CAAQ;CAAK;CAAQ;CAAU;CAAU;CAAW;CAChE;CAAQ;CAAS;CAAU;CAAQ;CAAU;CAAS;CAAO;CAC7D;CAAO;CAAS;CAAS;CAAM;CAAY;CAAY;CAAS;CAChE;CAAS;CAAQ;CAAS;CAAM;CAAS;CAAK;CAAM;CAAO;CAAS;CACrE,CAAC;AAEF,SAAgB,kBAAkB,iBAAmC;CACnE,MAAM,uBAAO,IAAI,KAAa;CAG9B,MAAM,WAAW;CACjB,IAAI;AACJ,SAAQ,IAAI,SAAS,KAAK,gBAAgB,MAAM,KAC9C,MAAK,IAAI,EAAE,GAAG;CAIhB,MAAM,UAAU;AAChB,SAAQ,IAAI,QAAQ,KAAK,gBAAgB,MAAM,KAC7C,KAAI,CAAC,cAAc,IAAI,EAAE,GAAG,CAC1B,MAAK,IAAI,EAAE,GAAG;AAIlB,QAAO,CAAC,GAAG,KAAK;;AAGlB,eAAsB,kBAAkB,MAAc,eAAuD;CAC3G,MAAM,sBAAM,IAAI,KAAqB;CAErC,MAAM,OAAO;EACX,QAAQ,WAAW,gBAAgB;EACnC,QAAQ,MAAM,aAAa;EAC3B,GAAG;EACJ,CAAC,QAAO,MAAK,WAAW,EAAE,CAAC;AAE5B,MAAK,MAAM,OAAO,MAAM;EACtB,MAAM,QAAQ,MAAM,KAAK,CAAC,WAAW,EAAE;GAAE,KAAK;GAAK,UAAU;GAAM,CAAC;AACpE,OAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,OAAO,SAAS,MAAM,OAAO;AAEnC,OAAI,IAAI,KAAK,aAAa,EAAE,KAAK;;;AAIrC,QAAO"}
@@ -1,8 +1,8 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, computed, onMounted, onUnmounted, watch, watchEffect } from 'vue'
3
3
  import { RouterLink, RouterView, useRoute, useRouter } from 'vue-router'
4
- import { Monitor, CodeXml, Smartphone, ChevronDown, ArrowUp, ArrowDown, CornerDownLeft, Check, Search, Camera, FileCode, FileText, Code, BookText, MailQuestion } from 'lucide-vue-next'
5
- import { toBlob } from 'html-to-image'
4
+ import { Monitor, CodeXml, Smartphone, ChevronDown, ArrowUp, ArrowDown, CornerDownLeft, Check, Search, FileCode, FileText, Code, BookText, MailQuestion, Moon, Sun } from 'lucide-vue-next'
5
+ import SidebarClose from '@/components/SidebarClose.vue'
6
6
  import logoUrl from '@/logo.svg'
7
7
  import logoGradientUrl from '@/logo-gradient.svg'
8
8
  import { Kbd } from '@/components/ui/kbd'
@@ -77,6 +77,7 @@ const devicePresets: DevicePreset[] = [
77
77
 
78
78
  const selectedDevice = ref<DevicePreset | null>(null)
79
79
  const deviceMenuOpen = ref(false)
80
+ const darkMode = ref(false)
80
81
  const panelWidth = ref(0)
81
82
  const panelHeight = ref(0)
82
83
  const isDragging = ref(false)
@@ -136,32 +137,6 @@ watch(commandOpen, (open) => {
136
137
  if (!open) commandSearch.value = ''
137
138
  })
138
139
 
139
- const screenshotting = ref(false)
140
-
141
- async function copyScreenshot() {
142
- commandOpen.value = false
143
-
144
- const iframe = document.querySelector('iframe') as HTMLIFrameElement | null
145
- const doc = iframe?.contentDocument
146
- if (!doc?.body) return
147
-
148
- screenshotting.value = true
149
-
150
- try {
151
- const blob = await toBlob(doc.body, {
152
- width: doc.body.scrollWidth,
153
- height: doc.body.scrollHeight,
154
- })
155
-
156
- if (blob) {
157
- await navigator.clipboard.write([
158
- new ClipboardItem({ 'image/png': blob })
159
- ])
160
- }
161
- } finally {
162
- screenshotting.value = false
163
- }
164
- }
165
140
 
166
141
  async function copyHtml() {
167
142
  commandOpen.value = false
@@ -225,12 +200,6 @@ function onKeydown(e: KeyboardEvent) {
225
200
  return
226
201
  }
227
202
 
228
- if ((e.metaKey || e.ctrlKey) && e.key === 'b') {
229
- e.preventDefault()
230
- sidebarOpen.value = !sidebarOpen.value
231
- return
232
- }
233
-
234
203
  if (e.key === '/' && !isInputFocused()) {
235
204
  e.preventDefault()
236
205
  commandOpen.value = true
@@ -240,10 +209,6 @@ function onKeydown(e: KeyboardEvent) {
240
209
  // Copy shortcuts (Cmd on Mac, Alt on Win/Linux)
241
210
  if ((isMac ? e.metaKey : e.altKey) && !e.shiftKey && isPreviewRoute.value) {
242
211
  switch (e.key.toLowerCase()) {
243
- case 's':
244
- e.preventDefault()
245
- copyScreenshot()
246
- return
247
212
  case 'c':
248
213
  e.preventDefault()
249
214
  copyHtml()
@@ -271,6 +236,11 @@ function onWindowBlur() {
271
236
  deviceMenuOpen.value = false
272
237
  }
273
238
 
239
+ function toggleDarkMode() {
240
+ commandOpen.value = false
241
+ darkMode.value = !darkMode.value
242
+ }
243
+
274
244
  onMounted(() => {
275
245
  document.addEventListener('keydown', onKeydown)
276
246
  window.addEventListener('blur', onWindowBlur)
@@ -289,13 +259,14 @@ onUnmounted(() => {
289
259
  <img :src="logoUrl" alt="Maizzle" class="h-4 dark:hidden">
290
260
  <img :src="logoGradientUrl" alt="Maizzle" class="hidden h-4 dark:block">
291
261
  </RouterLink>
292
- <button class="inline-flex items-center gap-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" @click="commandOpen = true">
262
+ <button class="hidden md:inline-flex items-center gap-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" @click="commandOpen = true">
293
263
  <Search class="size-3.5" />
294
264
  <kbd class="flex items-center gap-0.5 text-[10px] font-sans">
295
265
  <span>{{ modKey }}</span>
296
266
  <span class="text-gray-300 dark:text-gray-600">K</span>
297
267
  </kbd>
298
268
  </button>
269
+ <SidebarClose />
299
270
  </SidebarHeader>
300
271
 
301
272
  <SidebarContent>
@@ -363,7 +334,8 @@ onUnmounted(() => {
363
334
  >
364
335
  {{ panelWidth }} <button class="hover:text-gray-700 dark:hover:text-gray-300" @click="selectedDevice = null; isFullSize = true; viewMode = 'preview'; resetKey++">&times;</button> {{ panelHeight }}
365
336
  </span>
366
- <DropdownMenu v-if="isPreviewRoute" v-model:open="deviceMenuOpen" :modal="false">
337
+ <div v-if="isPreviewRoute" class="flex items-center gap-1">
338
+ <DropdownMenu v-model:open="deviceMenuOpen" :modal="false">
367
339
  <DropdownMenuTrigger as-child>
368
340
  <Button variant="ghost" size="sm" class="hidden min-[430px]:inline-flex gap-1.5 shadow-none border-none hover:bg-transparent">
369
341
  <Smartphone class="size-4 dark:text-gray-400" :stroke-width="1" />
@@ -387,33 +359,38 @@ onUnmounted(() => {
387
359
  <span class="ml-auto text-[11px] text-gray-400 dark:text-gray-500 tabular-nums tracking-tight">{{ device.width }}&times;{{ device.height }}</span>
388
360
  </DropdownMenuItem>
389
361
  </DropdownMenuContent>
390
- </DropdownMenu>
362
+ </DropdownMenu>
363
+ </div>
391
364
  </div>
392
365
  </header>
393
366
 
394
367
  <!-- Main content -->
395
368
  <div class="flex-1 overflow-hidden">
396
369
  <RouterView v-slot="{ Component }">
397
- <component :is="Component" v-model:view-mode="viewMode" :device="selectedDevice" :reset-key="resetKey" :templates="templates" v-model:panel-width="panelWidth" v-model:panel-height="panelHeight" v-model:is-dragging="isDragging" v-model:is-full-size="isFullSize" @clear-device="selectedDevice = null; isFullSize = false" />
370
+ <component :is="Component" v-model:view-mode="viewMode" :device="selectedDevice" :reset-key="resetKey" :templates="templates" v-model:panel-width="panelWidth" v-model:panel-height="panelHeight" v-model:is-dragging="isDragging" v-model:is-full-size="isFullSize" v-model:dark-mode="darkMode" @clear-device="selectedDevice = null; isFullSize = false" />
398
371
  </RouterView>
399
372
  </div>
400
373
  </SidebarInset>
401
374
 
402
375
  <CommandDialog v-model:open="commandOpen" title="Command palette" description="Run commands or search emails">
403
376
  <CommandInput v-model="commandSearch" placeholder="Type a command or find an email..." />
404
- <CommandList>
377
+ <CommandList class="max-h-[400px]">
405
378
  <CommandEmpty>No results found.</CommandEmpty>
406
379
 
407
- <!-- Copy to clipboard commands -->
408
- <CommandGroup v-if="isPreviewRoute" heading="Copy to clipboard">
380
+ <!-- Preview commands -->
381
+ <CommandGroup v-if="isPreviewRoute && viewMode === 'preview'" heading="Preview">
409
382
  <CommandItem
410
- value="Screenshot"
411
- @select="copyScreenshot"
383
+ :value="darkMode ? 'Disable dark mode' : 'Emulate dark mode'"
384
+ @select="toggleDarkMode"
412
385
  >
413
- <Camera class="size-3 shrink-0 opacity-50" />
414
- <span>Screenshot</span>
415
- <CommandShortcut>{{ isMac ? '' : 'ALT+' }}S</CommandShortcut>
386
+ <Sun v-if="darkMode" class="size-3 shrink-0 opacity-50" />
387
+ <Moon v-else class="size-3 shrink-0 opacity-50" />
388
+ <span>{{ darkMode ? 'Disable dark mode' : 'Emulate dark mode' }}</span>
416
389
  </CommandItem>
390
+ </CommandGroup>
391
+
392
+ <!-- Copy to clipboard commands -->
393
+ <CommandGroup v-if="isPreviewRoute" heading="Copy to clipboard">
417
394
  <CommandItem
418
395
  value="HTML"
419
396
  @select="copyHtml"
@@ -0,0 +1,12 @@
1
+ <script setup lang="ts">
2
+ import { PanelRightOpen } from 'lucide-vue-next'
3
+ import { useSidebar } from '@/components/ui/sidebar'
4
+
5
+ const { setOpenMobile } = useSidebar()
6
+ </script>
7
+
8
+ <template>
9
+ <button class="md:hidden text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" @click="setOpenMobile(false)">
10
+ <PanelRightOpen class="size-4" :stroke-width="1" />
11
+ </button>
12
+ </template>
@@ -80,6 +80,7 @@ provideCommandContext({
80
80
  <ListboxRoot
81
81
  data-slot="command"
82
82
  v-bind="forwarded"
83
+ highlight-on-hover
83
84
  :class="cn('bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md', props.class)"
84
85
  >
85
86
  <slot />
@@ -24,7 +24,7 @@ const modelValue = useVModel(props, "modelValue", emits, {
24
24
  v-model="modelValue"
25
25
  data-slot="input"
26
26
  :class="cn(
27
- 'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
27
+ 'file:text-foreground placeholder:text-gray-400 dark:placeholder:text-gray-500 selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
28
28
  'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
29
29
  'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
30
30
  props.class,
@@ -21,7 +21,7 @@ const { isMobile, open, toggleSidebar } = useSidebar()
21
21
  :class="cn('h-7 w-7 hover:bg-transparent', props.class)"
22
22
  @click="toggleSidebar"
23
23
  >
24
- <svg v-if="isMobile" class="size-4 dark:text-gray-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round"><line x1="3" y1="7" x2="21" y2="7" /><line x1="3" y1="12" x2="15" y2="12" /><line x1="3" y1="17" x2="18" y2="17" /></svg>
24
+ <PanelRightClose v-if="isMobile" class="size-4 dark:text-gray-400" :stroke-width="1" />
25
25
  <PanelRightOpen v-else-if="open" class="dark:text-gray-400" :stroke-width="1" />
26
26
  <PanelRightClose v-else class="dark:text-gray-400" :stroke-width="1" />
27
27
  <span class="sr-only">Toggle Sidebar</span>
@@ -13,5 +13,5 @@ const forwardedProps = useForwardProps(delegatedProps)
13
13
  </script>
14
14
 
15
15
  <template>
16
- <TagsInputInput v-bind="forwardedProps" :class="cn('text-sm min-h-5 focus:outline-none flex-1 bg-transparent px-1', props.class)" />
16
+ <TagsInputInput v-bind="forwardedProps" :class="cn('text-sm min-h-5 focus:outline-none flex-1 bg-transparent px-1 placeholder:text-gray-400 dark:placeholder:text-gray-500', props.class)" />
17
17
  </template>
@@ -0,0 +1,131 @@
1
+ import { parse, converter, formatRgb } from 'culori'
2
+ import safeParser from 'postcss-safe-parser'
3
+
4
+ const toLch = converter('lch')
5
+
6
+ type Mode = 'background' | 'foreground'
7
+
8
+ // CSS properties whose values contain colors we should invert, mapped to
9
+ // the inversion mode. Kebab-case for <style> decls; inline styles are
10
+ // handled via the same lookup after lowercasing.
11
+ const styleProps = new Map<string, Mode>([
12
+ ['background', 'background'],
13
+ ['background-color', 'background'],
14
+ ['border', 'foreground'],
15
+ ['border-color', 'foreground'],
16
+ ['border-top', 'foreground'],
17
+ ['border-right', 'foreground'],
18
+ ['border-bottom', 'foreground'],
19
+ ['border-left', 'foreground'],
20
+ ['border-top-color', 'foreground'],
21
+ ['border-right-color', 'foreground'],
22
+ ['border-bottom-color', 'foreground'],
23
+ ['border-left-color', 'foreground'],
24
+ ['color', 'foreground'],
25
+ ['outline', 'foreground'],
26
+ ['outline-color', 'foreground'],
27
+ ])
28
+
29
+ // Hex | color function with balanced-enough parens | bare word (possible named color).
30
+ const COLOR_TOKEN = /#[a-f0-9]{3,8}\b|(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\([^)]*\)|\b[a-z]{3,20}\b/gi
31
+
32
+ // Words that look color-shaped but aren't — skip to avoid a wasted parse().
33
+ const NON_COLOR_KEYWORDS = /^(?:none|transparent|inherit|initial|unset|revert|currentcolor|auto|normal|solid|dashed|dotted|double|groove|ridge|inset|outset|hidden|thin|thick|medium|center|left|right|top|bottom|cover|contain|repeat|no-repeat|fixed|scroll|local|url|var|calc|linear|radial|conic|gradient)$/i
34
+
35
+ function invertOne(color: string, mode: Mode): string {
36
+ try {
37
+ const parsed = parse(color)
38
+ if (!parsed) return color
39
+ const lch = toLch(parsed) as any
40
+ if (!lch || typeof lch.l !== 'number' || Number.isNaN(lch.l)) return color
41
+
42
+ if (mode === 'background' && lch.l >= 50) lch.l = 50 - (lch.l - 50) * 0.75
43
+ if (mode === 'foreground' && lch.l < 50) lch.l = 50 - (lch.l - 50) * 0.75
44
+ if (typeof lch.c === 'number' && !Number.isNaN(lch.c)) lch.c *= 0.8
45
+
46
+ return formatRgb(lch) || color
47
+ } catch {
48
+ return color
49
+ }
50
+ }
51
+
52
+ function invertValue(value: string, mode: Mode): string {
53
+ return value.replace(COLOR_TOKEN, (tok) => {
54
+ if (NON_COLOR_KEYWORDS.test(tok)) return tok
55
+ if (!parse(tok)) return tok
56
+ return invertOne(tok, mode)
57
+ })
58
+ }
59
+
60
+ // Splits an inline style attr by `;` (safe — color functions use `,` not `;`).
61
+ function invertInlineStyle(style: string): string {
62
+ return style.split(';').map((decl) => {
63
+ const i = decl.indexOf(':')
64
+ if (i === -1) return decl
65
+ const prop = decl.slice(0, i).trim().toLowerCase()
66
+ const mode = styleProps.get(prop)
67
+ if (!mode) return decl
68
+ return decl.slice(0, i + 1) + invertValue(decl.slice(i + 1), mode)
69
+ }).join(';')
70
+ }
71
+
72
+ function invertStyleTag(css: string): string {
73
+ try {
74
+ const root = safeParser(css)
75
+ root.walkDecls((decl) => {
76
+ const mode = styleProps.get(decl.prop.toLowerCase())
77
+ if (mode) decl.value = invertValue(decl.value, mode)
78
+ })
79
+ return root.toString()
80
+ } catch {
81
+ return css
82
+ }
83
+ }
84
+
85
+ const ORIG_INLINE = 'data-maizzle-orig-style'
86
+ const ORIG_STYLE_TAG = 'data-maizzle-orig-style-content'
87
+ const APPLIED_FLAG = 'data-maizzle-dark-applied'
88
+
89
+ function* walk(root: Node): Generator<Element> {
90
+ if (root.nodeType === 1) yield root as Element
91
+ for (const child of Array.from(root.childNodes)) yield* walk(child)
92
+ }
93
+
94
+ export function applyColorInversion(iframe: HTMLIFrameElement): void {
95
+ const doc = iframe.contentDocument
96
+ if (!doc || !doc.body || doc.body.hasAttribute(APPLIED_FLAG)) return
97
+
98
+ for (const el of walk(doc.documentElement)) {
99
+ const inline = el.getAttribute('style')
100
+ if (inline) {
101
+ el.setAttribute(ORIG_INLINE, inline)
102
+ el.setAttribute('style', invertInlineStyle(inline))
103
+ }
104
+ if (el.tagName === 'STYLE') {
105
+ const original = el.textContent ?? ''
106
+ el.setAttribute(ORIG_STYLE_TAG, original)
107
+ el.textContent = invertStyleTag(original)
108
+ }
109
+ }
110
+
111
+ doc.body.setAttribute(APPLIED_FLAG, '')
112
+ }
113
+
114
+ export function undoColorInversion(iframe: HTMLIFrameElement): void {
115
+ const doc = iframe.contentDocument
116
+ if (!doc || !doc.body || !doc.body.hasAttribute(APPLIED_FLAG)) return
117
+
118
+ for (const el of walk(doc.documentElement)) {
119
+ const origInline = el.getAttribute(ORIG_INLINE)
120
+ if (origInline !== null) {
121
+ el.setAttribute('style', origInline)
122
+ el.removeAttribute(ORIG_INLINE)
123
+ }
124
+ if (el.tagName === 'STYLE' && el.hasAttribute(ORIG_STYLE_TAG)) {
125
+ el.textContent = el.getAttribute(ORIG_STYLE_TAG) ?? ''
126
+ el.removeAttribute(ORIG_STYLE_TAG)
127
+ }
128
+ }
129
+
130
+ doc.body.removeAttribute(APPLIED_FLAG)
131
+ }