diff --git a/.gitignore b/.gitignore index 5fbe012..fec3bb6 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,9 @@ next-env.d.ts *.log *.png +# Canonical brand icons are generated from @mind-studio/ui/brand (npm run gen:icons) +# and committed so the favicon/PWA/apple-touch assets ship with the app. +!src/app/apple-icon.png +!public/icon-192.png +!public/icon-512.png .playwright-mcp/ diff --git a/package-lock.json b/package-lock.json index 819a829..966b50f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@inrupt/solid-client-authn-node": "^3.1.1", "@inrupt/solid-client-notifications": "^4.0.0", "@mind-studio/core": "^0.7.0", - "@mind-studio/ui": "^0.4.0", + "@mind-studio/ui": "^0.7.0", "better-sqlite3": "^11.8.1", "dotenv": "^17.4.2", "next": "16.2.6", @@ -31,6 +31,7 @@ "husky": "^9.1.7", "lint-staged": "^17.0.7", "playwright": "^1.60.0", + "sharp": "^0.34.5", "tailwindcss": "^4", "tsx": "^4.22.3", "typescript": "^5", @@ -383,6 +384,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -896,8 +898,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": ">=18" } @@ -909,6 +911,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -931,6 +934,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -953,6 +957,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -969,6 +974,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -985,6 +991,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1001,6 +1008,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1017,6 +1025,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1033,6 +1042,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1049,6 +1059,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1065,6 +1076,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1081,6 +1093,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1097,6 +1110,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1113,6 +1127,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1135,6 +1150,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1157,6 +1173,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1179,6 +1196,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1201,6 +1219,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1223,6 +1242,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1245,6 +1265,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1267,6 +1288,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1289,6 +1311,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -1308,6 +1331,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1327,6 +1351,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1346,6 +1371,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1613,9 +1639,9 @@ } }, "node_modules/@mind-studio/ui": { - "version": "0.4.0", - "resolved": "https://npm.pkg.github.com/download/@mind-studio/ui/0.4.0/4d57829589aba38f8123b53c858598ed84db4910", - "integrity": "sha512-GOMaHU1tR8z0JIOnOHMJBLcYDItcUuYVuxgBmsBXGoipoSxrknzVnarqiR/7Wm26AWkQ7ISyzDkDcFot6jTgyQ==", + "version": "0.7.0", + "resolved": "https://npm.pkg.github.com/download/@mind-studio/ui/0.7.0/70071acee41c5d7bcc3bed636a6473e35dac9f6a", + "integrity": "sha512-PvkztbjfiFM7W8Zk0/rWYlMn26DiIakuhBQfxphi5les6J6eo9W7zZzLhbGpEp7ytB1+SaKev95ZFOxVhKx6cw==", "license": "MIT", "dependencies": { "@base-ui/react": "^1.5.0", @@ -6618,9 +6644,9 @@ "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", diff --git a/package.json b/package.json index a5f80b3..3350ad9 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "test": "vitest run --passWithNoTests", "prepare": "husky", "seed:demo": "tsx scripts/seed-demo.ts", - "smoke:wish": "tsx scripts/smoke-wish.ts" + "smoke:wish": "tsx scripts/smoke-wish.ts", + "gen:icons": "node scripts/gen-icons.mjs" }, "dependencies": { "@inrupt/solid-client": "^3.0.0", @@ -21,7 +22,7 @@ "@inrupt/solid-client-authn-node": "^3.1.1", "@inrupt/solid-client-notifications": "^4.0.0", "@mind-studio/core": "^0.7.0", - "@mind-studio/ui": "^0.4.0", + "@mind-studio/ui": "^0.7.0", "better-sqlite3": "^11.8.1", "dotenv": "^17.4.2", "next": "16.2.6", @@ -39,6 +40,7 @@ "husky": "^9.1.7", "lint-staged": "^17.0.7", "playwright": "^1.60.0", + "sharp": "^0.34.5", "tailwindcss": "^4", "tsx": "^4.22.3", "typescript": "^5", diff --git a/public/icon-192.png b/public/icon-192.png new file mode 100644 index 0000000..6ef4ebe Binary files /dev/null and b/public/icon-192.png differ diff --git a/public/icon-512.png b/public/icon-512.png new file mode 100644 index 0000000..0b247e7 Binary files /dev/null and b/public/icon-512.png differ diff --git a/scripts/gen-icons.mjs b/scripts/gen-icons.mjs new file mode 100644 index 0000000..34165b4 --- /dev/null +++ b/scripts/gen-icons.mjs @@ -0,0 +1,83 @@ +/* + * Generates Builder's brand icons from the canonical Mind mark. + * + * The artwork is imported from `@mind-studio/ui/brand` — the design system's + * single source of truth — so these icons can never drift from the published + * logo. Builder shares the one Mind mark (icons are mark-only). Re-run after + * bumping the package: `npm run gen:icons`. + * + * Outputs: + * src/app/icon.svg favicon — gradient weave, transparent, square + * src/app/apple-icon.png 180×180 apple-touch — white mark on green, 22% radius + * public/icon-192.png 192×192 maskable — white mark on full-bleed green + * public/icon-512.png 512×512 maskable — white mark on full-bleed green + * + * The maskable icons bleed the green to the edges (the platform mask crops the + * corners) and keep the mark inside the ~80% safe zone. + */ + +import { mkdirSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { MIND_GREEN, MIND_MARK_SVG, monoMarkSvg } from "@mind-studio/ui/brand"; +import sharp from "sharp"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = resolve(__dirname, ".."); +const out = (p) => { + const abs = resolve(root, p); + mkdirSync(dirname(abs), { recursive: true }); + return abs; +}; + +// Brand green backplate (the solid surface behind the white mono mark). +const GREEN = MIND_GREEN.base; // #16B88A + +/* ---------------------------------------------------------------- favicon.svg */ +// Nest the full-colour mark inside a square canvas, centred with a little +// breathing room (meet → preserves the mark's aspect, no distortion). +const nested = MIND_MARK_SVG.replace( + ' +${nested} + +`; +writeFileSync(out("src/app/icon.svg"), faviconSvg, "utf8"); + +/* ------------------------------------------------------------------ raster ops */ +// Rasterise the monochrome mark to a transparent square of the given size. +async function rasterMark(px, color = "#ffffff") { + return sharp(Buffer.from(monoMarkSvg(color))) + .resize(px, px, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } }) + .png() + .toBuffer(); +} + +// Maskable: white mark on a full-bleed green square; mark kept in the safe zone. +async function maskable(size, file) { + const mark = await rasterMark(Math.round(size * 0.66)); + await sharp({ create: { width: size, height: size, channels: 4, background: GREEN } }) + .composite([{ input: mark, gravity: "center" }]) + .png() + .toFile(out(file)); +} + +// Apple touch: white mark on a green backplate with a 22% corner radius. +async function appleIcon(size, file) { + const r = Math.round(size * 0.22); + const backplate = Buffer.from( + ``, + ); + const mark = await rasterMark(Math.round(size * 0.62)); + await sharp(backplate).composite([{ input: mark, gravity: "center" }]).png().toFile(out(file)); +} + +await Promise.all([ + maskable(192, "public/icon-192.png"), + maskable(512, "public/icon-512.png"), + appleIcon(180, "src/app/apple-icon.png"), +]); + +console.log("[gen:icons] wrote icon.svg, apple-icon.png, icon-192.png, icon-512.png"); diff --git a/src/app/apple-icon.png b/src/app/apple-icon.png new file mode 100644 index 0000000..76f3c17 Binary files /dev/null and b/src/app/apple-icon.png differ diff --git a/src/app/icon.svg b/src/app/icon.svg index dba0df9..9ac8266 100644 --- a/src/app/icon.svg +++ b/src/app/icon.svg @@ -1,5 +1,66 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/manifest.ts b/src/app/manifest.ts new file mode 100644 index 0000000..c644de9 --- /dev/null +++ b/src/app/manifest.ts @@ -0,0 +1,42 @@ +import type { MetadataRoute } from "next"; + +// Web app manifest — lets mobile users add Mind Builder to the homescreen with +// the brand mark and Mind Green chrome. Maskable icons (generated by +// scripts/gen-icons.mjs from the canonical mark) bleed green to the edges so the +// platform mask can crop the corners without clipping the weave. +export default function manifest(): MetadataRoute.Manifest { + return { + name: "Mind Builder — wish it, we build it", + short_name: "Builder", + description: + "Describe the website or app you wish you had, in your own words. We build it and give you a link to share. No coding, no setup. Everything we make stays yours.", + start_url: "/", + display: "standalone", + background_color: "#07080b", + theme_color: "#16B88A", + // Both PNGs live in public/ (served at the root) and carry the green to the + // edges; the 512 doubles as the "any" icon (a little safe-zone padding is + // fine there). Separate entries per purpose — Next's manifest type rejects + // the space-separated "any maskable" form the spec allows. + icons: [ + { + src: "/icon-192.png", + sizes: "192x192", + type: "image/png", + purpose: "maskable", + }, + { + src: "/icon-512.png", + sizes: "512x512", + type: "image/png", + purpose: "maskable", + }, + { + src: "/icon-512.png", + sizes: "512x512", + type: "image/png", + purpose: "any", + }, + ], + }; +} diff --git a/src/app/opengraph-image.tsx b/src/app/opengraph-image.tsx new file mode 100644 index 0000000..6dd536d --- /dev/null +++ b/src/app/opengraph-image.tsx @@ -0,0 +1,74 @@ +import { MIND_MARK_SVG } from "@mind-studio/ui/brand"; +import { ImageResponse } from "next/og"; + +export const alt = "Mind Builder — wish it, we build it"; +export const size = { width: 1200, height: 630 }; +export const contentType = "image/png"; + +// The canonical Mind mark as a data URI, so the share card carries the real +// brand artwork (sourced from the design system, never a vendored copy). +const MARK_DATA_URI = `data:image/svg+xml;base64,${Buffer.from(MIND_MARK_SVG).toString("base64")}`; + +// Social share card. Uses the default font (no remote fetch) so it renders +// deterministically at build time. Mind Green on dark to match the brand +// (green is the single UI accent; cyan/teal only as atmospheric glow). +export default function OpengraphImage() { + return new ImageResponse( +
+
+ {/* biome-ignore lint/performance/noImgElement: Satori (next/og) only supports . */} + + Builder +
+
+
+ Just describe it · we build it +
+
+ Wish it.  + We build it. +
+
+ Describe the website or app you wish you had — in your own words. No coding, no setup, and + everything we make stays yours. +
+
+ mindpods.org +
+
, + { ...size }, + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 6ca6022..126c065 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,7 +1,7 @@ "use client"; import { MindLoginCard, writeLastIdentity } from "@mind-studio/core"; -import { Badge, Card, CardContent } from "@mind-studio/ui"; +import { Badge, Card, CardContent, Logo } from "@mind-studio/ui"; import { useRouter } from "next/navigation"; import { useEffect } from "react"; import { APP_NAME, oidcIssuer } from "@/lib/config"; @@ -177,12 +177,7 @@ export default function HomePage() { function TopBar() { return (
-
- - ✦ - - Mind Builder -
+