Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
38 changes: 32 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@
"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",
"@inrupt/solid-client-authn-browser": "^4.0.0",
"@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",
Expand All @@ -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",
Expand Down
Binary file added public/icon-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
83 changes: 83 additions & 0 deletions scripts/gen-icons.mjs
Original file line number Diff line number Diff line change
@@ -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(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1117 1000"',
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1117 1000" x="6" y="6" width="88" height="88" preserveAspectRatio="xMidYMid meet"',
);
const faviconSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100" role="img" aria-label="Mind">
${nested}
</svg>
`;
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(
`<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}"><rect width="${size}" height="${size}" rx="${r}" ry="${r}" fill="${GREEN}"/></svg>`,
);
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");
Binary file added src/app/apple-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading