diff --git a/package-lock.json b/package-lock.json index da4b01f..ec4df12 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", "dotenv": "^17.4.2", "next": "16.2.6", "react": "19.2.4", @@ -28,6 +28,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", @@ -380,6 +381,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": { @@ -893,8 +895,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" } @@ -906,6 +908,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -928,6 +931,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -950,6 +954,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -966,6 +971,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -982,6 +988,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -998,6 +1005,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1014,6 +1022,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1030,6 +1039,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1046,6 +1056,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1062,6 +1073,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1078,6 +1090,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1094,6 +1107,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1110,6 +1124,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1132,6 +1147,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1154,6 +1170,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1176,6 +1193,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1198,6 +1216,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1220,6 +1239,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1242,6 +1262,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1264,6 +1285,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1286,6 +1308,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -1305,6 +1328,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1324,6 +1348,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1343,6 +1368,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1610,9 +1636,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", @@ -6341,8 +6367,8 @@ "version": "7.8.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "devOptional": true, "license": "ISC", - "optional": true, "bin": { "semver": "bin/semver.js" }, @@ -6354,9 +6380,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 99a9fd0..186d1c0 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "build": "next build", "start": "next start --port 3030", "typecheck": "tsc --noEmit", + "gen:icons": "node scripts/gen-icons.mjs", "lint": "biome check .", "lint:fix": "biome check --write .", "format": "biome format --write .", @@ -28,7 +29,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", "dotenv": "^17.4.2", "next": "16.2.6", "react": "19.2.4", @@ -36,17 +37,18 @@ }, "devDependencies": { "@biomejs/biome": "^2.5.0", - "husky": "^9.1.7", - "lint-staged": "^17.0.7", - "vitest": "^4.1.9", "@tailwindcss/postcss": "^4", "@types/node": "^20.19.41", "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", + "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" + "typescript": "^5", + "vitest": "^4.1.9" }, "lint-staged": { "*.{js,jsx,ts,tsx,json,jsonc}": "biome check --write --no-errors-on-unmatched" 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..c173067 --- /dev/null +++ b/scripts/gen-icons.mjs @@ -0,0 +1,79 @@ +/* + * Generates Chat'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. Chat 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 { 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) => resolve(root, p); + +// 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 f081b89..9ac8266 100644 --- a/src/app/icon.svg +++ b/src/app/icon.svg @@ -1,16 +1,66 @@ - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/manifest.ts b/src/app/manifest.ts new file mode 100644 index 0000000..9f2b0eb --- /dev/null +++ b/src/app/manifest.ts @@ -0,0 +1,41 @@ +import type { MetadataRoute } from "next"; + +// Web app manifest — lets mobile users add Mind Chat 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 Chat", + short_name: "Chat", + description: "Privacy-first chat on Solid pods. Your messages live in your pod.", + 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..aa58a3c --- /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 Chat — your messages in your pod"; +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 . */} + + Chat +
+
+
+ Messages in your pod · built on Solid +
+
+ Your messages.  + Your pod. +
+
+ Privacy-first real-time chat built on Solid Pods — your messages live in the space you own, + delivered over WebSocketChannel2023 with no central chat server. +
+
+ mindpods.org +
+
, + { ...size }, + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index bd6a2a2..9b784da 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,7 @@ "use client"; import { MindLoginCard, writeLastIdentity } from "@mind-studio/core"; +import { Logo } from "@mind-studio/ui"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect } from "react"; @@ -34,9 +35,7 @@ export default function HomePage() { solid · ws2023 · long-chat
-

- mind/chat -

+

Messages live in your pod. Real-time delivery via WebSocketChannel2023. No chat server.