wizards 0.3.6 → 0.3.7
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/apps/runtime/dist/agents/studio-assistant.js +285 -31
- package/apps/runtime/dist/agents/website.js +256 -34
- package/apps/runtime/dist/db/schema.js +1 -1
- package/apps/runtime/dist/documents/parse.js +14 -0
- package/apps/runtime/dist/engine/steps.js +42 -6
- package/apps/runtime/dist/engine/template.js +50 -3
- package/apps/runtime/dist/film/project.js +54 -4
- package/apps/runtime/dist/routes/studio.js +37 -1
- package/apps/runtime/dist/services/brand.js +6 -3
- package/apps/runtime/dist/services/project-files.js +59 -3
- package/apps/runtime/dist/services/projects.js +150 -2
- package/apps/web/dist/assets/AboutDialog-C0dN86zd.js +1 -0
- package/apps/web/dist/assets/{ChangelogDialog-m2M7gZiB.js → ChangelogDialog-Cv81t7tj.js} +1 -1
- package/apps/web/dist/assets/ChatView-uogJ3nht.js +10 -0
- package/apps/web/dist/assets/EditorPage-n8YuTh6j.js +3 -0
- package/apps/web/dist/assets/{FlowPreview-BdRy7FO7.js → FlowPreview-D4dp1MWS.js} +1 -1
- package/apps/web/dist/assets/{Harness-DmJkTwOO.js → Harness-v7r0e6vK.js} +2 -2
- package/apps/web/dist/assets/NewWizardChannels-Be-7VhBi.js +1 -0
- package/apps/web/dist/assets/NewWizardPage-C8hPvrK8.js +7 -0
- package/apps/web/dist/assets/ProjectPage-B686ArYy.js +1 -0
- package/apps/web/dist/assets/PublicRunner-DeCpVfZP.js +1 -0
- package/apps/web/dist/assets/{ReadOnly-CaGzgOPe.js → ReadOnly-B-UCgnqW.js} +1 -1
- package/apps/web/dist/assets/RunDrawer-C6pzi8AJ.js +4 -0
- package/apps/web/dist/assets/{RunnerView-CJ4cLA_0.js → RunnerView-Deenymq0.js} +1 -1
- package/apps/web/dist/assets/SettingsPage-CK6L0yEe.js +1 -0
- package/apps/web/dist/assets/SetupPage-9CH3qXdH.js +1 -0
- package/apps/web/dist/assets/SharePage-B778xUFo.js +1 -0
- package/apps/web/dist/assets/changelog-DCJo2mbx.js +1 -0
- package/apps/web/dist/assets/{circle-alert-CpuMIhbH.js → circle-alert-DLvC9ePT.js} +1 -1
- package/apps/web/dist/assets/{esm-DRaZAcXQ.js → esm-BJskogLD.js} +1 -1
- package/apps/web/dist/assets/index-D6hUM8NU.css +1 -0
- package/apps/web/dist/assets/index-Df6UxElH.js +12 -0
- package/apps/web/dist/assets/{lock-BGotwAyN.js → lock-DR7NakPn.js} +1 -1
- package/apps/web/dist/assets/{phone-CA2A8W7I.js → phone-BORh3jfv.js} +1 -1
- package/apps/web/dist/assets/{settings-menu-DxswVOqQ.js → settings-menu-DV5D91TD.js} +1 -1
- package/apps/web/dist/assets/{ui-DAHMnAVc.js → ui-BOCXzzHi.js} +1 -1
- package/apps/web/dist/assets/{upload-DLbJVvfd.js → upload-B3yv8gsk.js} +1 -1
- package/apps/web/dist/index.html +3 -8
- package/apps/web/dist/mcp/flow.html +2 -2
- package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +3 -3
- package/node_modules/@engenty-wizards/shared/dist/projects.d.ts +138 -3
- package/node_modules/@engenty-wizards/shared/dist/projects.js +153 -4
- package/package.json +1 -1
- package/apps/web/dist/assets/AboutDialog-Dz_tYBSU.js +0 -1
- package/apps/web/dist/assets/ChatView-D21o-lq8.js +0 -10
- package/apps/web/dist/assets/EditorPage-D6j3dQpu.js +0 -3
- package/apps/web/dist/assets/Marketplace-B-gy2onK.js +0 -2
- package/apps/web/dist/assets/NewWizardChannels-B3FEpsJ-.js +0 -1
- package/apps/web/dist/assets/NewWizardPage-CT1lThNM.js +0 -7
- package/apps/web/dist/assets/ProjectPage-BIanCF0m.js +0 -2
- package/apps/web/dist/assets/PublicRunner-H1fXa9BK.js +0 -1
- package/apps/web/dist/assets/RunDrawer-8sjkAbm5.js +0 -4
- package/apps/web/dist/assets/SettingsPage-BsdwE1OA.js +0 -1
- package/apps/web/dist/assets/SetupPage-sqrv7Auf.js +0 -1
- package/apps/web/dist/assets/SharePage-B6ets1ot.js +0 -1
- package/apps/web/dist/assets/brand-BRw18WJF.js +0 -2
- package/apps/web/dist/assets/changelog-BOFdRjx2.js +0 -1
- package/apps/web/dist/assets/index-DY9CslcT.css +0 -1
- package/apps/web/dist/assets/index-DkDBEMwz.js +0 -11
- package/apps/web/dist/assets/session-DBOQL_bg.js +0 -1
- package/apps/web/dist/assets/useMutation-DKyQXgyj.js +0 -1
- package/apps/web/dist/assets/volume-2-KaKQ4zMJ.js +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FONT_WEIGHTS } from "@engenty-wizards/shared/projects";
|
|
1
2
|
import { webContext } from "../render/chromium.js";
|
|
2
3
|
import { htmlToMarkdown } from "../render/convert.js";
|
|
3
4
|
import { safeFetch } from "../tools/net-guard.js";
|
|
@@ -29,28 +30,60 @@ function hex(value) {
|
|
|
29
30
|
}
|
|
30
31
|
const rgb = v.match(/^rgba?\(\s*(\d{1,3})[\s,]+(\d{1,3})[\s,]+(\d{1,3})/);
|
|
31
32
|
if (rgb) {
|
|
32
|
-
return
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
return toHex(rgb.slice(1, 4).map(Number));
|
|
34
|
+
}
|
|
35
|
+
const hsl = v.match(/^hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
|
|
36
|
+
if (hsl) {
|
|
37
|
+
const [h, sat, l] = [Number(hsl[1]), Number(hsl[2]) / 100, Number(hsl[3]) / 100];
|
|
38
|
+
const f = (n) => {
|
|
39
|
+
const k = (n + h / 30) % 12;
|
|
40
|
+
return 255 * (l - sat * Math.min(l, 1 - l) * Math.max(-1, Math.min(k - 3, 9 - k, 1)));
|
|
41
|
+
};
|
|
42
|
+
return toHex([f(0), f(8), f(4)]);
|
|
43
|
+
}
|
|
44
|
+
const ok = v.match(/^oklch\(\s*([\d.]+)(%?)\s+([\d.]+)\s+([\d.]+)/);
|
|
45
|
+
if (ok) {
|
|
46
|
+
return toHex(oklchToRgb(Number(ok[1]) / (ok[2] ? 100 : 1), Number(ok[3]), Number(ok[4])));
|
|
36
47
|
}
|
|
37
48
|
return null;
|
|
38
49
|
}
|
|
50
|
+
const toHex = (rgb) => `#${rgb
|
|
51
|
+
.map((n) => Math.round(Math.max(0, Math.min(255, n)))
|
|
52
|
+
.toString(16)
|
|
53
|
+
.padStart(2, "0"))
|
|
54
|
+
.join("")}`;
|
|
55
|
+
/** OKLCH (as CSS writes it, Tailwind 4 among others) in sRGB, 0–255. */
|
|
56
|
+
function oklchToRgb(l, c, h) {
|
|
57
|
+
const a = c * Math.cos((h * Math.PI) / 180);
|
|
58
|
+
const b = c * Math.sin((h * Math.PI) / 180);
|
|
59
|
+
const l_ = (l + 0.3963377774 * a + 0.2158037573 * b) ** 3;
|
|
60
|
+
const m_ = (l - 0.1055613458 * a - 0.0638541728 * b) ** 3;
|
|
61
|
+
const s_ = (l - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
62
|
+
const linear = [
|
|
63
|
+
4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_,
|
|
64
|
+
-1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_,
|
|
65
|
+
-0.0041960863 * l_ - 0.7034186147 * m_ + 1.707614701 * s_,
|
|
66
|
+
];
|
|
67
|
+
return linear.map((x) => 255 * (x <= 0.0031308 ? 12.92 * x : 1.055 * Math.max(0, x) ** (1 / 2.4) - 0.055));
|
|
68
|
+
}
|
|
39
69
|
/** How far a colour is from grey, 0–255. */
|
|
40
70
|
function chroma(color) {
|
|
41
71
|
const [r, g, b] = [1, 3, 5].map((i) => Number.parseInt(color.slice(i, i + 2), 16));
|
|
42
72
|
return Math.max(r, g, b) - Math.min(r, g, b);
|
|
43
73
|
}
|
|
74
|
+
/** A colour as CSS writes it: hex, rgb(), hsl() or oklch(). */
|
|
75
|
+
const COLOUR = /#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)|hsla?\([^)]*\)|oklch\([^)]*\)/;
|
|
44
76
|
function coloursOf(css) {
|
|
45
77
|
const variables = [];
|
|
46
|
-
for (const m of css.matchAll(
|
|
78
|
+
for (const m of css.matchAll(new RegExp(`(--[\\w-]+)\\s*:\\s*(${COLOUR.source})`, "g"))) {
|
|
47
79
|
const value = hex(m[2]);
|
|
48
|
-
|
|
80
|
+
// Tailwind's own variables are its machinery, not the site's colours.
|
|
81
|
+
if (value && !m[1].startsWith("--tw-") && !variables.some((v) => v.name === m[1])) {
|
|
49
82
|
variables.push({ name: m[1], value });
|
|
50
83
|
}
|
|
51
84
|
}
|
|
52
85
|
const counts = new Map();
|
|
53
|
-
for (const m of css.matchAll(
|
|
86
|
+
for (const m of css.matchAll(new RegExp(COLOUR.source, "g"))) {
|
|
54
87
|
const value = hex(m[0]);
|
|
55
88
|
if (value) {
|
|
56
89
|
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
@@ -173,59 +206,247 @@ export async function readPage(raw, signal) {
|
|
|
173
206
|
jsonld: jsonLdOf(html),
|
|
174
207
|
};
|
|
175
208
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
209
|
+
const saysLogo = (tag) => /logo/i.test([
|
|
210
|
+
attr(tag, "src"),
|
|
211
|
+
attr(tag, "alt"),
|
|
212
|
+
attr(tag, "class"),
|
|
213
|
+
attr(tag, "id"),
|
|
214
|
+
attr(tag, "aria-label"),
|
|
215
|
+
]
|
|
216
|
+
.filter(Boolean)
|
|
217
|
+
.join(" "));
|
|
218
|
+
/**
|
|
219
|
+
* The logos a page shows, best first: the logo its structured data names, pictures that say
|
|
220
|
+
* "logo", what its home link in the header shows, and its icons. `inline`: SVGs drawn into the
|
|
221
|
+
* page count too, as `data:` addresses — for a person to pick from, too long for a model to read.
|
|
222
|
+
*/
|
|
223
|
+
function logosOf(html, base, inline = false) {
|
|
180
224
|
const logos = [];
|
|
181
|
-
const
|
|
182
|
-
const url = absolute(href, base);
|
|
225
|
+
const add = (href, hint) => {
|
|
226
|
+
const url = href.startsWith("data:image/svg+xml") ? href : absolute(href, base);
|
|
183
227
|
if (url && !logos.some((l) => l.url === url)) {
|
|
184
228
|
logos.push({ url, hint });
|
|
185
229
|
}
|
|
186
230
|
};
|
|
187
|
-
const
|
|
188
|
-
|
|
231
|
+
for (const item of jsonLdOf(html)) {
|
|
232
|
+
const logo = item?.logo;
|
|
233
|
+
const href = typeof logo === "string" ? logo : logo?.url;
|
|
234
|
+
if (typeof href === "string") {
|
|
235
|
+
add(href, "the logo the page names in its structured data");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
for (const tag of tagsOf(html, "img")) {
|
|
189
239
|
const src = attr(tag, "src") || attr(tag, "data-src");
|
|
190
|
-
|
|
191
|
-
|
|
240
|
+
if (src && !src.startsWith("data:") && saysLogo(tag)) {
|
|
241
|
+
const alt = attr(tag, "alt");
|
|
242
|
+
add(src, `image on the page${alt ? `, alt "${alt}"` : ""}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// What the link home shows is the logo on most sites, said so or not.
|
|
246
|
+
for (const m of html.matchAll(/<a\b([^>]*)>([\s\S]*?)<\/a>/gi)) {
|
|
247
|
+
const href = absolute(attr(`<a ${m[1]}>`, "href"), base);
|
|
248
|
+
if (!href || new URL(href).pathname !== "/" || new URL(href).host !== new URL(base).host) {
|
|
192
249
|
continue;
|
|
193
250
|
}
|
|
194
|
-
|
|
195
|
-
|
|
251
|
+
const img = tagsOf(m[2], "img")[0];
|
|
252
|
+
const src = img && (attr(img, "src") || attr(img, "data-src"));
|
|
253
|
+
if (src && !src.startsWith("data:")) {
|
|
254
|
+
add(src, "image in the link to the home page");
|
|
255
|
+
}
|
|
256
|
+
const svg = inline ? m[2].match(/<svg\b[\s\S]*?<\/svg>/i)?.[0] : undefined;
|
|
257
|
+
if (svg) {
|
|
258
|
+
add(svgUrl(svg), "drawing in the link to the home page");
|
|
196
259
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
260
|
+
}
|
|
261
|
+
if (inline) {
|
|
262
|
+
for (const m of html.matchAll(/<svg\b[^>]*>[\s\S]*?<\/svg>/gi)) {
|
|
263
|
+
const open = m[0].match(/^<svg\b[^>]*>/i)?.[0] ?? "";
|
|
264
|
+
const title = m[0].match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1] ?? "";
|
|
265
|
+
if (saysLogo(open) || /logo/i.test(title)) {
|
|
266
|
+
add(svgUrl(m[0]), "drawing on the page");
|
|
201
267
|
}
|
|
202
268
|
}
|
|
203
269
|
}
|
|
204
|
-
for (const tag of
|
|
270
|
+
for (const tag of tagsOf(html, "link")) {
|
|
205
271
|
const rel = attr(tag, "rel").toLowerCase();
|
|
206
272
|
if (/apple-touch-icon|mask-icon|(^|\s)icon/.test(rel)) {
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
const og = meta("og:image");
|
|
211
|
-
if (og) {
|
|
212
|
-
const url = absolute(og, base);
|
|
213
|
-
if (url) {
|
|
214
|
-
images.unshift({ url, alt: "share image (og:image)" });
|
|
273
|
+
add(attr(tag, "href"), `${rel} ${attr(tag, "sizes")}`.trim());
|
|
215
274
|
}
|
|
216
275
|
}
|
|
217
|
-
|
|
276
|
+
return logos;
|
|
277
|
+
}
|
|
278
|
+
/** An inline SVG as a picture of its own: with its namespace, as a `data:` address. */
|
|
279
|
+
function svgUrl(svg) {
|
|
280
|
+
const own = /\bxmlns\s*=/.test(svg.match(/^<svg\b[^>]*>/i)?.[0] ?? "")
|
|
281
|
+
? svg
|
|
282
|
+
: svg.replace(/^<svg\b/i, '<svg xmlns="http://www.w3.org/2000/svg"');
|
|
283
|
+
return `data:image/svg+xml;base64,${Buffer.from(own).toString("base64")}`;
|
|
284
|
+
}
|
|
285
|
+
/** The logos on a web page, for a person to pick from (Logos › from a web address). */
|
|
286
|
+
export async function scanLogos(raw, signal) {
|
|
287
|
+
const { base, html } = await loadPage(raw, signal);
|
|
288
|
+
return logosOf(html, base, true)
|
|
289
|
+
.filter((l) => !l.url.startsWith("data:") || l.url.length < 400_000)
|
|
290
|
+
.slice(0, 16);
|
|
291
|
+
}
|
|
292
|
+
/** The CSS a page brings: its `<style>` blocks, style attributes and first stylesheets. */
|
|
293
|
+
async function cssOf(html, base, signal) {
|
|
294
|
+
const sheets = tagsOf(html, "link")
|
|
218
295
|
.filter((t) => /stylesheet/i.test(attr(t, "rel")))
|
|
219
296
|
.map((t) => absolute(attr(t, "href"), base))
|
|
220
297
|
.filter((u) => Boolean(u))
|
|
221
298
|
.slice(0, 4);
|
|
222
299
|
const inline = [...html.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi)].map((m) => m[1]);
|
|
223
300
|
const styled = [...html.matchAll(/\bstyle\s*=\s*"([^"]*)"/gi)].map((m) => m[1]);
|
|
224
|
-
|
|
301
|
+
return [
|
|
225
302
|
...inline,
|
|
226
303
|
...styled,
|
|
227
304
|
...(await Promise.all(sheets.map((s) => stylesheet(s, signal)))),
|
|
228
305
|
].join("\n");
|
|
306
|
+
}
|
|
307
|
+
/** Family names that are no brand's own: generic and system faces. */
|
|
308
|
+
const GENERIC_FONT = /^(serif|sans-serif|monospace|cursive|fantasy|system-ui|ui-[a-z-]+|-apple-system|blinkmacsystemfont|inherit|initial|unset|revert|emoji|math|fangsong|segoe ui( emoji| symbol)?|helvetica( neue)?|arial|roboto|noto (sans|color emoji)|apple color emoji|courier( new)?|times( new roman)?|georgia|menlo|monaco|consolas|var\(.*|liberation mono|sfmono-regular|sf mono|sf pro( text| display)?|dejavu sans( mono)?)$/i;
|
|
309
|
+
/** "Geist Variable" is Geist: a variable font's file name, not another family. */
|
|
310
|
+
const familyName = (raw) => raw
|
|
311
|
+
.replace(/^[\s"'(]+|[\s"')]+$/g, "")
|
|
312
|
+
.replace(/\s+variable$/i, "")
|
|
313
|
+
.trim();
|
|
314
|
+
/**
|
|
315
|
+
* The typefaces a page uses, most used first, with the weights its `@font-face` rules or its
|
|
316
|
+
* Google / Bunny Fonts links name.
|
|
317
|
+
*/
|
|
318
|
+
function fontsOf(css, html) {
|
|
319
|
+
const counts = new Map();
|
|
320
|
+
const weights = new Map();
|
|
321
|
+
const count = (family, n = 1) => counts.set(family, (counts.get(family) ?? 0) + n);
|
|
322
|
+
const weigh = (family, list) => {
|
|
323
|
+
const set = weights.get(family) ?? new Set();
|
|
324
|
+
for (const w of list) {
|
|
325
|
+
set.add(w);
|
|
326
|
+
}
|
|
327
|
+
weights.set(family, set);
|
|
328
|
+
};
|
|
329
|
+
const first = (decl) => decl
|
|
330
|
+
.split(",")
|
|
331
|
+
.map(familyName)
|
|
332
|
+
.find((f) => /[a-z]/i.test(f) && !GENERIC_FONT.test(f));
|
|
333
|
+
// --font-weight-…, --font-size-… and the like are no families.
|
|
334
|
+
for (const m of css.matchAll(/(?:font-family|--font-(?!weight|size|feature|variation|style|stretch)[\w-]+)\s*:\s*([^;}]+)/gi)) {
|
|
335
|
+
const family = first(m[1]);
|
|
336
|
+
if (family) {
|
|
337
|
+
count(family);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
for (const m of css.matchAll(/@font-face\s*\{([^}]*)\}/gi)) {
|
|
341
|
+
const family = first(m[1].match(/font-family\s*:\s*([^;]+)/i)?.[1] ?? "");
|
|
342
|
+
const range = (m[1].match(/font-weight\s*:\s*([^;]+)/i)?.[1] ?? "400")
|
|
343
|
+
.split(/\s+/)
|
|
344
|
+
.map(Number)
|
|
345
|
+
.filter((n) => n >= 100 && n <= 900);
|
|
346
|
+
if (family && range.length) {
|
|
347
|
+
const [lo, hi] = [Math.min(...range), Math.max(...range)];
|
|
348
|
+
weigh(family, FONT_WEIGHTS.filter((w) => w >= lo && w <= hi));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// fonts.googleapis.com/css2?family=Inter:wght@400;700 and fonts.bunny.net/css?family=inter:400,700
|
|
352
|
+
for (const m of html.matchAll(/fonts\.(?:googleapis\.com|bunny\.net)\/css2?\?([^"'\s>]+)/gi)) {
|
|
353
|
+
for (const part of m[1].replace(/&/g, "&").split("&")) {
|
|
354
|
+
if (!part.startsWith("family=")) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
for (const one of decodeURIComponent(part.slice(7)).split("|")) {
|
|
358
|
+
const [name, spec = ""] = one.split(":");
|
|
359
|
+
const family = familyName(name.replace(/\+/g, " ").replace(/-/g, " ")).replace(/\b\w/g, (c) => c.toUpperCase());
|
|
360
|
+
count(family, 3);
|
|
361
|
+
weigh(family, (spec.match(/\d{3}/g) ?? []).map(Number).filter((w) => w >= 100 && w <= 900));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return [...counts.entries()]
|
|
366
|
+
.sort((a, b) => b[1] - a[1])
|
|
367
|
+
.slice(0, 6)
|
|
368
|
+
.map(([family]) => ({
|
|
369
|
+
family,
|
|
370
|
+
weights: [...(weights.get(family) ?? [])].sort((a, b) => a - b),
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
/** The typefaces of a web page, for a person to pick from (Typography › from a web address). */
|
|
374
|
+
export async function scanFonts(raw, signal) {
|
|
375
|
+
const { base, html } = await loadPage(raw, signal);
|
|
376
|
+
return fontsOf(await cssOf(html, base, signal), html);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* The colours of a web page, for a person to pick from (Colours › from a web address): its
|
|
380
|
+
* theme colour, the colours its stylesheet names, then the ones it uses most. A named one is
|
|
381
|
+
* named after its variable ("--brand-primary" → "brand primary").
|
|
382
|
+
*/
|
|
383
|
+
export async function scanColors(raw, signal) {
|
|
384
|
+
const { base, html } = await loadPage(raw, signal);
|
|
385
|
+
const { variables, frequent } = coloursOf(await cssOf(html, base, signal));
|
|
386
|
+
const out = [];
|
|
387
|
+
const add = (name, value) => {
|
|
388
|
+
if (value && !out.some((c) => c.value === value)) {
|
|
389
|
+
out.push({ name, value });
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
add("theme color", hex(metaOf(html, "theme-color")));
|
|
393
|
+
for (const v of variables) {
|
|
394
|
+
add(v.name.replace(/^--/, "").replace(/[-_]+/g, " "), v.value);
|
|
395
|
+
}
|
|
396
|
+
for (const value of frequent) {
|
|
397
|
+
add("", value);
|
|
398
|
+
}
|
|
399
|
+
return out.slice(0, 24);
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* The pictures of a web page, for a person to pick assets from: its share image, then the
|
|
403
|
+
* images it shows, logos left out.
|
|
404
|
+
*/
|
|
405
|
+
export async function scanImages(raw, signal) {
|
|
406
|
+
const { base, html } = await loadPage(raw, signal);
|
|
407
|
+
const out = [];
|
|
408
|
+
const add = (href, hint) => {
|
|
409
|
+
const url = absolute(href, base);
|
|
410
|
+
if (url && !out.some((i) => i.url === url)) {
|
|
411
|
+
out.push({ url, hint });
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
const og = metaOf(html, "og:image");
|
|
415
|
+
if (og) {
|
|
416
|
+
add(og, "share image");
|
|
417
|
+
}
|
|
418
|
+
for (const tag of tagsOf(html, "img")) {
|
|
419
|
+
const src = attr(tag, "src") || attr(tag, "data-src");
|
|
420
|
+
if (src && !src.startsWith("data:") && !saysLogo(tag)) {
|
|
421
|
+
add(src, attr(tag, "alt"));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return out.slice(0, 24);
|
|
425
|
+
}
|
|
426
|
+
export async function readWebsite(raw, signal) {
|
|
427
|
+
const { res, base, html } = await loadPage(raw, signal);
|
|
428
|
+
const tags = (name) => tagsOf(html, name);
|
|
429
|
+
const meta = (key) => metaOf(html, key);
|
|
430
|
+
const logos = logosOf(html, base);
|
|
431
|
+
const images = [];
|
|
432
|
+
for (const tag of tags("img")) {
|
|
433
|
+
const src = attr(tag, "src") || attr(tag, "data-src");
|
|
434
|
+
if (!src || src.startsWith("data:") || saysLogo(tag)) {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
const url = absolute(src, base);
|
|
438
|
+
if (url && images.length < 12 && !images.some((i) => i.url === url)) {
|
|
439
|
+
images.push({ url, alt: attr(tag, "alt") });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const og = meta("og:image");
|
|
443
|
+
if (og) {
|
|
444
|
+
const url = absolute(og, base);
|
|
445
|
+
if (url) {
|
|
446
|
+
images.unshift({ url, alt: "share image (og:image)" });
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const css = await cssOf(html, base, signal);
|
|
229
450
|
const host = new URL(base).hostname;
|
|
230
451
|
const links = [];
|
|
231
452
|
for (const m of html.matchAll(/<a\b([^>]*)>([\s\S]*?)<\/a>/gi)) {
|
|
@@ -252,6 +473,7 @@ export async function readWebsite(raw, signal) {
|
|
|
252
473
|
...coloursOf(css),
|
|
253
474
|
logos: logos.slice(0, 10),
|
|
254
475
|
images,
|
|
476
|
+
fonts: fontsOf(css, html),
|
|
255
477
|
},
|
|
256
478
|
links: links.slice(0, 10),
|
|
257
479
|
};
|
|
@@ -209,7 +209,7 @@ export const projectFile = sqliteTable("project_file", {
|
|
|
209
209
|
projectId: text("project_id")
|
|
210
210
|
.notNull()
|
|
211
211
|
.references(() => project.id, { onDelete: "cascade" }),
|
|
212
|
-
kind: text("kind", { enum: ["logo", "asset", "document"] }).notNull(),
|
|
212
|
+
kind: text("kind", { enum: ["logo", "asset", "document", "pattern"] }).notNull(),
|
|
213
213
|
name: text("name").notNull(),
|
|
214
214
|
mime: text("mime").notNull(),
|
|
215
215
|
size: integer("size").notNull(),
|
|
@@ -35,6 +35,20 @@ const MIME_BY_EXT = {
|
|
|
35
35
|
jpeg: "image/jpeg",
|
|
36
36
|
webp: "image/webp",
|
|
37
37
|
gif: "image/gif",
|
|
38
|
+
svg: "image/svg+xml",
|
|
39
|
+
avif: "image/avif",
|
|
40
|
+
mp4: "video/mp4",
|
|
41
|
+
mov: "video/quicktime",
|
|
42
|
+
webm: "video/webm",
|
|
43
|
+
mp3: "audio/mpeg",
|
|
44
|
+
wav: "audio/wav",
|
|
45
|
+
m4a: "audio/mp4",
|
|
46
|
+
woff2: "font/woff2",
|
|
47
|
+
woff: "font/woff",
|
|
48
|
+
ttf: "font/ttf",
|
|
49
|
+
otf: "font/otf",
|
|
50
|
+
glb: "model/gltf-binary",
|
|
51
|
+
gltf: "model/gltf+json",
|
|
38
52
|
};
|
|
39
53
|
/** The type to read a file as: mail servers and browsers often only say "octet-stream". */
|
|
40
54
|
export function documentMime(name, mime) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { pOf } from "@engenty-wizards/shared/decision";
|
|
2
2
|
import { allFields, dataRef, isDecisionStep, LOCALES, templateRefs, wizardLang, } from "@engenty-wizards/shared/definition";
|
|
3
|
+
import { mainLogo, ruleLines } from "@engenty-wizards/shared/projects";
|
|
3
4
|
import { SURFACE_GUIDE, validateSurface, } from "@engenty-wizards/shared/surface";
|
|
4
5
|
import { Agent } from "@mastra/core/agent";
|
|
5
6
|
import { generateText, Output } from "ai";
|
|
@@ -18,7 +19,7 @@ import { runWidgetStep } from "../widgets/step.js";
|
|
|
18
19
|
import { decide } from "./decide.js";
|
|
19
20
|
import { prepareInputs } from "./prepare.js";
|
|
20
21
|
import { runSurfaceStep } from "./surface.js";
|
|
21
|
-
import { answersAsText, colorLine, factLines, renderTemplate, resolveRef, } from "./template.js";
|
|
22
|
+
import { answersAsText, assetSectionLine, colorLine, factLines, logoSectionLine, renderTemplate, resolveRef, typeLines, } from "./template.js";
|
|
22
23
|
import { StepError } from "./types.js";
|
|
23
24
|
/** Today's date as the wizard's people write it. */
|
|
24
25
|
function today(ctx) {
|
|
@@ -28,13 +29,26 @@ function today(ctx) {
|
|
|
28
29
|
day: "numeric",
|
|
29
30
|
});
|
|
30
31
|
}
|
|
32
|
+
const sectionTitle = (s) => s.description ? `${s.title} – ${s.description}` : s.title;
|
|
31
33
|
function brandBlock(ctx) {
|
|
32
34
|
const b = ctx.scope.brand;
|
|
33
35
|
const parts = [
|
|
34
36
|
b.name,
|
|
35
37
|
b.about,
|
|
36
38
|
b.facts?.length ? `Facts:\n${factLines(b.facts)}` : "",
|
|
37
|
-
b.colors?.length
|
|
39
|
+
b.colors?.length
|
|
40
|
+
? `Colours (the first is the accent): ${colorLine(b.colors, b.colorSections, b.gradients)}`
|
|
41
|
+
: "",
|
|
42
|
+
b.colorRules?.length
|
|
43
|
+
? `Rules for the colours, in the brand's words:\n${ruleLines(b.colorRules)}`
|
|
44
|
+
: "",
|
|
45
|
+
b.logoRules?.length
|
|
46
|
+
? `Rules for the logo, in the brand's words:\n${ruleLines(b.logoRules)}`
|
|
47
|
+
: "",
|
|
48
|
+
b.fonts?.length || b.textStyles?.length
|
|
49
|
+
? `Typography (set text in these fonts, with system fallbacks):\n${typeLines(b.fonts, b.textStyles)}`
|
|
50
|
+
: "",
|
|
51
|
+
b.typeRules?.length ? `Rules for type, in the brand's words:\n${ruleLines(b.typeRules)}` : "",
|
|
38
52
|
].filter(Boolean);
|
|
39
53
|
return parts.length ? `# WHO THIS WIZARD BELONGS TO\n${parts.join("\n")}` : "";
|
|
40
54
|
}
|
|
@@ -656,13 +670,35 @@ function placeableImages(step, ctx) {
|
|
|
656
670
|
}
|
|
657
671
|
}
|
|
658
672
|
const logos = ctx.projectFiles.filter((f) => f.kind === "logo");
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
673
|
+
const main = mainLogo(logos, ctx.scope.brand.logoSections);
|
|
674
|
+
for (const f of logos) {
|
|
675
|
+
const section = logoSectionLine(ctx.scope.brand.logoSections, f.id);
|
|
676
|
+
out.push({
|
|
677
|
+
id: f.id,
|
|
678
|
+
label: [
|
|
679
|
+
f === main ? "Company logo" : "Logo variant",
|
|
680
|
+
section ? ` — section ${section}` : "",
|
|
681
|
+
f.description ? `: ${f.description}` : "",
|
|
682
|
+
].join(""),
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
for (const p of ctx.scope.brand.patterns ?? []) {
|
|
686
|
+
const file = ctx.projectFiles.find((f) => f.id === p.file);
|
|
687
|
+
const section = ctx.scope.brand.colorSections?.find((s) => s.id === p.section);
|
|
688
|
+
if (file) {
|
|
689
|
+
out.push({
|
|
690
|
+
id: file.id,
|
|
691
|
+
label: `${p.tile ? "Pattern to tile" : "Background fill"}: ${p.name || file.name}${section ? ` (${sectionTitle(section)})` : ""}`,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
662
694
|
}
|
|
663
695
|
for (const f of ctx.projectFiles) {
|
|
664
696
|
if (f.kind === "asset" && f.mime.startsWith("image/")) {
|
|
665
|
-
|
|
697
|
+
const section = assetSectionLine(ctx.scope.brand.assetSections, f.id);
|
|
698
|
+
out.push({
|
|
699
|
+
id: f.id,
|
|
700
|
+
label: `${f.description || f.name} (from the project's assets${section ? `, section ${section}` : ""})`,
|
|
701
|
+
});
|
|
666
702
|
}
|
|
667
703
|
}
|
|
668
704
|
return out;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { allFields, isAudioValue, isLocationValue, itemsTotals, LOCALES, locationText, wizardLang, } from "@engenty-wizards/shared/definition";
|
|
2
|
+
import { gradientCss, textStyleLine, } from "@engenty-wizards/shared/projects";
|
|
2
3
|
import { listAsMarkdown } from "@engenty-wizards/shared/store";
|
|
3
4
|
const TEMPLATE_RE = /\{\{\s*([^}]+?)\s*\}\}/g;
|
|
4
5
|
function money(n, currency, locale) {
|
|
@@ -34,8 +35,52 @@ export function itemsAsMarkdown(field, value, values, lang = "de") {
|
|
|
34
35
|
export function factLines(facts) {
|
|
35
36
|
return facts.map((f) => `${f.label}: ${f.value}`).join("\n");
|
|
36
37
|
}
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
/**
|
|
39
|
+
* The colours as a model reads them; a section's after the standard ones, with when to use them.
|
|
40
|
+
* A gradients section names its gradients as CSS.
|
|
41
|
+
*/
|
|
42
|
+
export function colorLine(colors, sections = [], gradients = []) {
|
|
43
|
+
const line = (list) => list.map((c) => (c.name ? `${c.name} ${c.value}` : c.value)).join(", ");
|
|
44
|
+
const known = (c) => sections.some((s) => s.id === c.section);
|
|
45
|
+
return [
|
|
46
|
+
line(colors.filter((c) => !known(c))),
|
|
47
|
+
...sections.map((s) => {
|
|
48
|
+
const head = s.description ? `${s.title} (${s.description})` : s.title;
|
|
49
|
+
if (s.kind === "gradients") {
|
|
50
|
+
const own = gradients.filter((g) => g.section === s.id);
|
|
51
|
+
return own.length
|
|
52
|
+
? `${head}: ${own.map((g) => `${g.name || "gradient"} ${gradientCss(g)}`).join("; ")}`
|
|
53
|
+
: "";
|
|
54
|
+
}
|
|
55
|
+
const own = colors.filter((c) => c.section === s.id);
|
|
56
|
+
return own.length ? `${head}: ${line(own)}` : "";
|
|
57
|
+
}),
|
|
58
|
+
]
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.join(" · ");
|
|
61
|
+
}
|
|
62
|
+
/** The brand's type as a model reads it: its fonts, then its text styles. */
|
|
63
|
+
export function typeLines(fonts = [], styles = []) {
|
|
64
|
+
const families = fonts.map((f) => {
|
|
65
|
+
const weights = f.weights.length ? `, weights ${f.weights.join("/")}` : "";
|
|
66
|
+
return `- ${f.family}${f.role ? ` (${f.role})` : ""}${weights}`;
|
|
67
|
+
});
|
|
68
|
+
const scale = styles.map((s) => `- ${s.name}: ${textStyleLine(s, fonts)}`);
|
|
69
|
+
return [
|
|
70
|
+
families.length ? `Fonts:\n${families.join("\n")}` : "",
|
|
71
|
+
scale.length ? `Text styles:\n${scale.join("\n")}` : "",
|
|
72
|
+
]
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.join("\n");
|
|
75
|
+
}
|
|
76
|
+
const sectionWords = (s) => !s ? "" : s.description ? `${s.title} (${s.description})` : s.title;
|
|
77
|
+
/** The logo's section in words — "Logomark (small spaces, app icons)" — or "" where it has none. */
|
|
78
|
+
export function logoSectionLine(sections, fileId) {
|
|
79
|
+
return sectionWords(sections?.find((x) => x.logos.includes(fileId)));
|
|
80
|
+
}
|
|
81
|
+
/** The title of the asset's section — "Icons" — or "" where it has none. */
|
|
82
|
+
export function assetSectionLine(sections, fileId) {
|
|
83
|
+
return sections?.find((x) => x.assets.includes(fileId))?.title ?? "";
|
|
39
84
|
}
|
|
40
85
|
function brandRef(brand, key) {
|
|
41
86
|
switch (key) {
|
|
@@ -49,7 +94,9 @@ function brandRef(brand, key) {
|
|
|
49
94
|
case "accent":
|
|
50
95
|
return brand.colors?.[0]?.value ?? "";
|
|
51
96
|
case "colors":
|
|
52
|
-
return colorLine(brand.colors ?? []);
|
|
97
|
+
return colorLine(brand.colors ?? [], brand.colorSections, brand.gradients);
|
|
98
|
+
case "fonts":
|
|
99
|
+
return typeLines(brand.fonts, brand.textStyles);
|
|
53
100
|
default:
|
|
54
101
|
return "";
|
|
55
102
|
}
|
|
@@ -3,8 +3,9 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, join, normalize } from "node:path";
|
|
5
5
|
import { dataRef, FILM_SIZES } from "@engenty-wizards/shared/definition";
|
|
6
|
+
import { gradientCss, ruleLines, textStyleLine } from "@engenty-wizards/shared/projects";
|
|
6
7
|
import { unzipSync } from "fflate";
|
|
7
|
-
import { renderTemplate, resolveRef } from "../engine/template.js";
|
|
8
|
+
import { logoSectionLine, renderTemplate, resolveRef } from "../engine/template.js";
|
|
8
9
|
import { env } from "../env.js";
|
|
9
10
|
import { getBlob } from "../files/blobs.js";
|
|
10
11
|
import { extFor, loadAsset } from "../files/storage.js";
|
|
@@ -128,23 +129,72 @@ export async function prepareFilmFolder(step, ctx) {
|
|
|
128
129
|
// The space's brand.
|
|
129
130
|
const b = ctx.scope.brand;
|
|
130
131
|
const logos = [];
|
|
132
|
+
const patterns = [];
|
|
133
|
+
for (const p of b.patterns ?? []) {
|
|
134
|
+
const f = ctx.projectFiles.find((x) => x.id === p.file);
|
|
135
|
+
try {
|
|
136
|
+
if (f) {
|
|
137
|
+
const { data } = await projectFileContent(f.projectId, f.id);
|
|
138
|
+
const name = `patterns/${safeName(f.name.replace(/\.[^.]+$/, ""))}.${extFor(f.mime) === "bin" ? (f.name.split(".").pop() ?? "bin") : extFor(f.mime)}`;
|
|
139
|
+
await put(join(root, "brand"), name, data);
|
|
140
|
+
patterns.push({ file: name, name: p.name, tile: Boolean(p.tile) });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// a pattern less
|
|
145
|
+
}
|
|
146
|
+
}
|
|
131
147
|
for (const f of ctx.projectFiles.filter((p) => p.kind === "logo")) {
|
|
132
148
|
try {
|
|
133
149
|
const { data } = await projectFileContent(f.projectId, f.id);
|
|
134
150
|
const name = `logos/${safeName(f.name.replace(/\.[^.]+$/, ""))}.${extFor(f.mime) === "bin" ? (f.name.split(".").pop() ?? "bin") : extFor(f.mime)}`;
|
|
135
151
|
await put(join(root, "brand"), name, data);
|
|
136
|
-
|
|
152
|
+
const section = logoSectionLine(b.logoSections, f.id);
|
|
153
|
+
logos.push({
|
|
154
|
+
file: name,
|
|
155
|
+
...(section ? { section } : {}),
|
|
156
|
+
...(f.description ? { description: f.description } : {}),
|
|
157
|
+
});
|
|
137
158
|
}
|
|
138
159
|
catch {
|
|
139
160
|
// a logo less
|
|
140
161
|
}
|
|
141
162
|
}
|
|
142
163
|
await put(join(root, "brand"), "brand.json", JSON.stringify({
|
|
143
|
-
note: "The space's brand. Colours are named by what they are for; the first is the accent.",
|
|
164
|
+
note: "The space's brand. Colours are named by what they are for; the first is the accent. A logo's or a colour's section says when to use it; colorRules, logoRules and typeRules, where there are any, say how colours, logos and type may be used; text is set in the fonts.",
|
|
144
165
|
name: b.name ?? "",
|
|
145
166
|
about: b.about ?? "",
|
|
146
|
-
colors: (b.colors ?? []).map((c) =>
|
|
167
|
+
colors: (b.colors ?? []).map((c) => {
|
|
168
|
+
const s = b.colorSections?.find((x) => x.id === c.section);
|
|
169
|
+
return { name: c.name, value: c.value, ...(s ? { section: s.title } : {}) };
|
|
170
|
+
}),
|
|
171
|
+
...(b.gradients?.length
|
|
172
|
+
? {
|
|
173
|
+
gradients: b.gradients.map((g) => ({
|
|
174
|
+
name: g.name,
|
|
175
|
+
section: b.colorSections?.find((x) => x.id === g.section)?.title,
|
|
176
|
+
css: gradientCss(g),
|
|
177
|
+
})),
|
|
178
|
+
}
|
|
179
|
+
: {}),
|
|
180
|
+
...(patterns.length ? { patterns } : {}),
|
|
147
181
|
logos,
|
|
182
|
+
...(b.colorRules?.length ? { colorRules: ruleLines(b.colorRules).split("\n") } : {}),
|
|
183
|
+
...(b.fonts?.length
|
|
184
|
+
? {
|
|
185
|
+
fonts: b.fonts.map(({ family, role, weights }) => ({ family, role, weights })),
|
|
186
|
+
}
|
|
187
|
+
: {}),
|
|
188
|
+
...(b.textStyles?.length
|
|
189
|
+
? {
|
|
190
|
+
textStyles: b.textStyles.map((s) => ({
|
|
191
|
+
name: s.name,
|
|
192
|
+
style: textStyleLine(s, b.fonts ?? []),
|
|
193
|
+
})),
|
|
194
|
+
}
|
|
195
|
+
: {}),
|
|
196
|
+
...(b.typeRules?.length ? { typeRules: ruleLines(b.typeRules).split("\n") } : {}),
|
|
197
|
+
...(b.logoRules?.length ? { logoRules: ruleLines(b.logoRules).split("\n") } : {}),
|
|
148
198
|
facts: (b.facts ?? []).map((f) => `${f.label}: ${f.value}`),
|
|
149
199
|
}, null, 1));
|
|
150
200
|
// What the run made before.
|