diff --git a/apps/electron/build/icon.icns b/apps/electron/build/icon.icns index 33c060439..9237768c5 100644 Binary files a/apps/electron/build/icon.icns and b/apps/electron/build/icon.icns differ diff --git a/apps/electron/build/icon.ico b/apps/electron/build/icon.ico index 5695d57e2..b3edf7296 100644 Binary files a/apps/electron/build/icon.ico and b/apps/electron/build/icon.ico differ diff --git a/apps/electron/build/icon.png b/apps/electron/build/icon.png index 98ad38252..7b8f2649d 100644 Binary files a/apps/electron/build/icon.png and b/apps/electron/build/icon.png differ diff --git a/apps/expo/assets/adaptive-icon.png b/apps/expo/assets/adaptive-icon.png index 6451e8480..e145faa52 100644 Binary files a/apps/expo/assets/adaptive-icon.png and b/apps/expo/assets/adaptive-icon.png differ diff --git a/apps/expo/assets/icon.png b/apps/expo/assets/icon.png index 98ad38252..aca582759 100644 Binary files a/apps/expo/assets/icon.png and b/apps/expo/assets/icon.png differ diff --git a/apps/web/index.html b/apps/web/index.html index 069fc0d45..6f07c4a21 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -24,8 +24,8 @@ content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' ws://localhost:* http://localhost:* ws://127.0.0.1:* http://127.0.0.1:* wss://* https://* https://hub.xnet.fyi https://cloud.xnet.fyi https://*.xnet.fyi https://demo-bucket.protomaps.com https://tiles.openfreemap.org https://www.youtube.com https://publish.twitter.com https://huggingface.co https://*.huggingface.co https://*.hf.co https://raw.githubusercontent.com; font-src 'self' data:; worker-src 'self' blob:; frame-src https://www.youtube.com https://www.youtube-nocookie.com https://player.vimeo.com https://open.spotify.com https://w.soundcloud.com https://platform.twitter.com https://www.instagram.com https://instagram.com https://www.tiktok.com https://codepen.io https://codesandbox.io https://stackblitz.com https://figma.com https://www.figma.com" /> - + xNet diff --git a/apps/web/public/apple-touch-icon.png b/apps/web/public/apple-touch-icon.png index 407466bc9..2e4d34f04 100644 Binary files a/apps/web/public/apple-touch-icon.png and b/apps/web/public/apple-touch-icon.png differ diff --git a/apps/web/public/favicon.ico b/apps/web/public/favicon.ico index 5695d57e2..30c59faf2 100644 Binary files a/apps/web/public/favicon.ico and b/apps/web/public/favicon.ico differ diff --git a/apps/web/public/favicon.svg b/apps/web/public/favicon.svg index 038f0d788..172a9baec 100644 --- a/apps/web/public/favicon.svg +++ b/apps/web/public/favicon.svg @@ -1 +1,2 @@ - \ No newline at end of file + + diff --git a/apps/web/public/icons/icon-192.png b/apps/web/public/icons/icon-192.png index bb5a8c846..212b3ed43 100644 Binary files a/apps/web/public/icons/icon-192.png and b/apps/web/public/icons/icon-192.png differ diff --git a/apps/web/public/icons/icon-512.png b/apps/web/public/icons/icon-512.png index f2d96ef78..5e8469e7b 100644 Binary files a/apps/web/public/icons/icon-512.png and b/apps/web/public/icons/icon-512.png differ diff --git a/scripts/generate-brand-icons.mjs b/scripts/generate-brand-icons.mjs new file mode 100644 index 000000000..d98b397b2 --- /dev/null +++ b/scripts/generate-brand-icons.mjs @@ -0,0 +1,148 @@ +#!/usr/bin/env node +/** + * Regenerate xNet icons and PNG exports: node scripts/generate-brand-icons.mjs + * Source artwork lives in site/public/mark.svg and site/public/wordmark.svg. + * Uses the site's existing sharp dependency (pnpm --dir site install --ignore-workspace). + */ +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { createRequire } from 'node:module' + +const require = createRequire(new URL('../site/package.json', import.meta.url)) +const sharp = require('sharp') +const file = (path) => new URL(`../${path}`, import.meta.url) +const source = await readFile(file('site/public/mark.svg'), 'utf8') +const body = source.match(/]*>([\s\S]*?)<\/svg>\s*$/)?.[1].trim() +if (!body || !source.includes('viewBox="0 0 100 100"')) { + throw new Error('Expected site/public/mark.svg to use a 100 × 100 viewBox') +} + +const svg = (content, color = '#fff') => + `${content}` +const mark = (scale) => + `${body}` +const tile = (radius, scale = 0.74) => + `${mark(scale)}` +const artwork = { + // iOS and web install surfaces apply their own corner masks. + app: svg(tile(0)), + desktop: svg(tile(23)), + // Legacy ICNS files need their own tile and breathing room on older macOS. + mac: svg(`${tile(23)}`), + favicon: svg(tile(23, 0.84)), + mask: svg(body), + // Keep every arm inside Android's 66 dp safe circle in a 108 dp layer. + // https://developer.android.com/develop/ui/compose/system/icon_design_adaptive + adaptive: svg(mark(0.52)) +} + +const png = (kind, size, opaque = false) => { + const rendered = sharp(Buffer.from(artwork[kind]), { density: (size / 100) * 72 }).resize( + size, + size + ) + return (opaque ? rendered.removeAlpha() : rendered).png().toBuffer() +} + +const rasterFiles = [ + ['apps/electron/build/icon.png', 'desktop', 1024], + ['apps/expo/assets/icon.png', 'app', 1024, true], + ['apps/expo/assets/adaptive-icon.png', 'adaptive', 1024], + ['apps/web/public/apple-touch-icon.png', 'app', 180, true], + ['apps/web/public/icons/icon-192.png', 'app', 192, true], + ['apps/web/public/icons/icon-512.png', 'app', 512, true], + ['site/public/mark.png', 'mask', 96] +] + +// ICO stores a directory of PNG representations, including real small-size renders. +const ico = async (kind, sizes) => { + const images = await Promise.all(sizes.map((size) => png(kind, size))) + const header = Buffer.alloc(6) + header.writeUInt16LE(1, 2) + header.writeUInt16LE(images.length, 4) + const entries = images.map((image, index) => { + const entry = Buffer.alloc(16) + entry[0] = entry[1] = sizes[index] === 256 ? 0 : sizes[index] + entry.writeUInt16LE(1, 4) + entry.writeUInt16LE(32, 6) + entry.writeUInt32LE(image.length, 8) + entry.writeUInt32LE( + 6 + images.length * 16 + images.slice(0, index).reduce((sum, item) => sum + item.length, 0), + 12 + ) + return entry + }) + return Buffer.concat([header, ...entries, ...images]) +} + +// PNG-backed ICNS chunks keep generation portable; no macOS-only iconutil required. +const icns = async () => { + const representations = [ + ['icp4', 16], + ['icp5', 32], + ['icp6', 64], + ['ic07', 128], + ['ic08', 256], + ['ic09', 512], + ['ic10', 1024], + ['ic11', 32], + ['ic12', 64], + ['ic13', 256], + ['ic14', 512] + ] + const chunks = await Promise.all( + representations.map(async ([type, size]) => { + const image = await png('mac', size) + const header = Buffer.alloc(8) + header.write(type, 0, 4, 'ascii') + header.writeUInt32BE(image.length + 8, 4) + return Buffer.concat([header, image]) + }) + ) + const header = Buffer.alloc(8) + header.write('icns', 0, 4, 'ascii') + header.writeUInt32BE(8 + chunks.reduce((sum, chunk) => sum + chunk.length, 0), 4) + return Buffer.concat([header, ...chunks]) +} + +const favicon = ` +${artwork.favicon} +` + +const wordmarkSource = await readFile(file('site/public/wordmark.svg'), 'utf8') +const brandSources = [ + ['mark', source, 2048], + ['wordmark', wordmarkSource, 2400] +] +const brandExports = brandSources.flatMap(([name, artwork, width]) => + [ + ['black', '#000000'], + ['white', '#ffffff'] + ].map(async ([variant, color]) => { + const colored = artwork.replaceAll('currentColor', color) + const data = await sharp(Buffer.from(colored), { density: 600 }) + .resize({ width }) + .png() + .toBuffer() + return [`site/public/brand/xnet-${name}-${variant}.png`, data] + }) +) + +// Prepare every asset before replacing any existing file. +const outputs = await Promise.all([ + ...brandExports, + ...rasterFiles.map(async ([path, kind, size, opaque]) => [path, await png(kind, size, opaque)]), + Promise.resolve(['apps/web/public/favicon.svg', favicon]), + Promise.resolve(['site/public/favicon.svg', favicon]), + ico('desktop', [16, 24, 32, 48, 64, 128, 256]).then((data) => [ + 'apps/electron/build/icon.ico', + data + ]), + ico('favicon', [16, 24, 32, 48, 64, 128, 256]).then((data) => [ + 'apps/web/public/favicon.ico', + data + ]), + icns().then((data) => ['apps/electron/build/icon.icns', data]) +]) +await mkdir(file('site/public/brand'), { recursive: true }) +await Promise.all(outputs.map(([path, data]) => writeFile(file(path), data))) +console.log(`Generated ${outputs.length} xNet icons and PNG exports`) diff --git a/site/public/brand/quicksand-ofl.txt b/site/public/brand/quicksand-ofl.txt new file mode 100644 index 000000000..c94d5ac36 --- /dev/null +++ b/site/public/brand/quicksand-ofl.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Quicksand Project Authors (https://github.com/andrew-paglinawan/QuicksandFamily), with Reserved Font Name “Quicksand”. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/site/public/brand/xnet-mark-black.png b/site/public/brand/xnet-mark-black.png new file mode 100644 index 000000000..347bffc2d Binary files /dev/null and b/site/public/brand/xnet-mark-black.png differ diff --git a/site/public/brand/xnet-mark-white.png b/site/public/brand/xnet-mark-white.png new file mode 100644 index 000000000..891f73724 Binary files /dev/null and b/site/public/brand/xnet-mark-white.png differ diff --git a/site/public/brand/xnet-wordmark-black.png b/site/public/brand/xnet-wordmark-black.png new file mode 100644 index 000000000..bb3c3c57a Binary files /dev/null and b/site/public/brand/xnet-wordmark-black.png differ diff --git a/site/public/brand/xnet-wordmark-white.png b/site/public/brand/xnet-wordmark-white.png new file mode 100644 index 000000000..6c413b100 Binary files /dev/null and b/site/public/brand/xnet-wordmark-white.png differ diff --git a/site/public/favicon.svg b/site/public/favicon.svg index 038f0d788..172a9baec 100644 --- a/site/public/favicon.svg +++ b/site/public/favicon.svg @@ -1 +1,2 @@ - \ No newline at end of file + + diff --git a/site/public/mark.png b/site/public/mark.png index da40575ba..ad26f53f0 100644 Binary files a/site/public/mark.png and b/site/public/mark.png differ diff --git a/site/public/mark.svg b/site/public/mark.svg new file mode 100644 index 000000000..85e48dffa --- /dev/null +++ b/site/public/mark.svg @@ -0,0 +1,3 @@ + + + diff --git a/site/public/wordmark.svg b/site/public/wordmark.svg new file mode 100644 index 000000000..7c5c139c7 --- /dev/null +++ b/site/public/wordmark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/site/src/components/BrandWordmark.astro b/site/src/components/BrandWordmark.astro new file mode 100644 index 000000000..904a2e3e3 --- /dev/null +++ b/site/src/components/BrandWordmark.astro @@ -0,0 +1,13 @@ + + + diff --git a/site/src/components/docs/SiteTitle.astro b/site/src/components/docs/SiteTitle.astro index 0a4116f51..32088dcc1 100644 --- a/site/src/components/docs/SiteTitle.astro +++ b/site/src/components/docs/SiteTitle.astro @@ -1,15 +1,16 @@ --- +import BrandWordmark from '../BrandWordmark.astro' + /** * Custom Starlight SiteTitle — "xNet" links to landing page, "Docs" links to docs root. */ --- - - - xNet +
+ / Docs - +
diff --git a/site/src/data/changelog/2026-10-09-a-clearer-xnet-app-icon.json b/site/src/data/changelog/2026-10-09-a-clearer-xnet-app-icon.json new file mode 100644 index 000000000..000d3b9c4 --- /dev/null +++ b/site/src/data/changelog/2026-10-09-a-clearer-xnet-app-icon.json @@ -0,0 +1,8 @@ +{ + "id": "2026-10-09-a-clearer-xnet-app-icon", + "date": "October 9, 2026", + "title": "A clearer xNet icon and wordmark", + "summary": "A white mesh cell on black makes xNet’s app icon and favicon easier to recognize, with a softer center and matching rounded lettering across the website.", + "highlights": [], + "tags": ["platform"] +}