favicon-env 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ssr.d.ts CHANGED
@@ -1 +1,20 @@
1
- export { f as faviconDataUri, s as svgToDataUri, t as tintSvg } from './ssr-CI5GvAY9.js';
1
+ import type { EnvConfig } from './types';
2
+ /**
3
+ * Return `svg` (an SVG *string*) with the environment's tint and/or badge baked
4
+ * in — the form that survives being rendered as an `<img>` / favicon, with no
5
+ * first-paint flash. The tint is a CSS `filter` on a wrapping group; the badge
6
+ * is an appended `<g>` positioned via the SVG's `viewBox` (a badge is skipped if
7
+ * no `viewBox` or `width`/`height` can be read).
8
+ *
9
+ * Returns the SVG unchanged when `tint` is falsy, has nothing to apply, or the
10
+ * input isn't a recognisable `<svg>…</svg>` document.
11
+ */
12
+ export declare function tintSvg(svg: string, tint: EnvConfig): string;
13
+ /** Percent-encode an SVG string as a `data:` URI suitable for a favicon `href`. */
14
+ export declare function svgToDataUri(svg: string): string;
15
+ /**
16
+ * Convenience: `tintSvg` + `svgToDataUri`. Give it your favicon SVG and the
17
+ * config for the current build's environment; get back a ready-to-use
18
+ * `<link rel="icon" href="…">` value with no first-paint flash.
19
+ */
20
+ export declare function faviconDataUri(svg: string, tint: EnvConfig): string;
package/dist/ssr.js CHANGED
@@ -1,3 +1,3 @@
1
- export { faviconDataUri, svgToDataUri, tintSvg } from './chunk-NRMMGQRU.js';
1
+ export { faviconDataUri, svgToDataUri, tintSvg } from './chunk-OA3YX73Z.js';
2
2
  //# sourceMappingURL=ssr.js.map
3
3
  //# sourceMappingURL=ssr.js.map
package/dist/tint.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { EnvFaviconOptions, EnvRule, EnvTint } from './types';
2
+ /**
3
+ * Return the tint of the first `rule` whose `match` matches `url`, or `null` if
4
+ * none do. A `RegExp` `match` is tested against `url.host` and its captures are
5
+ * interpolated into `badge.text` (`$1`, `$<name>`); a function `match` receives
6
+ * the `URL`. Exposed so the same rules can drive server-side rendering from a
7
+ * request URL — pair the result with `favicon-env/ssr`'s `faviconDataUri`.
8
+ */
9
+ export declare function matchRules(rules: EnvRule[], url: URL): EnvTint | null;
10
+ /**
11
+ * Tint / decorate the page favicon for the current environment. Resolves a tint
12
+ * from `rules` (URL match) → `auto` → `environments`/`detect`, then reads the
13
+ * existing `<link rel="icon">` (or a per-env `src` / `options.source`), redraws
14
+ * it on a `<canvas>` with the hue / filter / badge, and swaps in the result as a
15
+ * PNG data URL.
16
+ *
17
+ * A no-op during SSR (no `document`) or when nothing resolves. Works with any
18
+ * favicon format (svg / png / ico). A custom `src` with no recolour or badge is
19
+ * swapped in directly, skipping the canvas — which avoids cross-origin taint and
20
+ * keeps vector sources sharp. If a canvas source is cross-origin without CORS
21
+ * headers the canvas is tainted and the favicon is left untouched.
22
+ *
23
+ * @returns a promise that resolves once the swap has been attempted.
24
+ */
25
+ export declare function envFavicon(options?: EnvFaviconOptions): Promise<void>;
@@ -1,10 +1,23 @@
1
1
  /** A tint / decoration to apply to the favicon for a given environment. */
2
- interface EnvTint {
2
+ export interface EnvTint {
3
3
  /** Hue rotation in degrees applied to the whole icon (e.g. `120`). */
4
4
  hue?: number;
5
+ /**
6
+ * Invert the icon's colours — `true` for a full invert, or a number `0`–`1`
7
+ * for a partial one (maps to CSS `invert()`). Composes *with* `hue` (both are
8
+ * applied); an explicit `filter` beats it, and `tint` (a duotone) wins over it.
9
+ */
10
+ invert?: boolean | number;
11
+ /**
12
+ * Colourise the icon to this exact colour (any CSS colour), preserving the
13
+ * artwork's shape and relative shading — a duotone from black up to `tint`.
14
+ * Unlike `hue` (a *relative* rotation) this sets an *absolute* colour, so a
15
+ * white logo becomes solid `tint`. Beats `hue`; an explicit `filter` beats it.
16
+ */
17
+ tint?: string;
5
18
  /**
6
19
  * Explicit CSS filter string, e.g. `'hue-rotate(120deg) saturate(1.4)'`.
7
- * Takes precedence over `hue` when both are set.
20
+ * Takes precedence over `hue` / `tint` when set.
8
21
  */
9
22
  filter?: string;
10
23
  /**
@@ -20,7 +33,7 @@ interface EnvTint {
20
33
  badge?: string | Badge;
21
34
  }
22
35
  /** A badge drawn on top of the icon. */
23
- interface Badge {
36
+ export interface Badge {
24
37
  /** Text to render, e.g. a PR number (`344`, `'#344'`). Omit for a plain dot. */
25
38
  text?: string | number;
26
39
  /** Background colour. Default `'#ef4444'`. */
@@ -40,17 +53,17 @@ interface Badge {
40
53
  /** Badge opacity, 0–1 (default `1`). With `shape: 'cover'`, below `1` lets the icon show through. */
41
54
  opacity?: number;
42
55
  }
43
- type BadgeCorner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center';
56
+ export type BadgeCorner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center';
44
57
  /** Per-environment config. `false` / `null` / omitted leaves the favicon untouched. */
45
- type EnvConfig = EnvTint | false | null | undefined;
58
+ export type EnvConfig = EnvTint | false | null | undefined;
46
59
  /** Computes badge text from a regex match (if any) and the current URL. */
47
- type BadgeTextFn = (match: RegExpMatchArray | null, url: URL) => string | number;
60
+ export type BadgeTextFn = (match: RegExpMatchArray | null, url: URL) => string | number;
48
61
  /**
49
62
  * A `Badge` for use in a `rule`, whose `text` may additionally be a function, or
50
63
  * a template with `$1` / `$<name>` placeholders filled from the rule's regex
51
64
  * captures — e.g. `'#$1'` against `/^pr-(\d+)\./`.
52
65
  */
53
- type RuleBadge = Omit<Badge, 'text'> & {
66
+ export type RuleBadge = Omit<Badge, 'text'> & {
54
67
  text?: string | number | BadgeTextFn;
55
68
  };
56
69
  /**
@@ -58,7 +71,7 @@ type RuleBadge = Omit<Badge, 'text'> & {
58
71
  * later rule is tried). Rules are checked in order, before `auto` and
59
72
  * `environments`/`detect`.
60
73
  */
61
- type EnvRule = Omit<EnvTint, 'badge'> & {
74
+ export type EnvRule = Omit<EnvTint, 'badge'> & {
62
75
  /**
63
76
  * A `RegExp` (tested against `location.host`, i.e. `hostname:port`) or a
64
77
  * function receiving the full `URL`. Regex captures feed `badge.text`.
@@ -68,7 +81,7 @@ type EnvRule = Omit<EnvTint, 'badge'> & {
68
81
  badge?: string | RuleBadge;
69
82
  };
70
83
  /** Options for `envFavicon`. */
71
- interface EnvFaviconOptions {
84
+ export interface EnvFaviconOptions {
72
85
  /** Map of environment name → tint. A missing entry (or `false`) means "leave as-is". */
73
86
  environments?: Record<string, EnvConfig>;
74
87
  /**
@@ -94,29 +107,7 @@ interface EnvFaviconOptions {
94
107
  size?: number;
95
108
  }
96
109
  /** Tuning for `EnvFaviconOptions.auto`. */
97
- interface AutoOptions {
110
+ export interface AutoOptions {
98
111
  /** Extra hue offset (deg) added to the derived hue — shifts the whole palette. */
99
112
  offset?: number;
100
113
  }
101
-
102
- /**
103
- * Return `svg` (an SVG *string*) with the environment's tint and/or badge baked
104
- * in — the form that survives being rendered as an `<img>` / favicon, with no
105
- * first-paint flash. The tint is a CSS `filter` on a wrapping group; the badge
106
- * is an appended `<g>` positioned via the SVG's `viewBox` (a badge is skipped if
107
- * no `viewBox` or `width`/`height` can be read).
108
- *
109
- * Returns the SVG unchanged when `tint` is falsy, has nothing to apply, or the
110
- * input isn't a recognisable `<svg>…</svg>` document.
111
- */
112
- declare function tintSvg(svg: string, tint: EnvConfig): string;
113
- /** Percent-encode an SVG string as a `data:` URI suitable for a favicon `href`. */
114
- declare function svgToDataUri(svg: string): string;
115
- /**
116
- * Convenience: `tintSvg` + `svgToDataUri`. Give it your favicon SVG and the
117
- * config for the current build's environment; get back a ready-to-use
118
- * `<link rel="icon" href="…">` value with no first-paint flash.
119
- */
120
- declare function faviconDataUri(svg: string, tint: EnvConfig): string;
121
-
122
- export { type AutoOptions as A, type Badge as B, type EnvFaviconOptions as E, type RuleBadge as R, type EnvRule as a, type EnvTint as b, type BadgeCorner as c, type BadgeTextFn as d, type EnvConfig as e, faviconDataUri as f, svgToDataUri as s, tintSvg as t };
package/package.json CHANGED
@@ -1,30 +1,39 @@
1
1
  {
2
2
  "name": "favicon-env",
3
- "version": "0.1.0",
4
- "type": "module",
3
+ "version": "0.3.0",
5
4
  "description": "Tint your favicon per environment (dev/staging/prod) so you can tell instances apart at a glance. Runtime canvas mode + build-time SVG mode. Zero dependencies.",
6
- "license": "MIT",
7
- "author": "Amir Abushanab",
8
5
  "keywords": [
9
- "favicon",
10
- "environment",
11
- "env",
12
- "dev",
13
- "staging",
14
- "hue",
15
- "tint",
16
6
  "canvas",
7
+ "dev",
17
8
  "developer-experience",
18
9
  "dx",
19
- "tanstack-intent"
10
+ "env",
11
+ "environment",
12
+ "favicon",
13
+ "hue",
14
+ "staging",
15
+ "tanstack-intent",
16
+ "tint"
20
17
  ],
18
+ "homepage": "https://github.com/Amir-Abushanab/favicon-env#readme",
19
+ "bugs": "https://github.com/Amir-Abushanab/favicon-env/issues",
20
+ "license": "MIT",
21
+ "author": "Amir Abushanab",
21
22
  "repository": {
22
23
  "type": "git",
23
- "url": "git+https://github.com/amir-abushanab/favicon-env.git"
24
+ "url": "git+https://github.com/Amir-Abushanab/favicon-env.git"
24
25
  },
25
- "homepage": "https://github.com/amir-abushanab/favicon-env#readme",
26
- "bugs": "https://github.com/amir-abushanab/favicon-env/issues",
26
+ "files": [
27
+ "dist",
28
+ "README.md",
29
+ "LICENSE",
30
+ "skills",
31
+ "!skills/_artifacts"
32
+ ],
33
+ "type": "module",
27
34
  "sideEffects": false,
35
+ "unpkg": "./dist/favicon-env.global.js",
36
+ "jsdelivr": "./dist/favicon-env.global.js",
28
37
  "exports": {
29
38
  ".": {
30
39
  "types": "./dist/index.d.ts",
@@ -36,36 +45,30 @@
36
45
  },
37
46
  "./global": "./dist/favicon-env.global.js"
38
47
  },
39
- "unpkg": "./dist/favicon-env.global.js",
40
- "jsdelivr": "./dist/favicon-env.global.js",
41
- "files": [
42
- "dist",
43
- "README.md",
44
- "LICENSE",
45
- "skills",
46
- "!skills/_artifacts"
47
- ],
48
48
  "publishConfig": {
49
49
  "access": "public"
50
50
  },
51
- "simple-git-hooks": {
52
- "pre-commit": "pnpm check"
53
- },
54
51
  "devDependencies": {
55
52
  "@changesets/cli": "^2.31.0",
53
+ "@playwright/test": "^1.61.1",
54
+ "@swc/core": "^1.15.43",
56
55
  "@tanstack/intent": "^0.3.5",
57
56
  "dependency-cruiser": "^18.0.0",
58
- "knip": "^6.24.0",
59
- "oxfmt": "^0.57.0",
60
- "oxlint": "^1.72.0",
57
+ "knip": "^6.26.0",
58
+ "oxfmt": "^0.58.0",
59
+ "oxlint": "^1.73.0",
61
60
  "publint": "^0.3.21",
62
61
  "simple-git-hooks": "^2.13.1",
63
62
  "tsup": "^8.5.1",
64
- "typescript": "^6.0.3"
63
+ "typescript": "^7.0.2"
64
+ },
65
+ "simple-git-hooks": {
66
+ "pre-commit": "pnpm check"
65
67
  },
66
68
  "scripts": {
67
- "build": "tsup",
69
+ "build": "tsup && tsc -p tsconfig.build.json",
68
70
  "dev": "node scripts/serve.mjs --watch",
71
+ "hero": "pnpm build && node scripts/hero.mjs",
69
72
  "typecheck": "tsc --noEmit",
70
73
  "lint": "oxlint src",
71
74
  "format": "oxfmt src",
@@ -74,8 +77,16 @@
74
77
  "check": "oxfmt --check src && oxlint src && knip && depcruise src",
75
78
  "publint": "publint --strict",
76
79
  "intent:validate": "intent validate skills",
80
+ "intent:stale": "intent stale skills",
77
81
  "pretest": "pnpm build",
78
- "test": "node --test",
82
+ "test": "node --test test/pure.test.mjs test/skill.test.mjs",
83
+ "test:skill": "node --test test/skill.test.mjs",
84
+ "test:frameworks": "node test/frameworks/run.mjs",
85
+ "test:frameworks:firefox": "node test/frameworks/run.mjs --browser firefox",
86
+ "test:frameworks:webkit": "node test/frameworks/run.mjs --browser webkit",
87
+ "test:frameworks:browsers": "pnpm test:frameworks && pnpm test:frameworks:firefox && pnpm test:frameworks:webkit",
88
+ "dev:frameworks": "node test/frameworks/serve-all.mjs",
89
+ "test:all": "pnpm test && pnpm test:frameworks",
79
90
  "ncu": "pnpm dlx npm-check-updates",
80
91
  "changeset": "changeset",
81
92
  "version": "changeset version",
@@ -3,13 +3,14 @@ name: core
3
3
  description: >
4
4
  favicon-env tints the browser favicon per environment (dev/staging/prod) so
5
5
  identical tabs are distinguishable. Load when calling envFavicon, choosing
6
- runtime (canvas) vs build-time (SSR) mode, adding hue/filter tints or
7
- badges/PR numbers, wiring it into Next.js/TanStack/Astro/Vite, or configuring
8
- detect, environments, rules, or auto mode.
6
+ runtime (canvas) vs build-time (SSR) mode, adding hue/invert/filter tints or
7
+ badges/PR numbers, wiring it into Next.js/TanStack/Astro/SvelteKit/SolidStart/
8
+ Angular/Nuxt/Vite/plain HTML, or configuring detect, environments, rules, or
9
+ auto mode.
9
10
  metadata:
10
11
  type: core
11
12
  library: favicon-env
12
- library_version: '0.1.0'
13
+ library_version: '0.2.0'
13
14
  sources:
14
15
  - 'Amir-Abushanab/favicon-env:README.md'
15
16
  - 'Amir-Abushanab/favicon-env:src/tint.ts'
@@ -27,24 +28,65 @@ when you control the favicon SVG.
27
28
 
28
29
  ## Setup
29
30
 
30
- Runtime — call once on the client, as early as possible:
31
+ Runtime — gate the import with a compile-time public environment value:
31
32
 
32
33
  ```js
33
- import { envFavicon } from 'favicon-env'
34
-
35
- void envFavicon({
36
- environments: {
37
- dev: { hue: 130 }, // hue-rotate degrees
38
- staging: { badge: '#f59e0b' }, // a corner dot; keeps the logo intact
39
- // prod omitted → favicon left untouched
40
- },
41
- })
34
+ const appEnv = import.meta.env.VITE_APP_ENV ?? 'prod';
35
+
36
+ if (appEnv === 'dev' || appEnv === 'staging') {
37
+ void import('favicon-env').then(({ envFavicon }) =>
38
+ envFavicon({
39
+ environments: {
40
+ dev: { tint: '#22c55e' },
41
+ staging: { badge: '#f59e0b' },
42
+ },
43
+ detect: () => appEnv,
44
+ }),
45
+ );
46
+ }
42
47
  ```
43
48
 
49
+ With a literal prod value, the bundler can remove the branch and package runtime.
50
+ An unconditional call remains bundled even when `prod` is absent from `environments`.
51
+
44
52
  The environment name defaults to a `location.hostname` heuristic (`defaultDetect`):
45
53
  `localhost`/`*.local`/raw IPs → `dev`; a `staging`/`preview`/`qa`/`uat`/… segment →
46
54
  `staging`; everything else → `prod`.
47
55
 
56
+ ### Framework placement
57
+
58
+ | Runtime | Put `envFavicon` here |
59
+ | -------------- | ------------------------------------------------------------------------------ |
60
+ | Next.js | Client component `useEffect`; render once from the root layout |
61
+ | TanStack Start | Optional `src/client.tsx`, before `hydrateRoot` |
62
+ | Astro | Bundled client `<script>` in the root layout |
63
+ | SvelteKit | `onMount` in persistent `src/routes/+layout.svelte` |
64
+ | SolidStart | `onMount` in `src/app.tsx`; use `onCleanup` for the observer |
65
+ | Angular SSR | `afterNextRender` in the standalone root component; clean up with `DestroyRef` |
66
+ | Nuxt | `app/plugins/favicon-env.client.ts`, using an explicit Vite build constant |
67
+ | Vite SPA | Client entry module (`src/main.ts`, etc.) |
68
+ | Plain HTML | Native module or the global build (`window.faviconEnv`) |
69
+
70
+ For any SSR router/head manager that declares the favicon itself, wrap the call in
71
+ the head-observer pattern below. Hydration or navigation can otherwise restore its
72
+ unmanaged `<link rel="icon">` after an early call. Apply only when an unmanaged icon
73
+ exists, queue one microtask at a time, and disconnect through the framework's native
74
+ cleanup hook when the integration is component-scoped.
75
+
76
+ ### Zero runtime bytes in prod
77
+
78
+ Put `import('favicon-env')` behind a value the client bundler replaces at build time.
79
+ Use `NEXT_PUBLIC_*` in Next and `import.meta.env.VITE_*`/`PUBLIC_*` where those values
80
+ are folded to literals. SvelteKit and Nuxt may preserve their normal public runtime
81
+ configuration, so define a dedicated constant through Vite's `define` option and use
82
+ that in the guard.
83
+
84
+ Angular's application builder can emit a lazy chunk even behind a false `define`
85
+ guard. Put the dynamic import in a local loader module and replace that module with a
86
+ typed no-op in the prod build using Angular `fileReplacements`. For plain HTML, omit
87
+ the ESM/global script from the generated prod page. Do not use `enabled: false`: the
88
+ runtime must already be imported to read that option.
89
+
48
90
  ## Core Patterns
49
91
 
50
92
  ### Custom environments — any name, but supply a matching `detect`
@@ -53,11 +95,11 @@ The environment name defaults to a `location.hostname` heuristic (`defaultDetect
53
95
  void envFavicon({
54
96
  environments: { canary: { hue: 280 }, demo: { badge: '#22c55e' } },
55
97
  detect: () => {
56
- if (location.hostname.startsWith('canary.')) return 'canary'
57
- if (location.hostname.endsWith('.demo.acme.com')) return 'demo'
58
- return 'prod' // not in the map → untouched
98
+ if (location.hostname.startsWith('canary.')) return 'canary';
99
+ if (location.hostname.endsWith('.demo.acme.com')) return 'demo';
100
+ return 'prod'; // not in the map → untouched
59
101
  },
60
- })
102
+ });
61
103
  ```
62
104
 
63
105
  `environments` keys are arbitrary strings, but `defaultDetect` only ever returns
@@ -72,7 +114,7 @@ void envFavicon({
72
114
  { match: /^pr-(\d+)\./, badge: { text: '#$1', color: '#8b5cf6', shape: 'cover' } },
73
115
  { match: /staging\./, hue: 45 },
74
116
  ],
75
- })
117
+ });
76
118
  ```
77
119
 
78
120
  `match` is a `RegExp` tested against `location.host` (includes `:port`) or a
@@ -82,7 +124,7 @@ it falls through to `auto`/`environments`.
82
124
  ### Auto mode — one stable colour per host, zero config
83
125
 
84
126
  ```js
85
- void envFavicon({ auto: true })
127
+ void envFavicon({ auto: true });
86
128
  ```
87
129
 
88
130
  Derives a deterministic hue from `location.host`, so every origin and port gets its
@@ -91,11 +133,11 @@ own colour — handy for telling several dev servers apart.
91
133
  ### Build-time SSR — no first-paint flash
92
134
 
93
135
  ```js
94
- import { faviconDataUri } from 'favicon-env/ssr'
95
- import favicon from './favicon.svg?raw'
136
+ import { faviconDataUri } from 'favicon-env/ssr';
137
+ import favicon from './favicon.svg?raw';
96
138
 
97
139
  // during an Astro/Vite build; pick the tint for the current env
98
- const href = faviconDataUri(favicon, { hue: 130 })
140
+ const href = faviconDataUri(favicon, { hue: 130 });
99
141
  // → render into <link rel="icon" type="image/svg+xml" href={href}>
100
142
  ```
101
143
 
@@ -109,7 +151,7 @@ CSS `filter` and/or badge into the SVG using its `viewBox`.
109
151
  Wrong:
110
152
 
111
153
  ```js
112
- void envFavicon({ environments: { canary: { hue: 280 } } })
154
+ void envFavicon({ environments: { canary: { hue: 280 } } });
113
155
  ```
114
156
 
115
157
  Correct:
@@ -118,7 +160,7 @@ Correct:
118
160
  void envFavicon({
119
161
  environments: { canary: { hue: 280 } },
120
162
  detect: () => (location.hostname.startsWith('canary.') ? 'canary' : 'prod'),
121
- })
163
+ });
122
164
  ```
123
165
 
124
166
  `defaultDetect` only returns `dev`/`staging`/`prod`, so `environments.canary` is
@@ -132,37 +174,106 @@ Wrong:
132
174
 
133
175
  ```jsx
134
176
  // app/page.tsx — a Next.js App Router Server Component
135
- import { envFavicon } from 'favicon-env'
136
- envFavicon({ environments: { dev: { hue: 130 } } }) // runs on the server
177
+ import { envFavicon } from 'favicon-env';
178
+ envFavicon({ environments: { dev: { hue: 130 } } }); // runs on the server
137
179
  ```
138
180
 
139
181
  Correct:
140
182
 
141
183
  ```jsx
142
- 'use client'
143
- import { useEffect } from 'react'
144
- import { envFavicon } from 'favicon-env'
184
+ 'use client';
185
+ import { useEffect } from 'react';
186
+
187
+ const appEnv = process.env.NEXT_PUBLIC_APP_ENV ?? 'prod';
145
188
 
146
189
  export function FaviconEnv() {
147
190
  useEffect(() => {
148
- void envFavicon({ environments: { dev: { hue: 130 } } })
149
- }, [])
150
- return null
191
+ if (appEnv !== 'dev' && appEnv !== 'staging') return;
192
+
193
+ let queued = false;
194
+ const apply = async () => {
195
+ queued = false;
196
+ const { envFavicon } = await import('favicon-env');
197
+ await envFavicon({
198
+ environments: { dev: { tint: '#22c55e' }, staging: { badge: '#f59e0b' } },
199
+ detect: () => appEnv,
200
+ });
201
+ };
202
+ const schedule = () => {
203
+ if (queued) return;
204
+ queued = true;
205
+ queueMicrotask(apply);
206
+ };
207
+ const observer = new MutationObserver(() => {
208
+ if (document.head.querySelector('link[rel~="icon"]:not([data-favicon-env])')) schedule();
209
+ });
210
+ observer.observe(document.head, {
211
+ childList: true,
212
+ subtree: true,
213
+ attributes: true,
214
+ attributeFilter: ['href', 'rel'],
215
+ });
216
+ schedule();
217
+ return () => observer.disconnect();
218
+ }, []);
219
+ return null;
151
220
  }
152
221
  ```
153
222
 
154
223
  Runtime mode needs `document`; on the server it is a no-op and never tints the
155
224
  client. Use `useEffect` in a client component (Next App Router), or call it at a
156
- client-entry module (Vite/TanStack `src/main.ts`, or an Astro `<script>`).
225
+ client-entry module (Vite/TanStack Router `src/main.ts`, TanStack Start's optional
226
+ `src/client.tsx`, an Astro `<script>`, SvelteKit/Solid `onMount`, Angular
227
+ `afterNextRender`, or a Nuxt `.client.ts` plugin). In TanStack Start, preserve the standard
228
+ `hydrateRoot(document, <StartClient />)` code. Install the same head observer shown
229
+ above before hydration because TanStack's head hydration can restore its route-managed
230
+ icon after an early `envFavicon` call; no React effect cleanup is needed for this
231
+ page-lifetime client entry.
232
+ Do not put the Next.js integration in `instrumentation-client.ts`: it runs before
233
+ hydration, so Next's metadata reconciliation can restore the original icon after
234
+ `envFavicon` replaces it. In the client component, observe `document.head` and
235
+ reapply only when an unmanaged icon appears; disconnect the observer in the Next effect
236
+ cleanup for React StrictMode. This also handles route-level metadata changes.
237
+
238
+ In SvelteKit put the observer in a persistent `+layout.svelte` and return cleanup
239
+ from `onMount`. In SolidStart register cleanup with `onCleanup` inside `onMount`.
240
+ In Angular register it with `DestroyRef.onDestroy` inside `afterNextRender`. In Nuxt
241
+ use a `.client.ts` plugin; a page-lifetime plugin observer does not require component
242
+ cleanup. Use public build constants (`NEXT_PUBLIC_*`, `PUBLIC_*`, `VITE_*`, or a Vite
243
+ `define`) rather than changing `NODE_ENV` to `staging`. Use Angular file replacement
244
+ and explicit Vite constants for SvelteKit/Nuxt when prod must emit no runtime chunk.
157
245
 
158
246
  Source: src/tint.ts (`typeof document` guard), README "Runtime mode"
159
247
 
248
+ ### MEDIUM — Using `hue` when you want an exact colour
249
+
250
+ Wrong:
251
+
252
+ ```js
253
+ // trying to make dev green — but hue-rotate is relative to the base icon
254
+ void envFavicon({ environments: { dev: { hue: 130 } } });
255
+ ```
256
+
257
+ Correct:
258
+
259
+ ```js
260
+ void envFavicon({ environments: { dev: { tint: '#22c55e' } } });
261
+ ```
262
+
263
+ `hue` _rotates_ the existing colours (the result depends on the base icon, and it
264
+ barely moves white/black/grey). `tint` colourises to an _exact_ colour while
265
+ preserving shape and shading. For a flat block, use a text-less `cover` badge. For a
266
+ quick high-contrast flip with no colour to choose, use `invert: true` (or a `0`–`1`
267
+ amount); it composes with `hue`, while `tint` and an explicit `filter` take precedence.
268
+
269
+ Source: src/types.ts (EnvTint.tint / EnvTint.invert), src/tint.ts (colorize path), src/filter.ts
270
+
160
271
  ### MEDIUM — Multi-digit badge as a corner pill
161
272
 
162
273
  Wrong:
163
274
 
164
275
  ```js
165
- void envFavicon({ environments: { preview: { badge: { text: '#344' } } } })
276
+ void envFavicon({ environments: { preview: { badge: { text: '#344' } } } });
166
277
  ```
167
278
 
168
279
  Correct:
@@ -170,7 +281,7 @@ Correct:
170
281
  ```js
171
282
  void envFavicon({
172
283
  environments: { preview: { badge: { text: '#344', shape: 'cover' } } },
173
- })
284
+ });
174
285
  ```
175
286
 
176
287
  The default `pill` badge sits in a corner at ~half the icon, so a 3–4 digit number
@@ -186,14 +297,14 @@ Wrong:
186
297
  void envFavicon({
187
298
  source: 'https://cdn.example.com/favicon.png', // served without CORS headers
188
299
  environments: { dev: { hue: 130 } },
189
- })
300
+ });
190
301
  ```
191
302
 
192
303
  Correct:
193
304
 
194
305
  ```js
195
306
  // serve the favicon same-origin, or with Access-Control-Allow-Origin
196
- void envFavicon({ source: '/favicon.png', environments: { dev: { hue: 130 } } })
307
+ void envFavicon({ source: '/favicon.png', environments: { dev: { hue: 130 } } });
197
308
  ```
198
309
 
199
310
  Tinting draws to a canvas; a cross-origin image without CORS taints it, so
@@ -207,16 +318,16 @@ Wrong:
207
318
 
208
319
  ```js
209
320
  // runtime mode always shows the untinted icon until JS runs
210
- import { envFavicon } from 'favicon-env'
211
- void envFavicon({ environments: { dev: { hue: 130 } } })
321
+ import { envFavicon } from 'favicon-env';
322
+ void envFavicon({ environments: { dev: { hue: 130 } } });
212
323
  ```
213
324
 
214
325
  Correct:
215
326
 
216
327
  ```js
217
328
  // bake the tint into the initial HTML at build time — no flash
218
- import { faviconDataUri } from 'favicon-env/ssr'
219
- const href = faviconDataUri(faviconSvg, { hue: 130 })
329
+ import { faviconDataUri } from 'favicon-env/ssr';
330
+ const href = faviconDataUri(faviconSvg, { hue: 130 });
220
331
  ```
221
332
 
222
333
  `envFavicon` runs after first paint, so the original icon flashes briefly. When
@@ -229,15 +340,15 @@ Source: README "First-paint flash", src/ssr.ts
229
340
  Wrong:
230
341
 
231
342
  ```js
232
- envFavicon({ environments: { dev: { hue: 130 } } })
233
- const href = document.querySelector('link[rel~="icon"]').href // old icon — not swapped yet
343
+ envFavicon({ environments: { dev: { hue: 130 } } });
344
+ const href = document.querySelector('link[rel~="icon"]').href; // old icon — not swapped yet
234
345
  ```
235
346
 
236
347
  Correct:
237
348
 
238
349
  ```js
239
- await envFavicon({ environments: { dev: { hue: 130 } } })
240
- const href = document.querySelector('link[rel~="icon"]').href
350
+ await envFavicon({ environments: { dev: { hue: 130 } } });
351
+ const href = document.querySelector('link[rel~="icon"]').href;
241
352
  ```
242
353
 
243
354
  `envFavicon` returns a `Promise` and loads the base image asynchronously before