diff --git a/package.json b/package.json
index 7f3fbe0..beb089d 100644
--- a/package.json
+++ b/package.json
@@ -64,7 +64,7 @@
"site:build": "node site/build-site.mjs",
"site:dev": "npm run site:build && node scripts/site-local-server.mjs",
"site:serve": "node scripts/site-local-server.mjs",
- "test:site": "node tests/system-map.test.mjs && node tests/site.test.mjs && node tests/site-brand-assets.test.mjs && node tests/site-local-server.test.mjs && node tests/site-header-browser.test.mjs && node tests/site-first-use-browser.test.mjs && node tests/patterns-browser.test.mjs",
+ "test:site": "node tests/system-map.test.mjs && node tests/site.test.mjs && node tests/site-brand-assets.test.mjs && node tests/site-favicon-browser.test.mjs && node tests/site-local-server.test.mjs && node tests/site-header-browser.test.mjs && node tests/site-first-use-browser.test.mjs && node tests/patterns-browser.test.mjs",
"test:components:inventory": "node tests/components/component-reference-inventory.test.mjs",
"test:components:contracts": "node tests/components/component-contract-registry.test.mjs",
"test:components:unit": "node tests/components/component-state-model.test.mjs && node tests/components/component-specimen-runtime.test.mjs && node tests/components/native-components.test.mjs && node tests/components/interactive-components.test.mjs",
diff --git a/scripts/build-site-brand-assets.py b/scripts/build-site-brand-assets.py
index ab794d8..658174f 100644
--- a/scripts/build-site-brand-assets.py
+++ b/scripts/build-site-brand-assets.py
@@ -59,11 +59,11 @@ def mark(parts, foreground, height, identifier='', monochrome=False):
return f'{polygons}'
-def icon_svg(theme='light', height=None, radius=None, optical=False, responsive=False, monochrome=False):
+def icon_svg(theme='light', height=None, radius=None, optical=False, responsive=False, monochrome=False, transparent=False):
height = height if height is not None else ICONS['visible_mark_height']
radius = radius if radius is not None else ICONS['browser_radius']
background, foreground = (CONFIG['ivory'], CONFIG['ink']) if theme == 'light' else (CONFIG['ink'], CONFIG['ivory'])
- body = '' if monochrome else f''
+ body = '' if monochrome or transparent else f''
if responsive:
body += f''
body += mark(GEOMETRY['canonical'], foreground, height, 'canonical') + mark(GEOMETRY['optical_16'], foreground, height, 'optical')
@@ -92,14 +92,17 @@ def icon_svg(theme='light', height=None, radius=None, optical=False, responsive=
with tempfile.TemporaryDirectory(prefix='judgmentkit-web-icons-') as temporary:
temporary = Path(temporary)
for theme in ['light', 'dark']:
- write(f'{ICONS["output_directory"]}/favicon-{theme}.svg', icon_svg(theme, responsive=True))
+ write(f'{ICONS["output_directory"]}/favicon-{theme}.svg', icon_svg(theme, responsive=True, transparent=ICONS['browser_background'] == 'transparent'))
for size in [16, 32, 48]:
source = temporary / f'{theme}-{size}.svg'
- source.write_text(icon_svg(theme, optical=size == 16))
+ source.write_text(icon_svg(theme, optical=size == 16, transparent=ICONS['browser_background'] == 'transparent'))
render(source, icons_dir / f'favicon-{theme}-{size}.png', (size, size))
- frames = [Image.open(icons_dir / f'favicon-{ICONS["fallback_theme"]}-{size}.png').convert('RGBA') for size in [16, 32, 48]]
+ frames = [Image.open(icons_dir / f'favicon-{theme}-{size}.png').convert('RGBA') for size in [16, 32, 48]]
+ ico = icons_dir / f'favicon-{theme}.ico'
+ frames[-1].save(ico, format='ICO', sizes=[(16, 16), (32, 32), (48, 48)], append_images=frames[:-1])
+ OUTPUTS.append(ico)
ico = icons_dir / 'favicon.ico'
- frames[-1].save(ico, format='ICO', sizes=[(16, 16), (32, 32), (48, 48)], append_images=frames[:-1])
+ ico.write_bytes((icons_dir / f'favicon-{ICONS["fallback_theme"]}.ico').read_bytes())
OUTPUTS.append(ico)
write(f'{ICONS["output_directory"]}/safari-pinned-tab.svg', icon_svg(monochrome=True))
for name, height in [('app-icon', ICONS['visible_mark_height']), ('maskable', ICONS['maskable_visible_mark_height'])]:
@@ -115,7 +118,7 @@ def icon_svg(theme='light', height=None, radius=None, optical=False, responsive=
'generator': 'scripts/build-site-brand-assets.py',
'canonical_lockup': SOCIAL['lockup_source'],
'canonical_lockup_sha256': digest(lockup_path),
- 'sources': {p: digest(ROOT / p) for p in [ICONS['geometry_source'], ICONS['master_source'], 'site/brand-assets.json']},
+ 'sources': {p: digest(ROOT / p) for p in [ICONS['geometry_source'], ICONS['master_source'], 'site/brand-assets.json', 'site/assets/brand/favicon-theme.js']},
'canvas': [1200, 630],
'social_placement': {'scale': scale, 'translation': [x, y], 'source_viewBox': [0, 0, social_width, social_height]},
'web_icons': ICONS,
diff --git a/scripts/verify-public-release.mjs b/scripts/verify-public-release.mjs
index c0c366b..30dea62 100644
--- a/scripts/verify-public-release.mjs
+++ b/scripts/verify-public-release.mjs
@@ -1180,7 +1180,7 @@ async function verifyPublicRoutes(baseUrl, options = {}) {
"See a screen repaired",
"Explore examples",
'',
- `',
],
"homepage",
@@ -1227,6 +1227,18 @@ async function verifyPublicRoutes(baseUrl, options = {}) {
const docs = await fetchText(baseUrl, "/docs/");
assert.equal(getAnalyticsScriptSrc(docs.text, "docs"), analyticsScriptSrc);
assertReleaseFooter(docs.text, expectedPackageVersion, "docs");
+ for (const [label, html] of [["home", home.text], ["value", value.text], ["docs", docs.text]]) {
+ const head = html.match(/
]*>([\s\S]*?)<\/head>/i)?.[1];
+ assert.ok(head, `${label}: document head`);
+ const controllers = [...head.matchAll(/
+
@@ -118,7 +114,8 @@ export function applySiteBrandHead(html, relativePath) {
const route = "/" + relativePath.replace(/index\.html$/, "");
const canonicalUrl = canonicalTag ? attribute(canonicalTag, "href") : SITE_BRAND.origin + route;
const existingContext = [...active.matchAll(/]*>/gi)].map(([tag]) => (attribute(tag, "name") || attribute(tag, "property")).toLowerCase());
- const head = original.replace(/|<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>|<(?:link|meta)\b[^>]*>/gi, tag => {
+ const head = original.replace(/