mirror of
https://github.com/supabase/supabase.git
synced 2026-10-26 21:43:22 +08:00
## Problem - Local apps are hard to distinguish from hosted apps in browser tabs. ## Solution - Add blue blueprint icons to local development in all 8 apps and Studio CLI. - Keep normal branding for production, hosted staging and self-hosted Studio. - Builds on #51419. | Before | After | | --- | --- | | <img width="1290" height="76" alt="CleanShot 2026-10-08 at 13 29 14@2x" src="https://github.com/user-attachments/assets/72bc50d7-37e0-47d0-8dfd-50e233a443bd" /> | <img width="1282" height="72" alt="CleanShot 2026-10-08 at 13 30 03@2x" src="https://github.com/user-attachments/assets/afb15a70-3938-4cff-9b41-dc6c25460b7c" /> | ## Review instructions - Start an app locally. Its browser tab should show the blue icon. - Check Studio CLI uses blue and hosted staging uses white. - Check production icons are unchanged, including Learn's black and white icon. - Run `node scripts/validate-favicons.mjs`. All 16 sets should pass. Asset and routing checks, formatting and Common/Studio type checks passed. Full builds have not been run. ## Checklist - [x] I have read [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
60 lines
2.5 KiB
JavaScript
60 lines
2.5 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { readFile } from 'node:fs/promises'
|
|
import { createRequire } from 'node:module'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
import { faviconTargets } from './favicon-targets.mjs'
|
|
|
|
const root = fileURLToPath(new URL('../', import.meta.url))
|
|
const sharp = createRequire(path.join(root, 'apps/kb/package.json'))('sharp')
|
|
|
|
for (const [variant, directory] of faviconTargets) {
|
|
const file = (name) => path.join(root, directory, name)
|
|
const svg = await readFile(path.join(root, `assets/favicons/${variant}.svg`), 'utf8')
|
|
assert.match(svg, /width="512" height="512"/)
|
|
assert.match(svg, /M0 153\.6C0 99\.835/)
|
|
if (variant === 'local') {
|
|
assert.doesNotMatch(svg, /stroke-dasharray|stroke-width/)
|
|
assert.match(svg, /<path opacity="0.3"/)
|
|
}
|
|
const ico = await readFile(file('favicon.ico'))
|
|
assert.equal(ico.readUInt16LE(0), 0)
|
|
assert.equal(ico.readUInt16LE(2), 1)
|
|
assert.equal(ico.readUInt16LE(4), 3)
|
|
for (const [index, size] of [16, 32, 48].entries()) {
|
|
const entry = 6 + index * 16
|
|
assert.equal(ico[entry], size)
|
|
assert.equal(ico[entry + 1], size)
|
|
assert.equal(ico.readUInt16LE(entry + 6), 32)
|
|
const offset = ico.readUInt32LE(entry + 12)
|
|
const length = ico.readUInt32LE(entry + 8)
|
|
assert.ok(offset + length <= ico.length)
|
|
const metadata = await sharp(ico.subarray(offset, offset + length)).metadata()
|
|
assert.equal(metadata.width, size)
|
|
assert.equal(metadata.height, size)
|
|
}
|
|
for (const size of [180, 192, 512]) {
|
|
const name = size === 180 ? 'apple-icon-180x180.png' : `android-icon-${size}x${size}.png`
|
|
const metadata = await sharp(file(name)).metadata()
|
|
assert.equal(metadata.width, size)
|
|
assert.equal(metadata.height, size)
|
|
const stats = await sharp(file(name)).stats()
|
|
assert.equal(stats.isOpaque, size === 180)
|
|
}
|
|
const manifest = JSON.parse(await readFile(file('manifest.json'), 'utf8'))
|
|
assert.equal(manifest.icons.length, 2)
|
|
for (const [index, size] of [192, 512].entries()) {
|
|
const icon = manifest.icons[index]
|
|
assert.equal(icon.sizes, `${size}x${size}`)
|
|
assert.equal(icon.purpose, 'any')
|
|
for (const base of ['', '/docs', '/kb', '/design-system', '/custom/base']) {
|
|
const url = new URL(icon.src, `https://example.com${base}/favicon/manifest.json`)
|
|
assert.equal(url.pathname, `${base}/favicon/android-icon-${size}x${size}.png`)
|
|
}
|
|
assert.ok((await readFile(file(icon.src))).length > 0)
|
|
}
|
|
}
|
|
|
|
console.log(`Validated ${faviconTargets.length} icon sets, ICO entries and manifest base paths.`)
|