@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.
- package/dist/build.mjs +4 -1
- package/dist/build.mjs.map +1 -1
- package/dist/components/Button.vue +2 -2
- package/dist/components/CodeBlock.vue +2 -1
- package/dist/components/Column.vue +28 -22
- package/dist/components/Container.vue +47 -9
- package/dist/components/Font.vue +96 -0
- package/dist/components/Layout.vue +9 -4
- package/dist/components/Overlap.vue +75 -18
- package/dist/components/Row.vue +40 -19
- package/dist/components/Section.vue +35 -8
- package/dist/components/utils.d.mts +14 -1
- package/dist/components/utils.d.mts.map +1 -1
- package/dist/components/utils.mjs +32 -1
- package/dist/components/utils.mjs.map +1 -1
- package/dist/components/utils.ts +39 -0
- package/dist/composables/renderContext.d.mts +8 -1
- package/dist/composables/renderContext.d.mts.map +1 -1
- package/dist/composables/renderContext.mjs.map +1 -1
- package/dist/composables/useFont.d.mts +50 -0
- package/dist/composables/useFont.d.mts.map +1 -0
- package/dist/composables/useFont.mjs +93 -0
- package/dist/composables/useFont.mjs.map +1 -0
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +2 -1
- package/dist/plugins/postcss/quoteFontFamilies.d.mts +13 -0
- package/dist/plugins/postcss/quoteFontFamilies.d.mts.map +1 -0
- package/dist/plugins/postcss/quoteFontFamilies.mjs +84 -0
- package/dist/plugins/postcss/quoteFontFamilies.mjs.map +1 -0
- package/dist/render/createRenderer.mjs +8 -2
- package/dist/render/createRenderer.mjs.map +1 -1
- package/dist/render/injectFonts.d.mts +15 -0
- package/dist/render/injectFonts.d.mts.map +1 -0
- package/dist/render/injectFonts.mjs +46 -0
- package/dist/render/injectFonts.mjs.map +1 -0
- package/dist/serve.d.mts.map +1 -1
- package/dist/serve.mjs +28 -12
- package/dist/serve.mjs.map +1 -1
- package/dist/server/compatibility.d.mts +54 -2
- package/dist/server/compatibility.d.mts.map +1 -1
- package/dist/server/compatibility.mjs +890 -76
- package/dist/server/compatibility.mjs.map +1 -1
- package/dist/server/linter.d.mts +15 -2
- package/dist/server/linter.d.mts.map +1 -1
- package/dist/server/linter.mjs +194 -43
- package/dist/server/linter.mjs.map +1 -1
- package/dist/server/sfc-utils.d.mts +18 -0
- package/dist/server/sfc-utils.d.mts.map +1 -0
- package/dist/server/sfc-utils.mjs +184 -0
- package/dist/server/sfc-utils.mjs.map +1 -0
- package/dist/server/ui/App.vue +27 -50
- package/dist/server/ui/components/SidebarClose.vue +12 -0
- package/dist/server/ui/components/ui/command/Command.vue +1 -0
- package/dist/server/ui/components/ui/input/Input.vue +1 -1
- package/dist/server/ui/components/ui/sidebar/SidebarTrigger.vue +1 -1
- package/dist/server/ui/components/ui/tags-input/TagsInputInput.vue +1 -1
- package/dist/server/ui/lib/emulated-dark-mode.ts +131 -0
- package/dist/server/ui/pages/Preview.vue +215 -156
- package/dist/transformers/addAttributes.mjs +10 -6
- package/dist/transformers/addAttributes.mjs.map +1 -1
- package/dist/transformers/columnWidth.d.mts +31 -0
- package/dist/transformers/columnWidth.d.mts.map +1 -0
- package/dist/transformers/columnWidth.mjs +166 -0
- package/dist/transformers/columnWidth.mjs.map +1 -0
- package/dist/transformers/index.d.mts.map +1 -1
- package/dist/transformers/index.mjs +4 -0
- package/dist/transformers/index.mjs.map +1 -1
- package/dist/transformers/inlineCSS.mjs +2 -2
- package/dist/transformers/inlineCSS.mjs.map +1 -1
- package/dist/transformers/msoWidthFromClass.d.mts +19 -0
- package/dist/transformers/msoWidthFromClass.d.mts.map +1 -0
- package/dist/transformers/msoWidthFromClass.mjs +61 -0
- package/dist/transformers/msoWidthFromClass.mjs.map +1 -0
- package/dist/transformers/purgeCSS.mjs +1 -1
- package/dist/transformers/purgeCSS.mjs.map +1 -1
- package/dist/transformers/tailwindcss.d.mts.map +1 -1
- package/dist/transformers/tailwindcss.mjs +6 -16
- package/dist/transformers/tailwindcss.mjs.map +1 -1
- package/dist/types/config.d.mts +42 -2
- package/dist/types/config.d.mts.map +1 -1
- package/dist/types/index.d.mts +2 -2
- package/dist/utils/decodeStyleEntities.d.mts +15 -0
- package/dist/utils/decodeStyleEntities.d.mts.map +1 -0
- package/dist/utils/decodeStyleEntities.mjs +18 -0
- package/dist/utils/decodeStyleEntities.mjs.map +1 -0
- package/package.json +2 -3
- package/dist/_virtual/_rolldown/runtime.mjs +0 -32
- package/dist/node_modules/picomatch/index.mjs +0 -13
- package/dist/node_modules/picomatch/index.mjs.map +0 -1
- package/dist/node_modules/picomatch/lib/constants.mjs +0 -174
- package/dist/node_modules/picomatch/lib/constants.mjs.map +0 -1
- package/dist/node_modules/picomatch/lib/parse.mjs +0 -1067
- package/dist/node_modules/picomatch/lib/parse.mjs.map +0 -1
- package/dist/node_modules/picomatch/lib/picomatch.mjs +0 -304
- package/dist/node_modules/picomatch/lib/picomatch.mjs.map +0 -1
- package/dist/node_modules/picomatch/lib/scan.mjs +0 -296
- package/dist/node_modules/picomatch/lib/scan.mjs.map +0 -1
- package/dist/node_modules/picomatch/lib/utils.mjs +0 -53
- 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"}
|
package/dist/server/ui/App.vue
CHANGED
|
@@ -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,
|
|
5
|
-
import
|
|
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++">×</button> {{ panelHeight }}
|
|
365
336
|
</span>
|
|
366
|
-
<
|
|
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 }}×{{ device.height }}</span>
|
|
388
360
|
</DropdownMenuItem>
|
|
389
361
|
</DropdownMenuContent>
|
|
390
|
-
|
|
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
|
-
<!--
|
|
408
|
-
<CommandGroup v-if="isPreviewRoute" heading="
|
|
380
|
+
<!-- Preview commands -->
|
|
381
|
+
<CommandGroup v-if="isPreviewRoute && viewMode === 'preview'" heading="Preview">
|
|
409
382
|
<CommandItem
|
|
410
|
-
value="
|
|
411
|
-
@select="
|
|
383
|
+
:value="darkMode ? 'Disable dark mode' : 'Emulate dark mode'"
|
|
384
|
+
@select="toggleDarkMode"
|
|
412
385
|
>
|
|
413
|
-
<
|
|
414
|
-
<
|
|
415
|
-
<
|
|
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>
|
|
@@ -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-
|
|
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
|
-
<
|
|
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
|
+
}
|